/* * 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 java.util.ArrayList; import java.util.List; import com.addthis.bundle.core.BundleField; import com.addthis.bundle.util.ValueUtil; import com.addthis.bundle.value.ValueFactory; import com.addthis.bundle.value.ValueMap; import com.addthis.bundle.value.ValueMapEntry; import com.addthis.bundle.value.ValueObject; import com.addthis.bundle.value.ValueString; import com.addthis.codec.annotations.FieldConfig; import com.addthis.hydra.data.filter.value.ValueFilter; import com.addthis.hydra.data.tree.DataTreeNode; /** * This {@link PathElement PathElement} <span class="hydra-summary">creates a single node with a specified value</span>. * <p/> * <p>Compare this path element to the "{@link PathKeyValue value}" path element. "value" creates one * or more nodes with a specified key.</p> * <p/> * <p>Example:</p> * <pre>paths : { * "ROOT" : [ * {type:"const", value:"date"}, * {type:"value", key:"DATE_YMD"}, * {type:"value", key:"DATE_HH"}, * ], * },</pre> * * @user-reference */ public class PathValue extends PathElement { public PathValue() { } public PathValue(String value) { this.value = value; } public PathValue(String value, boolean count) { this.value = value; this.count = count; } /** * Value to be stored in the constructed node. */ @FieldConfig(codable = true) protected String value; @FieldConfig(codable = true) protected String set; @FieldConfig(codable = true) protected ValueFilter vfilter; @FieldConfig(codable = true) protected boolean sync; @FieldConfig(codable = true) protected boolean create = true; @FieldConfig(codable = true) protected boolean once; @FieldConfig(codable = true) protected String mapTo; /** Deletes a node before attempting to (re)create or update it. Pure deletion requires {@code create: false}. */ @FieldConfig protected boolean delete; @FieldConfig(codable = true) protected boolean push; @FieldConfig(codable = true) protected PathElement each; /** * If positive then limit the number of nodes that can be created. * Multiple threads may be writing to the tree concurrently. * Therefore this limit is a best-effort limit it is not a strict * guarantee. Default is zero. **/ @FieldConfig(codable = true) protected int maxNodes = 0; private ValueString valueString; private BundleField setField; private BundleField mapField; public final ValueObject value() { if (valueString == null) { valueString = ValueFactory.create(value); } return valueString; } /** * override in subclasses */ public ValueObject getPathValue(final TreeMapState state) { return value(); } public final ValueObject getFilteredValue(final TreeMapState state) { ValueObject value = getPathValue(state); if (vfilter != null) { value = vfilter.filter(value, state.getBundle()); } return value; } @Override public void resolve(final TreeMapper mapper) { super.resolve(mapper); if (set != null) { setField = mapper.bindField(set); } if (mapTo != null) { mapField = mapper.bindField(mapTo); } if (each != null) { each.resolve(mapper); } } @Override public String toString() { return "PathValue[" + value + "]"; } /** * prevent subclasses from overriding as this is not used from here on */ @Override public final List<DataTreeNode> getNextNodeList(final TreeMapState state) { ValueObject value = getFilteredValue(state); if (setField != null) { state.getBundle().setValue(setField, value); } if (ValueUtil.isEmpty(value)) { return op ? TreeMapState.empty() : null; } if (op) { return TreeMapState.empty(); } List<DataTreeNode> list; if (sync) { synchronized (this) { list = processNodeUpdates(state, value); } } else { list = processNodeUpdates(state, value); } if (term) { if (list != null) { list.forEach(DataTreeNode::release); } return null; } else { return list; } } /** * Either get an existing node or optionally create a new node * if one does not exist. The {@link #create} field determines * whether or not to create a new node. If {@link #maxNodes} * is a positive integer then test the current node count * to determine whether to create a new node. * * @param state current state * @param name name of target node * @return existing node or newly created node */ public DataTreeNode getOrCreateNode(TreeMapState state, String name) { if (create && ((maxNodes == 0) || (state.getNodeCount() < maxNodes))) { return state.getOrCreateNode(name, state); } else { return state.getLeasedNode(name); } } /** * override this in subclasses. the rules for this path element are to be * applied to the child (next node) of the parent (current node). */ public List<DataTreeNode> processNodeUpdates(TreeMapState state, ValueObject name) { List<DataTreeNode> list = new ArrayList<>(1); int pushed = 0; if (name.getObjectType() == ValueObject.TYPE.ARRAY) { for (ValueObject o : name.asArray()) { if (o != null) { pushed += processNodeByValue(list, state, o); } } } else if (name.getObjectType() == ValueObject.TYPE.MAP) { ValueMap nameAsMap = name.asMap(); for (ValueMapEntry e : nameAsMap) { String key = e.getKey(); if (mapTo != null) { state.getBundle().setValue(mapField, e.getValue()); pushed += processNodeByValue(list, state, ValueFactory.create(key)); } else { PathValue mapValue = new PathValue(key, count); List<DataTreeNode> tnl = mapValue.processNode(state); if (tnl != null) { state.push(tnl); List<DataTreeNode> children = processNodeUpdates(state, e.getValue()); if (children != null) { list.addAll(children); } state.pop().release(); } } } } else { pushed += processNodeByValue(list, state, name); } while (pushed-- > 0) { state.pop().release(); } if (!list.isEmpty()) { return list; } else { return null; } } /** * can be called by subclasses to create/update nodes */ public final int processNodeByValue(List<DataTreeNode> list, TreeMapState state, ValueObject name) { if (each != null) { List<DataTreeNode> next = state.processPathElement(each); if (push) { if (next.size() > 1) { throw new RuntimeException("push and each are incompatible for > 1 return nodes"); } if (next.size() == 1) { state.push(next.get(0)); return 1; } } else if (next != null) { list.addAll(next); } return 0; } DataTreeNode parent = state.current(); String sv = ValueUtil.asNativeString(name); if (delete) { parent.deleteNode(sv); } /** get db for parent node once we're past it (since it has children) */ DataTreeNode child = getOrCreateNode(state, sv); boolean isnew = state.getAndClearLastWasNew(); /** can be null if parent is deleted by another thread or if create == false */ if (child == null) { return 0; } /* bail if only new nodes are required */ if (once && !isnew) { child.release(); return 0; } try { /** child node accounting and custom data updates */ if (assignHits()) { state.setAssignmentValue(hitsField.getLong(state.getBundle()).orElse(0l)); } child.updateChildData(state, this); /** update node data accounting */ parent.updateParentData(state, child, isnew); if (push) { state.push(child); return 1; } else { list.add(child); return 0; } } catch (Throwable t) { child.release(); throw t; } } }