spdxedit.PackageEditor.java Source code

Java tutorial

Introduction

Here is the source code for spdxedit.PackageEditor.java

Source

package spdxedit;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.stage.*;
import javafx.util.StringConverter;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.CheckListView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spdx.rdfparser.InvalidSPDXAnalysisException;
import org.spdx.rdfparser.SpdxDocumentContainer;
import org.spdx.rdfparser.model.Relationship;
import org.spdx.rdfparser.model.Relationship.RelationshipType;
import org.spdx.rdfparser.model.SpdxFile;
import org.spdx.rdfparser.model.SpdxFile.FileType;
import org.spdx.rdfparser.model.SpdxPackage;
import spdxedit.externalRef.ExternalRefListControl;
import spdxedit.license.FileLicenseEditor;
import spdxedit.license.LicenseEditControl;
import spdxedit.license.SpdxWithoutExeption;
import spdxedit.util.StringableWrapper;
import spdxedit.util.UiUtils;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class PackageEditor {

    private static final Logger logger = LoggerFactory.getLogger(PackageEditor.class);

    @FXML
    private TitledPane tabFiles;

    @FXML
    private TreeTableView<SpdxFile> filesTable;

    @FXML
    private TreeTableColumn<SpdxFile, String> tblColumnFile;

    @FXML
    private TitledPane tabRelationships;

    @FXML
    private Button btnOk;

    @FXML
    private Button btnDeleteFileFromPackage;

    @FXML
    private Button btnAddFile;

    @FXML
    private Button btnCopyright;

    @FXML
    private Button btnFileLicense;

    /**
     * Package properties
     */

    @FXML
    private TitledPane tabPackageProperties;

    @FXML
    private TitledPane tabExternalRefs;

    /***
     * FILE INFORMATION REPRESENTATIONS
     ***/

    @FXML
    private CheckListView<StringableWrapper<FileType>> chkListFileTypes;

    @FXML
    private CheckBox chkDataFile;

    @FXML
    private CheckBox chkTestCase;

    @FXML
    private CheckBox chkDocumentation;

    @FXML
    private CheckBox chkOptionalComponent;

    @FXML
    private CheckBox chkMetafile;

    @FXML
    private CheckBox chkBuildTool;

    @FXML
    private CheckBox chkExcludeFile;

    /**
     * PACKAGE RELATIONSHIP REPRESENTATIONS
     **/
    @FXML
    private ListView<SpdxPackage> lstTargetPackages;

    @FXML
    private ChoiceBox<RelationshipType> chcNewRelationshipType;

    @FXML
    private Button btnAddRelationship;

    @FXML
    private Button btnRemoveRelationship;

    /**
     * PACKAGE LICENSE EDITOR
     */

    @FXML
    private Tab tabPkgDeclaredLicense;

    @FXML
    private Tab tabPkgConcludedLicense;

    @FXML
    private TitledPane tabPkgLicenses;

    private LicenseEditControl pkgDeclaredLicenseEdit;

    private LicenseEditControl pkgConcludedLicenseEdit;

    @FXML
    private ListView<StringableWrapper<RelationshipType>> lstPackageRelationships;

    private static final StringConverter<RelationshipType> RELATIONSHIP_TYPE_STRING_CONVERTER = new StringConverter<RelationshipType>() {
        @Override
        public String toString(RelationshipType relationshipType) {
            return SpdxLogic.toString(relationshipType);
        }

        @Override
        public RelationshipType fromString(String string) {
            throw new UnsupportedOperationException("Shoudln't have to convert strings to relationship types");
        }
    };

    //The package being edited
    private SpdxPackage pkg;

    //The file currently being edited
    private SpdxFile currentFile;

    //The container of the document being edited.
    private SpdxDocumentContainer documentContainer;

    //Packages to which the edited package can have a relationship
    private List<SpdxPackage> otherPackages;

    @FXML
    private void initialize() {
        assert tabFiles != null : "fx:id=\"tabFiles\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert tabPackageProperties != null : "fx:id=\"tabPackageProperties\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert tabExternalRefs != null : "fx:id=\"tabExternalRefs\" was not injected: check your FXML file 'PackageEditor.fxml'.";

        assert filesTable != null : "fx:id=\"filesTable\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert tblColumnFile != null : "fx:id=\"tblColumnFile\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert chkListFileTypes != null : "fx:id=\"chkListFileTypes\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert tabRelationships != null : "fx:id=\"tabRelationships\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert btnOk != null : "fx:id=\"btnOk\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert btnDeleteFileFromPackage != null : "fx:id=\"btnDeleteFileFromPackage\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert btnAddFile != null : "fx:id=\"btnAddFile\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert btnCopyright != null : "fx:id=\"btnCopyright\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert btnFileLicense != null : "fx:id=\"btnFileLicense\" was not injected: check your FXML file 'PackageEditor.fxml'.";

        //File relationship checkboxes
        assert chkDataFile != null : "fx:id=\"chkDataFile\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert chkTestCase != null : "fx:id=\"chkTestCase\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert chkDocumentation != null : "fx:id=\"chkDocumentation\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert chkOptionalComponent != null : "fx:id=\"chkOptionalComponent\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert chkMetafile != null : "fx:id=\"chkMetafile\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert chkBuildTool != null : "fx:id=\"chkBuildTool\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert chkExcludeFile != null : "fx:id=\"chkExcludeFile\" was not injected: check your FXML file 'PackageEditor.fxml'.";

        //Initialise file relationship checkbox handling
        //TODO: Could make this easier by extending the CheckBox control?
        chkDataFile.selectedProperty().addListener((observable, oldValue,
                newValue) -> addOrRemoveFileRelationshipToPackage(RelationshipType.DATA_FILE_OF, newValue));
        chkTestCase.selectedProperty().addListener((observable, oldValue,
                newValue) -> addOrRemoveFileRelationshipToPackage(RelationshipType.TEST_CASE_OF, newValue));
        chkDocumentation.selectedProperty().addListener((observable, oldValue,
                newValue) -> addOrRemoveFileRelationshipToPackage(RelationshipType.DOCUMENTATION_OF, newValue));
        chkOptionalComponent.selectedProperty()
                .addListener((observable, oldValue, newValue) -> addOrRemoveFileRelationshipToPackage(
                        RelationshipType.OPTIONAL_COMPONENT_OF, newValue));
        chkMetafile.selectedProperty().addListener((observable, oldValue,
                newValue) -> addOrRemoveFileRelationshipToPackage(RelationshipType.METAFILE_OF, newValue));
        chkBuildTool.selectedProperty().addListener((observable, oldValue,
                newValue) -> addOrRemoveFileRelationshipToPackage(RelationshipType.BUILD_TOOL_OF, newValue));
        chkExcludeFile.selectedProperty()
                .addListener((observable, oldValue, newValue) -> handleChkExcludeFileChange(newValue));

        //Package relationship controls
        assert lstTargetPackages != null : "fx:id=\"lstTargetPackages\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert chcNewRelationshipType != null : "fx:id=\"chcNewRelationshipType\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert btnAddRelationship != null : "fx:id=\"btnAddRelationship\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert btnRemoveRelationship != null : "fx:id=\"btnRemoveRelationship\" was not injected: check your FXML file 'PackageEditor.fxml'.";

        //Initialize package relationship controls
        lstTargetPackages.getSelectionModel().selectedItemProperty()
                .addListener((observable1, oldValue1, newValue1) -> handleTargetPackageSelected(newValue1));
        lstTargetPackages.setCellFactory(listView -> new MainSceneController.SpdxPackageListCell());
        lstPackageRelationships.getSelectionModel().selectedItemProperty().addListener(
                (observable1, oldValue1, newValue1) -> btnRemoveRelationship.setDisable(newValue1 == null));
        //Package relationship types
        chcNewRelationshipType.setConverter(RELATIONSHIP_TYPE_STRING_CONVERTER);
        chcNewRelationshipType.getItems()
                .setAll(Stream.of(RelationshipType.AMENDS, RelationshipType.DESCENDANT_OF,
                        RelationshipType.DYNAMIC_LINK, RelationshipType.GENERATED_FROM, RelationshipType.GENERATES,
                        RelationshipType.HAS_PREREQUISITE, RelationshipType.PREREQUISITE_FOR,
                        RelationshipType.STATIC_LINK, RelationshipType.OTHER).collect(Collectors.toList()));
        chcNewRelationshipType.getSelectionModel().selectFirst();
        assert (otherPackages != null); //Constructor finished executing
        lstTargetPackages.getItems().setAll(otherPackages);

        //Package license editor
        assert tabPkgLicenses != null : "fx:id=\"tabPkgLicenses\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert tabPkgDeclaredLicense != null : "fx:id=\"tabPkgDeclaredLicense\" was not injected: check your FXML file 'PackageEditor.fxml'.";
        assert tabPkgConcludedLicense != null : "fx:id=\"tabPkgConcludedLicense\" was not injected: check your FXML file 'PackageEditor.fxml'.";

        pkgConcludedLicenseEdit = new LicenseEditControl(documentContainer);
        pkgConcludedLicenseEdit.setInitialValue(pkg.getLicenseConcluded());
        pkgConcludedLicenseEdit
                .setOnLicenseChange(license -> SpdxWithoutExeption.setLicenseConcluded(pkg, license));
        tabPkgConcludedLicense.setContent(pkgConcludedLicenseEdit.getUi());
        pkgDeclaredLicenseEdit = new LicenseEditControl(documentContainer);
        pkgDeclaredLicenseEdit.setInitialValue(SpdxWithoutExeption.getLicenseDeclared(pkg));
        pkgDeclaredLicenseEdit.setOnLicenseChange(license -> SpdxWithoutExeption.setLicenseDeclared(pkg, license));
        tabPkgDeclaredLicense.setContent(pkgDeclaredLicenseEdit.getUi());

        //Initialize other elements
        tblColumnFile.setCellValueFactory(
                (TreeTableColumn.CellDataFeatures<SpdxFile, String> param) -> new ReadOnlyStringWrapper(
                        param.getValue().getValue().getName()));
        //Load all file types into the file type list in order.
        chkListFileTypes.getItems().setAll(Stream.of(FileType.values()).sorted(Ordering.usingToString()) //Sort
                .map(fileType -> StringableWrapper.wrap(fileType, SpdxLogic::toString)) //Wrap so that the nice toString function gets used by the checkbox
                .collect(Collectors.toList()));
        chkListFileTypes.getCheckModel().getCheckedItems().addListener(this::handleFileTypeCheckedOrUnchecked);
        filesTable.getSelectionModel().selectedItemProperty()
                .addListener((observable, oldValue, newValue) -> handleFileSelected(newValue));
        filesTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        filesTable.setShowRoot(false);

        AnchorPane externalRefControl = new ExternalRefListControl(pkg).getUi();
        externalRefControl.setPadding(new Insets(12, 10, 5, 10));
        tabExternalRefs.setContent(externalRefControl);
        externalRefControl.setManaged(true);

        new PackagePropertyEditor(pkg).initialize(tabPackageProperties);

    }

    /**
     * Opens the modal package editor for the provided package.
     *
     * @param pkg               The package to edit.
     * @param relatablePackages Packages to which the edited package may optionally have defined relationships
     * @param parentWindow      The parent window.
     */
    public static void editPackage(final SpdxPackage pkg, final List<SpdxPackage> relatablePackages,
            SpdxDocumentContainer documentContainer, Window parentWindow) {

        final PackageEditor packageEditor = new PackageEditor(pkg, relatablePackages, documentContainer);
        final Stage dialogStage = new Stage();
        dialogStage.setTitle("Edit SPDX Package: " + pkg.getName());
        dialogStage.initStyle(StageStyle.DECORATED);
        dialogStage.initModality(Modality.APPLICATION_MODAL);
        dialogStage.setY(parentWindow.getX() + parentWindow.getWidth() / 2);
        dialogStage.setY(parentWindow.getY() + parentWindow.getHeight() / 2);
        dialogStage.setResizable(false);
        try {
            FXMLLoader loader = new FXMLLoader(NewPackageDialog.class.getResource("/PackageEditor.fxml"));
            loader.setController(packageEditor);
            Pane pane = loader.load();
            Scene scene = new Scene(pane);
            dialogStage.setScene(scene);
            dialogStage.getIcons().clear();
            dialogStage.getIcons().add(UiUtils.ICON_IMAGE_VIEW.getImage());
            //Populate the file list on appearance
            dialogStage.setOnShown(event -> {
                try {
                    final SpdxFile dummyfile = new SpdxFile(pkg.getName(), null, null, null, null, null, null, null,
                            null, null, null, null, null);
                    TreeItem<SpdxFile> root = new TreeItem<>(dummyfile);
                    packageEditor.filesTable.setRoot(root);
                    //Assume a package without is external
                    //TODO: replace with external packages or whatever alternate mechanism in 2.1
                    packageEditor.btnAddFile.setDisable(pkg.getFiles().length == 0);
                    root.getChildren()
                            .setAll(Stream.of(pkg.getFiles())
                                    .sorted(Comparator.comparing(file -> StringUtils.lowerCase(file.getName()))) //Sort by file name
                                    .map(TreeItem<SpdxFile>::new).collect(Collectors.toList()));
                } catch (InvalidSPDXAnalysisException e) {
                    logger.error("Unable to get files for package " + pkg.getName(), e);
                }

                packageEditor.tabFiles.setExpanded(true);

            });

            //Won't assign this event through FXML - don't want to propagate the stage beyond this point.
            packageEditor.btnOk.setOnMouseClicked(event -> dialogStage.close());
            dialogStage.showAndWait();

        } catch (IOException ioe) {
            throw new RuntimeException("Unable to load dialog", ioe);
        }
    }

    private PackageEditor(SpdxPackage pkg, List<SpdxPackage> relatablePackages,
            SpdxDocumentContainer documentContainer) {
        this.pkg = pkg;
        this.otherPackages = relatablePackages;
        this.documentContainer = documentContainer;
    }

    //Load the values for the file in all file editing contorls
    private void handleFileSelected(TreeItem<SpdxFile> newSelection) {
        btnDeleteFileFromPackage.setDisable(newSelection == null);
        if (newSelection == null) {
            currentFile = null;
        } else {
            //Set currentFile to null to make sure we don't accidentally edit the previous file
            currentFile = null;
            //Set the file type checkbox values to reflect this file's types
            chkListFileTypes.getCheckModel().clearChecks();

            //If multiple selections, then disable everything but the delete button
            boolean multipleSelections = filesTable.getSelectionModel().getSelectedItems().size() > 1;
            chkListFileTypes.setDisable(multipleSelections);
            //Disable/enable usage checkboxes for multiple selection
            Stream.of(chkExcludeFile, chkBuildTool, chkMetafile, chkOptionalComponent, chkDocumentation,
                    chkTestCase, chkDataFile).forEach(checkbox -> checkbox.setDisable(multipleSelections));
            if (multipleSelections)
                return;

            //The element lookup by index seems to be broken on the CheckListView control,
            //so we'll have to provide the indices

            chkListFileTypes.getItems().forEach(item -> {
                if (ArrayUtils.contains(newSelection.getValue().getFileTypes(), item.getValue()))
                    chkListFileTypes.getCheckModel().check(item);
                else
                    chkListFileTypes.getCheckModel().clearCheck(item);
            });

            //Reset the relationship checkboxes
            chkDataFile.setSelected(SpdxLogic
                    .findRelationship(newSelection.getValue(), RelationshipType.DATA_FILE_OF, pkg).isPresent());
            chkTestCase.setSelected(SpdxLogic
                    .findRelationship(newSelection.getValue(), RelationshipType.TEST_CASE_OF, pkg).isPresent());
            chkDocumentation.setSelected(SpdxLogic
                    .findRelationship(newSelection.getValue(), RelationshipType.DOCUMENTATION_OF, pkg).isPresent());
            chkMetafile.setSelected(SpdxLogic
                    .findRelationship(newSelection.getValue(), RelationshipType.METAFILE_OF, pkg).isPresent());
            chkOptionalComponent.setSelected(
                    SpdxLogic.findRelationship(newSelection.getValue(), RelationshipType.OPTIONAL_COMPONENT_OF, pkg)
                            .isPresent());
            chkBuildTool.setSelected(SpdxLogic
                    .findRelationship(newSelection.getValue(), RelationshipType.BUILD_TOOL_OF, pkg).isPresent());

            chkExcludeFile.setSelected(SpdxLogic.isFileExcludedFromVerification(pkg, newSelection.getValue()));

            //Set the file relationship checkboxes
            Relationship[] relationships = newSelection.getValue().getRelationships();

            currentFile = newSelection.getValue();
        }
    }

    private void handleFileTypeCheckedOrUnchecked(
            ListChangeListener.Change<? extends StringableWrapper<FileType>> change) {
        if (currentFile == null)
            return;
        FileType[] newFileTypes = change.getList().stream().map(wrappedType -> wrappedType.getValue()) //Unwrap the stringable wrapper
                .toArray(size -> new FileType[size]); //Get array
        try {
            currentFile.setFileTypes(newFileTypes);
        } catch (InvalidSPDXAnalysisException e) {
            logger.error("Unable to update types of file " + currentFile.getName());
        }
    }

    public void handleDeleteFileFromPackageClick(MouseEvent event) {
        List<TreeItem<SpdxFile>> itemsToRemove = ImmutableList
                .copyOf(filesTable.getSelectionModel().getSelectedItems());
        List<Integer> selectedIndices = ImmutableList.copyOf(filesTable.getSelectionModel().getSelectedIndices());
        filesTable.getSelectionModel().clearSelection();
        filesTable.getRoot().getChildren().removeAll(itemsToRemove);
        SpdxLogic.removeFilesFromPackage(pkg,
                itemsToRemove.stream().map(TreeItem::getValue).collect(Collectors.toList()));

    }

    public void handleAddFileClick(MouseEvent event) {
        FileChooser chooser = new FileChooser();
        chooser.setTitle("Add file");
        File file = chooser.showOpenDialog(btnAddFile.getParent().getScene().getWindow());
        if (file == null) //Dialog cancelled.
            return;
        Path path = Paths.get(file.getAbsolutePath());
        SpdxLogic.addFileToPackage(pkg, path, file.toURI().toString());
    }

    private void addOrRemoveFileRelationshipToPackage(RelationshipType relationshipType, boolean shouldExist) {
        if (currentFile != null) {
            SpdxLogic.setFileRelationshipToPackage(currentFile, pkg, relationshipType, shouldExist);
        }
    }

    public void handleBtnAddRelationshipClick(MouseEvent event) {
        assert (lstTargetPackages.getSelectionModel().getSelectedItems().size() > 0);
        assert (chcNewRelationshipType.getSelectionModel().getSelectedIndex() >= 0);
        SpdxPackage targetPackage = lstTargetPackages.getSelectionModel().getSelectedItem();
        RelationshipType relationshipType = chcNewRelationshipType.getSelectionModel().getSelectedItem();
        //TODO: remove existing relationship types from dropdown
        try {
            pkg.addRelationship(new Relationship(targetPackage, relationshipType, null));
            lstPackageRelationships.getItems().add(StringableWrapper.wrap(relationshipType, SpdxLogic::toString));
        } catch (InvalidSPDXAnalysisException e) {
            logger.error("Unable to add package relationship", e);
            new Alert(Alert.AlertType.ERROR, "Unable to add relationship").showAndWait();
        }
    }

    public void handleBtnRemoveRelationshipClick(MouseEvent event) {
        assert (lstTargetPackages.getSelectionModel().getSelectedItems().size() > 0);
        assert (lstPackageRelationships.getSelectionModel().getSelectedItems().size() > 0);
        StringableWrapper<RelationshipType> wrappedRelationshipType = lstPackageRelationships.getSelectionModel()
                .getSelectedItem();
        lstPackageRelationships.getItems().remove(wrappedRelationshipType);
        SpdxLogic.removeRelationship(pkg, wrappedRelationshipType.getValue(),
                lstTargetPackages.getSelectionModel().getSelectedItem());

    }

    private void handleTargetPackageSelected(SpdxPackage pkg) {
        //Get the relationships the edited package has to the selected target package.
        List<StringableWrapper<RelationshipType>> relationshipTypes = Arrays.stream(this.pkg.getRelationships())
                .filter(relationship -> Objects.equals(relationship.getRelatedSpdxElement(), pkg))
                .map(Relationship::getRelationshipType)
                .map(relationshipType -> StringableWrapper.wrap(relationshipType, SpdxLogic::toString))
                .collect(Collectors.toList());
        lstPackageRelationships.getItems().setAll(relationshipTypes);
        btnAddRelationship.setDisable(pkg == null);
    }

    private void handleChkExcludeFileChange(boolean newValue) {
        if (currentFile != null) {
            if (newValue) {
                SpdxLogic.excludeFileFromVerification(pkg, currentFile);
            } else {
                SpdxLogic.unexcludeFileFromVerification(pkg, currentFile);
            }
        }
    }

    public void handleBtnCopyrightClick(MouseEvent event) {
        String oldCopyright = currentFile.getCopyrightText();
        TextInputDialog dialog = new TextInputDialog();
        dialog.setTitle("Copyright");
        dialog.setHeaderText("Enter the copyright text");
        ((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons()
                .addAll(UiUtils.ICON_IMAGE_VIEW.getImage());
        Optional<String> newCopyrightText = dialog.showAndWait();
        if (newCopyrightText.isPresent()) {
            currentFile.setCopyrightText(newCopyrightText.get());
        }

    }

    public void handleBtnFileLicenseClick(MouseEvent event) {
        FileLicenseEditor.editConcludedLicense(this.currentFile, documentContainer);
    }
}