Coverage Summary for Class: Rule (org.kitodo.dataeditor.ruleset)
Class |
Class, %
|
Method, %
|
Line, %
|
Rule |
100%
(1/1)
|
85%
(17/20)
|
70,8%
(109/154)
|
/*
* (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.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kitodo.api.MetadataEntry;
import org.kitodo.dataeditor.ruleset.xml.Condition;
import org.kitodo.dataeditor.ruleset.xml.ConditionsMapInterface;
import org.kitodo.dataeditor.ruleset.xml.RestrictivePermit;
import org.kitodo.dataeditor.ruleset.xml.Ruleset;
import org.kitodo.dataeditor.ruleset.xml.Unspecified;
/**
* A rule that may be in place or not. If there is no corresponding rule element
* in the ruleset, the rule behaves as if there is a rule that declares no
* restrictions.
*/
public class Rule {
private static final Logger logger = LogManager.getLogger(Rule.class);
/**
* Maybe a rule, but maybe not.
*/
private Optional<RestrictivePermit> optionalRestrictivePermit;
/**
* The ruleset.
*/
private Ruleset ruleset;
/**
* Constructor for a new rule. May come with a restrictive permit or
* without.
*
* @param ruleset
* the ruleset
* @param optionalRestrictivePermit
* there may be a restrictive permit, or not
*/
public Rule(Ruleset ruleset, Optional<RestrictivePermit> optionalRestrictivePermit) {
this.ruleset = ruleset;
this.optionalRestrictivePermit = optionalRestrictivePermit;
}
/**
* Returns only the allowed sub-divisions by rule, possibly only resorted.
*
* @param divisions
* list input
* @return exit
*/
Map<String, String> getAllowedSubdivisions(Map<String, String> divisions) {
if (!optionalRestrictivePermit.isPresent()) {
return divisions;
}
Map<String, String> filteredPossibilities = new LinkedHashMap<>();
RestrictivePermit restrictivePermit = optionalRestrictivePermit.get();
for (RestrictivePermit permit : restrictivePermit.getPermits()) {
Optional<String> getterResult = permit.getDivision();
if (getterResult.isPresent()) {
String entry = getterResult.get();
if (divisions.containsKey(entry)) {
filteredPossibilities.put(entry, divisions.get(entry));
}
}
}
if (restrictivePermit.getUnspecified().equals(Unspecified.UNRESTRICTED)) {
for (Entry<String, String> entryPair : divisions.entrySet()) {
if (!filteredPossibilities.containsKey(entryPair.getKey())) {
filteredPossibilities.put(entryPair.getKey(), entryPair.getValue());
}
}
}
return filteredPossibilities;
}
/**
* Returns the key declarations explicitly allowed in the rule. This is done
* by looking into rule and making explicit key declarations for it.
*
* @return the key declarations explicitly allowed
*/
LinkedList<KeyDeclaration> getExplicitlyPermittedKeys(KeyDeclaration keyDeclaration) {
LinkedList<KeyDeclaration> explicitlyPermittedKeys = new LinkedList<>();
if (optionalRestrictivePermit.isPresent()) {
for (RestrictivePermit permit : optionalRestrictivePermit.get().getPermits()) {
Optional<String> optionalKey = permit.getKey();
if (optionalKey.isPresent()) {
explicitlyPermittedKeys.add(keyDeclaration.getSubkeyDeclaration(optionalKey.get()));
}
}
}
return explicitlyPermittedKeys;
}
/**
* Returns the maximum count, or maximum if undefined. This is not possible
* anyway, otherwise you have to switch to a long.
*
* @return maximum count
*/
int getMaxOccurs() {
if (optionalRestrictivePermit.isPresent() && optionalRestrictivePermit.get().getMaxOccurs() != null) {
return optionalRestrictivePermit.get().getMaxOccurs();
} else {
return Integer.MAX_VALUE;
}
}
/**
* Returns the minimum count, or zero if undefined.
*
* @return minimum count
*/
int getMinOccurs() {
if (optionalRestrictivePermit.isPresent() && optionalRestrictivePermit.get().getMinOccurs() != null) {
return optionalRestrictivePermit.get().getMinOccurs();
} else {
return 0;
}
}
/**
* Returns a permit rule for a key.
*
* @param keyId
* key for which a permit rule is to be returned
* @return permit rule for the key
*/
Rule getRuleForKey(String keyId, boolean division) {
Rule permitRuleForKey = optionalRestrictivePermit.isPresent()
? new Rule(ruleset,
optionalRestrictivePermit.get().getPermits().parallelStream()
.filter(rule -> keyId.equals(rule.getKey().orElse(null))).findAny())
: new Rule(ruleset, Optional.empty());
if (division) {
permitRuleForKey.merge(ruleset.getRuleForKey(keyId));
}
return permitRuleForKey;
}
/**
* Returns the selection items allowed by the rule.
*
* @param selectItems
* the selection items
* @param metadata
* metadata, for conditional select items
* @return the selection items
*/
Map<String, String> getSelectItems(Map<String, String> selectItems, List<Map<MetadataEntry, Boolean>> metadata) {
if (!optionalRestrictivePermit.isPresent()) {
return selectItems;
}
Map<String, String> filteredPossibilities = new LinkedHashMap<>();
RestrictivePermit restrictivePermit = optionalRestrictivePermit.get();
for (RestrictivePermit permit : getConditionalPermits(restrictivePermit, metadata)) {
Optional<String> getterResult = permit.getValue();
if (getterResult.isPresent()) {
String entry = getterResult.get();
if (selectItems.containsKey(entry)) {
filteredPossibilities.put(entry, selectItems.get(entry));
}
}
}
if (restrictivePermit.getUnspecified().equals(Unspecified.UNRESTRICTED)) {
for (Entry<String, String> entryPair : selectItems.entrySet()) {
if (!filteredPossibilities.containsKey(entryPair.getKey())) {
filteredPossibilities.put(entryPair.getKey(), entryPair.getValue());
}
}
}
return filteredPossibilities;
}
private static Collection<RestrictivePermit> getConditionalPermits(RestrictivePermit restrictivePermit,
List<Map<MetadataEntry, Boolean>> metadata) {
Collection<RestrictivePermit> result = new ArrayList<>(restrictivePermit.getPermits());
Map<String, Optional<MetadataEntry>> metadataCache = new HashMap<>();
getConditionalPermits(restrictivePermit, metadataCache, metadata, result); // start recursion
return result;
}
private static void getConditionalPermits(ConditionsMapInterface conditionsMapInterface,
Map<String, Optional<MetadataEntry>> metadataCache, List<Map<MetadataEntry, Boolean>> metadata,
Collection<RestrictivePermit> result) {
for (String conditionKey : conditionsMapInterface.getConditionKeys()) {
Optional<MetadataEntry> possibleMetadata = metadataCache.computeIfAbsent(conditionKey,
key -> getMetadataEntryForKey(key, metadata));
if (possibleMetadata.isPresent()) {
Condition condition = conditionsMapInterface.getCondition(conditionKey, possibleMetadata.get().getValue());
if (Objects.nonNull(condition)) {
result.addAll(condition.getPermits());
getConditionalPermits(condition, metadataCache, metadata, result); // recursive
}
}
}
}
private static Optional<MetadataEntry> getMetadataEntryForKey(final String key,
final List<Map<MetadataEntry, Boolean>> metadata) {
String effectiveKey = key;
int metadataIndex = metadata.size() - 1;
while (effectiveKey.startsWith("../")) {
effectiveKey = effectiveKey.substring(3);
metadataIndex--;
}
if (metadataIndex < 0) {
logger.warn("<condition key=\"{}\"> can never be met because metadata has only {} layers", key,
metadata.size());
return Optional.empty();
}
Map<MetadataEntry, Boolean> effectiveMetadata = metadata.get(metadataIndex);
MetadataEntry found = null;
for (Entry<MetadataEntry, Boolean> mapEntry : effectiveMetadata.entrySet()) {
MetadataEntry metadataEntry = mapEntry.getKey();
if (metadataEntry.getKey().equals(effectiveKey)) {
found = metadataEntry;
mapEntry.setValue(Boolean.TRUE);
break;
}
}
return Optional.ofNullable(found);
}
/**
* Returns if after rule this is repeatable. (That’s if it’s not a rule, or
* says more than 1.)
*
* @return whether this is repeatable
*/
boolean isRepeatable() {
return !optionalRestrictivePermit.isPresent() || optionalRestrictivePermit.get().getMaxOccurs() == null
|| optionalRestrictivePermit.get().getMaxOccurs() > 1;
}
/**
* Returns whether unspecified is unrestricted.
*
* @return whether unspecified is unrestricted
*/
boolean isUnspecifiedUnrestricted() {
return !optionalRestrictivePermit.isPresent()
|| optionalRestrictivePermit.get().getUnspecified().equals(Unspecified.UNRESTRICTED);
}
/**
* Combines two rules. The function happens in separate, this is just
* wrapping.
*
* @param other
* the other rule
*/
void merge(Rule other) {
if (optionalRestrictivePermit.isPresent()) {
if (other.optionalRestrictivePermit.isPresent()) {
optionalRestrictivePermit = Optional
.of(merge(optionalRestrictivePermit.get(), other.optionalRestrictivePermit.get()));
}
} else {
optionalRestrictivePermit = other.optionalRestrictivePermit;
}
}
/**
* Combines two rules into each other. The first rule, if in doubt, is more
* specific to the order of elements, otherwise it’s the same as around.
* This is so if rule is nesting, and additional rule is found for key, then
* merged and nesting rule is first and thus more specific to the case but
* other rule otherwise considered as well. This is important but difficult
* to implement and so it is done now.
*
* @param one
* a rule
* @param another
* the other rule
* @return merged rule
*/
private static RestrictivePermit merge(RestrictivePermit one, RestrictivePermit another) {
RestrictivePermit merged = new RestrictivePermit();
// we assume that both rules to merge are rules on the same entity
merged.setDivision(one.getDivision());
merged.setKey(one.getKey());
merged.setValue(one.getValue());
mergeQuantities(one, another, merged);
// if one of both is forbidden, then it is forbidden
merged.setUnspecified(
one.getUnspecified().equals(Unspecified.FORBIDDEN) || another.getUnspecified().equals(Unspecified.FORBIDDEN)
? Unspecified.FORBIDDEN
: Unspecified.UNRESTRICTED);
mergeConditions(one.getConditions(), another.getConditions(), merged.getConditions());
mergePermits(one.getPermits(), another.getPermits(), merged.getPermits());
return merged;
}
/**
* This is taken out because otherwise checkstyle is unfortunate because
* function is length 59 and should only be 50 lines. This is part of the
* {@link #merge(RestrictivePermit, RestrictivePermit)} function and connects the quantities. Merge is
* with strictness here, that is, the stricter value of both becomes valid.
*
* @param one
* one rule
* @param another
* another rule
* @param merged
* merged rule
*/
private static void mergeQuantities(RestrictivePermit one, RestrictivePermit another, RestrictivePermit merged) {
if (one.getMinOccurs() == null) {
merged.setMinOccurs(another.getMinOccurs());
} else {
if (another.getMinOccurs() == null) {
merged.setMinOccurs(one.getMinOccurs());
} else {
merged.setMinOccurs(Math.max(one.getMinOccurs(), another.getMinOccurs()));
}
}
if (one.getMaxOccurs() == null) {
merged.setMaxOccurs(another.getMaxOccurs());
} else {
if (another.getMaxOccurs() == null) {
merged.setMaxOccurs(one.getMaxOccurs());
} else {
merged.setMaxOccurs(Math.min(one.getMaxOccurs(), another.getMaxOccurs()));
}
}
}
private static void mergeConditions(List<Condition> one, List<Condition> another, List<Condition> merged) {
// if both have no conditions, there are no conditions
boolean oneHasNoConditions = one.isEmpty();
boolean anotherHasNoConditions = another.isEmpty();
if (oneHasNoConditions && anotherHasNoConditions) {
return;
}
// if just one side has conditions, apply them
if (!oneHasNoConditions && anotherHasNoConditions) {
merged.addAll(one);
return;
}
if (oneHasNoConditions && !anotherHasNoConditions) {
merged.addAll(another);
return;
}
// if both have conditions, they must both apply
Map<Pair<String, String>, Condition> anotherMap = another.stream().collect(
Collectors.toMap(condition -> Pair.of(condition.getKey(), condition.getEquals()), Function.identity()));
for (Condition oneCondition : one) {
Condition anotherCondition = anotherMap.get(Pair.of(oneCondition.getKey(), oneCondition.getEquals()));
if (Objects.nonNull(anotherCondition)) {
merged.add(mergeCondition(oneCondition, anotherCondition));
}
}
}
private static Condition mergeCondition(Condition one, Condition another) {
Condition result = new Condition();
// we assume that both conditions are on the same entity
result.setKey(one.getKey());
result.setEquals(one.getEquals());
mergeConditions(one.getConditions(), another.getConditions(), result.getConditions());
mergePermits(one.getPermits(), another.getPermits(), result.getPermits());
return result;
}
private static void mergePermits(List<RestrictivePermit> one, List<RestrictivePermit> another, List<RestrictivePermit> mergedPermits) {
HashMap<RestrictivePermit, RestrictivePermit> anotherPermits = new LinkedHashMap<>();
for (RestrictivePermit anotherPermit : another) {
anotherPermits.put(anotherPermit, anotherPermit);
}
for (RestrictivePermit onePermit : one) {
if (anotherPermits.containsKey(onePermit)) {
mergedPermits.add(merge(onePermit, anotherPermits.get(onePermit)));
anotherPermits.remove(onePermit);
} else {
mergedPermits.add(onePermit);
}
}
mergedPermits.addAll(anotherPermits.values());
}
}