//Dstl (c) Crown Copyright 2017 package uk.gov.dstl.baleen.uima.utils; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang.StringUtils; import com.google.common.base.Joiner; import uk.gov.dstl.baleen.core.utils.BuilderUtils; import uk.gov.dstl.baleen.exceptions.InvalidParameterException; import uk.gov.dstl.baleen.types.structure.Structure; /** * A selector path describes a location in a structure hierarchy. * <p> * Based on a CSS selector-like syntax for selecting structural annotations. * <p> * Currently only the (<code>></code> operator) is supported, and the optional * <code>nth-of-type(n)</code> pseudo selector. It is required that structural annotations have a * <code>depth</code> feature such that nesting can be approximated. * </p> * <p> * Example selectors: * </p> * <ul> * <li><code>Section > Heading</code> * <li><code>Heading:nth-of-type(2)</code> * <li><code>Section:nth-of-type(1) > Paragraph</code> * <li><code>Table:nth-of-type(2) > TableBody > TableRow:nth-of-type(3) > TableCell:nth-of-type(2) > Paragraph:nth-of-type(1)</code> * </ul> * */ public class SelectorPath { /** The separator of path elements */ private static final String SEPARATOR = " > "; /** The joiner for to string */ private static final Joiner JOINER = Joiner.on(SEPARATOR); /** The path of selector parts */ private final List<SelectorPart> path; /** * Constructor for a selector path * * @param path the path */ public SelectorPath(List<SelectorPart> path) { this.path = path; } /** * Get the path as a list of selector parts * * @return the path */ public List<SelectorPart> getParts() { return path; } /** * Trim the path to the depth required * * @param depth the depth * @return the path up to the depth given, if the path is shorter the whole path is returned */ public SelectorPath toDepth(int depth) { return new SelectorPath(path.subList(0, Math.min(depth, path.size()))); } /** * Get the remaining path after the given path to get this path * * @param from * @return the remaining path having remove the matching elements from the given path */ public SelectorPath from(SelectorPath from) { int i = 0; List<SelectorPart> remaining = new ArrayList<>(path); while (!remaining.isEmpty() && from.getDepth() > i && from.path.get(i).equals(remaining.get(0))) { i++; remaining.remove(0); } return new SelectorPath(remaining); } /** * Get the depth of this path * * @return the depth of the path */ public int getDepth() { return path.size(); } /** * Test if this path contains any parts * * @return true if this path is empty */ public boolean isEmpty() { return path.isEmpty(); } /** * Get the part of this path for the supplied index * * @param i the index * @return the part at the given index */ public SelectorPart get(int i) { return path.get(i); } /** * Step one part of the path * * @return new path of remaining parts */ public SelectorPath step() { return new SelectorPath(path.subList(Math.min(getDepth(), 1), getDepth())); } @Override public String toString() { return JOINER.join(path); } /** * Parses the selector string and returns an ordered List of {@link SelectorPart} objects that * represent each clause between <code>><code> symbols. * * @param value the value * @return the list * @throws InvalidParameterException the invalid parameter exception */ public static SelectorPath parse(String value) throws InvalidParameterException { List<SelectorPart> selectorParts = new ArrayList<>(); if (value == null) { return new SelectorPath(selectorParts); } String[] parts = value.split("\\s*>\\s*"); for (String part : parts) { if (StringUtils.isNotEmpty(part)) { int colon = part.indexOf(':'); if (colon != -1) { String[] typeAndQualifier = part.split(":"); selectorParts.add(new SelectorPart(getType(typeAndQualifier[0]), typeAndQualifier[1])); } else { selectorParts.add(new SelectorPart(getType(part))); } } } return new SelectorPath(selectorParts); } /** * Gets the {@link Structure} type for the given type name, within the given packages. * * @param typeName the type name * @param packages the packages * @return the type * @throws InvalidParameterException the invalid parameter exception */ private static Class<Structure> getType(String typeName) throws InvalidParameterException { return BuilderUtils.getClassFromString(typeName, Structure.class.getPackage().getName()); } }