All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.brooklyn.util.yaml.Yamls Maven / Gradle / Ivy

Go to download

Utility classes and methods developed for Brooklyn but not dependendent on Brooklyn or much else

There is a newer version: 1.1.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.brooklyn.util.yaml;

import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.Nullable;

import org.apache.brooklyn.util.collections.Jsonya;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.UserFacingException;
import org.apache.brooklyn.util.internal.BrooklynSystemProperties;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.error.Mark;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeId;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.SequenceNode;

import com.google.common.annotations.Beta;
import com.google.common.collect.Iterables;

public class Yamls {

    private static final Logger log = LoggerFactory.getLogger(Yamls.class);

    private static Yaml newYaml() {
        return new Yaml(
                BrooklynSystemProperties.YAML_TYPE_INSTANTIATION.isEnabled()
                        ? new Constructor() // allows instantiation of arbitrary Java types
                        : new SafeConstructor() // allows instantiation of limited set of types only
        );
    }

    /** returns the given (yaml-parsed) object as the given yaml type.
     * 

* if the object is an iterable or iterator this method will fully expand it as a list. * if the requested type is not an iterable or iterator, and the list contains a single item, this will take that single item. *

* in other cases this method simply does a type-check and cast (no other type coercion). *

* @throws IllegalArgumentException if the input is an iterable not containing a single element, * and the cast is requested to a non-iterable type * @throws ClassCastException if cannot be casted */ @SuppressWarnings({ "unchecked", "rawtypes" }) public static T getAs(Object x, Class type) { if (x==null) return null; if (x instanceof Iterable || x instanceof Iterator) { List result = new ArrayList(); Iterator xi; if (Iterator.class.isAssignableFrom(x.getClass())) { xi = (Iterator)x; } else { xi = ((Iterable)x).iterator(); } while (xi.hasNext()) { result.add( xi.next() ); } if (type.isAssignableFrom(List.class)) return (T)result; if (type.isAssignableFrom(Iterator.class)) return (T)result.iterator(); x = Iterables.getOnlyElement(result); } if (type.isInstance(x)) return (T)x; throw new ClassCastException("Cannot convert "+x+" ("+x.getClass()+") to "+type); } /** * Parses the given yaml, and walks the given path to return the referenced object. * * @see #getAt(Object, List) */ @Beta public static Object getAt(String yaml, List path) { Iterable result = newYaml().loadAll(yaml); Object current = result.iterator().next(); return getAtPreParsed(current, path); } /** * For pre-parsed yaml, walks the maps/lists to return the given sub-item. * In the given path: *
    *
  • A vanilla string is assumed to be a key into a map. *
  • A string in the form like "[0]" is assumed to be an index into a list *
* * Also see {@link Jsonya}, such as {@code Jsonya.of(current).at(path).get()}. * * @return The object at the given path, or {@code null} if that path does not exist. */ @Beta @SuppressWarnings("unchecked") public static Object getAtPreParsed(Object current, List path) { for (String pathPart : path) { if (pathPart.startsWith("[") && pathPart.endsWith("]")) { String index = pathPart.substring(1, pathPart.length()-1); try { current = Iterables.get((Iterable)current, Integer.parseInt(index)); } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid index '"+index+"', in path "+path); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException("Invalid index '"+index+"', in path "+path); } } else { current = ((Map)current).get(pathPart); } if (current == null) return null; } return current; } @SuppressWarnings("rawtypes") public static void dump(int depth, Object r) { if (r instanceof Iterable) { for (Object ri : ((Iterable)r)) dump(depth+1, ri); } else if (r instanceof Map) { for (Object re: ((Map)r).entrySet()) { for (int i=0; i"); else System.out.println("<"+r.getClass().getSimpleName()+">"+" "+r); } } /** simplifies new Yaml().loadAll, and converts to list to prevent single-use iterable bug in yaml */ @SuppressWarnings("unchecked") public static Iterable parseAll(String yaml) { Iterable result = newYaml().loadAll(yaml); return getAs(result, List.class); } /** as {@link #parseAll(String)} */ @SuppressWarnings("unchecked") public static Iterable parseAll(Reader yaml) { Iterable result = newYaml().loadAll(yaml); return getAs(result, List.class); } public static Object removeMultinameAttribute(Map obj, String ...equivalentNames) { Object result = null; for (String name: equivalentNames) { Object candidate = obj.remove(name); if (candidate!=null) { if (result==null) result = candidate; else if (!result.equals(candidate)) { log.warn("Different values for attributes "+Arrays.toString(equivalentNames)+"; " + "preferring '"+result+"' to '"+candidate+"'"); } } } return result; } public static Object getMultinameAttribute(Map obj, String ...equivalentNames) { Object result = null; for (String name: equivalentNames) { Object candidate = obj.get(name); if (candidate!=null) { if (result==null) result = candidate; else if (!result.equals(candidate)) { log.warn("Different values for attributes "+Arrays.toString(equivalentNames)+"; " + "preferring '"+result+"' to '"+candidate+"'"); } } } return result; } @Beta public static class YamlExtract { String yaml; NodeTuple focusTuple; Node prev, key, focus, next; Exception error; boolean includeKey = false, includePrecedingComments = true, includeOriginalIndentation = false; private int indexStart(Node node, boolean defaultIsYamlEnd) { if (node==null) return defaultIsYamlEnd ? yaml.length() : 0; return index(node.getStartMark()); } private int indexEnd(Node node, boolean defaultIsYamlEnd) { if (!found() || node==null) return defaultIsYamlEnd ? yaml.length() : 0; return index(node.getEndMark()); } private int index(Mark mark) { try { return mark.getIndex(); } catch (NoSuchMethodError e) { try { getClass().getClassLoader().loadClass("org.testng.TestNG"); } catch (ClassNotFoundException e1) { // not using TestNG Exceptions.propagateIfFatal(e1); throw e; } if (!LOGGED_TESTNG_WARNING.getAndSet(true)) { log.warn("Detected TestNG/SnakeYAML version incompatibilities: " + "some YAML source reconstruction will be unavailable. " + "This can happen with TestNG plugins which force an older version of SnakeYAML " + "which does not support Mark.getIndex. " + "It should not occur from maven CLI runs. " + "(Subsequent occurrences will be silently dropped, and source code reconstructed from YAML.)"); } // using TestNG throw new KnownClassVersionException(e); } } static AtomicBoolean LOGGED_TESTNG_WARNING = new AtomicBoolean(); static class KnownClassVersionException extends IllegalStateException { private static final long serialVersionUID = -1620847775786753301L; public KnownClassVersionException(Throwable e) { super("Class version error. This can happen if using a TestNG plugin in your IDE " + "which is an older version, dragging in an older version of SnakeYAML which does not support Mark.getIndex.", e); } } public int getEndOfPrevious() { return indexEnd(prev, false); } @Nullable public Node getKey() { return key; } public Node getResult() { return focus; } public int getStartOfThis() { if (includeKey && focusTuple!=null) return indexStart(focusTuple.getKeyNode(), false); return indexStart(focus, false); } private int getStartColumnOfThis() { if (includeKey && focusTuple!=null) return focusTuple.getKeyNode().getStartMark().getColumn(); return focus.getStartMark().getColumn(); } public int getEndOfThis() { return getEndOfThis(false); } private int getEndOfThis(boolean goToEndIfNoNext) { if (next==null && goToEndIfNoNext) return yaml.length(); return indexEnd(focus, false); } public int getStartOfNext() { return indexStart(next, true); } private static int initialWhitespaceLength(String x) { int i=0; while (i < x.length() && x.charAt(i)==' ') i++; return i; } public String getFullYamlTextOriginal() { return yaml; } /** Returns the original YAML with the found item replaced by the given replacement YAML. * @param replacement YAML to put in for the found item; * this YAML typically should not have any special indentation -- if required when replacing it will be inserted. *

* if replacing an inline map entry, the supplied entry must follow the structure being replaced; * for example, if replacing the value in key: value with a map, * supplying a replacement subkey: value would result in invalid yaml; * the replacement must be supplied with a newline, either before the subkey or after. * (if unsure we believe it is always valid to include an initial newline or comment with newline.) */ public String getFullYamlTextWithExtractReplaced(String replacement) { if (!found()) throw new IllegalStateException("Cannot perform replacement when item was not matched."); String result = yaml.substring(0, getStartOfThis()); String[] newLines = replacement.split("\n"); for (int i=1; i result = MutableList.of(); if (includePrecedingComments) { if (getEndOfPrevious() > getStartOfThis()) { throw new UserFacingException("YAML not in expected format; when scanning, previous end "+getEndOfPrevious()+" exceeds this start "+getStartOfThis()); } String[] preceding = yaml.substring(getEndOfPrevious(), getStartOfThis()).split("\n"); // suppress comments which are on the same line as the previous item or indented more than firstLineIndentation, // ensuring appropriate whitespace is added to preceding[0] if it starts mid-line if (preceding.length>0 && prev!=null) { preceding[0] = Strings.makePaddedString("", prev.getEndMark().getColumn(), "", " ") + preceding[0]; } for (String p: preceding) { int w = initialWhitespaceLength(p); p = p.substring(w); if (p.startsWith("#")) { // only add if the hash is not indented further than the first line if (w <= firstLineIndentationOfFirstThing) { if (includeOriginalIndentation) p = firstLineIndentationToAddS + p; result.add(p); } } } } boolean doneFirst = false; for (String p: body) { if (!doneFirst) { if (includeOriginalIndentation) { // have to insert the right amount of spacing p = firstLineIndentationToAddS + p; } result.add(p); doneFirst = true; } else { if (includeOriginalIndentation) { result.add(p); } else { result.add(Strings.removeFromStart(p, subsequentLineIndentationToRemoveS)); } } } return Strings.join(result, "\n"); } boolean found() { return focus != null; } public Exception getError() { return error; } @Override public String toString() { return "Extract["+focus+";prev="+prev+";key="+key+";next="+next+"]"; } } private static void findTextOfYamlAtPath(YamlExtract result, int pathIndex, Object ...path) { if (pathIndex>=path.length) { // we're done return; } Object pathItem = path[pathIndex]; Node node = result.focus; if (node.getNodeId()==NodeId.mapping && pathItem instanceof String) { // find key Iterator ti = ((MappingNode)node).getValue().iterator(); while (ti.hasNext()) { NodeTuple t = ti.next(); Node key = t.getKeyNode(); if (key.getNodeId()==NodeId.scalar) { if (pathItem.equals( ((ScalarNode)key).getValue() )) { result.key = key; result.focus = t.getValueNode(); if (pathIndex+1 nl = ((SequenceNode)node).getValue(); int i = ((Number)pathItem).intValue(); if (i>=nl.size()) throw new IllegalStateException("Index "+i+" is out of bounds in "+node+", searching for "+pathItem+" at depth "+pathIndex+" of "+Arrays.asList(path)); if (i>0) result.prev = nl.get(i-1); result.key = null; result.focus = nl.get(i); findTextOfYamlAtPath(result, pathIndex+1, path); if (result.next==null) { if (nl.size()>i+1) result.next = nl.get(i+1); } return; } else { throw new IllegalStateException("Node "+node+" does not match selector "+pathItem+" at depth "+pathIndex+" of "+Arrays.asList(path)); } // unreachable } /** Given a path, where each segment consists of a string (key) or number (element in list), * this will find the YAML text for that element *

* If not found this will return a {@link YamlExtract} * where {@link YamlExtract#isMatch()} is false and {@link YamlExtract#getError()} is set. */ public static YamlExtract getTextOfYamlAtPath(String yaml, Object ...path) { YamlExtract result = new YamlExtract(); if (yaml==null) return result; try { int pathIndex = 0; result.yaml = yaml; result.focus = newYaml().compose(new StringReader(yaml)); findTextOfYamlAtPath(result, pathIndex, path); return result; } catch (NoSuchMethodError e) { throw new IllegalStateException("Class version error. This can happen if using a TestNG plugin in your IDE " + "which is an older version, dragging in an older version of SnakeYAML which does not support Mark.getIndex.", e); } catch (Exception e) { Exceptions.propagateIfFatal(e); log.debug("Unable to find element in yaml (setting in result): "+e); result.error = e; return result; } } }