/* * 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.addthis.hydra.data.query; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeSet; import java.util.regex.Pattern; import com.addthis.basis.util.LessBytes; import com.addthis.basis.util.LessStrings; import com.addthis.bundle.core.BundleField; import com.addthis.bundle.core.BundleFormat; import com.addthis.codec.annotations.FieldConfig; import com.addthis.codec.binary.CodecBin2; import com.addthis.codec.codables.Codable; import com.addthis.hydra.data.query.QueryElement.ReferencePathIterator; import com.addthis.hydra.data.tree.DataTreeNode; import com.addthis.hydra.data.tree.DataTreeNodeActor; import com.addthis.hydra.data.tree.ReadTreeNode; import com.addthis.hydra.data.tree.TreeNodeData; import com.addthis.hydra.data.tree.prop.VirtualTreeNode; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.AbstractIterator; import com.google.common.collect.Iterators; import org.apache.commons.lang3.mutable.MutableInt; /** * For retrieval of nodes by name. * * @user-reference */ public class QueryElementNode implements Codable { private static final String DEFAULT_NODE = "+["; private static final String DEFAULT_ATTACHMENT = "+%["; private static final String memKey = ""; @FieldConfig(codable = true) public String[] match; @FieldConfig(codable = true) public String data; @FieldConfig(codable = true) public String dataKey; @FieldConfig(codable = true) public String defaultValue; @FieldConfig(codable = true) public int defaultHits; @FieldConfig(codable = true) public Boolean flat; // output column this element is bound to 'show' (or null if dropped) @FieldConfig(codable = true) private String column; @FieldConfig(codable = true) public Boolean regex; @FieldConfig(codable = true) public Boolean range; @FieldConfig(codable = true) public Boolean rangeStrict; @FieldConfig(codable = true) public Boolean not; @FieldConfig(codable = true) public String[] path; @FieldConfig(codable = true) public Boolean up; private BundleField field; private Pattern[] regexPatterns; public QueryElementNode parse(String tok, MutableInt nextColumn) { if (tok.equals("+..")) { up = true; column = Integer.toString(nextColumn.intValue()); nextColumn.increment(); return this; } if (tok.equals("..")) { up = true; return this; } List<String> matchList = new ArrayList<>(3); if (tok.indexOf(';') > 0) { tok = tok.replace(';', ','); flat = true; } if (tok.startsWith("!")) { not = true; regex = true; tok = tok.substring(1); } if (tok.startsWith("|")) { tok = tok.substring(1); regex = true; } tok = extractDefaultValue(tok); String[] list = LessStrings.splitArray(tok, ","); for (String component : list) { if (component.startsWith("*")) { component = component.substring(1); } else if (component.startsWith("+")) { component = component.substring(1); if (component.startsWith("+")) { component = component.substring(1); range = true; } if (component.startsWith("@")) { component = component.substring(1); rangeStrict = true; } int close; if (component.startsWith("{") && (close = component.indexOf("}")) > 0) { column = component.substring(1, close); component = component.substring(close + 1); } else { column = Integer.toString(nextColumn.intValue()); nextColumn.increment(); } } if (component.startsWith("%?")) { data = memKey; regex = true; continue; } if (component.startsWith("%") && !(component.startsWith("%2d") || component.startsWith("%2c"))) { String[] kv = LessBytes.urldecode(component.substring(1)).split("=", 2); if (kv.length == 2) { data = kv[0]; dataKey = kv[1]; } else if (kv.length == 1) { data = kv[0]; } continue; } if (component.startsWith("@@")) { path = LessStrings.splitArray(LessBytes.urldecode(component.substring(2)), "/"); continue; } else if (component.startsWith("@")) { component = component.substring(1); rangeStrict = true; } component = LessBytes.urldecode(component); if (component.length() > 0) { matchList.add(component); } } if (matchList.size() > 0) { String[] out = new String[matchList.size()]; match = matchList.toArray(out); if (tok.startsWith(",")) { TreeSet<String> sorted = new TreeSet<>(); sorted.addAll(matchList); match = sorted.toArray(out); } } return this; } /** * The prefix "+[text]" or "+%[text] is interpreted as a default * value. Extract the default value and remove it from * the input token. Optionally "+[text|hits]" can be used to set * the number of hits on the default node. */ @VisibleForTesting String extractDefaultValue(String tok) { // this parsing method is not compatible with valid regex queries, and a fix is somewhat non-trivial if (regex()) { return tok; } int startDefault = -1; if (tok.startsWith(DEFAULT_NODE)) { startDefault = DEFAULT_NODE.length(); } else if (tok.startsWith(DEFAULT_ATTACHMENT)) { startDefault = DEFAULT_ATTACHMENT.length(); } int endDefault = tok.indexOf(']'); if ((startDefault != -1) && (endDefault != -1)) { int hitsLocation = tok.indexOf('|'); if ((hitsLocation >= 0) && (hitsLocation < endDefault)) { defaultHits = Integer.parseInt(tok.substring(hitsLocation + 1, endDefault)); defaultValue = tok.substring(startDefault, hitsLocation); } else { defaultValue = tok.substring(startDefault, endDefault); } // tok must either start with "+[" or "+%[" to have // reached this line if (tok.startsWith(DEFAULT_NODE)) { tok = "+" + tok.substring(endDefault + 1); } else if (tok.startsWith(DEFAULT_ATTACHMENT)) { tok = "+%" + tok.substring(endDefault + 1); } else { throw new IllegalStateException("Unexpected state in default value extraction"); } } return tok; } void toCompact(StringBuilder sb) { if (column == null && match == null) { sb.append("*"); } if (regex()) { sb.append("|"); } if (show()) { sb.append("+"); } if (show() && range()) { sb.append("+"); } if (show() && rangeStrict()) { sb.append("@"); } if (match != null && match.length > 0) { int i = 0; for (String m : match) { if (i++ > 0) { sb.append(","); } sb.append(LessBytes.urlencode(m)); } } if (data != null) { sb.append("%"); sb.append(regex() ? "?" : data); } if (dataKey != null) { sb.append("=").append(LessBytes.urlencode(dataKey)); } } public boolean up() { return up != null && up; } public boolean flat() { return flat != null && flat; } public String column() { return column; } public boolean show() { return column != null;// show != null && show.booleanValue(); } public BundleField field(BundleFormat format) { if (field == null) { field = format.getField(column); } return field; } public boolean regex() { return regex != null && regex; } public boolean range() { return range != null && range; } public boolean rangeStrict() { return rangeStrict != null && rangeStrict; } public boolean not() { return not != null && not; } private DataTreeNode followPath(DataTreeNode from, String[] path) { DataTreeNode node = from; for (String name : path) { node = node.getNode(name); if (node == null) { return null; } } return node; } private static class LazyNodeMatch extends AbstractIterator<DataTreeNode> { final DataTreeNode parent; final String[] match; final DataTreeNode defaultNode; int index; boolean first; LazyNodeMatch(DataTreeNode parent, String[] match, DataTreeNode defaultNode) { this.parent = parent; this.match = match; this.defaultNode = defaultNode; this.index = 0; this.first = true; } protected DataTreeNode computeNext() { DataTreeNode next = null; while ((next == null) && (index < match.length)) { next = parent.getNode(match[index]); index++; } if (next == null) { if (first && (defaultNode != null)) { first = false; return defaultNode; } else { return endOfData(); } } else { first = false; return next; } } } public Iterator<DataTreeNode> getNodes(LinkedList<DataTreeNode> stack) { List<DataTreeNode> ret = null; if (up()) { ret = new ArrayList<>(1); ret.add(stack.get(1)); return ret.iterator(); } DataTreeNode parent = stack.peek(); DataTreeNode defaultNode = null; if (defaultValue != null) { defaultNode = new VirtualTreeNode(defaultValue, defaultHits); } try { DataTreeNode tmp; if (path != null) { DataTreeNode refnode = followPath(parent.getTreeRoot(), path); return refnode != null ? new ReferencePathIterator(refnode, parent) : null; } if ((match == null) && (regex == null) && (data == null)) { Iterator<DataTreeNode> result = parent.getIterator(); if (result.hasNext() || (defaultNode == null)) { return result; } else { return Iterators.singletonIterator(defaultNode); } } ret = new LinkedList<>(); if (match != null) { if (regex()) { if (regexPatterns == null) { regexPatterns = new Pattern[match.length]; for (int i = 0; i < match.length; i++) { regexPatterns[i] = Pattern.compile(match[i]); } } for (Iterator<DataTreeNode> iter = parent.getIterator(); iter.hasNext();) { tmp = iter.next(); for (Pattern name : regexPatterns) { if (name.matcher(tmp.getName()).matches() ^ not()) { ret.add(tmp); } } } } else if (range()) { if (match.length == 0) { return parent.getIterator(); } else if (match.length == 1) { return parent.getIterator(match[0]); } else { ArrayList<Iterator<DataTreeNode>> metaIterator = new ArrayList<>(); for (String name : match) { metaIterator.add(parent.getIterator(name)); } return Iterators.concat(metaIterator.iterator()); } } else if (rangeStrict()) { if (match.length <= 2) { return parent.getIterator(match.length > 0 ? match[0] : null, match.length > 1 ? match[1] : null); } else { List<Iterator<DataTreeNode>> metaIterator = new ArrayList<>((match.length + 1) / 2); for (int i = 0; i < match.length; i += 2) { if (match.length > (i + 1)) { metaIterator.add(parent.getIterator(match[i], match[i + 1])); } else { metaIterator.add(parent.getIterator(match[i], null)); } } return Iterators.concat(metaIterator.iterator()); } } else if (data == null) { return new LazyNodeMatch(parent, match, defaultNode); } else { for (String name : match) { DataTreeNode find = parent.getNode(name); if (find != null) { ret.add(find); } } } } if (data != null) { if (regex()) { if (parent.getDataMap() != null) { for (Map.Entry<String, TreeNodeData> actor : parent.getDataMap().entrySet()) { int memSize = CodecBin2.encodeBytes(actor.getValue()).length; ReadTreeNode memNode = new ReadTreeNode(actor.getKey(), memSize); ret.add(memNode); } } } else { DataTreeNodeActor actor = parent.getData(data); if (actor != null) { Collection<DataTreeNode> nodes = actor.onNodeQuery(dataKey); if (nodes != null) { ret.addAll(nodes); } } } } } catch (RuntimeException ex) { throw ex; } catch (Exception ex) { ex.printStackTrace(); } if ((ret.size() == 0) && (defaultNode != null)) { return Iterators.singletonIterator(defaultNode); } else { return ret.iterator(); } } }