sk.nociar.jpacloner.graphs.GraphExplorer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jpa-cloner Show documentation
Show all versions of jpa-cloner Show documentation
JPA Cloner allows cloning of JPA entity subgraphs.
/* JPA cloner project.
*
* Copyright (C) 2013 Miroslav Nociar
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
*/
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:
*
* - Dot "." separates paths.
* - Plus "+" generates at least one preceding path.
* - Split "|" divides the path into two ways.
* - Terminator "$" ends the preceding path.
* - Parentheses "(", ")" groups the paths.
* - Wildcards "*", "?" in property names.
*
* Some examples follow:
*
* - device.*
* - device.(interfaces.type|driver.author)
* - company.department+.(boss|employees).address
* - *+
*
*
* NOTE: 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 operators = new HashSet();
private static final Map operatorToPriority = new HashMap();
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 cache = new ConcurrentHashMap();
/**
* 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 tokens = new ArrayList();
List priorities = new ArrayList();
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 tokens, List 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 ta = tokens.subList(0, idx);
List tb = tokens.subList(idx + 1, tokens.size());
List pa = priorities.subList(0, idx);
List 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 ta = tokens.subList(0, idx);
List 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);
}