Java tutorial
/* * 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); } }