/* * 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.tree.prop; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map.Entry; import com.addthis.basis.collect.HotMap; import com.addthis.basis.util.LessStrings; import com.addthis.basis.util.Varint; import com.addthis.bundle.core.BundleField; 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.hydra.data.tree.DataTreeNode; import com.addthis.hydra.data.tree.DataTreeNodeUpdater; import com.addthis.hydra.data.tree.TreeDataParameters; import com.addthis.hydra.data.tree.TreeNodeData; import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import io.netty.buffer.Unpooled; public class DataMap extends TreeNodeData<DataMap.Config> implements SuperCodable { static final boolean IGNORE_DESERIALIZATION_ERROR = Boolean.getBoolean("hydra.tree.data.map"); /** * <p><span class="hydra-summary">maintains a KV map maintained as an LRU up to a given size</span>. * That is, new elements added to the map that would increase its size over the max are added but the element * of least interest based on last access time are deleted. * <p/> * <p>${attachment}={key} will return what (if anything) the map has stored for that key * <p/> * <p>%{attachment}={comma sep'd list of keys}[/+] will return all matching keys with a virtual node for each that represents * the stored value for that key. The /+ is what includes the virtual layer of child nodes containing the values.</p> * * @user-reference */ public static final class Config extends TreeDataParameters<DataMap> { /** * Field to get the map key from. This field is required. */ @FieldConfig(codable = true, required = true) private String key; /** * Field to get the mapped value from. This field is required. */ @FieldConfig(codable = true, required = true) private String val; /** * Size of the map. When a new key would be placed into the map, and it would put the size over this value, * the oldest entry is evicted. This field is required. */ @FieldConfig(codable = true, required = true) private Integer size; @Override public DataMap newInstance() { DataMap top = new DataMap(); top.size = size; return top; } } public DataMap() { } public DataMap(int size) { this.size = size; } @Override public void postDecode() { for (int i = 0; i < keys.length; i++) { map.put(keys[i], ValueFactory.create(vals[i])); } } /** * The temporary variables are used because it is possible * for concurrent serialization threads to be encoding the object. * The tree node that contains this data attachment is protected * by a reader lock for the encoding process. */ @Override public void preEncode() { String[] newKeys = new String[map.size()]; String[] newVals = new String[map.size()]; int pos = 0; for (Entry<String, ValueObject> next : map) { newKeys[pos] = next.getKey(); newVals[pos++] = next.getValue().toString(); } keys = newKeys; vals = newVals; } @FieldConfig(codable = true, required = true) private String[] keys; @FieldConfig(codable = true, required = true) private String[] vals; @FieldConfig(codable = true, required = true) private int size; private BundleField keyAccess; private BundleField valAccess; private HotMap<String, ValueObject> map = new HotMap<>(new HashMap()); @Override public boolean updateChildData(DataTreeNodeUpdater state, DataTreeNode childNode, DataMap.Config conf) { if (keyAccess == null) { keyAccess = state.getBundle().getFormat().getField(conf.key); valAccess = state.getBundle().getFormat().getField(conf.val); } ValueObject key = state.getBundle().getValue(keyAccess); ValueObject val = state.getBundle().getValue(valAccess); if (key != null) { put(key.toString(), val); return true; } else { return false; } } public void put(String key, ValueObject val) { synchronized (map) { if (val == null) { map.remove(key); } else { map.put(key, val); if (map.size() > size) { map.removeEldest(); } } } } @Override public ValueObject getValue(String key) { synchronized (map) { return map.get(key); } } /** * return types of synthetic nodes returned */ @Override public List<String> getNodeTypes() { return Arrays.asList(new String[]{"#"}); } @Override public List<DataTreeNode> getNodes(DataTreeNode parent, String key) { String[] keys = key != null ? LessStrings.splitArray(key, ",") : null; ArrayList<DataTreeNode> list = new ArrayList<>(map.size()); synchronized (map) { if (keys != null && keys.length > 0) { for (String k : keys) { ValueObject val = map.get(k); if (val != null) { VirtualTreeNode child = new VirtualTreeNode(val.toString(), 1); VirtualTreeNode vtn = new VirtualTreeNode(k, 1, new VirtualTreeNode[]{child}); list.add(vtn); } } } else { for (Entry<String, ValueObject> e : map) { VirtualTreeNode child = new VirtualTreeNode(e.getValue().toString(), 1); VirtualTreeNode vtn = new VirtualTreeNode(e.getKey(), 1, new VirtualTreeNode[]{child}); list.add(vtn); } } } return list; } @Override public byte[] bytesEncode(long version) { byte[] encodedBytes = null; ByteBuf buf = PooledByteBufAllocator.DEFAULT.buffer(); try { synchronized (map) { preEncode(); Varint.writeUnsignedVarInt(keys.length, buf); for (String key : keys) { writeString(buf, key); } for (String val : vals) { writeString(buf, val); } Varint.writeUnsignedVarInt(size, buf); } encodedBytes = new byte[buf.readableBytes()]; buf.readBytes(encodedBytes); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } finally { buf.release(); } return encodedBytes; } private void writeString(ByteBuf buf, String str) throws UnsupportedEncodingException { byte[] keyBytes = str.getBytes("UTF-8"); Varint.writeUnsignedVarInt(keyBytes.length, buf); buf.writeBytes(keyBytes); } @Override public void bytesDecode(byte[] b, long version) { ByteBuf buf = Unpooled.wrappedBuffer(b); try { int length = Varint.readUnsignedVarInt(buf); keys = new String[length]; vals = new String[length]; try { for (int i = 0; i < length; i++) { keys[i] = readString(buf); } for (int i = 0; i < length; i++) { vals[i] = readString(buf); } if (buf.readableBytes() > 0) { size = Varint.readUnsignedVarInt(buf); } else { if (!IGNORE_DESERIALIZATION_ERROR) { throw new RuntimeException("Tried to deserialize a corrupted DataMap attachment. " + "set the system property hydra.tree.data.map=true to ignore (Map Attachment will be empty on old nodes)"); } } } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } finally { buf.release(); } postDecode(); } private String readString(ByteBuf buf) throws UnsupportedEncodingException { int kl = Varint.readUnsignedVarInt(buf); byte[] kb = new byte[kl]; buf.readBytes(kb); return new String(kb, "UTF-8"); } public int getSize() { return size; } }