package sk.nociar.jpacloner.graphs; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Generic explorer of paths in a graph. It is required that the graph is stable * i.e. does not change during the exploring. The explorer will generate * property paths upon the pattern passed in the factory method, see * {@link GraphExplorer#get(String)}. The pattern supports following operators: * <ul> * <li>Dot "." separates paths.</li> * <li>Plus "+" generates at least one preceding path.</li> * <li>Split "|" divides the path into two ways.</li> * <li>Terminator "$" ends the preceding path.</li> * <li>Parentheses "(", ")" groups the paths.</li> * <li>Wildcards "*", "?" in property names.</li> * </ul> * Some examples follow: * <ul> * <li>device.*</li> * <li>device.(interfaces.type|driver.author)</li> * <li>company.department+.(boss|employees).address</li> * <li>*+</li> * </ul> * * <b>NOTE</b>: Entities MUST correctly implement * {@link Object#equals(Object obj)} and {@link Object#hashCode()}. * * @author Miroslav Nociar */ public abstract class GraphExplorer { private static final Pattern p; private static final Set<String> operators = new HashSet<String>(); private static final Map<String, Integer> operatorToPriority = new HashMap<String, Integer>(); private static final int LITERAL_PRIORITY = 10; static { // init operators operators.add("("); operators.add(")"); operators.add("."); operators.add("|"); operators.add("+"); operators.add("$"); // init operator priorities operatorToPriority.put("|", 1); operatorToPriority.put(".", 2); operatorToPriority.put("+", 3); operatorToPriority.put("$", 4); // init pattern StringBuilder sb = new StringBuilder(); sb.append("["); for (String op : operators) { sb.append("\\").append(op); } sb.append("]|[^\\s"); for (String op : operators) { sb.append("\\").append(op); } sb.append("]+"); p = Pattern.compile(sb.toString()); } private static final Map<String, GraphExplorer> cache = new ConcurrentHashMap<String, GraphExplorer>(); /** * Factory method for complex {@link GraphExplorer}. Returned instance is * thread safe i.e. can be used by multiple threads in parallel. */ public static GraphExplorer get(String pattern) { // check the cache first GraphExplorer explorer = cache.get(pattern); if (explorer != null) { return explorer; } Matcher m = p.matcher(pattern); // evaluate priority of each token List<String> tokens = new ArrayList<String>(); List<Integer> priorities = new ArrayList<Integer>(); int offset = 0; while (m.find()) { String token = m.group(); if ("(".equals(token)) { offset += LITERAL_PRIORITY; continue; } if (")".equals(token)) { offset -= LITERAL_PRIORITY; continue; } // add to tokens tokens.add(token); // compute the priority Integer priority = operatorToPriority.get(token); if (priority == null) { // literal priorities.add(offset + LITERAL_PRIORITY); } else { // operator priorities.add(offset + priority); } } if (offset != 0) { throw new IllegalArgumentException("Wrong parentheses!"); } explorer = getExplorer(tokens, priorities); // save in the cache cache.put(pattern, explorer); return explorer; } private static GraphExplorer getExplorer(List<String> tokens, List<Integer> priorities) { if (tokens.size() != priorities.size()) { throw new IllegalStateException("Tokens & priorities does not match!"); } if (tokens.isEmpty()) { throw new IllegalStateException("No tokens!"); } // find the leftmost operator with the lowest priority int idx = -1; int minPriority = Integer.MAX_VALUE; for (int i = 0; i < priorities.size(); i++) { int priority = priorities.get(i); if (priority < minPriority) { minPriority = priority; idx = i; } } String token = tokens.get(idx); if (!operators.contains(token)) { if (tokens.size() != 1) { throw new IllegalArgumentException("Missing operator near: " + token); } if (token.contains("*") || token.contains("?")) { return WildcardPattern.get(token); } return new Literal(token); } if (".".equals(token) || "|".equals(token)) { List<String> ta = tokens.subList(0, idx); List<String> tb = tokens.subList(idx + 1, tokens.size()); List<Integer> pa = priorities.subList(0, idx); List<Integer> pb = priorities.subList(idx + 1, priorities.size()); GraphExplorer a = getExplorer(ta, pa); GraphExplorer b = getExplorer(tb, pb); if (".".equals(token)) { return new Dot(a, b); } else { return new Or(a, b); } } if ("+".equals(token) || "$".equals(token)) { if (idx != (tokens.size() - 1)) { throw new IllegalArgumentException("Postfix unary operator must be the last token!"); } List<String> ta = tokens.subList(0, idx); List<Integer> pa = priorities.subList(0, idx); GraphExplorer a = getExplorer(ta, pa); if ("$".equals(token)) { return new Terminator(a); } else { return new Multi(a); } } throw new IllegalStateException("Unknown tokens: " + tokens); } public abstract Set<?> explore(Collection<?> entities, EntityExplorer explorer); }