/*
* 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.Iterator;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import com.addthis.basis.util.ClosableIterator;
import com.addthis.basis.util.LessStrings;
import com.addthis.bundle.value.ValueFactory;
import com.addthis.bundle.value.ValueObject;
import com.addthis.codec.annotations.FieldConfig;
import com.addthis.codec.codables.SuperCodable;
import com.addthis.codec.json.CodecJSON;
import com.addthis.hydra.data.tree.DataTree;
import com.addthis.hydra.data.tree.DataTreeNode;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import org.apache.commons.lang3.mutable.MutableInt;
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
setterVisibility = JsonAutoDetect.Visibility.NONE)
public class QueryElement implements SuperCodable {
/**
*
*/
public QueryElement() {
}
/**
* Node names to iterate over.
*/
@FieldConfig(codable = true)
private QueryElementNode node;
/**
* For each node the field values to collect.
*/
@FieldConfig(codable = true)
private ArrayList<QueryElementField> field;
/**
* For each node the property values to collect.
*/
@FieldConfig(codable = true)
private ArrayList<QueryElementProperty> prop;
/**
* If true then do not fail when a node or property is missing. Default is false.
*/
@FieldConfig(codable = true)
private Boolean nullok;
/**
* Skip the first N nodes for this element.
*/
@FieldConfig(codable = true)
private Integer skip;
/**
* Limit the total number of nodes for this element.
*/
@FieldConfig(codable = true)
private Integer limit;
// internal state
private boolean hasdata;
/**
* simplified compact, not complete
*/
public String toCompact(StringBuilder sb) {
if (emptyok()) {
sb.append("~");
}
if (limit() > 0 || skip() > 0) {
sb.append("(" + (skip == null ? 0 : skip) + "-" + limit + ")");
}
node.toCompact(sb);
if (prop != null && prop.size() > 0) {
sb.append(":");
int i = 0;
for (QueryElementProperty p : prop) {
if (i++ > 0) {
sb.append(",");
}
p.toCompact(sb);
}
}
if (field != null && field.size() > 0) {
sb.append("$");
int i = 0;
for (QueryElementField f : field) {
if (i++ > 0) {
sb.append(",");
}
f.toCompact(sb);
}
}
return sb.toString();
}
@Override
public String toString() {
try {
return CodecJSON.encodeString(this);
} catch (Exception e) {
return super.toString();
}
}
/**
* element parser
*/
public QueryElement parse(String q, MutableInt nextColumn) {
int pos = 0;
if (q.startsWith("(") && (pos = q.indexOf(")")) > 0) {
String[] range = LessStrings.splitArray(q.substring(1, pos), "-");
if (range.length == 1) {
limit = Integer.parseInt(range[0]);
} else {
skip = Integer.parseInt(range[0]);
limit = Integer.parseInt(range[1]);
}
q = q.substring(pos + 1);
}
if (q.startsWith("~")) {
q = q.substring(1);
nullok = true;
}
StringTokenizer st = new StringTokenizer(q, ":$", true);
while (st.hasMoreTokens()) {
String tok = st.nextToken();
node = new QueryElementNode().parse(tok, nextColumn);
while (st.hasMoreTokens()) {
String sep = st.nextToken();
if (st.hasMoreTokens()) {
tok = st.nextToken();
if (sep.equals(":")) {
String[] ps = LessStrings.splitArray(tok, ",");
if (prop == null) {
prop = new ArrayList<>(ps.length);
}
for (String p : ps) {
prop.add(new QueryElementProperty().parse(p, nextColumn));
}
} else if (sep.equals("$")) {
if (field == null) {
field = new ArrayList<>(1);
}
field.add(new QueryElementField().parse(tok, nextColumn));
}
}
}
}
postDecode();
return this;
}
public int skip() {
return skip != null ? skip : 0;
}
public int limit() {
return limit != null ? limit : 0;
}
public boolean flatten() {
return node.flat();
}
public boolean emptyok() {
return nullok != null && nullok;
}
public boolean hasData() {
return hasdata;
}
public Iterator<DataTreeNode> matchNodes(DataTree tree, LinkedList<DataTreeNode> stack) {
return node != null ? node.getNodes(stack) : null;
}
public int update(FieldValueList fvlist, DataTreeNode tn) {
if (tn == null) {
// whether or not the entire node being null is the business of the isnullokay operator in the caller
return 0;
}
int updates = 0;
if (node != null && node.show()) {
fvlist.push(node.field(fvlist.getFormat()), ValueFactory.create(tn.getName()));
updates++;
}
if (prop != null) {
for (QueryElementProperty p : prop) {
ValueObject val = p.getValue(tn);
if (val == null && !emptyok()) {
fvlist.rollback();
return -1;
}
if (p.show()) {
fvlist.push(p.field(fvlist.getFormat()), val);
updates++;
}
}
}
if (field != null) {
for (QueryElementField f : field) {
for (ValueObject val : f.getValues(tn)) {
if (val == null && !emptyok()) {
fvlist.rollback();
return -1;
}
if (f.show()) {
fvlist.push(f.field(fvlist.getFormat()), val);
updates++;
}
}
}
}
fvlist.commit();
return updates;
}
// preset 'hasdata' instead of computing it each time
@Override
public void postDecode() {
if (node != null) {
hasdata = node.show();
}
if (field != null) {
for (QueryElementField f : field) {
hasdata |= f.show();
}
}
if (prop != null) {
for (QueryElementProperty p : prop) {
hasdata |= p.show();
}
}
}
@Override
public void preEncode() {
}
/**
* for node iteration using a reference node's keys
*/
static final class ReferencePathIterator implements ClosableIterator<DataTreeNode> {
private final ClosableIterator<DataTreeNode> refiter;
private final DataTreeNode node;
private DataTreeNode next;
ReferencePathIterator(DataTreeNode refnode, DataTreeNode node) {
this.refiter = refnode.getIterator();
this.node = node;
}
@Override
public void close() {
refiter.close();
}
@Override
public boolean hasNext() {
while (next == null && refiter.hasNext()) {
DataTreeNode nextRef = refiter.next();
next = node.getNode(nextRef.getName());
}
return next != null;
}
@Override
public DataTreeNode next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
DataTreeNode ret = next;
next = null;
return ret;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
public QueryElementNode getNode() {
return node;
}
public ArrayList<QueryElementProperty> getProp() {
return prop;
}
}