/* * 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.task.output.tree; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.List; import com.addthis.basis.util.ClosableIterator; import com.addthis.basis.util.LessStrings; import com.addthis.bundle.core.Bundle; import com.addthis.bundle.util.ValueUtil; import com.addthis.codec.annotations.FieldConfig; import com.addthis.hydra.data.query.FieldValueList; import com.addthis.hydra.data.tree.DataTreeNode; import com.addthis.hydra.data.tree.DataTreeUtil; /** * This {@link PathElement PathElement} <span class="hydra-summary">performs a query against * a pre-existing node in the data tree</span>. * <p/> * <p>The location of the target node is specified as a path * to the target node using the 'path' parameter. By default the traversal * to the target node begins at the root of the tree. If the 'relativeUp' * parameter is a positive integer then the tree traversal begins with * an ancestor of the current location.</p> * <p/> * <p>The query is performed against the target node and the query results are * injected into the current input bundle with specified named fields.</p> * <p/> * @user-reference */ public final class PathQuery extends PathOp { /** * Path traversal to the target node. This field is required. */ @FieldConfig(codable = true, required = true) private PathValue[] path; /** * When traversing the tree in search of the target node, * if this parameter is a positive integer then begin * the traversal this many levels higher than the * current location. A negative value will begin traversal * at the current location. Default is zero. */ @FieldConfig(codable = true) private int relativeUp; /** * Specifies the operation to perform on the matching * node or nodes. This field is required. */ @FieldConfig(codable = true, required = true) private PathQueryElement values; /** * If non-zero then begin emitting * debugging output after N bundles * have been observed. Default is zero. */ @FieldConfig(codable = true) private int debug; /** * If 'debug' is non-zero then * append this prefix to the debugging * output. Default is null. */ @FieldConfig(codable = true) private String debugKey; /** * Optionally process all the children of the node * that we have matched against. Append all the matching results * into either an array or a map. Possible values are * "AS_LIST" and "AS_MAP" or "NONE". Default is "NONE". */ @FieldConfig(codable = true) @Nonnull private ChildMatch childMatch = ChildMatch.NONE; private int match; private int miss; public PathQuery() { } public static enum ChildMatch { AS_LIST { @Override boolean updateBundle(FieldValueList valueList, Bundle bundle) { return valueList.updateBundleWithListAppend(bundle); } }, AS_MAP { @Override boolean updateBundle(FieldValueList valueList, Bundle bundle) { return valueList.updateBundleWithMapAppend(bundle); } }, NONE { @Override boolean updateBundle(FieldValueList valueList, Bundle bundle) { return valueList.updateBundle(bundle); } }; abstract boolean updateBundle(FieldValueList valueList, Bundle bundle); } @Override public void resolve(TreeMapper mapper) { super.resolve(mapper); for (PathValue pv : path) { pv.resolve(mapper); } if (values != null) { values.resolve(mapper); } } private void logFailure(String[] pathValues) { if (debug > 0) { debug(false); } if (log.isDebugEnabled() || (debug == 1)) { log.warn("query fail, missing {}", LessStrings.join(pathValues, " / ")); } } @Nullable @Override public List<DataTreeNode> getNextNodeList(TreeMapState state) { String[] pathValues = new String[path.length]; for (int i = 0; i < pathValues.length; i++) { pathValues[i] = ValueUtil.asNativeString(path[i].getPathValue(state)); if (pathValues[i] == null) { return null; } } DataTreeNode reference; if (relativeUp > 0) { reference = DataTreeUtil.pathLocateFrom(state.peek(relativeUp), pathValues); } else if (relativeUp < 0) { reference = DataTreeUtil.pathLocateFrom(state.current(), pathValues); } else { reference = DataTreeUtil.pathLocateFrom(state.current().getTreeRoot(), pathValues); } if (reference == null) { logFailure(pathValues); return null; } boolean updated = false; if (childMatch != ChildMatch.NONE) { ClosableIterator<DataTreeNode> children = null; try { children = reference.getIterator(); /** * Apply evaluation to all elements. * Do not short circuit on first update. */ while (children.hasNext()) { DataTreeNode child = children.next(); updated |= evaluateNode(state, pathValues, child); } } finally { if (children != null) { children.close(); } } } else { updated = evaluateNode(state, pathValues, reference); } return updated ? TreeMapState.empty() : null; } private boolean evaluateNode(TreeMapState state, String[] pathValues, DataTreeNode reference) { boolean updated = false; if (reference != null) { FieldValueList valueList = new FieldValueList(state.getFormat()); if (values.update(valueList, reference, state) == 0) { return false; } updated = childMatch.updateBundle(valueList, state.getBundle()); if (updated) { if (debug > 0) { debug(true); } } } else { logFailure(pathValues); } return updated; } private synchronized void debug(boolean hit) { if (hit) { match++; } else { miss++; } if ((match + miss) >= debug) { log.warn("query[{}]: match={} miss={}", debugKey, match, miss); match = 0; miss = 0; } } }