org.kitodo.dataeditor.ruleset.UniversalRule.java Source code

Java tutorial

Introduction

Here is the source code for org.kitodo.dataeditor.ruleset.UniversalRule.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.dataeditor.ruleset;

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.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.lang3.tuple.Triple;
import org.kitodo.dataeditor.ruleset.xml.Rule;
import org.kitodo.dataeditor.ruleset.xml.Ruleset;
import org.kitodo.dataeditor.ruleset.xml.Unspecified;

/**
 * A universal rule is a rule that may be in place or not. If there is no
 * corresponding rule element in the rule set, the rule behaves as if there is a
 * rule that declares no restrictions.
 */
public class UniversalRule {
    /**
     * Generates a triplet of rule with triple as a key. This is due to the
     * problem because the rule is basically the key is three fields and applies
     * everything.
     *
     * @param rule
     *            rule for which a hashish key is to be formed
     * @return key is tripple
     */
    private static Triple<String, String, String> formAKeyForARuleInATemporaryMap(Rule rule) {
        return Triple.of(rule.getDivision().orElse(null), rule.getKey().orElse(null), rule.getValue().orElse(null));
    }

    /**
     * Maybe a rule, but maybe not.
     */
    private Optional<Rule> optionalRule;

    /**
     * The ruleset.
     */
    private Ruleset ruleset;

    /**
     * Constructor for a new universal rule. Can with rule or without.
     *
     * @param ruleset
     *            the ruleset
     * @param optionalRule
     *            maybe a rule, but maybe not
     */
    public UniversalRule(Ruleset ruleset, Optional<Rule> optionalRule) {
        this.ruleset = ruleset;
        this.optionalRule = optionalRule;
    }

    /**
     * A filter to generate the possibilities based on a rule. This is because
     * the rule restricts the possibilities and gives order to elements, Or does
     * not restrict and gives order anyway for elements where mentioned and rest
     * is just like that. We have that twice for subdivisions and options so
     * this is summarized here and only getter is fetched from outside.
     *
     * @param possibilities
     *            list of possibilities unfiltered
     * @param getter
     *            which field to read
     * @return list is filtered
     */
    private Map<String, String> filterPossibilitiesBasedOnRule(Map<String, String> possibilities,
            Function<Rule, Optional<String>> getter) {
        if (optionalRule.isPresent()) {
            Map<String, String> result = new LinkedHashMap<>();
            Rule rule = optionalRule.get();
            for (Rule permit : rule.getPermits()) {
                Optional<String> getterResult = getter.apply(permit);
                if (getterResult.isPresent()) {
                    String entry = getterResult.get();
                    if (possibilities.containsKey(entry)) {
                        result.put(entry, possibilities.get(entry));
                    }
                }
            }
            if (rule.getUnspecified().equals(Unspecified.UNRESTRICTED)) {
                for (Entry<String, String> entryPair : possibilities.entrySet()) {
                    if (!result.containsKey(entryPair.getKey())) {
                        result.put(entryPair.getKey(), entryPair.getValue());
                    }
                }
            }
            return result;
        } else {
            return possibilities;
        }
    }

    /**
     * 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) {
        return filterPossibilitiesBasedOnRule(divisions, Rule::getDivision);
    }

    /**
     * Returns the universal keys explicitly allowed in the rule. This is done
     * by looking into rule and making explicit universal keys for it.
     *
     * @return the universal keys explicitly allowed
     */
    LinkedList<UniversalKey> getExplicitlyPermittedUniversalKeys(UniversalKey universalKey) {
        LinkedList<UniversalKey> result = new LinkedList<>();
        if (optionalRule.isPresent()) {
            for (Rule rule : optionalRule.get().getPermits()) {
                Optional<String> optionalKey = rule.getKey();
                if (optionalKey.isPresent()) {
                    result.add(universalKey.getUniversalKey(optionalKey.get()));
                }
            }
        }
        return result;
    }

    /**
     * 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 (optionalRule.isPresent() && optionalRule.get().getMaxOccurs() != null) {
            return optionalRule.get().getMaxOccurs();
        } else {
            return Integer.MAX_VALUE;
        }
    }

    /**
     * Returns the minimum count, or zero if undefined.
     *
     * @return minimum count
     */
    int getMinOccurs() {
        if (optionalRule.isPresent() && optionalRule.get().getMinOccurs() != null) {
            return optionalRule.get().getMinOccurs();
        } else {
            return 0;
        }
    }

    /**
     * Returns a permission universal rule for a key.
     *
     * @param keyId
     *            key for which a permission universal rule is to be returned
     * @return permission universal rule for the key
     */
    UniversalRule getUniversalPermitRuleForKey(String keyId, boolean division) {
        UniversalRule result = optionalRule.isPresent()
                ? new UniversalRule(ruleset,
                        optionalRule.get().getPermits().parallelStream()
                                .filter(rule -> keyId.equals(rule.getKey().orElse(null))).findAny())
                : new UniversalRule(ruleset, Optional.empty());
        if (division) {
            result.merge(ruleset.getUniversalRestrictionRuleForKey(keyId));
        }
        return result;
    }

    /**
     * Returns the selection items.
     *
     * @param selectItems
     *            the selection items
     * @return the selection items
     */
    Set<String> getSelectItems(Set<String> selectItems) {
        return getSelectItems(
                selectItems.stream().collect(Collectors.toMap(Function.identity(), Function.identity()))).keySet();
    }

    /**
     * Returns the selection items. This is with filter, and besides, if it is
     * not multiple choice but optional then the first field is empty with empty
     * to select nothing as option. The question is if this must be here but I
     * have now made it for convenience, otherwise goes elsewhere.
     *
     * @param selectItems
     *            the selection items
     * @return the selection items
     */
    Map<String, String> getSelectItems(Map<String, String> selectItems) {
        Map<String, String> filteredOptions = filterPossibilitiesBasedOnRule(selectItems, Rule::getValue);
        if (!isRepeatable() && (!optionalRule.isPresent() || optionalRule.get().getMinOccurs() == null
                || optionalRule.get().getMinOccurs() < 1)) {
            Map<String, String> mapWithANonselectedElement = new LinkedHashMap<>(
                    (int) Math.ceil((filteredOptions.size() + 1) / 0.75));
            mapWithANonselectedElement.put("", "");
            mapWithANonselectedElement.putAll(filteredOptions);
            return mapWithANonselectedElement;
        } else {
            return filteredOptions;
        }
    }

    /**
     * Returns if after rule this is repeatable. (Thats if its not a rule, or
     * says more than 1.)
     *
     * @return whether this is repeatable
     */
    boolean isRepeatable() {
        return !optionalRule.isPresent() || optionalRule.get().getMaxOccurs() == null
                || optionalRule.get().getMaxOccurs() > 1;
    }

    /**
     * Returns whether unspecified is unrestricted.
     *
     * @return whether unspecified is unrestricted
     */
    boolean isUnspecifiedUnrestricted() {
        return !optionalRule.isPresent() || optionalRule.get().getUnspecified().equals(Unspecified.UNRESTRICTED);
    }

    /**
     * Combines two rules into each other. The first rule, if in doubt, is more
     * specific to the order of elements, otherwise its 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 Rule merge(Rule one, Rule another) {
        Rule merged = new Rule();

        /*
         * We assume that both rules are the same only, otherwise this would be
         * a problem. Recursively, this is not a problem, because the program
         * pays attention.
         */
        merged.setDivision(one.getDivision());
        merged.setKey(one.getKey());
        merged.setValue(one.getValue());

        mergeQuantities(one, another, merged);

        // here too, if one is forbidden then forbidden
        merged.setUnspecified(one.getUnspecified().equals(Unspecified.FORBIDDEN)
                || another.getUnspecified().equals(Unspecified.FORBIDDEN) ? Unspecified.FORBIDDEN
                        : Unspecified.UNRESTRICTED);

        // and for sub-rule is recursive
        HashMap<Triple<String, String, String>, Rule> anotherPermits = new LinkedHashMap<>();
        for (Rule anotherPermit : another.getPermits()) {
            anotherPermits.put(formAKeyForARuleInATemporaryMap(anotherPermit), anotherPermit);
        }
        List<Rule> mergedPermits = new LinkedList<>();
        for (Rule onePermit : one.getPermits()) {
            Triple<String, String, String> key = formAKeyForARuleInATemporaryMap(onePermit);
            if (anotherPermits.containsKey(key)) {
                mergedPermits.add(merge(onePermit, anotherPermits.get(key)));
                anotherPermits.remove(key);
            } else {
                mergedPermits.add(onePermit);
            }
        }
        mergedPermits.addAll(anotherPermits.values());
        merged.setPermits(mergedPermits);

        return merged;
    }

    /**
     * Connects two universal rules. The function happens in separate, this is
     * just wrapping.
     *
     * @param other
     *            the other universal rule
     */
    void merge(UniversalRule other) {
        if (optionalRule.isPresent()) {
            if (other.optionalRule.isPresent()) {
                optionalRule = Optional.of(merge(optionalRule.get(), other.optionalRule.get()));
            }
        } else {
            optionalRule = other.optionalRule;
        }
    }

    /**
     * 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(Rule, Rule)} 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 void mergeQuantities(Rule one, Rule another, Rule 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()));
            }
        }
    }
}