org.kitodo.production.forms.dataeditor.FieldedMetadataTableRow.java Source code

Java tutorial

Introduction

Here is the source code for org.kitodo.production.forms.dataeditor.FieldedMetadataTableRow.java

Source

/*
 * (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
 *
 * This file is part of the Kitodo project.
 *
 * It is licensed under GNU General Public License version 3 or later.
 *
 * For the full copyright and license information, please read the
 * GPL3-License.txt file that was distributed with this source code.
 */

package org.kitodo.production.forms.dataeditor;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.faces.model.SelectItem;

import org.apache.commons.lang3.tuple.Pair;
import org.kitodo.api.Metadata;
import org.kitodo.api.MetadataEntry;
import org.kitodo.api.MetadataGroup;
import org.kitodo.api.dataeditor.rulesetmanagement.ComplexMetadataViewInterface;
import org.kitodo.api.dataeditor.rulesetmanagement.Domain;
import org.kitodo.api.dataeditor.rulesetmanagement.MetadataViewInterface;
import org.kitodo.api.dataeditor.rulesetmanagement.MetadataViewWithValuesInterface;
import org.kitodo.api.dataeditor.rulesetmanagement.SimpleMetadataViewInterface;
import org.kitodo.api.dataeditor.rulesetmanagement.StructuralElementViewInterface;
import org.kitodo.api.dataformat.IncludedStructuralElement;
import org.kitodo.production.helper.Helper;

/**
 * Represents the meta-data panel in the meta-data editor, or a meta-data group
 * within that panel. Basically both are the same, with the exception that the
 * label of the meta-data panel is not used.
 */
public class FieldedMetadataTableRow extends MetadataTableRow implements Serializable {

    /**
     * An empty meta-data group for the empty meta-data panel showing. The empty
     * meta-data panel can be displayed if the element selected in the structure
     * window isnt a structure (has no meta-data).
     */
    static final FieldedMetadataTableRow EMPTY = new FieldedMetadataTableRow();

    /**
     * Fields the user has selected to show in addition, with no data yet.
     */
    private Collection<String> additionallySelectedFields = new ArrayList<>();

    /**
     * Meta-data which is excluded by the rule set.
     */
    private Collection<Metadata> hiddenMetadata;

    /**
     * The meta-data object with the content of this panel.
     */
    private Collection<Metadata> metadata;

    /**
     * The definition of this panel in the rule set.
     */
    private ComplexMetadataViewInterface metadataView;

    /**
     * The rows that JSF has to display.
     */
    private List<MetadataTableRow> rows = new ArrayList<>();

    /**
     * The structure this panel is related to, if it isnt a sub-panel.
     */
    private IncludedStructuralElement structure;

    /**
     * Creates an empty meta-data group. This constructor is used to create the
     * {@link #EMPTY} constant above.
     */
    private FieldedMetadataTableRow() {
        super(null, null, null);
        this.rows = Collections.emptyList();
        this.metadata = Collections.emptyList();
        this.hiddenMetadata = Collections.emptyList();
    }

    /**
     * Creates a new root meta-data group representing the meta-data table
     * content in the meta-data panel.
     *
     * @param panel
     *            MetadataPanel to which the FieldedMetadataTableRow is added
     * @param structure
     *            structure selected by the user
     * @param divisionView
     *            information about that structure from the rule set
     */
    FieldedMetadataTableRow(MetadataPanel panel, IncludedStructuralElement structure,
            StructuralElementViewInterface divisionView) {
        this(panel, null, structure, divisionView, structure.getMetadata());
    }

    /**
     * Creates a new root meta-data group representing the meta-data table
     * content in the meta-data panel.
     *
     * @param panel
     *            meta-data panel on which this row is showing
     * @param metadata
     *            content for the meta-data table
     * @param divisionView
     *            information about that structure from the rule set
     */
    FieldedMetadataTableRow(MetadataPanel panel, Collection<Metadata> metadata,
            StructuralElementViewInterface divisionView) {
        this(panel, null, null, divisionView, metadata);
    }

    /**
     * Creates a sub-panel for a meta-data group.
     *
     * @param panel
     *            meta-data panel on which this row is showing
     * @param container
     *            parental meta-data group
     * @param metadataView
     *            information about that group from the rule set
     * @param metadata
     *            data of the group, may be empty but must be modifiable
     */
    private FieldedMetadataTableRow(MetadataPanel panel, FieldedMetadataTableRow container,
            ComplexMetadataViewInterface metadataView, Collection<Metadata> metadata) {
        this(panel, container, null, metadataView, metadata);
    }

    /**
     * Creates a new fielded meta-data panel. This constructor is called from
     * one of the above ones and does the work.
     *
     * @param panel
     *            meta-data panel on which this row is showing
     * @param container
     *            parental meta-data group
     * @param structure
     *            structure selected by the user, null in case of a sub-panel
     * @param metadataView
     *            information about that structure or group from the rule set
     * @param metadata
     *            meta-data, may be empty but must be modifiable
     */
    private FieldedMetadataTableRow(MetadataPanel panel, FieldedMetadataTableRow container,
            IncludedStructuralElement structure, ComplexMetadataViewInterface metadataView,
            Collection<Metadata> metadata) {
        super(panel, container, metadataView.getId());
        this.structure = structure;
        this.metadata = metadata;
        this.metadataView = metadataView;
        createMetadataTable();
    }

    /**
     * The method for building the meta-data table.
     */
    private void createMetadataTable() {
        // the existing meta-data is passed to the rule set, which sorts it
        Map<Metadata, String> metadataWithKeys = addLabels(metadata).parallelStream()
                .collect(Collectors.toMap(Function.identity(), Metadata::getKey));
        List<MetadataViewWithValuesInterface<Metadata>> tableData = metadataView
                .getSortedVisibleMetadata(metadataWithKeys, additionallySelectedFields);

        rows.clear();
        hiddenMetadata = Collections.emptyList();
        for (MetadataViewWithValuesInterface<Metadata> rowData : tableData) {
            Optional<MetadataViewInterface> optionalMetadataView = rowData.getMetadata();
            Collection<Metadata> values = rowData.getValues();
            if (optionalMetadataView.isPresent()) {
                MetadataViewInterface metadataView = optionalMetadataView.get();
                if (metadataView.isComplex()) {
                    rows.add(createMetadataGroupPanel((ComplexMetadataViewInterface) metadataView, values));
                } else {
                    rows.add(createMetadataEntryEdit((SimpleMetadataViewInterface) metadataView, values));
                }
            } else {
                hiddenMetadata = values;
            }
        }
    }

    /**
     * Reads the labels from the structure (if any) and adds them to the
     * returned meta-data collection.
     *
     * @param metadata
     *            available meta-data
     * @return meta-data with labels, if any
     */
    private Collection<Metadata> addLabels(Collection<Metadata> metadata) {
        Collection<Metadata> displayMetadata = metadata;
        if (Objects.nonNull(structure)) {
            displayMetadata = new ArrayList<>(metadata);
            if (Objects.nonNull(structure.getLabel())) {
                MetadataEntry label = new MetadataEntry();
                label.setKey("LABEL");
                label.setValue(structure.getLabel());
                displayMetadata.add(label);
            }
            if (Objects.nonNull(structure.getOrderlabel())) {
                MetadataEntry label = new MetadataEntry();
                label.setKey("ORDERLABEL");
                label.setValue(structure.getLabel());
                displayMetadata.add(label);
            }
        }
        return displayMetadata;
    }

    /**
     * Creates an object to represent a meta-data group. This is done by
     * creating a {@code FieldedMetadataGroup} recursively.
     *
     * @param complexMetadataView
     *            information about that group from the rule set
     * @param values
     *            data for that group, must contain at most one element
     * @return a sub-panel for JSF to render
     */
    private FieldedMetadataTableRow createMetadataGroupPanel(ComplexMetadataViewInterface complexMetadataView,
            Collection<Metadata> values) {

        Collection<Metadata> value;
        switch (values.size()) {
        case 0:
            value = new ArrayList<>();
            break;
        case 1: {
            Metadata metadata = values.iterator().next();
            if (metadata instanceof MetadataGroup) {
                MetadataGroup metadataGroup = (MetadataGroup) metadata;
                value = metadataGroup.getGroup();
            } else {
                throw new IllegalStateException("Got simple meta-data entry with key \"" + metadataView.getId()
                        + "\" which is declared as substructured key in the rule set.");
            }
            break;
        }
        default:
            throw new IllegalStateException("Too many (" + values.size() + ") complex meta-data of type \""
                    + metadataView.getId() + "\" in a single row. Must be 0 or 1 per row.");
        }
        return new FieldedMetadataTableRow(panel, this, complexMetadataView, value);
    }

    /**
     * Creates an object to represent a single-row meta-data input.
     *
     * @param simpleMetadataView
     *            presentation information about the meta-data entry from the
     *            ruleset
     * @param values
     *            the value(s) to be displayed
     * @return a backing bean for the row
     */
    private MetadataTableRow createMetadataEntryEdit(SimpleMetadataViewInterface simpleMetadataView,
            Collection<Metadata> values) {
        switch (simpleMetadataView.getInputType()) {
        case MULTIPLE_SELECTION:
        case MULTI_LINE_SINGLE_SELECTION:
        case ONE_LINE_SINGLE_SELECTION:
            return new SelectMetadataTableRow(panel, this, simpleMetadataView, simpleValues(values));
        case BOOLEAN:
            return new BooleanMetadataTableRow(panel, this, simpleMetadataView, oneSimpleValue(values));
        case DATE:
        case INTEGER:
        case MULTI_LINE_TEXT:
        case ONE_LINE_TEXT:
            return new TextMetadataTableRow(panel, this, simpleMetadataView, oneSimpleValue(values));
        default:
            throw new IllegalStateException("complete switch");
        }
    }

    /**
     * Returns the collection of simple meta-data entries. Throws an
     * IllegalStateException if a cannot be casted.
     *
     * @param values
     *            values obtained
     * @return a collection of simple meta-data entries
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private Collection<MetadataEntry> simpleValues(Collection<Metadata> values) {
        Optional<Metadata> fault = values.parallelStream().filter(entry -> !(entry instanceof MetadataEntry))
                .findAny();
        if (fault.isPresent()) {
            throw new IllegalStateException("Got complex meta-data entry with key \"" + fault.get().getKey()
                    + "\" which isn't declared as substructured key in the rule set.");
        }
        return (Collection) values;
    }

    /**
     * Returns the only meta-data entry or null. Throws an IllegalStateException
     * if the value is ambiguous or cannot be casted.
     *
     * @param values
     *            values obtained
     * @return the only entry or null
     */
    private MetadataEntry oneSimpleValue(Collection<Metadata> values) {
        switch (values.size()) {
        case 0:
            return null;
        case 1: {
            Metadata metadata = values.iterator().next();
            if (metadata instanceof MetadataEntry) {
                return (MetadataEntry) metadata;
            } else {
                throw new IllegalStateException("Got complex meta-data entry with key \"" + metadata.getKey()
                        + "\" which isn't declared as substructured key in the rule set.");
            }
        }
        default:
            throw new IllegalStateException("Too many (" + values.size() + ") meta-data of type \""
                    + values.iterator().next().getKey() + "\" in a single row. Must be 0 or 1 per row.");
        }
    }

    void addAdditionallySelectedField(String additionallySelectedField)
            throws InvalidMetadataValueException, NoSuchMetadataFieldException {
        additionallySelectedFields.add(additionallySelectedField);
        preserve();
        createMetadataTable();
    }

    /**
     * Returns the elements for the drop-down addable meta-data picklist.
     *
     * @return the addable meta-data elements
     */
    List<SelectItem> getAddableMetadata() {
        Map<Metadata, String> metadataWithKeys = addLabels(metadata).parallelStream()
                .collect(Collectors.toMap(Function.identity(), Metadata::getKey));
        return metadataView.getAddableMetadata(metadataWithKeys, additionallySelectedFields).stream()
                .map(addable -> new SelectItem(addable.getId(), addable.getLabel())).collect(Collectors.toList());
    }

    @Override
    public String getInput() {
        return "dataTable";
    }

    /**
     * Returns the meta-data of a meta-data group, when used recursively.
     *
     * @return the meta-data of the meta-data group
     * @throws InvalidMetadataValueException
     *             if some value is invalid
     */
    @Override
    Collection<Metadata> getMetadata() throws InvalidMetadataValueException {
        assert structure == null;
        MetadataGroup result = new MetadataGroup();
        result.setKey(metadataView.getId());
        result.setDomain(DOMAIN_TO_MDSEC.get(metadataView.getDomain().orElse(Domain.DESCRIPTION)));
        try {
            this.preserve();
        } catch (NoSuchMetadataFieldException e) {
            /*
             * The domain attribute is not evaluated on members of meta-data
             * groups, so writing to fields can never happen here and thus
             * cannot cause the exception.
             */
            throw new IllegalStateException("never happening exception");
        }
        result.setGroup(metadata instanceof List ? (List<Metadata>) metadata : new ArrayList<>(metadata));
        return Collections.singletonList(result);
    }

    /**
     * Returns the rows that JSF has to display.
     *
     * @return the rows that JSF has to display
     */
    public List<MetadataTableRow> getRows() {
        return rows;
    }

    @Override
    Pair<Method, Object> getStructureFieldValue()
            throws InvalidMetadataValueException, NoSuchMetadataFieldException {
        return null;
    }

    @Override
    public boolean isUndefined() {
        return metadataView.isUndefined();
    }

    /**
     * Reads the contents of the meta-data panel and stores the values in the
     * appropriate place. If the line is used to edit a field of the METS
     * structure, this field is set, otherwise the meta-data will be stored in
     * the list. The hidden meta-data is also written back there again.
     *
     * @throws InvalidMetadataValueException
     *             if the content of a meta-data input field is syntactically
     *             wrong
     * @throws NoSuchMetadataFieldException
     *             if an input shall be saved to a field of the structure, but
     *             there is no setter corresponding to the name configured in
     *             the rule set
     */
    void preserve() throws InvalidMetadataValueException, NoSuchMetadataFieldException {
        try {
            metadata.clear();
            for (MetadataTableRow row : rows) {
                Pair<Method, Object> metsFieldValue = row.getStructureFieldValue();
                if (Objects.nonNull(metsFieldValue)) {
                    try {
                        metsFieldValue.getKey().invoke(structure, metsFieldValue.getValue());
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        throw new IllegalStateException(e);
                    }
                } else {
                    metadata.addAll(row.getMetadata());
                }
            }
            metadata.addAll(hiddenMetadata);
        } catch (InvalidMetadataValueException invalidValueException) {
            if (Objects.isNull(structure)) {
                invalidValueException.addParent(metadataView.getId());
            }
            throw invalidValueException;
        }
    }

    void remove(MetadataTableRow rowToDelete) {
        rows.remove(rowToDelete);
    }

    /**
     * This method is triggered when the user clicks the insert meta-data
     * button.
     */
    void pasteClick() {
        try {
            Collection<Metadata> clipboard = panel.getClipboard();
            preserve();
            metadata.addAll(clipboard);
            clipboard.clear();
            createMetadataTable();
        } catch (NoSuchMetadataFieldException | InvalidMetadataValueException e) {
            Helper.setErrorMessage(e.getLocalizedMessage());
        }
    }
}