blob: b620ca11ef1aa94273976cf75e1d62195c592e34 [file] [log] [blame]
/*
* 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.core.text;
import com.google.common.annotations.Beta;
import com.google.common.base.Charsets;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import freemarker.cache.StringTemplateLoader;
import freemarker.core.Environment;
import freemarker.core.Expression;
import freemarker.core.TemplateElement;
import freemarker.core._CoreAPI;
import freemarker.template.*;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.Group;
import org.apache.brooklyn.api.entity.drivers.EntityDriver;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.objs.BrooklynObject;
import org.apache.brooklyn.api.objs.Identifiable;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.effector.EffectorBase;
import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.location.internal.LocationInternal;
import org.apache.brooklyn.core.sensor.DependentConfiguration;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.core.workflow.WorkflowExpressionResolution;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.ThreadLocalStack;
import org.apache.brooklyn.util.core.task.CrossTaskThreadLocalStack;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.RuntimeInterruptedException;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.*;
import static com.google.common.base.Preconditions.checkNotNull;
/** A variety of methods to assist in Freemarker template processing,
* including passing in maps with keys flattened (dot-separated namespace),
* and accessing {@link ManagementContext} brooklyn.properties
* and {@link Entity}, {@link EntityDriver}, and {@link Location} methods and config.
* <p>
* See {@link #processTemplateContents(String, ManagementContext, Map)} for
* a description of how management access is done.
*/
public class TemplateProcessor {
private static final Logger log = LoggerFactory.getLogger(TemplateProcessor.class);
static {
if (System.getProperty(freemarker.log.Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY)==null) {
System.setProperty(freemarker.log.Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY, freemarker.log.Logger.LIBRARY_NAME_SLF4J);
}
}
static BrooklynFreemarkerUnwrappableObjectWrapper BROOKLYN_WRAPPER = new BrooklynFreemarkerUnwrappableObjectWrapper();
public static TemplateModel wrapAsTemplateModel(Object o) throws TemplateModelException { return BROOKLYN_WRAPPER.wrap(o); }
public static Maybe<Object> unwrapTemplateModelMaybe(TemplateModel templateModel) { return BROOKLYN_WRAPPER.unwrapMaybe(templateModel); }
static ThreadLocalStack<Map<TemplateModel,Object>> TEMPLATE_MODEL_UNWRAP_CACHE = new ThreadLocalStack<>();
/** A cache is used to be able to retrieve the object from which a TemplateModel was created, if needed,
* because Freemarker doesn't support that except on selected UnwrappableTemplateModel subclasses.
* Use wrap and unwrap methods above to access.
* Calls to this must be balanced with a 'close' to avoid memory leaks */
public static void openLocalTemplateModelCache() { TEMPLATE_MODEL_UNWRAP_CACHE.push(MutableMap.of()); }
public static void closeLocalTemplateModelCache() { TEMPLATE_MODEL_UNWRAP_CACHE.pop(); }
static ThreadLocalStack<String> TEMPLATE_FILE_WANTING_LEGACY_SYNTAX = new ThreadLocalStack<>();
static CrossTaskThreadLocalStack<Boolean> IS_FOR_WORKFLOW = new CrossTaskThreadLocalStack<>();
public interface UnwrappableTemplateModel {
// could make sense to distinguish 'unwrappable as result' from 'unwrappable for alternative lookup'
Maybe<Object> unwrap();
}
static class BrooklynFreemarkerUnwrappableObjectWrapper extends BrooklynFreemarkerObjectWrapper {
public Maybe<Object> unwrapMaybe(TemplateModel model) {
Maybe<Object> result;
if (model instanceof UnwrappableTemplateModel) {
result = ((UnwrappableTemplateModel) model).unwrap();
if (result.isPresent()) return result;
}
Maybe<Map<TemplateModel, Object>> unwrappingMapM = TEMPLATE_MODEL_UNWRAP_CACHE.peek();
if (unwrappingMapM.isAbsent()) {
return Maybe.absent("This thread does not support unwrapping");
}
if (!unwrappingMapM.get().containsKey(model)) {
// happens if we return a constant within a model
if (model instanceof TemplateScalarModel) {
try {
return Maybe.ofAllowingNull( ((TemplateScalarModel)model).getAsString() );
} catch (TemplateModelException e) {
throw Exceptions.propagate(e);
}
}
return Maybe.absent("Type and source of model is unknown: " + model);
}
return Maybe.ofAllowingNull(unwrappingMapM.get().get(model));
}
public TemplateModel rememberWrapperIfSupported(Object o, TemplateModel m) {
Map<TemplateModel, Object> unwrappingMap = TEMPLATE_MODEL_UNWRAP_CACHE.peek().orNull();
if (unwrappingMap!=null) unwrappingMap.put(m, o);
return m;
}
@Override
public TemplateModel wrap(Object o) throws TemplateModelException {
return rememberWrapperIfSupported(o, super.wrap(o));
}
@Override
public TemplateModel wrapAsBean(Object o) throws TemplateModelException {
return rememberWrapperIfSupported(o, super.wrapAsBean(o));
}
}
/** instead of this:
* new DefaultObjectWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build();
* this class ensures our extensions are applied recursively, and we get our special model plus ths bean model for common types */
static class BrooklynFreemarkerObjectWrapper extends DefaultObjectWrapper {
public BrooklynFreemarkerObjectWrapper() {
this(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
}
public BrooklynFreemarkerObjectWrapper(Version incompatibleImprovements) {
super(incompatibleImprovements);
}
@Override
public TemplateModel wrap(Object obj) throws TemplateModelException {
if (obj == null) {
return super.wrap(null);
}
if (obj instanceof TemplateModel) {
return (TemplateModel) obj;
}
if (obj instanceof Map) {
// use our map recursively, so a map with `a.b` as a single key can be referenced as` ${a.b}` in the freemarker template
return new DotSplittingTemplateModel((Map<?,?>)obj);
}
if (obj instanceof Instant) {
// Freemarker doesn't support Instant, so we add
return super.wrap(Date.from( (Instant)obj ));
}
Object objOrig = obj;
if (obj.getClass().isArray()) {
obj = convertArray(obj);
}
if (obj instanceof Collection) {
if (!((Collection<?>) obj).isEmpty() && ((Collection<?>) obj).stream().allMatch(x -> x instanceof Identifiable)) {
}
return new SimpleSequenceWithLookup((Collection<?>) obj, this);
}
return super.wrap(objOrig);
}
static final ThreadLocal<Boolean> handleUnknownTypeLoopPrevention = new ThreadLocal<>();
@Override
protected TemplateModel handleUnknownType(final Object o) throws TemplateModelException {
if (handleUnknownTypeLoopPrevention.get()!=null) {
return super.handleUnknownType(o);
}
try {
handleUnknownTypeLoopPrevention.set(true);
// can get "Class inrospection data lookup aborded" from freemarker ClassIntrospector:250
// if thread is interrupted because class lookup uses wait on a shared cache;
// if the "interruption" is because of us, retry in this instance
while (true) {
try {
return handleUnknownTypeReal(o);
} catch (Exception e) {
if (WorkflowExpressionResolution.isInterruptSetToPreventWaiting()) {
if (Exceptions.isRootCauseIsInterruption(e) || e.toString().contains(InterruptedException.class.getSimpleName())) {
Thread.yield();
continue;
}
}
throw e;
}
}
} finally {
handleUnknownTypeLoopPrevention.remove();
}
}
protected TemplateModel handleUnknownTypeReal(final Object o) throws TemplateModelException {
if (o instanceof EntityInternal) return EntityAndMapTemplateModel.forEntity((EntityInternal)o, null);
if (o instanceof Location) return LocationAndMapTemplateModel.forLocation((LocationInternal)o, null);
return super.handleUnknownType(o);
}
public TemplateModel wrapAsBean(Object o) throws TemplateModelException {
if (o instanceof BrooklynObject) {
// deproxy to reduce freemarker introspection interrupted errors
o = Entities.deproxy((BrooklynObject) o);
}
return handleUnknownType(o);
}
}
static class SimpleSequenceWithLookup extends SimpleSequence implements TemplateHashModel {
SimpleSequenceWithLookup(Collection<?> collection, ObjectWrapper wrapper) {
super(collection, wrapper);
}
protected Object findKey(String key) {
for (Object l: list) {
if (l instanceof Entity) {
String planId = ((Entity) l).config().get(BrooklynConfigKeys.PLAN_ID);
if (Strings.isNonEmpty(planId) && Objects.equals(planId, key)) return l;
}
if (l instanceof Identifiable && Objects.equals(((Identifiable)l).getId(), key)) return l;
}
return null;
}
@Override
public TemplateModel get(String key) throws TemplateModelException {
Object match = findKey(key);
if (match==null) return null;
return getObjectWrapper().wrap(match);
}
@Override
public boolean isEmpty() throws TemplateModelException {
return false;
}
}
/** As per {@link #processTemplateContents(String, Map)}, but taking a file. */
public static String processTemplateFile(String templateFileName, Map<String, ? extends Object> substitutions) {
String templateContents;
try {
templateContents = Files.toString(new File(templateFileName), Charsets.UTF_8);
} catch (IOException e) {
log.warn("Error loading file " + templateFileName, e);
throw Exceptions.propagate(e);
}
return processTemplateContents(templateFileName, templateContents, substitutions);
}
/** Processes template contents according to {@link EntityAndMapTemplateModel}. */
public static String processTemplateFile(String templateFileName, EntityDriver driver, Map<String, ? extends Object> extraSubstitutions) {
String templateContents;
try {
templateContents = Files.toString(new File(templateFileName), Charsets.UTF_8);
} catch (IOException e) {
log.warn("Error loading file " + templateFileName, e);
throw Exceptions.propagate(e);
}
return processTemplateContents(templateFileName, templateContents, driver, extraSubstitutions);
}
/** Processes template contents according to {@link EntityAndMapTemplateModel}. */
public static String processTemplateContents(String templateContents, EntityDriver driver, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents("unknown", templateContents, EntityAndMapTemplateModel.forDriver(driver, extraSubstitutions));
}
/** Processes template contents according to {@link EntityAndMapTemplateModel}. */
public static String processTemplateContents(String templateContents, ManagementContext managementContext, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents("unknown", templateContents, EntityAndMapTemplateModel.forManagementContext(managementContext, extraSubstitutions));
}
/** Processes template contents according to {@link EntityAndMapTemplateModel}. */
public static String processTemplateContents(String templateContents, Location location, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents("unknown", templateContents, LocationAndMapTemplateModel.forLocation((LocationInternal)location, extraSubstitutions));
}
/** Processes template contents according to {@link EntityAndMapTemplateModel}. */
public static String processTemplateContents(String context, String templateContents, EntityDriver driver, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents(context, templateContents, EntityAndMapTemplateModel.forDriver(driver, extraSubstitutions));
}
/** Processes template contents according to {@link EntityAndMapTemplateModel}. */
public static String processTemplateContents(String context, String templateContents, ManagementContext managementContext, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents(context, templateContents, EntityAndMapTemplateModel.forManagementContext(managementContext, extraSubstitutions));
}
/** Processes template contents according to {@link EntityAndMapTemplateModel}. */
public static String processTemplateContents(String context, String templateContents, Location location, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents(context, templateContents, LocationAndMapTemplateModel.forLocation((LocationInternal)location, extraSubstitutions));
}
public static final class FirstAvailableTemplateModel implements TemplateHashModel, UnwrappableTemplateModel {
MutableList<TemplateHashModel> models = MutableList.of();
public FirstAvailableTemplateModel(Iterable<TemplateHashModel> modelsO) {
for (TemplateHashModel m : modelsO) {
if (m!=null) this.models.add(m);
}
}
public FirstAvailableTemplateModel(TemplateHashModel ...modelsO) {
this(Arrays.asList(modelsO));
}
@Override
public Maybe<Object> unwrap() {
return Maybe.of(models.stream().filter(m -> m instanceof UnwrappableTemplateModel).findAny()).mapMaybe(m -> ((UnwrappableTemplateModel)m).unwrap());
}
@Override
public TemplateModel get(String s) throws TemplateModelException {
for (TemplateHashModel m: models) {
TemplateModel result = m.get(s);
if (result!=null) return result;
}
return null;
}
@Override
public boolean isEmpty() throws TemplateModelException {
for (TemplateHashModel m: models) {
if (!m.isEmpty()) return false;
}
return true;
}
}
/**
* A Freemarker {@link TemplateHashModel} which will correctly handle entries of the form "a.b" in this map,
* matching against template requests for "${a.b}".
* <p>
* Freemarker requests "a" in a map when given such a request, and expects that to point to a map
* with a key "b". This model provides such maps even for "a.b" in a map.
* <p>
* However if "a" <b>and</b> "a.b" are in the map, this will <b>not</b> currently do the deep mapping.
* (It does not have enough contextual information from Freemarker to handle this case.) */
public static final class DotSplittingTemplateModel implements TemplateHashModelEx2, UnwrappableTemplateModel {
protected final Map<?,?> map;
protected DotSplittingTemplateModel(Map<?,?> map) {
this.map = map;
}
@Override
public Maybe<Object> unwrap() {
return Maybe.of(map);
}
@Override
public boolean isEmpty() { return map!=null && map.isEmpty(); }
public boolean contains(String key) {
if (map==null) return false;
if (map.containsKey(key)) return true;
for (Map.Entry<?,?> entry: map.entrySet()) {
String k = Strings.toString(entry.getKey());
if (k.startsWith(key+".")) {
// contains this prefix
return true;
}
}
return false;
}
@Override
public TemplateModel get(String key) {
if (map==null) return null;
try {
if (map.containsKey(key))
return wrapAsTemplateModel( map.get(key) );
Map<String,Object> result = MutableMap.of();
for (Map.Entry<?,?> entry: map.entrySet()) {
String k = Strings.toString(entry.getKey());
if (k.startsWith(key+".")) {
String k2 = Strings.removeFromStart(k, key+".");
result.put(k2, entry.getValue());
}
}
if (!result.isEmpty())
return wrapAsTemplateModel( result );
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
throw new IllegalStateException("Error accessing config '"+key+"'"+": "+e, e);
}
return null;
}
@Override
public String toString() {
return getClass().getName()+"["+map+"]";
}
public int size() {
return map.size();
}
public TemplateCollectionModel keys() {
return new SimpleCollection(map.keySet(), BROOKLYN_WRAPPER);
}
public TemplateCollectionModel values() {
return new SimpleCollection(map.values(), BROOKLYN_WRAPPER);
}
public KeyValuePairIterator keyValuePairIterator() {
return new MapKeyValuePairIterator(map, BROOKLYN_WRAPPER);
}
}
/** FreeMarker {@link TemplateHashModel} which resolves keys inside the given entity or management context.
* Callers are required to include dots for dot-separated keys.
* Freemarker will only do this when in inside bracket notation in an outer map, as in <code>${outer['a.b.']}</code>;
* as a result this is intended only for use by {@link EntityAndMapTemplateModel} where
* a caller has used bracked notation, as in <code>${mgmt['key.subkey']}</code>. */
protected static final class EntityConfigTemplateModel implements TemplateHashModel {
protected final EntityInternal entity;
protected final ManagementContext mgmt;
protected EntityConfigTemplateModel(EntityInternal entity) {
this.entity = checkNotNull(entity, "entity");
this.mgmt = entity.getManagementContext();
}
@Override
public boolean isEmpty() { return false; }
@Override
public TemplateModel get(String key) throws TemplateModelException {
try {
Object result = entity.getConfig(ConfigKeys.builder(Object.class).name(key).build());
if (result==null)
result = mgmt.getConfig().getConfig(ConfigKeys.builder(Object.class).name(key).build());
if (result!=null)
return wrapAsTemplateModel( result );
} catch (Exception e) {
throw handleModelError("Error accessing config '"+key+"' on "+entity, e);
}
return null;
}
@Override
public String toString() {
return getClass().getName()+"["+entity+"]";
}
}
/** FreeMarker {@link TemplateHashModel} which resolves keys inside the given management context.
* Callers are required to include dots for dot-separated keys.
* Freemarker will only do this when in inside bracket notation in an outer map, as in <code>${outer['a.b.']}</code>;
* as a result this is intended only for use by {@link EntityAndMapTemplateModel} where
* a caller has used bracked notation, as in <code>${mgmt['key.subkey']}</code>. */
protected static final class MgmtConfigTemplateModel implements TemplateHashModel {
protected final ManagementContext mgmt;
protected MgmtConfigTemplateModel(ManagementContext mgmt) {
this.mgmt = checkNotNull(mgmt, "mgmt");
}
@Override
public boolean isEmpty() { return false; }
@Override
public TemplateModel get(String key) throws TemplateModelException {
try {
Object result = mgmt.getConfig().getConfig(ConfigKeys.builder(Object.class).name(key).build());
if (result!=null)
return wrapAsTemplateModel( result );
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
throw Exceptions.propagateAnnotated("Error accessing config '"+key+"': "+e, e);
}
return null;
}
@Override
public String toString() {
return getClass().getName()+"["+mgmt+"]";
}
}
/** FreeMarker {@link TemplateHashModel} which resolves keys inside the given location.
* Callers are required to include dots for dot-separated keys.
* Freemarker will only do this when in inside bracket notation in an outer map, as in <code>${outer['a.b.']}</code>;
* as a result this is intended only for use by {@link LocationAndMapTemplateModel} where
* a caller has used bracked notation, as in <code>${mgmt['key.subkey']}</code>. */
protected static final class LocationConfigTemplateModel implements TemplateHashModel {
protected final LocationInternal location;
protected final ManagementContext mgmt;
protected LocationConfigTemplateModel(LocationInternal location) {
this.location = checkNotNull(location, "location");
this.mgmt = location.getManagementContext();
}
@Override
public boolean isEmpty() { return false; }
@Override
public TemplateModel get(String key) throws TemplateModelException {
try {
Object result = null;
result = location.getConfig(ConfigKeys.builder(Object.class).name(key).build());
if (result==null && mgmt!=null)
result = mgmt.getConfig().getConfig(ConfigKeys.builder(Object.class).name(key).build());
if (result!=null)
return wrapAsTemplateModel( result );
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
throw Exceptions.propagateAnnotated("Error accessing config '"+key+"'"
+ (location!=null ? " on "+location : "")+": "+e, e);
}
return null;
}
@Override
public String toString() {
return getClass().getName()+"["+location+"]";
}
}
public static class TemplateModelDataUnavailableException extends TemplateModelException {
public TemplateModelDataUnavailableException(String s, Throwable cause) {
super(s, cause);
}
}
@Beta
public static Error handleModelError(String msg, Throwable cause) throws TemplateModelException {
// up to caller to determine if it was an interruption
if (Exceptions.isCausedByInterruptInAnyThread(cause)) {
// we can only catch exceptions in expressions if we throw InvalidReferenceException, but that is checked and not allowed (also doesn't allow cause)
// throw new InvalidReferenceException("Sensor '" + key + "' unavailable", null);
// we could return null but that might mask errors. instead let caller handle (eg WorkflowExpressionResolution)
// throw a custom exception of our own
// maybe do not include the InterruptException cause because that causes any thread who propagates this to also be interrupted?
// or change propagateIfFatal not to set interrupt flag again?
throw new TemplateModelDataUnavailableException(msg+": "+Exceptions.collapseText(cause), cause);
} else {
// interruptions not treated as fatal here
Exceptions.propagateIfFatal(cause);
}
// throwing this doesn't buy us much but a bit of efficiency in the catching and consistency with other freemarker code
throw new TemplateModelException(msg+": "+Exceptions.collapseText(cause), cause);
}
protected final static class EntityAttributeTemplateModel implements TemplateHashModel {
protected final EntityInternal entity;
private final SensorResolutionMode mode;
enum SensorResolutionMode { SENSOR_DEFINITION,
ATTRIBUTE_VALUE,
ATTRIBUTE_WHEN_READY_FOR_TEMPLATES,
ATTRIBUTE_WHEN_READY_FOR_WORKFLOW }
protected EntityAttributeTemplateModel(EntityInternal entity, SensorResolutionMode mode) {
this.entity = entity;
if (TEMPLATE_FILE_WANTING_LEGACY_SYNTAX.peek().isPresentAndNonNull()) {
// in templates, we have only ever supported attribute when ready. preserve that for now, but warn of deprecation.
if (mode != SensorResolutionMode.ATTRIBUTE_WHEN_READY_FOR_TEMPLATES) {
log.warn("Using deprecated legacy attributeWhenReady behaviour of ${entity.attribute...} or ${entity.sensor...}. Template should be updated to use ${entity.attributeWhenReady...} if that is required: "
+ TEMPLATE_FILE_WANTING_LEGACY_SYNTAX.peek());
mode = SensorResolutionMode.ATTRIBUTE_WHEN_READY_FOR_TEMPLATES;
}
}
this.mode = mode;
}
@Override
public boolean isEmpty() throws TemplateModelException {
return false;
}
@Override
public TemplateModel get(String key) throws TemplateModelException {
Object result;
try {
result =
mode == SensorResolutionMode.ATTRIBUTE_WHEN_READY_FOR_TEMPLATES ?
((EntityInternal)entity).getExecutionContext().get( DependentConfiguration.attributeWhenReady(entity,
Sensors.builder(Object.class, key).persistence(AttributeSensor.SensorPersistenceMode.NONE).build()))
: mode == SensorResolutionMode.ATTRIBUTE_WHEN_READY_FOR_WORKFLOW ?
((EntityInternal)entity).getExecutionContext().get( DependentConfiguration.attributeWhenReadyAllowingOnFire(entity,
Sensors.builder(Object.class, key).persistence(AttributeSensor.SensorPersistenceMode.NONE).build()))
: mode == SensorResolutionMode.ATTRIBUTE_VALUE ?
entity.sensors().get( Sensors.newSensor(Object.class, key) )
: mode == SensorResolutionMode.SENSOR_DEFINITION ?
entity.getEntityType().getSensor(key)
: Exceptions.propagate(new IllegalStateException("Invalid mode "+mode));
} catch (Exception e) {
throw handleModelError("Error resolving attribute '"+key+"' on "+entity, e);
}
if (result == null) {
return null;
} else {
return wrapAsTemplateModel(result);
}
}
@Override
public String toString() {
return getClass().getName()+"["+entity+"]";
}
}
private static TemplateHashModel dotOrNull(Map<String,?> extraSubstitutions) {
if (extraSubstitutions==null) return null;
return new DotSplittingTemplateModel(extraSubstitutions);
}
private static TemplateHashModel wrappedBeanToHashOrNull(Object o) {
if (o==null) return null;
TemplateModel wrapped = null;
try {
wrapped = BROOKLYN_WRAPPER.wrapAsBean(o);
} catch (TemplateModelException e) {
throw Exceptions.propagate(e);
}
if (wrapped instanceof TemplateHashModel) return (TemplateHashModel) wrapped;
return null;
}
/**
* Provides access to config on an entity or management context, using
* <code>${config['entity.config.key']}</code> or <code>${mgmt['brooklyn.properties.key']}</code> notation,
* and also allowing access to <code>getX()</code> methods on entity (interface) or driver
* using <code>${entity.x}</code> or <code><${driver.x}</code>.
* Optional extra properties can be supplied, treated as per {@link DotSplittingTemplateModel}.
*/
public static final class EntityAndMapTemplateModel implements TemplateHashModel, UnwrappableTemplateModel {
protected final EntityInternal entity;
protected final EntityDriver driver;
protected final ManagementContext mgmt;
protected final DotSplittingTemplateModel extraSubstitutionsModel;
protected boolean isForWorkflow = false;
// note: the extra substitutions here (and in LocationAndMapTemplateModel) could be replaced with
// FirstAvailableTemplateModel(entityModel, mapHashModel)
protected EntityAndMapTemplateModel(ManagementContext mgmt, EntityInternal entity, EntityDriver driver) {
this.driver = driver;
this.entity = entity !=null ? entity : driver!=null ? (EntityInternal) driver.getEntity() : null;
this.mgmt = mgmt != null ? mgmt : this.entity!=null ? this.entity.getManagementContext() : null;
extraSubstitutionsModel = new DotSplittingTemplateModel(null);
}
public EntityAndMapTemplateModel setIsForWorkflow(boolean isForWorkflow) {
this.isForWorkflow = isForWorkflow;
return this;
}
@Override
public Maybe<Object> unwrap() {
return Maybe.ofDisallowingNull(entity!=null ? entity : mgmt!=null ? mgmt : extraSubstitutionsModel.unwrap().orNull());
}
public static TemplateHashModel forDriver(EntityDriver driver, Map<String,? extends Object> extraSubstitutions) {
return new FirstAvailableTemplateModel(new EntityAndMapTemplateModel(null, null, driver), wrappedBeanToHashOrNull(driver), wrappedBeanToHashOrNull(driver.getEntity()), dotOrNull(extraSubstitutions));
}
public static TemplateHashModel forEntity(Entity entity, Map<String,? extends Object> extraSubstitutions) {
EntityAndMapTemplateModel entityModel = new EntityAndMapTemplateModel(null, (EntityInternal) entity, null);
if (Boolean.TRUE.equals(IS_FOR_WORKFLOW.peek().orNull())) entityModel.setIsForWorkflow(true);
return new FirstAvailableTemplateModel(entityModel, wrappedBeanToHashOrNull(entity), dotOrNull(extraSubstitutions));
}
public static TemplateHashModel forEntityPossiblyInWorkflow(Entity entity, Map<String,? extends Object> extraSubstitutions, boolean isInWorkflow) {
return new FirstAvailableTemplateModel(
new EntityAndMapTemplateModel(null, (EntityInternal) entity, null).setIsForWorkflow(isInWorkflow),
wrappedBeanToHashOrNull(entity), dotOrNull(extraSubstitutions));
}
public static TemplateHashModel forManagementContext(ManagementContext mgmt, Map<String,? extends Object> extraSubstitutions) {
return new FirstAvailableTemplateModel(new EntityAndMapTemplateModel(mgmt, null, null), dotOrNull(extraSubstitutions));
}
@Deprecated /** @deprecated since 1.1 use {@link #forEntity(Entity, Map)} and related instead; substitions added separately using {@link FirstAvailableTemplateModel }*/
protected EntityAndMapTemplateModel(ManagementContext mgmt, Map<String,? extends Object> extraSubstitutions) {
this.entity = null;
this.driver = null;
this.mgmt = mgmt;
this.extraSubstitutionsModel = new DotSplittingTemplateModel(extraSubstitutions);
}
@Deprecated /** @deprecated since 1.1 use {@link #forEntity(Entity, Map)} and related instead; substitions added separately using {@link FirstAvailableTemplateModel }*/
protected EntityAndMapTemplateModel(EntityDriver driver, Map<String,? extends Object> extraSubstitutions) {
this.driver = driver;
this.entity = (EntityInternal) driver.getEntity();
this.mgmt = entity.getManagementContext();
this.extraSubstitutionsModel = new DotSplittingTemplateModel(extraSubstitutions);
}
@Deprecated /** @deprecated since 1.1 use {@link #forEntity(Entity, Map)} and related instead; substitions added separately using {@link FirstAvailableTemplateModel }*/
protected EntityAndMapTemplateModel(EntityInternal entity, Map<String,? extends Object> extraSubstitutions) {
this.entity = entity;
this.driver = null;
this.mgmt = entity.getManagementContext();
this.extraSubstitutionsModel = new DotSplittingTemplateModel(extraSubstitutions);
}
@Override
public boolean isEmpty() { return false; }
@Override
public TemplateModel get(String key) throws TemplateModelException {
if (extraSubstitutionsModel.contains(key))
return wrapAsTemplateModel( extraSubstitutionsModel.get(key) );
if ("entity".equals(key) && entity!=null)
return wrapAsTemplateModel( entity );
if ("config".equals(key)) {
if (entity!=null)
return new EntityConfigTemplateModel(entity);
else
return new MgmtConfigTemplateModel(mgmt);
}
if ("mgmt".equals(key)) {
return new MgmtConfigTemplateModel(mgmt);
}
if ("driver".equals(key) && driver!=null)
return wrapAsTemplateModel( driver );
if ("location".equals(key)) {
if (driver!=null && driver.getLocation()!=null)
return wrapAsTemplateModel( driver.getLocation() );
if (entity!=null)
return wrapAsTemplateModel( Iterables.getOnlyElement( entity.getLocations() ) );
}
if ("children".equals(key) && entity!=null)
return wrapAsTemplateModel( entity.getChildren() );
if ("members".equals(key) && entity!=null)
return wrapAsTemplateModel( entity instanceof Group ? ((Group)entity).getMembers() : MutableList.of() );
if ("sensor".equals(key)) return new EntityAttributeTemplateModel(entity, EntityAttributeTemplateModel.SensorResolutionMode.ATTRIBUTE_VALUE);
if ("attribute".equals(key)) return new EntityAttributeTemplateModel(entity, EntityAttributeTemplateModel.SensorResolutionMode.ATTRIBUTE_VALUE);
if ("attributeWhenReady".equals(key)) return new EntityAttributeTemplateModel(entity,
isForWorkflow ? EntityAttributeTemplateModel.SensorResolutionMode.ATTRIBUTE_WHEN_READY_FOR_WORKFLOW : EntityAttributeTemplateModel.SensorResolutionMode.ATTRIBUTE_WHEN_READY_FOR_TEMPLATES);
// new option
if ("sensor_definition".equals(key)) return new EntityAttributeTemplateModel(entity, EntityAttributeTemplateModel.SensorResolutionMode.SENSOR_DEFINITION);
if ("effector".equals(key)) return wrapAsTemplateModel(Maps.transformValues(entity.getMutableEntityType().getEffectors(), EffectorBase::of));
// // getters work for these
// if ("id".equals(key)) return wrapAsTemplateModel(entity.getId());
// if ("displayName".equals(key)) return wrapAsTemplateModel(entity.getDisplayName());
// if ("parent".equals(key)) return wrapAsTemplateModel(entity.getParent());
// if ("application".equals(key)) return wrapAsTemplateModel(entity.getApplication());
if ("name".equals(key)) return wrapAsTemplateModel(entity.getDisplayName());
if ("tags".equals(key)) return wrapAsTemplateModel(entity.tags().getTags());
// bit of hack, but sometimes we use ${javaSysProps.JVM_SYSTEM_PROPERTY}
if ("javaSysProps".equals(key))
return wrapAsTemplateModel( System.getProperties() );
return null;
}
@Override
public String toString() {
return getClass().getName()+"["+(entity!=null ? entity : mgmt)+"]";
}
}
/**
* Provides access to config on an entity or management context, using
* <code>${config['entity.config.key']}</code> or <code>${mgmt['brooklyn.properties.key']}</code> notation,
* and also allowing access to <code>getX()</code> methods on entity (interface) or driver
* using <code>${entity.x}</code> or <code><${driver.x}</code>.
* Optional extra properties can be supplied, treated as per {@link DotSplittingTemplateModel}.
*/
protected static final class LocationAndMapTemplateModel implements TemplateHashModel, UnwrappableTemplateModel {
protected final LocationInternal location;
protected final ManagementContext mgmt;
protected final DotSplittingTemplateModel extraSubstitutionsModel;
@Deprecated /** @deprecated since 1.1 use {@link #forLocation(LocationInternal, Map)} instead; substitions added separately using {@link FirstAvailableTemplateModel }*/
protected LocationAndMapTemplateModel(LocationInternal location, Map<String,? extends Object> extraSubstitutions) {
this.location = checkNotNull(location, "location");
this.mgmt = location.getManagementContext();
this.extraSubstitutionsModel = new DotSplittingTemplateModel(extraSubstitutions);
}
@Override
public Maybe<Object> unwrap() {
return Maybe.of(location);
}
static TemplateHashModel forLocation(LocationInternal location, Map<String,? extends Object> extraSubstitutions) {
return new FirstAvailableTemplateModel(new LocationAndMapTemplateModel(location, null), wrappedBeanToHashOrNull(location), dotOrNull(extraSubstitutions));
}
@Override
public boolean isEmpty() { return false; }
@Override
public TemplateModel get(String key) throws TemplateModelException {
if (extraSubstitutionsModel.contains(key))
return wrapAsTemplateModel( extraSubstitutionsModel.get(key) );
if ("location".equals(key))
return wrapAsTemplateModel( location );
if ("config".equals(key)) {
return new LocationConfigTemplateModel(location);
}
if ("mgmt".equals(key)) {
return new MgmtConfigTemplateModel(mgmt);
}
if (mgmt!=null) {
// TODO deprecated in 0.7.0, remove after next version
// ie not supported to access global props without qualification
Object result = mgmt.getConfig().getConfig(ConfigKeys.builder(Object.class).name(key).build());
if (result!=null) {
log.warn("Deprecated access of global brooklyn.properties value for "+key+"; should be qualified with 'mgmt.'");
return wrapAsTemplateModel( result );
}
}
if ("name".equals(key))
return wrapAsTemplateModel( location.getDisplayName() );
if ("javaSysProps".equals(key))
return wrapAsTemplateModel( System.getProperties() );
return null;
}
@Override
public String toString() {
return getClass().getName()+"["+location+"]";
}
}
/** Processes template contents with the given items in scope as per {@link EntityAndMapTemplateModel}. */
public static String processTemplateContents(String templateContents, final EntityInternal entity, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents("unknown", templateContents, entity, extraSubstitutions);
}
public static String processTemplateContents(String context, String templateContents, final EntityInternal entity, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents(context, templateContents, EntityAndMapTemplateModel.forEntity(entity, extraSubstitutions));
}
/** Processes template contents using the given map, passed to freemarker,
* with dot handling as per {@link DotSplittingTemplateModel}. */
public static String processTemplateContents(String templateContents, final Map<String, ? extends Object> substitutions) {
return processTemplateContents("unknown", templateContents, substitutions);
}
public static String processTemplateContents(String context, String templateContents, final Map<String, ? extends Object> substitutions) {
TemplateHashModel root;
try {
root = substitutions != null
? (TemplateHashModel)wrapAsTemplateModel(substitutions)
: null;
} catch (TemplateModelException e) {
throw new IllegalStateException("Unable to set up TemplateHashModel to parse template, given "+substitutions+": "+e, e);
}
return processTemplateContents(context, templateContents, root);
}
/** Processes template contents against the given {@link TemplateHashModel}. */
public static String processTemplateContents(String templateContents, final TemplateHashModel substitutions) {
return (String) processTemplateContents("unknown", templateContents, substitutions);
}
private static String processTemplateContents(String context, String templateContents, final TemplateHashModel substitutions) {
return (String) processTemplateContentsLegacy(context, templateContents, substitutions, false, true);
}
@Deprecated /** since 1.1, used to warn about deprecated use of ${entity.sensor.value} to have attribute-when-ready behaviour */
private static Object processTemplateContentsLegacy(String context, String templateContents, final TemplateHashModel substitutions, boolean allowSingleVariableObject, boolean logErrors) {
try {
TEMPLATE_FILE_WANTING_LEGACY_SYNTAX.push(context);
return processTemplateContents(context, templateContents, substitutions, allowSingleVariableObject, logErrors);
} finally {
TEMPLATE_FILE_WANTING_LEGACY_SYNTAX.pop();
}
}
public static Object processTemplateContents(String context, String templateContents, final TemplateHashModel substitutions, boolean allowSingleVariableObject, boolean logErrors) {
return processTemplateContents(context, templateContents, substitutions, allowSingleVariableObject, logErrors, InterpolationErrorMode.FAIL);
}
public static Object processTemplateContents(String context, String templateContents, final TemplateHashModel substitutions, boolean allowSingleVariableObject, boolean logErrors, InterpolationErrorMode errorMode) {
try {
Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
cfg.setLogTemplateExceptions(logErrors);
cfg.setNumberFormat("computer");
StringTemplateLoader templateLoader = new StringTemplateLoader();
templateLoader.putTemplate(context, templateContents);
cfg.setTemplateLoader(templateLoader);
Template template = cfg.getTemplate(context);
if (allowSingleVariableObject && template.getRootTreeNode().getClass().getName().equals("freemarker.core.DollarVariable")) {
Object dollarVariable = template.getRootTreeNode();
// calculateInterpolatedStringOrMarkup calls escapedExpression.eval(env); unv very little accessible, so we use reflection
Maybe<Object> escapedExpression = Reflections.getFieldValueMaybe(dollarVariable, "escapedExpression");
Environment env = template.createProcessingEnvironment(substitutions, null);
Maybe<Method> evalMethod = Reflections.findMethodMaybe(Expression.class, "eval", Environment.class);
try {
openLocalTemplateModelCache();
Maybe<Object> model = evalMethod.isAbsent() ? Maybe.Absent.castAbsent(evalMethod) : escapedExpression.map(expr -> {
try {
return Reflections.invokeMethodFromArgs(expr,
evalMethod.get(), MutableList.of(env), true);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
TemplateException te = Exceptions.getFirstThrowableOfType(e, TemplateException.class);
if (te!=null) {
try {
return new ForgivingFreemarkerTemplateExceptionHandler(errorMode).handleSingleVariableExpressionTemplate(te, templateContents);
} catch (TemplateException ex) {
throw Exceptions.propagate(ex);
}
}
throw Exceptions.propagate(e);
}
});
if (model.isPresent()) {
if (model.get() instanceof TemplateModel) {
return unwrapTemplateModelMaybe((TemplateModel) model.get()).get();
} else if (model.get()==null) {
// key not found, fall through to below for proper error handling
} else {
log.warn("Unable to find model in local cache for unwrapping: " + model.get());
}
} else {
log.warn("Unable to access FreeMarker internals to resolve " + templateContents + "; will cast argument as string");
}
} finally {
closeLocalTemplateModelCache();
}
}
// TODO could expose CAMP '$brooklyn:' style dsl, based on template.createProcessingEnvironment
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Writer out = new OutputStreamWriter(baos);
template.setTemplateExceptionHandler(new ForgivingFreemarkerTemplateExceptionHandler(errorMode));
template.process(substitutions, out);
out.flush();
return new String(baos.toByteArray());
} catch (Exception e) {
if (logErrors) {
if (e instanceof RuntimeInterruptedException) {
log.warn("Template not currently resolvable: " + Exceptions.collapseText(e));
} else {
log.warn("Error processing template (propagating): " + Exceptions.collapseText(e), e);
}
log.debug("Template which could not be parsed (causing " + e + ") is:"
+ (Strings.isMultiLine(templateContents) ? "\n" + templateContents : templateContents));
}
throw Exceptions.propagate(e);
}
}
public static Object processTemplateContentsForWorkflow(String context, String templateContents, final TemplateHashModel substitutions, boolean allowSingleVariableObject, boolean logErrors, InterpolationErrorMode errorMode) {
try {
IS_FOR_WORKFLOW.push(true);
return processTemplateContents(context, templateContents, substitutions, allowSingleVariableObject, logErrors, errorMode);
} finally {
IS_FOR_WORKFLOW.pop();
}
}
public enum InterpolationErrorMode {
FAIL,
BLANK,
IGNORE,
}
InterpolationErrorMode interpolationErrorMode;
public void setInterpolationErrorMode(InterpolationErrorMode interpolationErrorMode) {
this.interpolationErrorMode = interpolationErrorMode;
}
public static class ForgivingFreemarkerTemplateExceptionHandler implements TemplateExceptionHandler {
private final InterpolationErrorMode errorMode;
public ForgivingFreemarkerTemplateExceptionHandler(InterpolationErrorMode errorMode) {
this.errorMode = errorMode;
}
public void handleTemplateException(TemplateException te, Environment env, Writer out) throws TemplateException {
if (errorMode==null || errorMode==InterpolationErrorMode.FAIL) throw te;
if (errorMode==InterpolationErrorMode.BLANK) return;
if (errorMode==InterpolationErrorMode.IGNORE) {
try {
// below won't work for complex expressions but those are discouraged anyways
// it also doesn't work for nested maps where an early variable is unavailable, and those _are_ supported
// eg ${entity.config.exists_but_does_not_have_key.key} -> it returns ${entity.config.exists_but_does_not_have_key}
/// out.write("${" + te.getBlamedExpressionString() + "}");
// this could work, but gives a string result so we need to intercept the renderer (which is a static :( ) -- or parse it to find the element we want
// te.getFTLInstructionStack();
// this would work better, if we want to access private fields
// TemplateElement els[] = env.instructionStack;
// env.instructionStackSize - 1
// aha but this allows us access!
TemplateElement[] instructions = _CoreAPI.getInstructionStackSnapshot(env);
if (instructions.length>0) {
out.write(instructions[instructions.length-1].getCanonicalForm());
return;
}
// can't find the instructions, so throw
throw Exceptions.propagateAnnotated("Unable to retrieve instruction so cannot ignore error in processing it", te);
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
}
public TemplateModel handleSingleVariableExpressionTemplate(TemplateException te, String templateText) throws TemplateException {
if (errorMode == InterpolationErrorMode.BLANK) return new SimpleScalar("");
if (errorMode == InterpolationErrorMode.IGNORE) return new SimpleScalar(templateText);
//otherwise -- if (errorMode==null || errorMode==InterpolationErrorMode.FAIL)
throw te;
}
}
}