/* * Copyright (c) 2012 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * HUMBOLDT EU Integrated Project #030962 * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.common.instance.helper; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import javax.xml.namespace.QName; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Collections2; import eu.esdihumboldt.hale.common.instance.model.DataSet; import eu.esdihumboldt.hale.common.instance.model.Group; import eu.esdihumboldt.hale.common.instance.model.Instance; import eu.esdihumboldt.hale.common.instance.model.impl.DefaultInstance; import eu.esdihumboldt.hale.common.schema.model.ChildDefinition; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; /** * This class provides plubic static methods for resolving propertys from * instances. A cache provides that former accessed propertys are found faster * and the programm does not need to search over the whole definitiontree of the * instances again. Note: stringquery or querypath in comments references to a * path of indicies reassambling a path of definitions inside the * instance-definition-tree * * @author Sebastian Reinhardt */ public class PropertyResolver { // the cache for storing found paths in instance definitions for certain // querys private static final Map<QueryDefinitionIndex, LinkedList<String>> definitioncache = new ConcurrentHashMap<QueryDefinitionIndex, LinkedList<String>>(); /** * This variable holds state about the last * {@link #hasProperty(Instance, String)} call. */ private static final ThreadLocal<QueryDefinitionIndex> lastQDI = new ThreadLocal<QueryDefinitionIndex>(); /** * Method for retrieving values from instances using a certain path query * for searching through the instance definitions. Calls methods for * traversing the definition tree.<br> * <br> * If at the end of the path there is an instance, its value will be * returned. * * @param instance the instance * @param propertyPath the property path * @return the values contained in the instance matching the path */ public static Collection<Object> getValues(Instance instance, String propertyPath) { return getValues(instance, propertyPath, true); } /** * Method for retrieving values from instances using a certain path query * for searching through the instance definitions. Calls methods for * traversing the definition tree. * * @param instance the instance * @param propertyPath the property path * @param forceValue if this is <code>true</code>, when the object at the * end of a path is an instance, its value will be returned * @return the values or instances contained in the instance matching the * given path, may be <code>null</code> */ public static Collection<Object> getValues(Instance instance, String propertyPath, boolean forceValue) { if (instance.getDefinition() == null) { // instance w/o a definition -> search only in instance structure // XXX group hiding and incomplete names not supported! Collection<Object> result = new ArrayList<Object>(); Collection<Object> parents = new ArrayList<Object>(); parents.add(instance); Queue<QName> names = new LinkedList<QName>(getQNamesFromPath(propertyPath)); while (!names.isEmpty() && !parents.isEmpty()) { QName property = names.poll(); Collection<Object> values = new ArrayList<Object>(); for (Object parent : parents) { if (parent instanceof Group) { Object[] propertyValues = ((Group) parent).getProperty(property); if (propertyValues != null) { for (Object propertyValue : propertyValues) { if (propertyValue instanceof Instance && ((Instance) propertyValue).getDefinition() != null) { Instance pi = (Instance) propertyValue; // call getValues again an perform the // search based on definitions // with a reduced path String path = pathString(names); Collection<Object> deepValues = getValues(pi, path, forceValue); if (deepValues != null) { result.addAll(deepValues); } } else { values.add(propertyValue); } } } } } parents = values; } if (names.isEmpty()) { // return all values found for (Object value : parents) { if (!forceValue) { result.add(value); } else { if (value instanceof Instance) { result.add(((Instance) value).getValue()); } else if (!(value instanceof Group)) { result.add(value); } } } } return result; } // definition based check and retrieval if (hasProperty(instance, propertyPath)) { LinkedList<String> paths = getQueryPath(instance, propertyPath); Collection<Object> result = new ArrayList<Object>(); for (String path : paths) { List<QName> qnames = getQNamesFromPath(path); Object[] props = instance.getProperty(qnames.get(0)); if (props == null) { continue; } Queue<Object> currentQueue = new LinkedList<Object>(); Queue<Object> nextQueue = new LinkedList<Object>(); for (Object prop : props) { currentQueue.add(prop); } for (int i = 1; i < qnames.size(); i++) { while (!currentQueue.isEmpty()) { Object prop = currentQueue.poll(); if (prop instanceof Group) { Object[] nextPropertys = ((Group) prop).getProperty(qnames.get(i)); if (nextPropertys == null) { continue; } for (Object np : nextPropertys) { nextQueue.add(np); } } else { // TODO ERROR wrong path given from the cache } } while (!nextQueue.isEmpty()) { currentQueue.add(nextQueue.poll()); } } while (!currentQueue.isEmpty()) { Object finalProp = currentQueue.poll(); if (finalProp instanceof Instance) { if (forceValue) { result.add(((Instance) finalProp).getValue()); } else { result.add(finalProp); } } else if (finalProp instanceof Group && forceValue) { // TODO error } else result.add(finalProp); } } if (!result.isEmpty()) { return result; } else { return null; } } else return null; } private static String pathString(Collection<QName> names) { return Joiner.on('.').join(Collections2.transform(names, new Function<QName, String>() { @Override public String apply(QName input) { return input.toString(); } })); } /** * Method for spliting up the path in the given query. The Method splits the * String when a dot occurs. Are there an URL-parts inside the path, all * dots inside thos parts are ignored ( checks of "{}" ) * * @param propertyPath The definitionpath part of the query * @return An arraylist of split up parts of the path */ private static ArrayList<String> splitPath(String propertyPath) { ArrayList<String> pathParts = new ArrayList<String>(); boolean dotsplit = true; int lastSplitPosition = 0; for (int i = 0; i < propertyPath.length(); i++) { char c = propertyPath.charAt(i); // check if there is an URL-part if (c == '{') // dont split if a dot occurs now dotsplit = false; else if (c == '}') dotsplit = true; if (dotsplit == true && (c == '/' || c == '.')) { pathParts.add(propertyPath.substring(lastSplitPosition, i)); lastSplitPosition = i + 1; } } pathParts.add(propertyPath.substring(lastSplitPosition)); return pathParts; } /** * Split a property path into a list of {@link QName}s. * * @param propertyPath the property path * @return the list of represented qualified names */ public static List<QName> getQNamesFromPath(String propertyPath) { ArrayList<String> pathParts = splitPath(propertyPath); ArrayList<QName> qnames = new ArrayList<QName>(); for (int i = 0; i < pathParts.size(); i++) { String current = pathParts.get(i); if (current.startsWith("{")) { String uri = current.substring(current.indexOf("{") + 1, current.indexOf("}")); String name = current.substring(current.indexOf("}") + 1); qnames.add(new QName(uri, name)); } else { qnames.add(new QName(current)); } } return qnames; } /** * this method starts the analysis of the instance-definition-tree * * @param instance the given instance we are analysing * @param qdi the cache index object with the querypath * @return true, if the cache is not empty for the given cache index object * after the analysis */ private static boolean analyzeDefinition(Instance instance, QueryDefinitionIndex qdi) { List<QName> qnames = getQNamesFromPath(qdi.getQuery()); definitioncache.put(qdi, new LinkedList<String>()); // this can be used to search a single index over the whole // Instance-Definition-Tree /* * if (qnames.size() == 1) { * * analyzeSimpleQueryChildDefinition(instance * .getDefinition().getChildren(), qnames, qdi); * * return !definitioncache.get(qdi).isEmpty(); } * * else { */ analyzeSpecialQueryChildDefinition(instance.getDefinition().getChildren(), qnames, qdi); return !definitioncache.get(qdi).isEmpty(); // } } /** * Determines of the given Instance contains certain definitions questioned * by a given stringquery. If the cache allready contains this special path * of the instance-definition-tree, true will be returned, else the method * calls the analysismethods for searching of the definition-tree * * @param instance the given instance we are searching in * @param query the given pathquery we are searching inside the * definition-tree * @return true if the path was found, else false */ public static boolean hasProperty(Instance instance, String query) { QueryDefinitionIndex qdi = new QueryDefinitionIndex(instance.getDefinition(), instance.getDataSet(), query); lastQDI.set(qdi); if (definitioncache.containsKey(qdi)) return !definitioncache.get(qdi).isEmpty(); else return analyzeDefinition(instance, qdi); } /** * Returns all possible fully qualified (with namespaces) paths matching the * given query. * * @param typeDef the type definition * @param dataSet the data set * @param query the query * @return a list of all possible paths matching the query (which may be * empty) */ public static List<List<QName>> getQueryPaths(TypeDefinition typeDef, DataSet dataSet, String query) { Instance instance = new DefaultInstance(typeDef, dataSet); List<String> paths = getQueryPath(instance, query); ArrayList<List<QName>> result = new ArrayList<List<QName>>(paths.size()); for (String path : paths) result.add(getQNamesFromPath(path)); return Collections.unmodifiableList(result); } /** * this method can be used to search a single index over the whole * instance-definition-tree (for example "*" queries) the method writes the * found paths into the cache * * @param children a list of ChildDefinitions from the root definition of * the instance-definition-tree * @param path the list of QNames split up from the original querypath * @param qdi the cacheindex produced from the instance root definition and * the querypath */ @SuppressWarnings("unused") private static void analyzeSimpleQueryChildDefinition( Collection<? extends ChildDefinition<?>> children, ArrayList<QName> path, QueryDefinitionIndex qdi) { QName current = path.get(0); Queue<QueueDefinitionItem> propertyqueue = new LinkedList<QueueDefinitionItem>(); Iterator<? extends ChildDefinition<?>> childIterator = children.iterator(); while (childIterator.hasNext()) { ChildDefinition<?> child = childIterator.next(); QueueDefinitionItem queueItem = new QueueDefinitionItem(child, child.getName()); propertyqueue.add(queueItem); } while (!propertyqueue.isEmpty()) { QueueDefinitionItem currentItem = propertyqueue.poll(); if (compareQName(current, currentItem.getDefinition().getName()) && isProperty(currentItem.getDefinition())) { definitioncache.get(qdi).add(currentItem.qNamesToString()); } if (isInstance(currentItem.getDefinition()) || isGroup(currentItem.getDefinition())) { Iterator<? extends ChildDefinition<?>> tempit; if (isGroup(currentItem.getDefinition())) { tempit = currentItem.getDefinition().asGroup().getDeclaredChildren().iterator(); } else { tempit = currentItem.getDefinition().asProperty().getPropertyType() .getChildren().iterator(); } while (tempit.hasNext()) { ChildDefinition<?> tempdef = tempit.next(); for (List<QName> loop : currentItem.getLoopQNames()) { if (loop.contains(tempdef.getName())) { continue; } } if (currentItem.getQnames().contains(tempdef.getName())) { List<QName> loops = new ArrayList<QName>(); for (int i = currentItem.getQnames().indexOf(tempdef.getName()); i < currentItem .getQnames().size(); i++) { loops.add(currentItem.getQnames().get(i)); } currentItem.addLoopQNames(loops); continue; } QueueDefinitionItem qudi = new QueueDefinitionItem(tempdef, tempdef.getName()); qudi.addQnames(currentItem.getQnames()); for (List<QName> loop : currentItem.getLoopQNames()) { qudi.addLoopQNames(loop); } propertyqueue.add(qudi); } } } } /** * this method searches for the indices given from the querypath inside the * instance-definition-tree but only for one iteration. this is used to * avoid recursion and is used by the analyzeSpecialQueryChild method. the * indices must be children in order to their appearance in the path. only * groups may be between them. * * @param current the current searched index as a QName * @param qudi a queue item of the current found index and its definition * @return returns a queue item of the searched index, if it has been found */ private static QueueDefinitionItem analyzeSubChild(QueueDefinitionItem qudi, QName current) { Queue<QueueDefinitionItem> propertyqueue = new LinkedList<QueueDefinitionItem>(); Iterator<? extends ChildDefinition<?>> childIterator = qudi.getDefinition().asProperty() .getPropertyType().getChildren().iterator(); while (childIterator.hasNext()) { ChildDefinition<?> child = childIterator.next(); QueueDefinitionItem queueItem = new QueueDefinitionItem(child, child.getName()); queueItem.addQnames(qudi.getQnames()); propertyqueue.add(queueItem); } while (!propertyqueue.isEmpty()) { QueueDefinitionItem currentItem = propertyqueue.poll(); if (compareQName(current, currentItem.getDefinition().getName()) && isProperty(currentItem.getDefinition())) { return currentItem; } if (isGroup(currentItem.getDefinition())) { Iterator<? extends ChildDefinition<?>> tempit; tempit = currentItem.getDefinition().asGroup().getDeclaredChildren().iterator(); while (tempit.hasNext()) { ChildDefinition<?> tempdef = tempit.next(); // FIXME this condition is never true because LoopQNames is // a list of lists if (currentItem.getLoopQNames().contains(tempdef.getName())) { continue; } if (currentItem.getQnames().contains(tempdef.getName())) { if (!compareQName(current, tempdef.getName())) { ArrayList<QName> loops = new ArrayList<QName>(); for (int i = currentItem.getQnames().indexOf(tempdef.getName()); i < currentItem .getQnames().size(); i++) { loops.add(currentItem.getQnames().get(i)); } currentItem.addLoopQNames(loops); continue; } } QueueDefinitionItem quditemp = new QueueDefinitionItem(tempdef, tempdef.getName()); quditemp.addQnames(currentItem.getQnames()); for (List<QName> loop : currentItem.getLoopQNames()) { qudi.addLoopQNames(loop); } propertyqueue.add(quditemp); } } } return null; } /** * this method searches for the indices given from the querypath inside the * instance-definition-tree the indices must be children in order to their * appearance in the path. only groups may be between them. the method * writes the found paths into the cache * * @param children a list of Childdefinitions from the rootdefinition of the * instance-definition-tree * @param path the list of QNames split up from the original querypath * @param qdi the cacheindex produced from the instance root definition and * the querypath */ private static void analyzeSpecialQueryChildDefinition( Collection<? extends ChildDefinition<?>> children, List<QName> path, QueryDefinitionIndex qdi) { QName current = path.get(0); Queue<QueueDefinitionItem> propertyqueue = new LinkedList<QueueDefinitionItem>(); Iterator<? extends ChildDefinition<?>> childIterator = children.iterator(); while (childIterator.hasNext()) { ChildDefinition<?> child = childIterator.next(); QueueDefinitionItem queueItem = new QueueDefinitionItem(child, child.getName()); propertyqueue.add(queueItem); } while (!propertyqueue.isEmpty()) { QueueDefinitionItem currentItem = propertyqueue.poll(); if (compareQName(current, currentItem.getDefinition().getName()) && isProperty(currentItem.getDefinition())) { for (int i = 1; i < path.size(); i++) { currentItem = analyzeSubChild(currentItem, path.get(i)); if (currentItem == null) { break; } } if (currentItem != null) { definitioncache.get(qdi).add(currentItem.qNamesToString()); } } else if (isGroup(currentItem.getDefinition())) { Iterator<? extends ChildDefinition<?>> tempit; tempit = currentItem.getDefinition().asGroup().getDeclaredChildren().iterator(); while (tempit.hasNext()) { ChildDefinition<?> tempdef = tempit.next(); // FIXME this condition is never true because LoopQNames is // a list of lists if (currentItem.getLoopQNames().contains(tempdef.getName())) { continue; } if (currentItem.getQnames().contains(tempdef.getName())) { ArrayList<QName> loops = new ArrayList<QName>(); for (int i = currentItem.getQnames().indexOf(tempdef.getName()); i < currentItem .getQnames().size(); i++) { loops.add(currentItem.getQnames().get(i)); } currentItem.addLoopQNames(loops); continue; } QueueDefinitionItem qudi = new QueueDefinitionItem(tempdef, tempdef.getName()); qudi.addQnames(currentItem.getQnames()); for (List<QName> loop : currentItem.getLoopQNames()) { qudi.addLoopQNames(loop); } propertyqueue.add(qudi); } } } } private static boolean isGroup(ChildDefinition<?> def) { return def.asGroup() != null && def.asProperty() == null; } private static boolean isProperty(ChildDefinition<?> def) { return def.asGroup() == null && def.asProperty() != null; } private static boolean isInstance(ChildDefinition<?> def) { if (def.asProperty() == null) { return false; } else if (!def.asProperty().getPropertyType().getChildren().isEmpty()) { return true; } else return false; } /** * Method for easy comparing of two QName objects. The first QName can miss * the URI part. Then only the local parts getting compared. * * @param qname1 the QName (usually from the filterquery), which can miss an * URI part * @param qname2 the second QName * @return true, if both are equal or if the first QName doesn't have an URI * part and both localparts are equal. Else false... */ private static boolean compareQName(QName qname1, QName qname2) { // contains the first QName an URI part? if (qname1.getNamespaceURI().isEmpty()) { // only compare the local parts if (qname1.getLocalPart().equals(qname2.getLocalPart())) { return true; } else return false; } // first Qname does contain the URI part -> compare them completely else if (qname1.equals(qname2)) { return true; } else return false; } /** * Returns possible fully qualified (with namespace) paths for the given * String which could not contain all namespaces.<br> * The path can be splitted using {@link #getQNamesFromPath(String)} * * @param instance the given instance which should contain the definitions * mentioned in the paths * @param query the pathstring from the filterquery * @return a list of Strings with possible paths inside the definitions of * the instance */ public static LinkedList<String> getQueryPath(Instance instance, String query) { QueryDefinitionIndex qdi = new QueryDefinitionIndex(instance.getDefinition(), instance.getDataSet(), query); lastQDI.set(qdi); if (!definitioncache.containsKey(qdi)) analyzeDefinition(instance, qdi); return definitioncache.get(qdi); } /** * Determines if the last query path was unique. This will only yield a * reliable result if the last call to * {@link #hasProperty(Instance, String)} was done from the current thread. * The information on the last {@link #hasProperty(Instance, String)} call * will be reset on calling this method. * * @return <code>true</code> if the last query path was unique or if there * is no information on the last query path, <code>false</code> * otherwise */ public static boolean isLastQueryPathUnique() { QueryDefinitionIndex qdi = lastQDI.get(); lastQDI.remove(); if (qdi != null) { LinkedList<String> paths = definitioncache.get(qdi); if (paths != null && paths.size() > 1) { return false; } } return true; } /** * Clear the definition cache, e.g. when the type definitions may no longer * be valid. * * FIXME cache in service instead? */ public static void clearCache() { definitioncache.clear(); } }