com.openkappa.rst.Classifier.java Source code

Java tutorial

Introduction

Here is the source code for com.openkappa.rst.Classifier.java

Source

/*
 * Copyright 2017 OpenKappa Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.openkappa.rst;

import org.apache.commons.collections4.trie.PatriciaTrie;
import org.roaringbitmap.ArrayContainer;
import org.roaringbitmap.Container;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.SortedMap;

public class Classifier<T> {

    public static <T> Builder<T> builder() {
        return new Builder<>();
    }

    public static class Builder<T> {
        private List<List<String>> rules = new ArrayList<>();
        private List<T> classifications = new ArrayList<>();
        private T unmatchedValue = null;
        int attributeCount = -1;

        /**
         * Maps combinations of values to an outcome
         * @param conditions the conditions
         * @param classification the outcome
         * @return this
         */
        public Builder<T> withRule(List<String> conditions, T classification) {
            validateRule(conditions);
            rules.add(Collections.unmodifiableList(conditions));
            classifications.add(classification);
            return this;
        }

        /**
         * Sets the value to return if no matching rule is found.
         * @param unmatchedValue the value when no rule matches
         * @return this
         */
        public Builder<T> withUnmatchedValue(T unmatchedValue) {
            this.unmatchedValue = unmatchedValue;
            return this;
        }

        /**
         * Builds a classifier if the builder has rules, attributes and
         * @return an immutable classifier
         */
        public Classifier<T> build() {
            if (rules.isEmpty()) {
                throw new RuntimeException("Cannot build classifier without rules");
            }
            return new Classifier<>(buildPredicates(), buildClassifications(), unmatchedValue);
        }

        private void validateRule(List<String> rule) {
            if (attributeCount == -1) {
                attributeCount = rule.size();
            } else if (attributeCount != rule.size()) {
                throw new RuntimeException("All rules must have the same number of attributes" + " ("
                        + attributeCount + " != " + rule.size());
            }
        }

        private T[] buildClassifications() {
            return (T[]) classifications.toArray();
        }

        private PatriciaTrie<Container>[] buildPredicates() {
            PatriciaTrie<Container>[] criteria = new PatriciaTrie[attributeCount];
            for (int i = 0; i < attributeCount; ++i) {
                criteria[i] = new PatriciaTrie<>();
            }
            short rid = 0;
            for (List<String> rule : rules) {
                final short ruleIndex = rid++;
                for (int i = 0; i < rule.size(); ++i) {
                    PatriciaTrie<Container> map = criteria[i];
                    String value = rule.get(i);
                    map.put(value, map.getOrDefault(value, EMPTY.clone()).add(ruleIndex));
                }
            }
            for (PatriciaTrie<Container> index : criteria) {
                Container wildcard = index.get("*");
                if (null != wildcard) {
                    index.keySet().stream().filter(k -> !"*".equals(k)).forEach(k -> index.get(k).ior(wildcard));
                }
            }
            return criteria;
        }
    }

    public static final Container EMPTY = new ArrayContainer();

    private final PatriciaTrie<Container>[] criteria;
    private final T[] outcomes;
    private final T unmatchedValue;
    private final ThreadLocal<Container> existence;
    private final ThreadLocal<StringBuilder> searcher;

    private Classifier(PatriciaTrie<Container>[] criteria, T[] outcomes, T unmatchedValue) {
        this.criteria = criteria;
        this.outcomes = outcomes;
        this.existence = ThreadLocal.withInitial(() -> new ArrayContainer(0, outcomes.length));
        this.unmatchedValue = unmatchedValue;
        this.searcher = ThreadLocal.withInitial(StringBuilder::new);
    }

    /**
     * Classifies the attribute values
     * @param query - ordered attribte values to be classified
     * @return the classification of the values, or null if no classification was found
     */
    public T classify(String... query) {
        try {
            final Container matching = existence.get();
            int numberMatches;
            for (int i = 0; (numberMatches = matching.getCardinality()) != 0 && i < query.length; ++i) {
                matching.iand(matchTerm(i, query[i]));
            }
            return numberMatches == 0 || numberMatches == outcomes.length ? unmatchedValue
                    : outcomes[matching.first()];
        } finally {
            reset();
        }
    }

    private Container matchTerm(int attributeIndex, String term) {
        if (term == null || "*".equals(term)) {
            return fallback(attributeIndex);
        } else if (term.endsWith("*")) {
            return findWithPrefix(attributeIndex, term.substring(0, term.length() - 1));
        } else {
            return criteria[attributeIndex].getOrDefault(term, findWildcardRule(attributeIndex, term));
        }
    }

    private Container findWithPrefix(int attributeIndex, String prefix) {
        SortedMap<String, Container> prefixMap = criteria[attributeIndex].prefixMap(prefix);
        if (null == prefixMap || prefixMap.isEmpty()) {
            return fallback(attributeIndex);
        }
        return prefixMap.get(prefixMap.firstKey());
    }

    private Container fallback(int attributeIndex) {
        return criteria[attributeIndex].getOrDefault("*", EMPTY);
    }

    private Container findWildcardRule(int attributeIndex, String term) {
        StringBuilder sb = searcher.get().append(term, 0, term.length());
        Container container;
        for (int c = term.length() - 1; c > -1; --c) {
            sb.setCharAt(c, '*');
            container = criteria[attributeIndex].get(sb.toString());
            if (null != container) {
                return container;
            }
            sb.setLength(c);
        }
        return EMPTY;
    }

    private void reset() {
        existence.get().iadd(0, outcomes.length);
        searcher.get().setLength(0);
    }
}