/*
* Copyright 2013 Martin Kouba
*
* Licensed 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.trimou.engine.segment;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.engine.MustacheEngine;
import org.trimou.engine.MustacheTagInfo;
import org.trimou.engine.context.ExecutionContext;
import org.trimou.engine.context.ValueWrapper;
import org.trimou.engine.interpolation.LiteralSupport;
import org.trimou.engine.parser.Template;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.handlebars.Helper;
import org.trimou.handlebars.HelperDefinition;
import org.trimou.handlebars.HelperDefinition.ValuePlaceholder;
import org.trimou.handlebars.Options;
import org.trimou.util.Checker;
import org.trimou.util.ImmutableList;
import org.trimou.util.ImmutableList.ImmutableListBuilder;
import org.trimou.util.ImmutableMap;
import org.trimou.util.ImmutableMap.ImmutableMapBuilder;
import org.trimou.util.Strings;
/**
* Wraps {@link Helper} instance and handles its execution (e.g. builds
* {@link Options} instance).
*
* @author Martin Kouba
* @see HelperAwareSegment
*/
class HelperExecutionHandler {
private final Helper helper;
private final OptionsBuilder optionsBuilder;
/**
*
* @param helper
* @param optionsBuilder
*/
private HelperExecutionHandler(Helper helper,
OptionsBuilder optionsBuilder) {
this.helper = helper;
this.optionsBuilder = optionsBuilder;
}
/**
*
* @param name
* @param configuration
* @param segment
* @return a handler for the given name or <code>null</code> if no such
* helper exists
*/
static HelperExecutionHandler from(String name, MustacheEngine engine,
HelperAwareSegment segment) {
// Split the name and detect unterminated literals
Iterator<String> parts = splitHelperName(name, segment);
Helper helper = engine.getConfiguration().getHelpers()
.get(parts.next());
if (helper == null) {
// No helper with the given name found
return null;
}
ImmutableListBuilder<Object> params = ImmutableList.builder();
ImmutableMapBuilder<String, Object> hash = ImmutableMap.builder();
LiteralSupport literalSupport = engine.getConfiguration()
.getLiteralSupport();
while (parts.hasNext()) {
// Next part is a param or a hash entry
String part = parts.next();
if (Strings.isListLiteral(part)) {
params.add(new ListValuePlaceholder(part, engine,
literalSupport, segment));
} else {
int equalsPosition = getFirstDeterminingEqualsCharPosition(
part);
if (equalsPosition != -1) {
String value = part.substring(equalsPosition + 1,
part.length());
if (Strings.isListLiteral(value)) {
hash.put(part.substring(0, equalsPosition),
new ListValuePlaceholder(part, engine,
literalSupport, segment));
} else {
hash.put(part.substring(0, equalsPosition),
getLiteralOrPlaceholder(value, engine, segment,
literalSupport));
}
} else {
params.add(getLiteralOrPlaceholder(part, engine, segment,
literalSupport));
}
}
}
OptionsBuilder optionsBuilder = new OptionsBuilder(params.build(),
hash.build(), segment, engine);
// Let the helper validate the tag definition
helper.validate(optionsBuilder);
return new HelperExecutionHandler(helper, optionsBuilder);
}
/**
*
* @param appendable
* @param executionContext
*/
Appendable execute(Appendable appendable,
ExecutionContext executionContext) {
final DefaultOptions options = optionsBuilder.build(appendable,
executionContext);
try {
helper.execute(options);
return options.getAppendable();
} finally {
options.release();
}
}
/**
* Extracts parts from an input string. This implementation is quite naive
* and should be possibly rewritten. Note that we can't use a simple
* splitter because of string literals may contain whitespace chars.
*
* @param name
* @param segment
* @return the parts of the helper name
* @throws MustacheException
* If a compilation problem occurs
*/
static Iterator<String> splitHelperName(String name, Segment segment) {
boolean stringLiteral = false;
boolean arrayLiteral = false;
boolean space = false;
List<String> parts = new ArrayList<>();
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < name.length(); i++) {
if (name.charAt(i) == ' ') {
if (!space) {
if (!stringLiteral && !arrayLiteral) {
if (buffer.length() > 0) {
parts.add(buffer.toString());
buffer = new StringBuilder();
}
space = true;
} else {
buffer.append(name.charAt(i));
}
}
} else {
if (!arrayLiteral
&& Strings.isStringLiteralSeparator(name.charAt(i))) {
stringLiteral = !stringLiteral;
} else if (!stringLiteral
&& Strings.isListLiteralStart(name.charAt(i))) {
arrayLiteral = true;
} else if (!stringLiteral
&& Strings.isListLiteralEnd(name.charAt(i))) {
arrayLiteral = false;
}
space = false;
buffer.append(name.charAt(i));
}
}
if (buffer.length() > 0) {
if (stringLiteral || arrayLiteral) {
throw new MustacheException(
MustacheProblem.COMPILE_HELPER_VALIDATION_FAILURE,
"Unterminated string or array literal detected: %s",
segment);
}
parts.add(buffer.toString());
}
return parts.iterator();
}
/**
*
* @param part
* @return the index of an equals char outside of any string literal,
* <code>-1</code> if no such char is found
*/
static int getFirstDeterminingEqualsCharPosition(String part) {
boolean stringLiteral = false;
for (int i = 0; i < part.length(); i++) {
if (Strings.isStringLiteralSeparator(part.charAt(i))) {
if (i == 0) {
// The first char is a string literal separator
return -1;
}
stringLiteral = !stringLiteral;
} else {
if (!stringLiteral && part.charAt(i) == '=') {
return i;
}
}
}
return -1;
}
private static Object getLiteralOrPlaceholder(String value,
MustacheEngine engine, HelperAwareSegment segment,
LiteralSupport literalSupport) {
Object literal = literalSupport.getLiteral(value, segment.getTagInfo());
return literal != null ? literal
: new DefaultValuePlaceholder(value, engine);
}
private static class OptionsBuilder implements HelperDefinition {
private final List<Object> parameters;
private final Map<String, Object> hash;
private final HelperAwareSegment segment;
private final MustacheEngine engine;
// true if no placeholder found, also if params list is empty
private final boolean isParamValuePlaceholderFound;
// true if no placeholder found, also if hash map is empty
private final boolean isHashValuePlaceholderFound;
private OptionsBuilder(List<Object> parameters,
Map<String, Object> hash, HelperAwareSegment segment,
MustacheEngine engine) {
this.parameters = parameters;
this.hash = hash;
this.segment = segment;
this.engine = engine;
this.isParamValuePlaceholderFound = initParamValuePlaceholderFound(
parameters);
this.isHashValuePlaceholderFound = initHashValuePlaceholderFound(
hash);
}
@Override
public MustacheTagInfo getTagInfo() {
return segment.getTagInfo();
}
@Override
public List<Object> getParameters() {
return parameters;
}
@Override
public Map<String, Object> getHash() {
return hash;
}
@Override
public String getContentLiteralBlock() {
if (segment instanceof ContainerSegment) {
return ((ContainerSegment) segment).getContentLiteralBlock();
} else {
return Strings.EMPTY;
}
}
public DefaultOptions build(Appendable appendable,
ExecutionContext executionContext) {
List<ValueWrapper> valueWrappers = isParamValuePlaceholderFound
|| isHashValuePlaceholderFound ? new LinkedList<>() : null;
return new DefaultOptions(appendable, executionContext, segment,
getFinalParameters(executionContext, valueWrappers),
getFinalHash(executionContext, valueWrappers),
valueWrappers, engine);
}
private List<Object> getFinalParameters(
ExecutionContext executionContext,
List<ValueWrapper> valueWrappers) {
if (isParamValuePlaceholderFound) {
// At this point parameters list is never empty
int size = parameters.size();
switch (size) {
case 1:
// Very often there will be only single param
return Collections
.singletonList(resolveValue(parameters.get(0),
valueWrappers, executionContext));
default:
List<Object> finalParams = new ArrayList<>(size);
for (Object param : parameters) {
finalParams.add(resolveValue(param, valueWrappers,
executionContext));
}
return Collections.unmodifiableList(finalParams);
}
} else {
return parameters;
}
}
private Map<String, Object> getFinalHash(
ExecutionContext executionContext,
List<ValueWrapper> valueWrappers) {
if (isHashValuePlaceholderFound) {
// At this point hash map is never empty
int size = hash.size();
switch (size) {
case 1:
Entry<String, Object> singleEntry = hash.entrySet()
.iterator().next();
return Collections.singletonMap(singleEntry.getKey(),
resolveValue(singleEntry.getValue(), valueWrappers,
executionContext));
default:
Map<String, Object> finalHash = new HashMap<>();
for (Entry<String, Object> entry : hash.entrySet()) {
finalHash.put(entry.getKey(),
resolveValue(entry.getValue(), valueWrappers,
executionContext));
}
return Collections.unmodifiableMap(finalHash);
}
} else {
return hash;
}
}
private Object resolveValue(Object value,
List<ValueWrapper> valueWrappers,
ExecutionContext executionContext) {
if (value instanceof ValuePlaceholder) {
if (value instanceof ListValuePlaceholder) {
ListValuePlaceholder listValues = (ListValuePlaceholder) value;
if (listValues.hasValuePlaceholderElement) {
ImmutableListBuilder<Object> builder = ImmutableList
.builder();
for (Object element : listValues) {
builder.add(resolveValue(element, valueWrappers,
executionContext));
}
return builder.build();
} else {
// Values are immutable
return listValues.getValues();
}
} else {
final ValueWrapper wrapper;
if (value instanceof DefaultValuePlaceholder) {
wrapper = ((DefaultValuePlaceholder) value)
.getProvider().get(executionContext);
} else {
wrapper = executionContext
.getValue(((ValuePlaceholder) value).getName());
}
valueWrappers.add(wrapper);
return wrapper.get();
}
} else {
return value;
}
}
private boolean initParamValuePlaceholderFound(
List<Object> parameters) {
if (parameters.isEmpty()) {
return false;
}
for (Object param : parameters) {
if (param instanceof ValuePlaceholder) {
return true;
}
}
return false;
}
private boolean initHashValuePlaceholderFound(
Map<String, Object> hash) {
if (hash.isEmpty()) {
return false;
}
for (Entry<String, Object> entry : hash.entrySet()) {
if (entry.getValue() instanceof ValuePlaceholder) {
return true;
}
}
return false;
}
}
private static class DefaultOptions implements Options {
private static final Logger LOGGER = LoggerFactory
.getLogger(DefaultOptions.class);
protected List<ValueWrapper> valueWrappers;
protected Appendable appendable;
protected int pushed;
protected ExecutionContext executionContext;
private final MustacheEngine engine;
private final HelperAwareSegment segment;
private final List<Object> parameters;
private final Map<String, Object> hash;
/**
*
* @param appendable
* @param executionContext
* @param segment
* @param parameters
* @param hash
* @param valueWrappers
* @param engine
*/
DefaultOptions(Appendable appendable, ExecutionContext executionContext,
HelperAwareSegment segment, List<Object> parameters,
Map<String, Object> hash, List<ValueWrapper> valueWrappers,
MustacheEngine engine) {
this.appendable = appendable;
this.valueWrappers = valueWrappers;
this.executionContext = executionContext;
this.pushed = 0;
this.segment = segment;
this.parameters = parameters;
this.hash = hash;
this.engine = engine;
}
@Override
public List<Object> getParameters() {
return parameters;
}
@Override
public Map<String, Object> getHash() {
return hash;
}
@Override
public void append(CharSequence sequence) {
try {
appendable.append(sequence);
} catch (IOException e) {
throw new MustacheException(MustacheProblem.RENDER_IO_ERROR, e);
}
}
@Override
public void fn() {
appendable = segment.fn(appendable, executionContext);
}
@Override
public void partial(String templateId) {
partial(templateId, appendable);
}
@Override
public void push(Object contextObject) {
pushed++;
executionContext = executionContext.setContextObject(contextObject);
}
@Override
public Object pop() {
if (pushed > 0) {
pushed--;
Object top = executionContext.getFirstContextObject();
executionContext = executionContext.getParent();
return top;
}
throw new MustacheException(
MustacheProblem.RENDER_HELPER_INVALID_POP_OPERATION);
}
@Override
public Object peek() {
return executionContext.getFirstContextObject();
}
@Override
public Object getValue(String key) {
if (valueWrappers == null) {
valueWrappers = new ArrayList<>(5);
}
ValueWrapper wrapper = executionContext.getValue(key);
valueWrappers.add(wrapper);
return wrapper.get();
}
@Override
public void partial(String templateId, Appendable appendable) {
partial(templateId, appendable, executionContext);
}
@Override
public void executeAsync(final HelperExecutable executable) {
// For async execution we need to wrap the original appendable
final AsyncAppendable asyncAppendable = new AsyncAppendable(
appendable);
// Now submit the executable and get the future
ExecutorService executor = engine.getConfiguration()
.geExecutorService();
if (executor == null) {
throw new MustacheException(
MustacheProblem.RENDER_ASYNC_PROCESSING_ERROR,
"ExecutorService must be set in order to submit an asynchronous task");
}
Future<AsyncAppendable> future = executor
.submit(() -> {
// We need a separate appendable for the async
// execution
DefaultOptions asyncOptions = new DefaultOptions(
new AsyncAppendable(asyncAppendable),
executionContext, segment, parameters, hash,
new ArrayList<>(), engine);
executable.execute(asyncOptions);
return (AsyncAppendable) asyncOptions
.getAppendable();
});
asyncAppendable.setFuture(future);
this.appendable = asyncAppendable;
}
@Override
public String source(String templateId) {
Checker.checkArgumentNotEmpty(templateId);
String mustacheSource = engine.getMustacheSource(templateId);
if (mustacheSource == null) {
throw new MustacheException(
MustacheProblem.RENDER_INVALID_PARTIAL_KEY,
"No mustache template found for the given key: %s %s",
templateId, segment.getOrigin());
}
return mustacheSource;
}
@Override
public Appendable getAppendable() {
return appendable;
}
@Override
public void fn(Appendable appendable) {
segment.fn(appendable, executionContext);
}
@Override
public MustacheTagInfo getTagInfo() {
return segment.getTagInfo();
}
@Override
public String getContentLiteralBlock() {
if (segment instanceof ContainerSegment) {
return ((ContainerSegment) segment).getContentLiteralBlock();
} else {
return Strings.EMPTY;
}
}
protected void partial(String templateId, Appendable appendable,
ExecutionContext executionContext) {
Checker.checkArgumentsNotNull(templateId, appendable);
Template partialTemplate = Segments.lookupTemplate(templateId,
engine, segment.getOrigin().getTemplate());
if (partialTemplate == null) {
throw new MustacheException(
MustacheProblem.RENDER_INVALID_PARTIAL_KEY,
"No partial found for the given key: %s %s", templateId,
segment.getOrigin());
}
// Note that indentation is not supported
partialTemplate.getRootSegment().execute(appendable,
executionContext);
}
void release() {
if (valueWrappers != null) {
int wrappersSize = valueWrappers.size();
if (wrappersSize == 1) {
valueWrappers.get(0).release();
} else if (wrappersSize > 1) {
for (ValueWrapper wrapper : valueWrappers) {
wrapper.release();
}
}
}
if (pushed > 0) {
LOGGER.info(
"{} remaining objects pushed on the context stack will be automatically garbage collected [helperName: {}, template: {}]",
pushed, splitHelperName(segment.getTagInfo().getText(), segment).next(),
segment.getTagInfo().getTemplateName());
}
}
}
private static class DefaultValuePlaceholder implements ValuePlaceholder {
private final String name;
private final ValueProvider provider;
DefaultValuePlaceholder(String name, MustacheEngine engine) {
this.name = name;
this.provider = new ValueProvider(name, engine.getConfiguration());
}
public String getName() {
return name;
}
ValueProvider getProvider() {
return provider;
}
}
private static class ListValuePlaceholder
implements ValuePlaceholder, Iterable<Object> {
private final boolean hasValuePlaceholderElement;
private final List<Object> values;
ListValuePlaceholder(String value, MustacheEngine engine,
LiteralSupport literalSupport, HelperAwareSegment segment) {
List<String> elements = Strings
.split(value.substring(1, value.length() - 1), ",");
ImmutableListBuilder<Object> builder = ImmutableList.builder();
for (String element : elements) {
builder.add(getLiteralOrPlaceholder(element.trim(), engine, segment,
literalSupport));
}
values = builder.build();
hasValuePlaceholderElement = initHasValuePlaceholderElement();
}
@Override
public String getName() {
throw new UnsupportedOperationException();
}
@Override
public Iterator<Object> iterator() {
return values.iterator();
}
List<Object> getValues() {
return values;
}
private boolean initHasValuePlaceholderElement() {
for (Object val : values) {
if (val instanceof ValuePlaceholder) {
return true;
}
}
return false;
}
}
}