/*
* 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();
}
}
}