/*
* 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 static com.google.common.base.Preconditions.checkNotNull;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Map;
import org.apache.brooklyn.api.entity.Entity;
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.sensor.AttributeSensor;
import org.apache.brooklyn.core.config.ConfigKeys;
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.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.sensor.DependentConfiguration;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Charsets;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
/** 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, ManagementContextInternal, Map)} for
* a description of how management access is done.
*/
public class TemplateProcessor {
private static final Logger log = LoggerFactory.getLogger(TemplateProcessor.class);
protected static TemplateModel wrapAsTemplateModel(Object o) throws TemplateModelException {
if (o instanceof Map) return new DotSplittingTemplateModel((Map<?,?>)o);
return ObjectWrapper.DEFAULT_WRAPPER.wrap(o);
}
/** @deprecated since 0.7.0 use {@link #processTemplateFile(String, Map)} */ @Deprecated
public static String processTemplate(String templateFileName, Map<String, ? extends Object> substitutions) {
return processTemplateFile(templateFileName, substitutions);
}
/** 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(templateContents, substitutions);
}
/** @deprecated since 0.7.0 use {@link #processTemplateFile(String, EntityDriver, Map)} */ @Deprecated
public static String processTemplate(String templateFileName, EntityDriver driver, Map<String, ? extends Object> extraSubstitutions) {
return processTemplateFile(templateFileName, driver, extraSubstitutions);
}
/** 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(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(templateContents, new EntityAndMapTemplateModel(driver, extraSubstitutions));
}
/** Processes template contents according to {@link EntityAndMapTemplateModel}. */
public static String processTemplateContents(String templateContents, ManagementContext managementContext, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents(templateContents, new EntityAndMapTemplateModel(managementContext, extraSubstitutions));
}
/** Processes template contents according to {@link EntityAndMapTemplateModel}. */
public static String processTemplateContents(String templateContents, Location location, Map<String,? extends Object> extraSubstitutions) {
return processTemplateContents(templateContents, new LocationAndMapTemplateModel((LocationInternal)location, extraSubstitutions));
}
/**
* 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 TemplateHashModel {
protected final Map<?,?> map;
protected DotSplittingTemplateModel(Map<?,?> map) {
this.map = 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) throws TemplateModelException {
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+"]";
}
}
/** 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) {
Exceptions.propagateIfFatal(e);
throw new IllegalStateException("Error accessing config '"+key+"'"+" on "+entity+": "+e, 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 new IllegalStateException("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 new IllegalStateException("Error accessing config '"+key+"'"
+ (location!=null ? " on "+location : "")+": "+e, e);
}
return null;
}
@Override
public String toString() {
return getClass().getName()+"["+location+"]";
}
}
protected final static class EntityAttributeTemplateModel implements TemplateHashModel {
protected final EntityInternal entity;
protected EntityAttributeTemplateModel(EntityInternal entity) {
this.entity = entity;
}
@Override
public boolean isEmpty() throws TemplateModelException {
return false;
}
@Override
public TemplateModel get(String key) throws TemplateModelException {
Object result;
try {
result = Entities.submit(entity, DependentConfiguration.attributeWhenReady(entity,
Sensors.builder(Object.class, key).persistence(AttributeSensor.SensorPersistenceMode.NONE).build())).get();
} catch (Exception e) {
throw Exceptions.propagate(e);
}
if (result == null) {
return null;
} else {
return wrapAsTemplateModel(result);
}
}
@Override
public String toString() {
return getClass().getName()+"["+entity+"]";
}
}
/**
* 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 EntityAndMapTemplateModel implements TemplateHashModel {
protected final EntityInternal entity;
protected final EntityDriver driver;
protected final ManagementContext mgmt;
protected final DotSplittingTemplateModel extraSubstitutionsModel;
protected EntityAndMapTemplateModel(ManagementContext mgmt, Map<String,? extends Object> extraSubstitutions) {
this.entity = null;
this.driver = null;
this.mgmt = mgmt;
this.extraSubstitutionsModel = new DotSplittingTemplateModel(extraSubstitutions);
}
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);
}
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 ("attribute".equals(key)) {
return new EntityAttributeTemplateModel(entity);
}
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 ("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 {
protected final LocationInternal location;
protected final ManagementContext mgmt;
protected final DotSplittingTemplateModel extraSubstitutionsModel;
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 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 ("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(templateContents, new EntityAndMapTemplateModel(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) {
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(templateContents, root);
}
/** Processes template contents against the given {@link TemplateHashModel}. */
public static String processTemplateContents(String templateContents, final TemplateHashModel substitutions) {
try {
Configuration cfg = new Configuration();
StringTemplateLoader templateLoader = new StringTemplateLoader();
templateLoader.putTemplate("config", templateContents);
cfg.setTemplateLoader(templateLoader);
Template template = cfg.getTemplate("config");
// TODO could expose CAMP '$brooklyn:' style dsl, based on template.createProcessingEnvironment
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Writer out = new OutputStreamWriter(baos);
template.process(substitutions, out);
out.flush();
return new String(baos.toByteArray());
} catch (Exception e) {
log.warn("Error processing template (propagating): "+e, e);
log.debug("Template which could not be parsed (causing "+e+") is:"
+ (Strings.isMultiLine(templateContents) ? "\n"+templateContents : templateContents));
throw Exceptions.propagate(e);
}
}
}