diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /config-lib/src/main/java/com |
Publish
Diffstat (limited to 'config-lib/src/main/java/com')
24 files changed, 1720 insertions, 0 deletions
diff --git a/config-lib/src/main/java/com/yahoo/config/BooleanNode.java b/config-lib/src/main/java/com/yahoo/config/BooleanNode.java new file mode 100644 index 00000000000..8347b800272 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/BooleanNode.java @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The BooleanNode class represents a boolean in a configuration tree. + */ +public class BooleanNode extends LeafNode<Boolean> { + public BooleanNode() { + } + + public BooleanNode(boolean value) { + super(true); + this.value = value; + } + + public Boolean value() { + return value; + } + + @Override + public String getValue() { + return "" + value; + } + + @Override + public String toString() { + return getValue(); + } + + @Override + protected boolean doSetValue(@NonNull String value) { + if (! value.equalsIgnoreCase("false") && ! value.equalsIgnoreCase("true")) { + return false; + } + this.value = Boolean.valueOf(value); + return true; + } + + @Override + void serialize(String name, Serializer serializer) { + serializer.serialize(name, value); + } + + @Override + void serialize(Serializer serializer) { + serializer.serialize(value); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/ChangesRequiringRestart.java b/config-lib/src/main/java/com/yahoo/config/ChangesRequiringRestart.java new file mode 100644 index 00000000000..6c17a21c649 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/ChangesRequiringRestart.java @@ -0,0 +1,162 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.joining; + +/** + * @author <a href="mailto:magnarn@yahoo-inc.com">Magnar Nedland</a> + * + * This class aggregates information about config changes that causes a restart to be required. + */ +public class ChangesRequiringRestart { + static class ReportLine { + private String name; + private final Node from; + private final Node to; + private final String comment; + + public ReportLine(String name, Node from, Node to, String comment) { + this.name = name; + this.from = from; + this.to = to; + this.comment = comment; + } + + public void addNamePrefix(String prefix) { + if (!name.isEmpty()) { + name = prefix + "." + name; + } else { + name = prefix; + } + } + + private String getCommentAndName(String indent, String namePrefix) { + return indent + (comment.isEmpty()? "" : "# " + comment.replace("\n", "\n" + indent + "# ") + "\n" + indent) + + namePrefix + name; + } + + private static String formatValue(String indent, Node n) { + String str = n.toString(); + if (str.contains("\n")) { // Struct + str = "\n" + indent + " { " + str.replace("\n", "\n" + indent + " ") + " }"; + } + return str; + } + + @Override + public String toString() { + return toString("", ""); + } + + public String toString(String indent, String namePrefix) { + if (from == null) { + return getCommentAndName(indent, namePrefix) + " was added with value " + formatValue(indent, to); + } else if (to == null) { + return getCommentAndName(indent, namePrefix) + " with value " + formatValue(indent, from) + " was removed"; + } + return getCommentAndName(indent, namePrefix) + " has changed from " + formatValue(indent, from) + " to " + formatValue(indent, to); + } + } + + private ArrayList<ReportLine> report = new ArrayList<>(); + private String componentName; + + public ChangesRequiringRestart(String componentName) { + this.componentName = componentName; + } + + public String getName() { + return componentName; + } + + public ChangesRequiringRestart compare(Node from, Node to, String name, String comment) { + if (!from.equals(to)) { + report.add(new ReportLine(name, from, to, comment)); + } + return this; + } + + public void mergeChanges(String prefix, ChangesRequiringRestart childReport) { + for (ReportLine line : childReport.getReportLines()) { + line.addNamePrefix(prefix); + report.add(line); + } + } + + /** + * Interface used to pass lambda functions from generated code to compareArray/-Map functions. + */ + public interface CompareFunc { + // Generates a report based on a config change. + ChangesRequiringRestart getChangesRequiringRestart(Node from, Node to); + } + + public ChangesRequiringRestart compareArray(List<? extends Node> from, + List<? extends Node> to, + String name, + String comment, + CompareFunc func) { + if (!from.equals(to)) { + int commonElements = Math.min(from.size(), to.size()); + for (int i = 0; i < commonElements; ++i) { + ChangesRequiringRestart childReport = func.getChangesRequiringRestart(from.get(i), to.get(i)); + String prefix = childReport.componentName + "[" + Integer.toString(i) + "]"; + mergeChanges(prefix, childReport); + } + for (int i = commonElements; i < from.size(); ++i) { + report.add(new ReportLine(name + "[" + Integer.toString(i) + "]", from.get(i), null, comment)); + } + for (int i = commonElements; i < to.size(); ++i) { + report.add(new ReportLine(name + "[" + Integer.toString(i) + "]", null, to.get(i), comment)); + } + } + return this; + } + + public ChangesRequiringRestart compareMap(Map<String, ? extends Node> from, + Map<String, ? extends Node> to, + String name, + String comment, + CompareFunc func) { + if (!from.equals(to)) { + for (String key : from.keySet()) { + if (to.containsKey(key)) { + ChangesRequiringRestart childReport = func.getChangesRequiringRestart(from.get(key), to.get(key)); + String prefix = childReport.componentName + "{" + key + "}"; + mergeChanges(prefix, childReport); + } else { + report.add(new ReportLine(name + "{" + key + "}", from.get(key), null, comment)); + } + } + for (String key : to.keySet()) { + if (!from.containsKey(key)) { + report.add(new ReportLine(name + "{" + key + "}", null, to.get(key), comment)); + } + } + } + return this; + } + + List<ReportLine> getReportLines() { + return report; + } + + @Override + public String toString() { + return toString(""); + } + + public String toString(String indent) { + return report.stream() + .map(line -> line.toString(indent, componentName + ".")) + .collect(joining("\n")); + } + + public boolean needsRestart() { + return !report.isEmpty(); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/ConfigBuilder.java b/config-lib/src/main/java/com/yahoo/config/ConfigBuilder.java new file mode 100644 index 00000000000..dc90df9b12f --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/ConfigBuilder.java @@ -0,0 +1,11 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +/** + * Root interface for all config builders. + * + * @author gjoranv + * @since 5.1.6 + */ +public interface ConfigBuilder { +} diff --git a/config-lib/src/main/java/com/yahoo/config/ConfigInstance.java b/config-lib/src/main/java/com/yahoo/config/ConfigInstance.java new file mode 100644 index 00000000000..bb091a6b4e1 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/ConfigInstance.java @@ -0,0 +1,122 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Represents an instance of an application config with a specific configId. + * <p> + * An instance of this class contains all values (represented by Nodes) for the config object as it + * is the superclass of the generated config class used by the client. + */ +public abstract class ConfigInstance extends InnerNode { + + public interface Builder extends ConfigBuilder { + /** + * Dispatches a getConfig() call if this instance's producer is of the right type + * @param producer a config producer + * @return true if this instance's producer was the correct type, and hence a getConfig call was dispatched + */ + public boolean dispatchGetConfig(Producer producer); + + public String getDefName(); + public String getDefNamespace(); + public String getDefMd5(); + } + + public interface Producer {} + + private String configMd5 = ""; + + String configId; + + /** + * Gets the name of the given config instance + */ + public static String getDefName(Class<?> type) { + return getStaticStringField(type, "CONFIG_DEF_NAME"); + } + + /** + * Gets the namespace of the given config instance + */ + public static String getDefNamespace(Class<?> type) { + return getStaticStringField(type, "CONFIG_DEF_NAMESPACE"); + } + + /** + * Returns the serialized representation of the given node. + * <p> + * Declared static, instead of InnerNode member, to avoid a public 0-arg method with a commonly used name. + * + * @param node The inner node + * @return a list of strings, containing the serialized representation of this config + */ + public static List<String> serialize(InnerNode node) { + List<String> ret = new ArrayList<>(); + for (Map.Entry<String, LeafNode<?>> entry : getAllDescendantLeafNodes(node).entrySet()) { + ret.add(entry.getKey() + " " + entry.getValue().toString()); + } + return ret; + } + + public static void serialize(InnerNode node, Serializer serializer) { + serializeMap(node.getChildren(), serializer); + } + + @SuppressWarnings("unchecked") + private static void serializeObject(String name, Object child, Serializer serializer) { + if (child instanceof InnerNode) { + Serializer childSerializer = serializer.createInner(name); + serialize((InnerNode) child, childSerializer); + } else if (child instanceof Map) { + Serializer mapSerializer = serializer.createMap(name); + serializeMap((Map<String, Object>)child, mapSerializer); + } else if (child instanceof NodeVector) { + Serializer arraySerializer = serializer.createArray(name); + serializeArray((NodeVector) child, arraySerializer); + } else if (child instanceof LeafNode) { + ((LeafNode) child).serialize(name, serializer); + } + } + + private static void serializeMap(Map<String, Object> childMap, Serializer serializer) { + for (Map.Entry<String, Object> entry : childMap.entrySet()) { + String name = entry.getKey(); + Object child = entry.getValue(); + serializeObject(name, child, serializer); + } + } + + private static void serializeArray(NodeVector<?> nodeVector, Serializer arraySerializer) { + for (Object child : nodeVector.vector) { + if (child instanceof InnerNode) { + Serializer childSerializer = arraySerializer.createInner(); + serialize((InnerNode) child, childSerializer); + } else if (child instanceof LeafNode) { + ((LeafNode) child).serialize(arraySerializer); + } + } + } + + + public String getConfigMd5() { + return configMd5; + } + + public void setConfigMd5(String configMd5) { + this.configMd5 = configMd5; + } + + private static String getStaticStringField(Class<?> type, String fieldName) { + try { + return (String) type.getField(fieldName).get(null); + } catch (Exception e) { + throw new RuntimeException + (e.getMessage() + ": Static field " + fieldName + " not " + "accessible in " + type.getName()); + } + } + +} diff --git a/config-lib/src/main/java/com/yahoo/config/ConfigurationRuntimeException.java b/config-lib/src/main/java/com/yahoo/config/ConfigurationRuntimeException.java new file mode 100644 index 00000000000..56020906499 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/ConfigurationRuntimeException.java @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +/** + * This exception is thrown on internal errors in the configuration system. + */ +@SuppressWarnings("serial") +public class ConfigurationRuntimeException extends RuntimeException { + public ConfigurationRuntimeException(String message) { + super(message); + } + + public ConfigurationRuntimeException(String message, Throwable cause) { + super(message, cause); + } + + public ConfigurationRuntimeException(Throwable cause) { + super(cause); + } + +}
\ No newline at end of file diff --git a/config-lib/src/main/java/com/yahoo/config/DoubleNode.java b/config-lib/src/main/java/com/yahoo/config/DoubleNode.java new file mode 100644 index 00000000000..6ff97df579f --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/DoubleNode.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The DoubleNode class represents a double in a configuration tree. + */ +public class DoubleNode extends LeafNode<Double> { + public DoubleNode() { + } + + public DoubleNode(double value) { + super(true); + this.value = value; + } + + public Double value() { + return value; + } + + @Override + public String getValue() { + return "" + value; + } + + @Override + public String toString() { + return getValue(); + } + + @Override + protected boolean doSetValue(@NonNull String value) { + try { + this.value = Double.parseDouble(value); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + @Override + void serialize(String name, Serializer serializer) { + serializer.serialize(name, value); + } + + @Override + void serialize(Serializer serializer) { + serializer.serialize(value); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/EnumNode.java b/config-lib/src/main/java/com/yahoo/config/EnumNode.java new file mode 100644 index 00000000000..5443d063bbd --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/EnumNode.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +/** + * The EnumNode class is a superclass for Enumerations in a configuration tree. + */ +public abstract class EnumNode<ENUM extends Enum<?>> extends LeafNode<ENUM> { + public EnumNode() { + } + + public EnumNode(boolean b) { + super(b); + } + + @Override + public String toString() { + return (value == null) ? "(null)" : value.toString(); + } + + @Override + public String getValue() { + return (value == null) ? null : value.toString(); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/FileNode.java b/config-lib/src/main/java/com/yahoo/config/FileNode.java new file mode 100644 index 00000000000..0d6bccb59a5 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/FileNode.java @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Represents a 'file' in a {@link ConfigInstance}, usually a filename. + * + * @author gjoranv + */ +public class FileNode extends LeafNode<FileReference> { + + public FileNode() { + } + + public FileNode(String stringVal) { + super(true); + this.value = new FileReference(ReferenceNode.stripQuotes(stringVal)); + } + + public FileReference value() { + return value; + } + + @Override + public String getValue() { + return value.value(); + } + + @Override + public String toString() { + return (value == null) ? "(null)" : '"' + getValue() + '"'; + } + + @Override + protected boolean doSetValue(@NonNull String stringVal) { + value = new FileReference(ReferenceNode.stripQuotes(stringVal)); + return true; + } + + +} diff --git a/config-lib/src/main/java/com/yahoo/config/FileReference.java b/config-lib/src/main/java/com/yahoo/config/FileReference.java new file mode 100755 index 00000000000..de86fc09be7 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/FileReference.java @@ -0,0 +1,67 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * An immutable file reference that can only be created from classes within the same package. + * This is to prevent clients from creating arbitrary and invalid file references. + * + * @author tonytv + */ +public final class FileReference { + + private final String value; + + FileReference(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object other) { + return other instanceof FileReference && + value.equals(((FileReference)other).value); + } + + @Override + public String toString() { + return "file '" + value + "'"; + } + + public static List<String> toValues(Collection<FileReference> references) { + List<String> ret = new ArrayList<String>(); + for (FileReference r: references) { + ret.add(r.value()); + } + return ret; + } + + public static Map<String, String> toValueMap(Map<String, FileReference> map) { + Map<String, String> ret = new LinkedHashMap<>(); + for (Map.Entry<String, FileReference> e : map.entrySet()) { + ret.put(e.getKey(), e.getValue().value()); + } + return ret; + } + + public static FileReference mockFileReferenceForUnitTesting(File file) { + if (! file.exists()) + throw new IllegalArgumentException("File '" + file.getAbsolutePath() + "' does not exist."); + return new FileReference(file.getPath()); + } + +} diff --git a/config-lib/src/main/java/com/yahoo/config/InnerNode.java b/config-lib/src/main/java/com/yahoo/config/InnerNode.java new file mode 100644 index 00000000000..eb7f36bbaea --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/InnerNode.java @@ -0,0 +1,171 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Superclass for all inner nodes in a {@link ConfigInstance}. + * <p> + * This class cannot have non-private members because such members + * will interfere with the members in the generated subclass. + * + * @author gjoranv + */ +public abstract class InnerNode extends Node { + + /** + * Creates a new InnerNode. + */ + public InnerNode() { + } + + @Override + public String toString() { + return mkString(ConfigInstance.serialize(this), "\n"); + } + + private static <T> String mkString(Collection<T> collection, String sep) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + + for (T elem : collection) { + if (! first) + sb.append(sep); + first = false; + sb.append(elem); + } + return sb.toString(); + } + + /** + * Overrides {@link Node#postInitialize(String)}. + * Perform post initialization on this nodes children. + * + * @param configId The config id of this instance. + */ + @Override + public void postInitialize(String configId) { + Map<String, Node> children = getChildrenWithVectorsFlattened(); + for (Node node : children.values()) { + node.postInitialize(configId); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) + return true; + + if ( !(other instanceof InnerNode) || (other.getClass() != this.getClass())) + return false; + + Collection<Object> children = getChildren().values(); + Collection<Object> otherChildren = ((InnerNode)other).getChildren().values(); + + Iterator<Object> e1 = children.iterator(); + Iterator<Object> e2 = otherChildren.iterator(); + while(e1.hasNext() && e2.hasNext()) { + Object o1 = e1.next(); + Object o2 = e2.next(); + if (!(o1 == null ? o2 == null : o1.equals(o2))) + return false; + } + return !(e1.hasNext() || e2.hasNext()); + } + + @Override + public int hashCode() { + int res = 17; + for (Object child : getChildren().values()) + res = 31 * res + child.hashCode(); + return res; + } + + protected Map<String, Object> getChildren() { + HashMap<String, Object> ret = new LinkedHashMap<String, Object>(); + Field fields[] = getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + Object fieldValue; + try { + fieldValue = field.get(this); + } catch (IllegalAccessException e) { + throw new ConfigurationRuntimeException(e); + } + if (fieldValue instanceof Node + || fieldValue instanceof NodeVector<?> + || fieldValue instanceof Map<?,?>) + ret.put(field.getName(), fieldValue); + } + return ret; + } + + /** + * Returns a flat map of this node's direct children, including all NodeVectors' elements. + * Keys are the node name, including index for vector elements, e.g. 'arr[0]'. + */ + @SuppressWarnings("unchecked") + protected Map<String, Node> getChildrenWithVectorsFlattened() { + HashMap<String, Node> ret = new LinkedHashMap<String, Node>(); + + Map<String, Object> children = getChildren(); + for (Map.Entry<String, Object> childEntry : children.entrySet()) { + String name = childEntry.getKey(); + Object child = childEntry.getValue(); + if (child instanceof NodeVector) { + addNodeVectorEntries(ret, name, (NodeVector<?>) child); + } else if (child instanceof Map<?,?>) { + addMapEntries(ret, name, (Map<String, Node>) child); + } else if (child instanceof Node) { + ret.put(name, (Node)child); + } + } + return ret; + } + + private static void addMapEntries(HashMap<String, Node> ret, String name, Map<String, Node> map) { + for (Map.Entry<String, Node> entry : map.entrySet()) + ret.put(name + "{" + entry.getKey() + "}", entry.getValue()); + } + + + private static void addNodeVectorEntries(HashMap<String, Node> ret, String name, NodeVector<?> vector) { + for (int j = 0; j < vector.length(); j++) + ret.put(name + "[" + j + "]", (Node) vector.get(j)); + } + + protected static Map<String, LeafNode<?>> getAllDescendantLeafNodes(InnerNode node) { + return getAllDescendantLeafNodes("", node); + } + + /** + * Generates a map of all leaf nodes, with full.paths[3] in key + * + * @param parentName Name of the parent node, can be empty. + * @param node The node to get leaf nodes for. + * @return map of leaf nodes + */ + private static Map<String, LeafNode<?>> getAllDescendantLeafNodes(String parentName, InnerNode node) { + Map<String, LeafNode<?>> ret = new LinkedHashMap<String, LeafNode<?>>(); + String prefix = parentName.isEmpty() ? "" : parentName + "."; + Map<String, Node> children = node.getChildrenWithVectorsFlattened(); + for (Map.Entry<String, Node> childEntry : children.entrySet()) { + String name = childEntry.getKey(); + String prefixedName = prefix + name; + name=name.replaceAll("\\[.*", ""); + Node child = childEntry.getValue(); + if (child instanceof LeafNode) { + ret.put(prefixedName, (LeafNode<?>) child); + } else if (child instanceof InnerNode) { + ret.putAll(getAllDescendantLeafNodes(prefixedName, (InnerNode) child)); + } + } + return ret; + } + +} diff --git a/config-lib/src/main/java/com/yahoo/config/InnerNodeVector.java b/config-lib/src/main/java/com/yahoo/config/InnerNodeVector.java new file mode 100644 index 00000000000..72ca0138a53 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/InnerNodeVector.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import java.util.List; + +/** + * @author gjoranv + * @since 5.1.4 + */ +public class InnerNodeVector<NODE extends InnerNode> extends NodeVector<NODE> { + + NODE defaultNode; + + /** + * Creates a new vector with the given default node. + */ + // TODO: remove this ctor when the library uses reflection via builders, and resizing won't be necessary + public InnerNodeVector(NODE defaultNode) { + assert (defaultNode != null) : "The default node cannot be null"; + + this.defaultNode = defaultNode; + if (createNew() == null) { + throw new NullPointerException("Unable to duplicate the default node."); + } + } + + public InnerNodeVector(List<NODE> nodes, NODE defaultNode) { + this(defaultNode); + for (NODE node : nodes) { + vector.add(node); + } + } + + /** + * Creates a new Node by creating a new instance with the 0-argument constructor + */ + // TODO: remove when the library uses reflection via builders + @SuppressWarnings("unchecked") + protected NODE createNew() { + try { + return (NODE) defaultNode.getClass().newInstance(); + } catch (IllegalAccessException | InstantiationException ex) { + throw new ConfigurationRuntimeException(ex); + } + } + +} diff --git a/config-lib/src/main/java/com/yahoo/config/IntegerNode.java b/config-lib/src/main/java/com/yahoo/config/IntegerNode.java new file mode 100644 index 00000000000..82db4cab030 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/IntegerNode.java @@ -0,0 +1,52 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The IntegerNode class represents an integer in a configuration tree. + */ +public class IntegerNode extends LeafNode<Integer> { + + public IntegerNode() { + } + + public IntegerNode(int value) { + super(true); + this.value = value; + } + + public Integer value() { + return value; + } + + @Override + public String getValue() { + return "" + value; + } + + @Override + public String toString() { + return getValue(); + } + + @Override + protected boolean doSetValue(@NonNull String value) { + try { + this.value = Integer.parseInt(value); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + @Override + void serialize(String name, Serializer serializer) { + serializer.serialize(name, value); + } + + @Override + void serialize(Serializer serializer) { + serializer.serialize(value); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/LeafNode.java b/config-lib/src/main/java/com/yahoo/config/LeafNode.java new file mode 100644 index 00000000000..24ec534e222 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/LeafNode.java @@ -0,0 +1,120 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Superclass for all leaf nodes in a {@link ConfigInstance}. + * <p> + * Subclasses represents leaf nodes with different types. These + * implementations should implement method value() with return-value + * corresponding to the actual type. + * + */ +public abstract class LeafNode<T> extends Node implements Cloneable { + + protected boolean initialized; + protected T value; + + /** + * Creates a new, uninitialized LeafNode + */ + protected LeafNode() { + initialized = false; + } + + /** + * Creates a new LeafNode. + * + * @param initialized true if this node is initialized. + */ + protected LeafNode(boolean initialized) { + this.initialized = initialized; + } + + public T value() { + return value; + } + + /** + * Try to initialize this node with the given value. Returns true + * on success, false otherwise. + * + * @param value the string represention of the desired node value. + * @return true on success, false otherwise. + */ + final boolean initialize(String value) { + boolean success = setValue(value); + initialized |= success; + return success; + } + + /** + * Subclasses must implement this, in compliance with the rules given in the return tag. + * + * @return the String representation of the node value, or the string "(null)" if the value is null. + */ + public abstract String toString(); + + /** + * Subclasses must implement this, in compliance with the rules given in the return tag. + * + * @return the String representation of the node value, or the 'null' object if the node value is null. + */ + public abstract String getValue(); + + /** + * Sets the value based on a string representation. Returns false if the value could + * not be set from the given string. + * TODO: return void (see doSetValue) + * + * @param value the value to set + * @return true on success, false otherwise + * @throws IllegalArgumentException when value is null + */ + protected final boolean setValue(String value) { + if (value == null) + throw new IllegalArgumentException("Null value is not allowed"); + return doSetValue(value); + } + + // TODO: should throw exception instead of return false. + protected abstract boolean doSetValue(@NonNull String value); + + /** + * This method is meant for internal use in the configuration + * system. Overrides Object.clone(). + * + * @return a new instance similar to this object. + */ + @Override + protected Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new ConfigurationRuntimeException(e); + } + } + + @Override + public boolean equals(Object o) { + if (! (o instanceof LeafNode)) + return false; + + LeafNode<?> other = (LeafNode)o; + return value == null ? other.value == null : value().equals(other.value); + } + + @Override + public int hashCode() { + return (value == null) ? 0 : value.hashCode(); + } + + void serialize(String name, Serializer serializer) { + serializer.serialize(name, getValue()); + } + + void serialize(Serializer serializer) { + serializer.serialize(getValue()); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/LeafNodeMaps.java b/config-lib/src/main/java/com/yahoo/config/LeafNodeMaps.java new file mode 100644 index 00000000000..789969662da --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/LeafNodeMaps.java @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @author gjoranv + * @since 5.1.17 + */ +public class LeafNodeMaps { + + /** + * Converts a map of String→NODE to String→REAL, where REAL is the underlying value type. + */ + public static <NODE extends LeafNode<REAL>, REAL> + Map<String, REAL> asValueMap(Map<String, NODE> input) + { + Map<String, REAL> ret = new LinkedHashMap<>(); + for(String key : input.keySet()) { + ret.put(key, input.get(key).value()); + } + return Collections.unmodifiableMap(ret); + } + + /** + * Converts a map of String→REAL to String→NODE, where REAL is the underlying value type. + */ + @SuppressWarnings("unchecked") + public static <NODE extends LeafNode<REAL>, REAL> + Map<String, NODE> asNodeMap(Map<String, REAL> input, NODE defaultNode) + { + Map<String, NODE> ret = new LinkedHashMap<>(); + for(String key : input.keySet()) { + NODE node = (NODE)defaultNode.clone(); + node.value = input.get(key); + ret.put(key, node); + } + return Collections.unmodifiableMap(ret); + } + + + /** + * Special case for file type, since FileNode param type (FileReference) is not same as type (String) in config builder + */ + public static Map<String, FileNode> asFileNodeMap(Map<String, String> stringMap) { + Map<String, FileNode> fileNodeMap = new LinkedHashMap<>(); + for (Map.Entry<String, String> e : stringMap.entrySet()) { + fileNodeMap.put(e.getKey(), new FileNode(e.getValue())); + } + return Collections.unmodifiableMap(fileNodeMap); + } + + public static Map<String, PathNode> asPathNodeMap(Map<String, FileReference> fileReferenceMap) { + Map<String, PathNode> pathNodeMap = new LinkedHashMap<>(); + for (Map.Entry<String, FileReference> e : fileReferenceMap.entrySet()) { + pathNodeMap.put(e.getKey(), new PathNode(e.getValue())); + } + return Collections.unmodifiableMap(pathNodeMap); + } + +} diff --git a/config-lib/src/main/java/com/yahoo/config/LeafNodeVector.java b/config-lib/src/main/java/com/yahoo/config/LeafNodeVector.java new file mode 100644 index 00000000000..59e070f1d56 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/LeafNodeVector.java @@ -0,0 +1,79 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * A vector of leaf nodes. + * + * @author gjoranv + * @since 5.1.4 + */ +public class LeafNodeVector<REAL, NODE extends LeafNode<REAL>> extends NodeVector<NODE> { + + NODE defaultNode; + + /** + * Creates a new vector with the given default node. + */ + // TODO: remove this ctor when the library uses reflection via builders, and resizing won't be necessary + public LeafNodeVector(NODE defaultNode) { + assert (defaultNode != null) : "The default node cannot be null"; + + this.defaultNode = defaultNode; + if (createNew() == null) { + throw new NullPointerException("Unable to duplicate the default node."); + } + } + + // TODO: take class instead of default node when the library uses reflection via builders + public LeafNodeVector(List<REAL> values, NODE defaultNode) { + this(defaultNode); + for (REAL value : values) { + NODE node = createNew(); + node.value = value; + vector.add(node); + } + } + + /** + * Creates a new Node by cloning the default node. + */ + @SuppressWarnings("unchecked") + protected NODE createNew() { + return (NODE) (defaultNode).clone(); + } + + // TODO: create unmodifiable list in ctor when the library uses reflection via builders + @SuppressWarnings("unchecked") + public List<REAL> asList() { + List<REAL> ret = new ArrayList<REAL>(); + for(NODE node : vector) { + ret.add(node.value()); + } + return Collections.unmodifiableList(ret); + } + + // TODO: Try to eliminate the need for this method when we have moved FileAcquirer to the config library + // It is needed now because the builder has a list of String, while REAL=FileReference. + public static LeafNodeVector<FileReference, FileNode> createFileNodeVector(Collection<String> values) { + List<FileReference> fileReferences = new ArrayList<FileReference>(); + for (String s : values) + fileReferences.add(new FileReference(ReferenceNode.stripQuotes(s))); + + return new LeafNodeVector<FileReference, FileNode>(fileReferences, new FileNode()); + } + + public static LeafNodeVector<Path, PathNode> createPathNodeVector(Collection<FileReference> values) { + List<Path> paths = new ArrayList<>(); + for (FileReference fileReference : values) + paths.add(Paths.get(fileReference.value())); + + return new LeafNodeVector<Path, PathNode>(paths, new PathNode()); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/LongNode.java b/config-lib/src/main/java/com/yahoo/config/LongNode.java new file mode 100755 index 00000000000..7a3b29c1101 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/LongNode.java @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Represents a long in a configuration tree. + * @author gjoranv + */ +public class LongNode extends LeafNode<Long> { + + public LongNode() { + } + + public LongNode(long value) { + super(true); + this.value = value; + } + + public Long value() { + return value; + } + + @Override + public String getValue() { + return "" + value; + } + + @Override + public String toString() { + return getValue(); + } + + @Override + protected boolean doSetValue(@NonNull String value) { + try { + this.value = Long.parseLong(value); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + @Override + void serialize(String name, Serializer serializer) { + serializer.serialize(name, value); + } + + @Override + void serialize(Serializer serializer) { + serializer.serialize(value); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/Node.java b/config-lib/src/main/java/com/yahoo/config/Node.java new file mode 100644 index 00000000000..3dba5d083f4 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/Node.java @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +/** + * The Node class is superclass for all nodes in a {@link + * ConfigInstance}. Important subclasses of this node are {@link + * InnerNode} and {@link LeafNode}. + * + */ +public abstract class Node { + + /** + * Postinitialize this node. Any node needing to process its values depending on the config + * id should override this method. + * + * @param configId the configId of the ConfigInstance that owns (or is) this node + */ + public void postInitialize(String configId) { return; } + + /** + * This method is meant for internal use in the configuration system. + * Overrides Object.clone(), and is overriden by LeafNode.clone(). + * + * @return a new instance similar to this object. + */ + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/NodeVector.java b/config-lib/src/main/java/com/yahoo/config/NodeVector.java new file mode 100644 index 00000000000..8ce5689937e --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/NodeVector.java @@ -0,0 +1,151 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import java.util.*; + + +/** + * A NodeVector represents an array declared with '[]' in a config definition file. + * It is a List that stores nodes with a given type. A given default node must + * be given, and this node will be cloned as the NodeVector size are increased. + * + */ +public abstract class NodeVector<NODE> implements java.util.List<NODE> { + + protected final ArrayList<NODE> vector = new ArrayList<NODE>(); + + /** + * Creates a new Node. + */ + protected abstract NODE createNew(); + + /** + * Returns the number of elements in this NodeVector. + * Alias for size(). + * + * @return the number of elements in this NodeVector. + */ + public int length() { + return size(); + } + + /** + * Resizes this NodeVector. Removes or adds new nodes as needed. + * + * @param n the new size of this NodeVector + */ + // TODO: remove when the library uses reflection via builders, and resizing won't be necessary + public void setSize(int n) { + while (size() > n) vector.remove(n); + while (size() < n) vector.add(createNew()); + } + + @SuppressWarnings("serial") + public static class ReadOnlyException extends RuntimeException { + } + + private static final ReadOnlyException e = new ReadOnlyException(); + + public void add(int index, NODE element) { + throw e; + } + + public boolean add(NODE o) { + throw e; + } + + public boolean addAll(Collection<? extends NODE> c) { + throw e; + } + + public boolean addAll(int index, Collection<? extends NODE> c) { + throw e; + } + + public void clear() { + throw e; + } + + public NODE remove(int index) { + throw e; + } + + public boolean remove(Object o) { + throw e; + } + + public boolean removeAll(Collection<?> c) { + throw e; + } + + public boolean retainAll(Collection<?> c) { + throw e; + } + + public NODE set(int index, NODE element) { + throw e; + } + + public boolean contains(Object o) { + return vector.contains(o); + } + + public boolean containsAll(Collection<?> c) { + return vector.containsAll(c); + } + + @Override + public boolean equals(Object o) { + return o instanceof NodeVector && vector.equals(((NodeVector) o).vector); + } + + @Override + public int hashCode() { + return vector.hashCode(); + } + + @SuppressWarnings("unchecked") + public NODE get(int index) { + return vector.get(index); + } + + public int indexOf(Object o) { + return vector.indexOf(o); + } + + public boolean isEmpty() { + return vector.isEmpty(); + } + + public Iterator<NODE> iterator() { + return vector.iterator(); + } + + public int lastIndexOf(Object o) { + return vector.lastIndexOf(o); + } + + public ListIterator<NODE> listIterator() { + return vector.listIterator(); + } + + public ListIterator<NODE> listIterator(int index) { + return vector.listIterator(index); + } + + public int size() { + return vector.size(); + } + + public List<NODE> subList(int fromIndex, int toIndex) { + return vector.subList(fromIndex, toIndex); + } + + public Object[] toArray() { + return vector.toArray(); + } + + public <T> T[] toArray(T[] a) { + return vector.toArray(a); + } +} diff --git a/config-lib/src/main/java/com/yahoo/config/PathNode.java b/config-lib/src/main/java/com/yahoo/config/PathNode.java new file mode 100644 index 00000000000..91676137214 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/PathNode.java @@ -0,0 +1,71 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a 'path' in a {@link ConfigInstance}, usually a filename. + * + * @author gjoranv + * @since 5.1.30 + */ +public class PathNode extends LeafNode<Path> { + + private final FileReference fileReference; + + public PathNode() { + fileReference = null; + } + + public PathNode(FileReference fileReference) { + super(true); + this.value = new File(fileReference.value()).toPath(); + this.fileReference = fileReference; + } + + public Path value() { + return value; + } + + @Override + public String getValue() { + return value.toString(); + } + + @Override + public String toString() { + return (value == null) ? "(null)" : '"' + getValue() + '"'; + } + + @Override + protected boolean doSetValue(@NonNull String stringVal) { + throw new UnsupportedOperationException("doSetValue should not be necessary since the library anymore!"); + } + + public FileReference getFileReference() { + return fileReference; + } + + public static List<FileReference> toFileReferences(List<PathNode> pathNodes) { + List<FileReference> fileReferences = new ArrayList<>(); + for (PathNode pathNode : pathNodes) + fileReferences.add(pathNode.getFileReference()); + return fileReferences; + } + + public static Map<String, FileReference> toFileReferenceMap(Map<String, PathNode> map) { + Map<String, FileReference> ret = new LinkedHashMap<>(); + for (Map.Entry<String, PathNode> e : map.entrySet()) { + ret.put(e.getKey(), e.getValue().getFileReference()); + } + return ret; + } + +} diff --git a/config-lib/src/main/java/com/yahoo/config/ReferenceNode.java b/config-lib/src/main/java/com/yahoo/config/ReferenceNode.java new file mode 100644 index 00000000000..a5e6400f456 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/ReferenceNode.java @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A ReferenceNode class represents a reference (that is a config-id) + * in a {@link ConfigInstance}. + */ +public class ReferenceNode extends LeafNode<String> { + + public ReferenceNode() { + } + + /** + * Creates a new ReferenceNode with the given value. + * + * @param value the value of this ReferenceNode + */ + public ReferenceNode(String value) { + super(true); + this.value = stripQuotes(value); + } + + /** + * Returns the value of this reference node. Same as {@link + * #toString()} since the value of a ReferenceNode is a String (but + * implementations in other {@link LeafNode} sub-classes differ). + * + * @return the string representation of this ReferenceNode. + */ + public String value() { + return value; + } + + @Override + public String getValue() { + return value(); + } + + @Override + public String toString() { + return (value == null) ? "(null)" : getValue(); + } + + @Override + protected boolean doSetValue(@NonNull String value) { + this.value = stripQuotes(value); + return true; + } + + /** + * Overrides {@link Node#postInitialize(String)} + * Checks for ":parent:" values, which will be replaced by the configId. + * + * @param configId the configId of the ConfigInstance that owns (or is) this node + */ + @Override + public void postInitialize(String configId) { + super.postInitialize(configId); + if (":parent:".equals(value())) { + doSetValue(configId); + } + } + + /** + * Strips the quotes before or after the value, if present. + */ + static String stripQuotes(String value) { + if (value == null) { + return value; + } + StringBuffer buffer = new StringBuffer(value.trim()); + if (buffer.length() > 0 && buffer.charAt(0) == '"') { + buffer.deleteCharAt(0); + } + if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == '"') { + buffer.setLength(buffer.length() - 1); + } + return buffer.toString(); + } + +} diff --git a/config-lib/src/main/java/com/yahoo/config/Serializer.java b/config-lib/src/main/java/com/yahoo/config/Serializer.java new file mode 100644 index 00000000000..95d20011f2c --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/Serializer.java @@ -0,0 +1,31 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +/** +* @author lulf +* @since 5.1 +*/ +public interface Serializer { + Serializer createInner(String name); + Serializer createArray(String name); + Serializer createInner(); + Serializer createMap(String name); + + /** + * Serialize leaf values. + */ + void serialize(String name, boolean value); + void serialize(String name, double value); + void serialize(String name, long value); + void serialize(String name, int value); + void serialize(String name, String value); + + /** + * Serialize array values. + */ + void serialize(boolean value); + void serialize(double value); + void serialize(long value); + void serialize(int value); + void serialize(String value); +} diff --git a/config-lib/src/main/java/com/yahoo/config/StringNode.java b/config-lib/src/main/java/com/yahoo/config/StringNode.java new file mode 100644 index 00000000000..344a7e2dd40 --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/StringNode.java @@ -0,0 +1,123 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config; + +import com.yahoo.config.text.StringUtilities; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A StringNode class represents a string in a {@link ConfigInstance}. + * + * @author larschr + */ +public class StringNode extends LeafNode<String> { + + /** + * Creates a new un-initialized StringNode. + */ + public StringNode() { + } + + /** + * Creates a new StringNode, initialized to <code>value</code>. + * + * @param value the value of this StringNode. + */ + public StringNode(String value) { + super(true); + this.value = value; + } + + /** + * Returns the value of this string. Same as {@link #getValue()} + * since the value of this node is a String (but implementations + * in other {@link LeafNode} sub-classes differ). + * + * @return the string representation of this StringNode, or null if + * the vlaue is explicitly set to null + */ + public String value() { + return value; + } + + @Override + public String getValue() { + return value(); + } + + @Override + public String toString() { + return (value == null) ? "(null)" : '"' + StringUtilities.escape(getValue()) + '"'; + } + + /** + * Remove character escape codes. + * + * @param string escaped string + * @return unescaped string + */ + public static String unescapeQuotedString(String string) { + StringBuilder sb = new StringBuilder(string); + for (int i = 0; i < sb.length(); i++) { + if (sb.charAt(i) == '\\') { + sb.deleteCharAt(i); + if (i == sb.length()) { + throw new IllegalArgumentException("Parse error" + string); + } + switch (sb.charAt(i)) { + case'n': + sb.setCharAt(i, '\n'); + break; + case'r': + sb.setCharAt(i, '\r'); + break; + case't': + sb.setCharAt(i, '\t'); + break; + case'f': + sb.setCharAt(i, '\f'); + break; + case'x': + if (i + 2 >= sb.length()) { + throw new IllegalArgumentException + ("Could not parse hex value " + string); + } + sb.setCharAt(i, (char) Integer.parseInt + (sb.substring(i + 1, i + 3), 16)); + sb.delete(i + 1, i + 3); + break; + case'\\': + sb.setCharAt(i, '\\'); + break; + } + } + } + + if (sb.length() > 0 && (sb.charAt(0) == '"') && sb.charAt(sb.length() - 1) == '"') { + sb.deleteCharAt(sb.length() - 1);//remove last quote + if (sb.length() > 0) { + sb.deleteCharAt(0); //remove first quote + } + } + return sb.toString(); + } + + /** + * Sets the value of this string from a the string representation + * of this value in the (escaped) input configuration. The value + * supplied to this method needs un-escaping and will be + * un-escaped. + * + * @param value the new value of this node. + */ + @Override + protected boolean doSetValue(@NonNull String value) { + if (value.startsWith("\"") && value.endsWith("\"")) + this.value = unescapeQuotedString(value); + else { + //TODO: unquoted strings can be probably be prohibited now.(?) -gv + this.value = value; + } + return true; + } + +} diff --git a/config-lib/src/main/java/com/yahoo/config/package-info.java b/config-lib/src/main/java/com/yahoo/config/package-info.java new file mode 100644 index 00000000000..c1069b6649a --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.config; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/config-lib/src/main/java/com/yahoo/config/text/StringUtilities.java b/config-lib/src/main/java/com/yahoo/config/text/StringUtilities.java new file mode 100644 index 00000000000..1b05ae779bf --- /dev/null +++ b/config-lib/src/main/java/com/yahoo/config/text/StringUtilities.java @@ -0,0 +1,88 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.text; + +import java.nio.charset.Charset; +import java.io.ByteArrayOutputStream; + +/** + * Escapes strings into and out of a format where they only contain printable characters. + * + * Need to duplicate escape / unescape of strings as we have in C++ for java version of system states. + * + * @author <a href="mailto:humbe@yahoo-inc.com">Haakon Humberset</a> + */ +public class StringUtilities { + private static Charset UTF8 = Charset.forName("utf8"); + + private static byte toHex(int val) { return (byte) (val < 10 ? '0' + val : 'a' + (val - 10)); } + + private static class ReplacementCharacters { + public byte needEscape[] = new byte[256]; + public byte replacement1[] = new byte[256]; + public byte replacement2[] = new byte[256]; + + public ReplacementCharacters() { + for (int i=0; i<256; ++i) { + if (i >= 32 && i <= 126) { + needEscape[i] = 0; + } else if (i > 127) { + needEscape[i] = 0; + } else { + needEscape[i] = 3; + replacement1[i] = toHex((i >> 4) & 0xF); + replacement2[i] = toHex(i & 0xF); + } + } + makeSimpleEscape('"', '"'); + makeSimpleEscape('\\', '\\'); + makeSimpleEscape('\t', 't'); + makeSimpleEscape('\n', 'n'); + makeSimpleEscape('\r', 'r'); + makeSimpleEscape('\f', 'f'); + } + + private void makeSimpleEscape(char source, char dest) { + needEscape[source] = 1; + replacement1[source] = '\\'; + replacement2[source] = (byte) dest; + } + } + + private final static ReplacementCharacters replacementCharacters = new ReplacementCharacters(); + + public static String escape(String source) { return escape(source, '\0'); } + + /** + * Escapes strings into a format with only printable ASCII characters. + * + * @param source The string to escape + * @param delimiter Escape this character too, even if it is printable. + * @return The escaped string + */ + public static String escape(String source, char delimiter) { + byte bytes[] = source.getBytes(UTF8); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + for (byte b : bytes) { + int val = b; + if (val < 0) val += 256; + if (b == delimiter) { + result.write('\\'); + result.write('x'); + result.write(toHex((val >> 4) & 0xF)); + result.write(toHex(val & 0xF)); + } else if (replacementCharacters.needEscape[val] == 0) { + result.write(b); + } else { + if (replacementCharacters.needEscape[val] == 3) { + result.write('\\'); + result.write('x'); + } + result.write(replacementCharacters.replacement1[val]); + result.write(replacementCharacters.replacement2[val]); + } + } + return new String(result.toByteArray(), UTF8); + } + + +} |