Coverage Summary for Class: NestedKeyView (org.kitodo.dataeditor.ruleset)
Class |
Class, %
|
Method, %
|
Line, %
|
NestedKeyView |
100%
(1/1)
|
100%
(22/22)
|
94,5%
(86/91)
|
/*
* (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.dataeditor.ruleset;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale.LanguageRange;
import java.util.Optional;
import java.util.TreeMap;
import org.kitodo.api.Metadata;
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.dataeditor.ruleset.xml.Key;
import org.kitodo.dataeditor.ruleset.xml.Ruleset;
/**
* A nested key view opens a view of a nested key. This is a key that breaks
* down into subkey.
*
* @param <D>
* Type of declaration. Normally a key declaration, but since the
* division view is also a nested key view, it will then be a
* division declaration.
*/
class NestedKeyView<D extends KeyDeclaration> extends AbstractKeyView<D> implements ComplexMetadataViewInterface {
/**
* Marks keys for which the user has requested an additional blank field.
* This process can be parallelized because the auxiliary table is not
* changed. Apart from the parallelization, the implementation has the same
* meaning with the following code:
*
* <pre>
* for (String additionallySelectedKey : additionallySelectedKeys) {
* auxiliaryTable.get(additionallySelectedKey).addOneAdditionalField();
* }
* </pre>
*
* @param <V>
* the type of metadata values
* @param additionallySelectedKeys
* key for which the user has requested an additional blank field
* @param auxiliaryTable
* auxiliary table
* @throws NullPointerException
* When the API caller tries to add a field that does not exist
* in the ruleset.
*/
private static <V> void addFieldsForAdditionallySelectedKeys(Collection<String> additionallySelectedKeys,
LinkedHashMap<String,
AuxiliaryTableRow> auxiliaryTable) {
additionallySelectedKeys.parallelStream().map(auxiliaryTable::get)
.forEach(AuxiliaryTableRow::addOneAdditionalField);
}
/**
* Appends the rows to the auxiliary table. Since the order of the rows is
* relevant, you cannot parallelize here.
*
* @param rows
* rows to append
* @param auxiliaryTable
* auxiliary table to append to
*/
private static void appendRowsToAuxiliaryTable(Collection<AuxiliaryTableRow> rows,
LinkedHashMap<String, AuxiliaryTableRow> auxiliaryTable) {
for (AuxiliaryTableRow auxiliaryTableRow : rows) {
auxiliaryTable.put(auxiliaryTableRow.getId(), auxiliaryTableRow);
}
}
/**
* Sorts the table by copying it into a TreeMap with the translated label as
* the key. The label will also be appended with the id if there are ever
* two metadata keys with the same label in the rule set. Since TreeMap is
* not thread safe, it cannot be parallelized here.
*
* @param <V>
* the type of metadata values
* @param auxiliaryTableToBeSorted
* auxiliary table to be sorted
* @param priorityList
* the wish list of the user’s preferred human language
* @return sorted list of fields
*/
private static <V> Collection<AuxiliaryTableRow> sort(
HashMap<String, AuxiliaryTableRow> auxiliaryTableToBeSorted, List<LanguageRange> priorityList) {
TreeMap<String, AuxiliaryTableRow> sorted = new TreeMap<>();
for (AuxiliaryTableRow auxiliaryTableRow : auxiliaryTableToBeSorted.values()) {
sorted.put(auxiliaryTableRow.getLabel(priorityList) + '\037' + auxiliaryTableRow.getId(),
auxiliaryTableRow);
}
return sorted.values();
}
/**
* Whether the nested key view is a division.
*/
protected boolean division;
/**
* The ruleset.
*/
protected Ruleset ruleset;
/**
* The settings.
*/
protected Settings settings;
/**
* Manufacturer for a nested subkey. As you can see from the constructor
* being private, this shows that it is called only from this class to
* create a nested subkey view for a nested key view. In that case, this is
* always a grouped metadata.
*
* @param ruleset
* the ruleset
* @param declaration
* the declaration
* @param rule
* the rule
* @param settings
* the settings
* @param priorityList
* the user’s wish list for the best possible translation
*/
public NestedKeyView(Ruleset ruleset, D declaration, Rule rule, Settings settings,
List<LanguageRange> priorityList) {
super(declaration, rule, priorityList);
this.ruleset = ruleset;
this.settings = settings;
this.division = false;
}
/**
* Creates a new nested key view. Showing that the constructor is protected
* shows that it is being used by a subclass, that’s division view. A
* division view is a nested key view with additional division function.
*
* @param ruleset
* the ruleset
* @param divisionDeclaration
* the division declaration
* @param rule
* the rule
* @param settings
* the settings
* @param priorityList
* the user’s wish list for the best possible translation
* @param division
* Whether it is a division. Always true. This is a marker
* parameter because two different constructors in Java can only
* differ in their parameters and otherwise they would be the
* same.
*/
protected NestedKeyView(Ruleset ruleset, D divisionDeclaration, Rule rule, Settings settings,
List<LanguageRange> priorityList, boolean division) {
super(divisionDeclaration, rule, priorityList);
this.ruleset = ruleset;
this.settings = settings;
this.division = division;
}
/**
* Adds any permission rules the children of the restriction rule used here
* are added to the table, as they affect the representation of the
* children.
*
* @param auxiliaryTable
* auxiliary table
*/
private void addAnyRules(LinkedHashMap<String, AuxiliaryTableRow> auxiliaryTable) {
auxiliaryTable.entrySet().parallelStream().forEach(entry -> entry.getValue()
.setRule(rule.getRuleForKey(entry.getKey(), division)));
}
/**
* Adds the keys selected by the user to be added to the list of keys to
* add. This procedure is a part of the function to
* {@link #createAuxiliaryTableWithKeysToBeSorted(LinkedHashMap, Rule, Collection, Collection)}.
*
* @param auxiliaryTable
* the target table. If the key is already here, it will not be
* added a second time.
* @param rule
* the rule provides the key
* @param additionalKeys
* list of the key marked as being added by the user
* @param toBeSorted
* write access to the list of keys to be sorted
*/
private void addAdditionalKeys(LinkedHashMap<String, AuxiliaryTableRow> auxiliaryTable,
Rule rule, Collection<String> additionalKeys,
HashMap<String, AuxiliaryTableRow> toBeSorted) {
for (String additionalKey : additionalKeys) {
if (!auxiliaryTable.containsKey(additionalKey) && !toBeSorted.containsKey(additionalKey)) {
Optional<Key> optionalKey = rule.isUnspecifiedUnrestricted() ? Optional.empty()
: ruleset.getKey(additionalKey);
KeyDeclaration keyDeclaration = optionalKey.map(key -> new KeyDeclaration(ruleset, key))
.orElseGet(() -> new KeyDeclaration(ruleset, additionalKey));
toBeSorted.put(additionalKey, new AuxiliaryTableRow(keyDeclaration, settings));
}
}
}
/**
* If the rule is unspecified unrestricted, the remaining keys are added to
* the keys to be sorted. This procedure is a part of the function to
* {@link #createAuxiliaryTableWithKeysToBeSorted(LinkedHashMap, Rule, Collection, Collection)}.
*
* @param auxiliaryTable
* the target table. If the key is already here, it will not be
* added a second time.
* @param keyDeclarations
* the keys to be checked
* @param toBeSorted
* write access to the list of keys to be sorted
*/
private void addRemainingKeys(LinkedHashMap<String, AuxiliaryTableRow> auxiliaryTable,
Collection<KeyDeclaration> keyDeclarations,
HashMap<String, AuxiliaryTableRow> toBeSorted) {
for (KeyDeclaration keyDeclaration : keyDeclarations) {
if (!auxiliaryTable.containsKey(keyDeclaration.getId())) {
toBeSorted.put(keyDeclaration.getId(), new AuxiliaryTableRow(keyDeclaration, settings));
}
}
}
/**
* Generates an auxiliary table for additional keys. These will later be
* sorted alphabetically and the key will be added below the given order.
* This table contains values if there is no restriction rule or if the
* restriction rule specifies unspecified as unrestricted. Otherwise the
* table is empty for now. However, it can still receive elements in the
* further course, namely when metadata entries name a key that is not
* specified in the rule set. Then in this table namely for undefined key
* views are created.
*
* @param auxiliaryTable
* table with already pre-sorted keys. Keys that are in this
* table are not included in the table of keys yet to be sorted.
* @param rule
* optionally a rule
* @param keyDeclarations
* all key declarations
* @param additionalKeys
* which keys the user has additionally selected
* @return an auxiliary table for additional keys yet to be sorted
*/
private HashMap<String, AuxiliaryTableRow> createAuxiliaryTableWithKeysToBeSorted(
LinkedHashMap<String, AuxiliaryTableRow> auxiliaryTable, Rule rule,
Collection<KeyDeclaration> keyDeclarations, Collection<String> additionalKeys) {
HashMap<String, AuxiliaryTableRow> toBeSorted = new HashMap<>();
if (rule.isUnspecifiedUnrestricted()) {
addRemainingKeys(auxiliaryTable, keyDeclarations, toBeSorted);
}
addAdditionalKeys(auxiliaryTable, rule, additionalKeys, toBeSorted);
return toBeSorted;
}
/**
* This is actually a major part of the functionality of this class that
* generates the helper table. You can see that here the individual
* functions are called from the class.
*
* @param currentEntries
* which metadata are present in the view
* @param additionalKeys
* which keys the user has additionally selected
* @return A helper table for all metadata keys
*/
private Collection<AuxiliaryTableRow> createAuxiliaryTable(Collection<Metadata> currentEntries,
Collection<String> additionalKeys) {
LinkedHashMap<String, AuxiliaryTableRow> auxiliaryTable = createAuxiliaryTableWithPreSortedKeys(
rule.getExplicitlyPermittedKeys(declaration));
HashMap<String, AuxiliaryTableRow> auxiliaryTableToBeSorted = createAuxiliaryTableWithKeysToBeSorted(
auxiliaryTable, rule, declaration.getKeyDeclarations(), additionalKeys);
storeValues(currentEntries, auxiliaryTable, auxiliaryTableToBeSorted);
appendRowsToAuxiliaryTable(sort(auxiliaryTableToBeSorted, priorityList), auxiliaryTable);
addAnyRules(auxiliaryTable);
addFieldsForAdditionallySelectedKeys(additionalKeys, auxiliaryTable);
return auxiliaryTable.values();
}
/**
* Generates the auxiliary table with the keys already specified by the
* restriction rule in their order. Since the order of the keys is relevant,
* you cannot parallelize them here.
*
* @param explicitlyPermittedKeys
* keys given in their order by the restriction rule
* @return the auxiliary table
*/
private LinkedHashMap<String, AuxiliaryTableRow> createAuxiliaryTableWithPreSortedKeys(
List<KeyDeclaration> explicitlyPermittedKeys) {
LinkedHashMap<String, AuxiliaryTableRow> auxiliaryTable = new LinkedHashMap<>();
for (KeyDeclaration keyDeclaration : explicitlyPermittedKeys) {
auxiliaryTable.put(keyDeclaration.getId(), new AuxiliaryTableRow(keyDeclaration, settings));
}
return auxiliaryTable;
}
/**
* Here the addable metadata keys are fetched. To do this, the table must
* first be made and then an output made.
*
* @param currentEntries
* metadata objects that have already been entered, along with
* their key
* @param additionalKeys
* metadata keys that the user has already selected
*/
@Override
public Collection<MetadataViewInterface> getAddableMetadata(Collection<Metadata> currentEntries,
Collection<String> additionalKeys) {
return getPossibleMetadata(currentEntries, additionalKeys, false);
}
private Collection<MetadataViewInterface> getPossibleMetadata(Collection<Metadata> currentEntries,
Collection<String> additionalKeys, boolean all) {
Collection<MetadataViewInterface> addableMetadata = new LinkedList<>();
for (AuxiliaryTableRow auxiliaryTableRow : createAuxiliaryTable(currentEntries, additionalKeys)) {
if (all || auxiliaryTableRow.isPossibleToExpandAnotherField()) {
addableMetadata.add(rowToView(auxiliaryTableRow));
}
}
return addableMetadata;
}
/**
* Returns a full list of all metadata entries that are allowed as children
* of this nested key view.
*
* @return all allowed children
*/
@Override
public Collection<MetadataViewInterface> getAllowedMetadata() {
return getPossibleMetadata(Collections.emptyList(), Collections.emptySet(), true);
}
/**
* Creates a metadata view for one line of the auxiliary table.
*
* @param row
* row to make a view for
* @return metadata view
*/
private MetadataViewInterface rowToView(AuxiliaryTableRow row) {
return row.isComplexKey() ? getNestedKeyView(row.getId())
: new KeyView(row.getKey(), rule.getRuleForKey(row.getId(), division), settings, priorityList);
}
/**
* Creates a key view for a grouped key. This is the case when when
* {@code <key>} elements occur within another {@code <key>} element in the
* ruleset.
*
* @param keyId
* identifier for key in the nest
* @return a view on the child of the group
*/
private NestedKeyView<KeyDeclaration> getNestedKeyView(String keyId) {
Rule ruleForKey = rule.getRuleForKey(keyId, division);
if (division) {
ruleForKey.merge(ruleset.getRuleForKey(keyId));
}
return new NestedKeyView<>(ruleset, declaration.getSubkeyDeclaration(keyId), ruleForKey,
settings.getSettingsForKey(keyId), priorityList);
}
/**
* Sorts and visibly outputs the metadata. That’s the main function of the
* mask. The structure of metadata mask is like this: If there is rule,
* then the order of elements is by rule. But if the rule allows more
* elements or if there are data elements that are not in the rule, then
* they come in alphabetical order below them. The metadata elements must
* be sorted and therefore sometimes more fields are needed. Exceptions here
* are multiple selections only once, and with multiple values.
*
* @param currentEntries
* metadata objects that have already been entered, along with
* heir key
* @param additionalKeys
* metadata keys that the user has already selected
* @return mask
*/
@Override
public List<MetadataViewWithValuesInterface> getSortedVisibleMetadata(Collection<Metadata> currentEntries,
Collection<String> additionalKeys) {
LinkedList<MetadataViewWithValuesInterface> sortedVisibleMetadata = new LinkedList<>();
Collection<Metadata> excludedDataObjects = new HashSet<>();
for (AuxiliaryTableRow auxiliaryTableRow : createAuxiliaryTable(currentEntries, additionalKeys)) {
if (auxiliaryTableRow.isContainingExcludedData()) {
excludedDataObjects.addAll(auxiliaryTableRow.getDataObjects(0));
} else {
for (int i = 0; i < auxiliaryTableRow.getNumberOfTypeViewsToGenerate(); i++) {
Optional<MetadataViewInterface> definedTypeView = Optional.of(rowToView(auxiliaryTableRow));
sortedVisibleMetadata.add(new FormRow(definedTypeView, auxiliaryTableRow.getDataObjects(i)));
}
}
}
if (!excludedDataObjects.isEmpty()) {
sortedVisibleMetadata.addFirst(new FormRow(Optional.empty(), excludedDataObjects));
}
return sortedVisibleMetadata;
}
/**
* Adds the already entered metadata to the helper tables. For metadata
* that does not belong to any field, an undefined field is created. Since
* LinkedHashMap is not thread safe, it cannot be parallelized here.
*
* @param enteredMetaData
* already entered metadata to be added to the auxiliary tables
* @param sortedAuxiliaryTable
* help table with rows with default sorting
* @param auxiliaryTableToBeSorted
* help table with rows that still have to be sorted
*/
private void storeValues(Collection<Metadata> enteredMetaData,
LinkedHashMap<String, AuxiliaryTableRow> sortedAuxiliaryTable,
HashMap<String, AuxiliaryTableRow> auxiliaryTableToBeSorted) {
for (Metadata metadata : enteredMetaData) {
String keyId = metadata.getKey();
if (sortedAuxiliaryTable.containsKey(keyId)) {
sortedAuxiliaryTable.get(keyId).add(metadata);
} else {
auxiliaryTableToBeSorted.computeIfAbsent(keyId,
missing -> retrieveOrCompute(keyId));
auxiliaryTableToBeSorted.get(keyId).add(metadata);
}
}
}
private AuxiliaryTableRow retrieveOrCompute(String keyId) {
Optional<KeyDeclaration> optionalKeyDeclaration = super.declaration.getKeyDeclarations().parallelStream()
.filter(childKeyDeclaration -> keyId.equals(childKeyDeclaration.getId())).findAny();
KeyDeclaration keyDeclaration = optionalKeyDeclaration.orElseGet(() -> new KeyDeclaration(ruleset, keyId));
return new AuxiliaryTableRow(keyDeclaration, settings);
}
@Override
public Optional<Domain> getDomain() {
return declaration.getDomain();
}
}