package com.floreysoft.jmte.template;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import com.floreysoft.jmte.DefaultModelAdaptor;
import com.floreysoft.jmte.Engine;
import com.floreysoft.jmte.ModelAdaptor;
import com.floreysoft.jmte.ProcessListener;
import com.floreysoft.jmte.ProcessListener.Action;
import com.floreysoft.jmte.ScopedMap;
import com.floreysoft.jmte.TemplateContext;
import com.floreysoft.jmte.token.ElseToken;
import com.floreysoft.jmte.token.EndToken;
import com.floreysoft.jmte.token.ExpressionToken;
import com.floreysoft.jmte.token.ForEachToken;
import com.floreysoft.jmte.token.IfToken;
import com.floreysoft.jmte.token.InvalidToken;
import com.floreysoft.jmte.token.PlainTextToken;
import com.floreysoft.jmte.token.StringToken;
import com.floreysoft.jmte.token.Token;
import com.floreysoft.jmte.token.TokenStream;
public class InterpretedTemplate extends AbstractTemplate {
protected final TokenStream tokenStream;
protected transient StringBuilder output;
protected transient TemplateContext context;
public InterpretedTemplate(String template, String sourceName, Engine engine) {
this.template = template;
this.engine = engine;
this.sourceName = sourceName;
tokenStream = new TokenStream(sourceName, template, engine
.getExprStartToken(), engine.getExprEndToken());
tokenStream.prefill();
}
@Override
@SuppressWarnings("unchecked")
public synchronized Set<String> getUsedVariables() {
if (this.usedVariables != null) {
return this.usedVariables;
}
this.usedVariables = new TreeSet<String>();
final Engine engine = new Engine();
final ScopedMap scopedMap = new ScopedMap(Collections.EMPTY_MAP);
ProcessListener processListener = new ProcessListener() {
@Override
public void log(TemplateContext context, Token token, Action action) {
if (token instanceof ExpressionToken) {
String variable = ((ExpressionToken) token).getExpression();
if (!isLocal(variable)) {
usedVariables.add(variable);
}
}
}
// a variable is local if any enclosing foreach has it as a step
// variable
private boolean isLocal(String variable) {
for (Token token : context.scopes) {
if (token instanceof ForEachToken) {
String foreachVarName = ((ForEachToken) token)
.getVarName();
if (foreachVarName.equals(variable)) {
return true;
}
}
}
return false;
}
};
final Locale locale = Locale.getDefault();
context = new TemplateContext(template, locale, sourceName, scopedMap,
new DefaultModelAdaptor(), engine, engine.getErrorHandler(), processListener);
transformPure(context);
return usedVariables;
}
@Override
public synchronized String transform(Map<String, Object> model, Locale locale,
ModelAdaptor modelAdaptor, ProcessListener processListener) {
try {
context = new TemplateContext(template, locale, sourceName, new ScopedMap(
model), modelAdaptor, engine, engine.getErrorHandler(), processListener);
String transformed = transformPure(context);
return transformed;
} finally {
context = null;
output = null;
}
}
protected String transformPure(TemplateContext context) {
tokenStream.reset();
output = new StringBuilder(
(int) (context.template.length() * context.engine
.getExpansionSizeFactor()));
tokenStream.nextToken();
while (tokenStream.currentToken() != null) {
content(false);
}
return output.toString();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void foreach(boolean inheritedSkip) {
ForEachToken feToken = (ForEachToken) tokenStream.currentToken();
Iterable iterable = (Iterable) feToken.evaluate(context);
// begin a fresh iteration with a reset index
feToken.setIterator(iterable.iterator());
feToken.resetIndex();
tokenStream.consume();
context.model.enterScope();
context.push(feToken);
try {
// in case we do not want to evaluate the body, we just do a quick
// scan until the matching end
if (inheritedSkip || !feToken.iterator().hasNext()) {
Token contentToken;
while ((contentToken = tokenStream.currentToken()) != null
&& !(contentToken instanceof EndToken)) {
content(true);
}
if (contentToken == null) {
engine.getErrorHandler().error("missing-end", feToken);
} else {
tokenStream.consume();
context.notifyProcessListener(contentToken, Action.END);
}
} else {
while (feToken.iterator().hasNext()) {
context.model.put(feToken.getVarName(), feToken.advance());
addSpecialVariables(feToken, context.model);
// for each iteration we need to rewind to the beginning
// of the for loop
tokenStream.rewind(feToken);
Token contentToken;
while ((contentToken = tokenStream.currentToken()) != null
&& !(contentToken instanceof EndToken)) {
content(false);
}
if (contentToken == null) {
engine.getErrorHandler().error("missing-end", feToken);
} else {
tokenStream.consume();
context.notifyProcessListener(contentToken, Action.END);
}
if (!feToken.isLast()) {
output.append(feToken.getSeparator());
}
}
}
} finally {
context.model.exitScope();
context.pop();
}
}
private void condition(boolean inheritedSkip) {
IfToken ifToken = (IfToken) tokenStream.currentToken();
tokenStream.consume();
context.push(ifToken);
try {
boolean localSkip;
if (inheritedSkip) {
localSkip = true;
} else {
localSkip = !(Boolean) ifToken.evaluate(context);
}
Token contentToken;
while ((contentToken = tokenStream.currentToken()) != null
&& !(contentToken instanceof EndToken)
&& !(contentToken instanceof ElseToken)) {
content(localSkip);
}
if (contentToken instanceof ElseToken) {
tokenStream.consume();
// toggle for else branch
if (!inheritedSkip) {
localSkip = !localSkip;
}
context.notifyProcessListener(contentToken,
inheritedSkip ? Action.SKIP : Action.EVAL);
while ((contentToken = tokenStream.currentToken()) != null
&& !(contentToken instanceof EndToken)) {
content(localSkip);
}
}
if (contentToken == null) {
engine.getErrorHandler().error("missing-end", ifToken);
} else {
tokenStream.consume();
context.notifyProcessListener(contentToken, Action.END);
}
} finally {
context.pop();
}
}
private void content(boolean skip) {
Token token = tokenStream.currentToken();
context.notifyProcessListener(token, skip ? Action.SKIP : Action.EVAL);
if (token instanceof PlainTextToken) {
tokenStream.consume();
if (!skip) {
output.append(token.getText());
}
} else if (token instanceof StringToken) {
tokenStream.consume();
if (!skip) {
String expanded = (String) token.evaluate(context);
output.append(expanded);
}
} else if (token instanceof ForEachToken) {
foreach(skip);
} else if (token instanceof IfToken) {
condition(skip);
} else if (token instanceof ElseToken) {
tokenStream.consume();
engine.getErrorHandler().error("else-out-of-scope", token);
} else if (token instanceof EndToken) {
tokenStream.consume();
engine.getErrorHandler().error("unmatched-end", token);
} else if (token instanceof InvalidToken) {
tokenStream.consume();
engine.getErrorHandler().error("invalid-expression", token);
} else {
tokenStream.consume();
// what ever else there may be, we just evaluate it
String evaluated = (String) token.evaluate(context);
output.append(evaluated);
}
}
@Override
public String toString() {
return template;
}
}