/* * 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.camp.spi.resolve.interpret; import java.util.Map; import org.apache.brooklyn.camp.spi.resolve.PlanInterpreter; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.text.StringPredicates; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Iterables; /** Helper class for {@link PlanInterpreter} instances, doing the recursive work */ public class PlanInterpretationNode { private static final Logger log = LoggerFactory.getLogger(PlanInterpretationNode.class); public enum Role { MAP_KEY, MAP_VALUE, LIST_ENTRY, YAML_PRIMITIVE } protected final PlanInterpretationNode parent; protected final Role roleInParent; protected final Object originalValue; protected final PlanInterpretationContext context; protected Object newValue = null; protected Boolean changed = null; protected boolean excluded = false; protected boolean immutable = false; /** creates a root node with {@link #apply()} called */ public PlanInterpretationNode(PlanInterpretationContext context) { this.parent = null; this.roleInParent = null; this.originalValue = context.getOriginalDeploymentPlan(); this.context = context; apply(); } /** internal use: creates an internal node on which {@link #apply()} has *not* been called */ protected PlanInterpretationNode(PlanInterpretationNode parent, Role roleInParent, Object originalItem) { this.parent = parent; this.roleInParent = roleInParent; this.originalValue = originalItem; this.context = parent.getContext(); } public PlanInterpretationContext getContext() { return context; } public PlanInterpretationNode getParent() { return parent; } public Role getRoleInParent() { return roleInParent; } protected void apply() { if (changed!=null) throw new IllegalStateException("can only be applied once"); if (!excluded) { if (originalValue instanceof Map) { applyToMap(); immutable(); } else if (originalValue instanceof Iterable) { applyToIterable(); immutable(); } else { applyToYamlPrimitive(); } } if (changed==null) changed = false; } /** convenience for interpreters, tests if nodes are not excluded, and if not: * for string nodes, true iff the current value equals the given target; * for nodes which are currently maps or lists, * true iff not excluded and the value contains such an entry (key, in the case of map) **/ public boolean matchesLiteral(String target) { if (isExcluded()) return false; if (getNewValue() instanceof CharSequence) return getNewValue().toString().equals(target); if (getNewValue() instanceof Map) return ((Map<?,?>)getOriginalValue()).containsKey(target); if (getNewValue() instanceof Iterable) return Iterables.contains((Iterable<?>)getOriginalValue(), target); return false; } /** convenience for interpreters, tests if nodes are not excluded, and if not: * for string nodes, true iff the current value starts with the given prefix; * for nodes which are currently maps or lists, * true iff not excluded and the value contains such an entry (key, in the case of map) */ public boolean matchesPrefix(String prefix) { if (isExcluded()) return false; if (getNewValue() instanceof CharSequence) return getNewValue().toString().startsWith(prefix); if (getNewValue() instanceof Map) return Iterables.tryFind(((Map<?,?>)getNewValue()).keySet(), StringPredicates.isStringStartingWith(prefix)).isPresent(); if (getNewValue() instanceof Iterable) return Iterables.tryFind((Iterable<?>)getNewValue(), StringPredicates.isStringStartingWith(prefix)).isPresent(); return false; } // TODO matchesRegex ? public Object getOriginalValue() { return originalValue; } public Object getNewValue() { if (changed==null || !isChanged()) return originalValue; return newValue; } public boolean isChanged() { if (changed==null) throw new IllegalStateException("not yet applied"); return changed; } public boolean isExcluded() { return excluded; } /** indicates that a node should no longer be translated */ public PlanInterpretationNode exclude() { this.excluded = true; return this; } public PlanInterpretationNode setNewValue(Object newItem) { if (immutable) throw new IllegalStateException("Node "+this+" has been set immutable"); this.newValue = newItem; this.changed = true; return this; } protected PlanInterpretationNode newPlanInterpretation(PlanInterpretationNode parent, Role roleInParent, Object item) { return new PlanInterpretationNode(parent, roleInParent, item); } protected void applyToMap() { Map<Object, Object> input = MutableMap.<Object,Object>copyOf((Map<?,?>)originalValue); Map<Object, Object> result = MutableMap.<Object,Object>of(); newValue = result; // first do a "whole-node" application if (getContext().getAllInterpreter().applyMapBefore(this, input)) { for (Map.Entry<Object,Object> entry: input.entrySet()) { // then recurse in to this node and do various in-the-node applications PlanInterpretationNode value = newPlanInterpretation(this, Role.MAP_VALUE, entry.getValue()); value.apply(); PlanInterpretationNode key = newPlanInterpretation(this, Role.MAP_KEY, entry.getKey()); key.apply(); if (key.isChanged() || value.isChanged()) changed = true; if (getContext().getAllInterpreter().applyMapEntry(this, input, result, key, value)) result.put(key.getNewValue(), value.getNewValue()); else changed = true; } // finally try applying to this node again getContext().getAllInterpreter().applyMapAfter(this, input, result); } if (changed==null) changed = false; } protected void applyToIterable() { MutableList<Object> input = MutableList.copyOf((Iterable<?>)originalValue); MutableList<Object> result = new MutableList<Object>(); newValue = result; // first do a "whole-node" application if (getContext().getAllInterpreter().applyListBefore(this, input)) { for (Object entry: input) { // then recurse in to this node and do various in-the-node applications PlanInterpretationNode value = newPlanInterpretation(this, Role.LIST_ENTRY, entry); value.apply(); if (value.isChanged()) changed = true; if (getContext().getAllInterpreter().applyListEntry(this, input, result, value)) result.add(value.getNewValue()); } // finally try applying to this node again getContext().getAllInterpreter().applyListAfter(this, input, result); } if (changed==null) changed = false; } protected void applyToYamlPrimitive() { getContext().getAllInterpreter().applyYamlPrimitive(this); } public void immutable() { if (!isChanged()) { if (!checkCollectionImmutable(getNewValue())) { // results of Yaml parse are not typically immutable, // so force them to be changed so result of interpretation is immutable changed = true; setNewValue(immutable(getNewValue())); } } else { setNewValue(immutable(getNewValue())); } checkImmutable(getNewValue()); immutable = true; } private void checkImmutable(Object in) { if (!checkCollectionImmutable(in)) log.warn("Node original value "+in+" at "+this+" should be immutable"); } private static boolean checkCollectionImmutable(Object in) { // not used -- input might now be UnmodifiableMap, as some args might be null, // and UnmodifiableMap is private :( ... (and same for list) return true; } private static Object immutable(Object in) { if (in instanceof Map) return MutableMap.copyOf((Map<?,?>)in).asUnmodifiable(); if (in instanceof Iterable) return MutableList.copyOf((Iterable<?>)in).asUnmodifiable(); return in; } }