+package de.juplo.thymeleaf;
+
+
+import com.fasterxml.jackson.core.JsonFactory;
+import de.juplo.jackson.SimpleMapper;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.thymeleaf.Arguments;
+import org.thymeleaf.Configuration;
+import org.thymeleaf.TemplateProcessingParameters;
+import org.thymeleaf.context.IContext;
+import org.thymeleaf.context.VariablesMap;
+import org.thymeleaf.dom.Element;
+import org.thymeleaf.dom.Node;
+import org.thymeleaf.IEngineConfiguration;
+import org.thymeleaf.context.ITemplateContext;
+import org.thymeleaf.engine.AttributeName;
+import org.thymeleaf.engine.EngineEventUtils;
+import org.thymeleaf.exceptions.TemplateProcessingException;
+import org.thymeleaf.model.IProcessableElementTag;
+import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
+import org.thymeleaf.processor.element.IElementTagStructureHandler;
+import org.thymeleaf.standard.expression.FragmentExpression;
+import org.thymeleaf.standard.expression.FragmentExpression.ExecutedFragmentExpression;
+import org.thymeleaf.standard.expression.IStandardExpression;
+import org.thymeleaf.standard.expression.NoOpToken;
+import org.thymeleaf.standard.expression.StandardExpressionExecutionContext;
+import org.thymeleaf.templatemode.TemplateMode;
+import org.thymeleaf.templateresolver.ITemplateResolver;
+import org.thymeleaf.templateresolver.TemplateResolution;
+
+
+
+/**
+ * Retrievs and parses JSON-data and imports the parsed variables as node-local
+ * variables.
+ * @author Kai Moritz
+ */
+public class ImportVariablesAttributeProcessor extends AbstractAttributeTagProcessor
+{
+ private static final Logger LOG =
+ LoggerFactory.getLogger(ImportVariablesAttributeProcessor.class);
+ private static final JsonFactory FACTORY = new JsonFactory();
+ private static final String PROPERTY_NAME =
+ ImportVariablesAttributeProcessor.class.getCanonicalName() + "_VARIABLES";
+
+ public static final Pattern PATTERN =
+ Pattern.compile(
+ "^\\s*(?:(?:(merge)|replace):)?\\s*(?:(\\{.*\\})|(.*))\\s*$",
+ Pattern.DOTALL | Pattern.CASE_INSENSITIVE
+ );
+
+ public static final String ATTR_NAME = "variables";
+ public static final int ATTR_PRECEDENCE = 200;
+
+
+ public ImportVariablesAttributeProcessor(String prefix)
+ {
+ super(TemplateMode.HTML, prefix, null, false, ATTR_NAME, true, ATTR_PRECEDENCE, true);
+ }
+
+
+ @Override
+ protected void doProcess(
+ final ITemplateContext context,
+ final IProcessableElementTag element,
+ final AttributeName name,
+ final String attribute,
+ final IElementTagStructureHandler handler
+ )
+ {
+ if (attribute == null)
+ return;
+
+ String location;
+ try
+ {
+ final Object result;
+ final IStandardExpression expression =
+ EngineEventUtils.computeAttributeExpression(
+ context,
+ element,
+ name,
+ attribute
+ );
+
+ if (expression != null && expression instanceof FragmentExpression)
+ {
+ final ExecutedFragmentExpression executedFragmentExpression =
+ FragmentExpression.createExecutedFragmentExpression(
+ context,
+ (FragmentExpression) expression,
+ StandardExpressionExecutionContext.NORMAL
+ );
+ result =
+ FragmentExpression.resolveExecutedFragmentExpression(
+ context,
+ executedFragmentExpression,
+ true
+ );
+ }
+ else
+ {
+ result = expression.execute(context);
+ }
+
+ // If the result of this expression is NO-OP, there is nothing to execute
+ if (result == NoOpToken.VALUE)
+ {
+ handler.removeAttribute(name);
+ return;
+ }
+
+ location = result.toString();
+ }
+ catch (final TemplateProcessingException e)
+ {
+ location = attribute;
+ }
+
+
+ Iterator<Entry<String, Object>> it = null;
+
+ Matcher matcher = PATTERN.matcher(location);
+ boolean merge = false;
+ String json = null;
+
+ if (matcher.matches())
+ {
+ merge = matcher.group(1) != null;
+ json = matcher.group(2);
+ location = matcher.group(3);
+ }
+
+ if (json != null)
+ {
+ LOG.info("parsing parameter as JSON");
+ LOG.debug("parameter: {}", json);
+ try
+ {
+ it = SimpleMapper.getObjectIterator(FACTORY.createParser(json));
+ }
+ catch (IOException e)
+ {
+ LOG.error("cannot parse parameter as JSON: {}", json, e.getMessage());
+ return;
+ }
+ }
+ else
+ {
+ LOG.info("retriving {} as Spring-resource", location);
+ IEngineConfiguration config = context.getConfiguration();
+ for (ITemplateResolver resolver : config.getTemplateResolvers())
+ {
+ TemplateResolution resolution =
+ resolver.resolveTemplate(
+ config,
+ context.getTemplateData().getTemplate(),
+ location,
+ null
+ );
+ if (resolution != null)
+ {
+ try
+ {
+ it = SimpleMapper.getObjectIterator(FACTORY.createParser(resolution.getTemplateResource().reader()));
+ break;
+ }
+ catch (IOException e) {}
+ }
+ }
+
+ if (it == null)
+ {
+ LOG.error("cannot resolve {} as JSON (not found)!", location);
+ return;
+ }
+ }
+
+ try
+ {
+ Map<String, Object> variables = getVariables(context);
+ if (merge)
+ {
+ while(it.hasNext())
+ {
+ Entry<String, Object> variable = it.next();
+ String key = variable.getKey();
+ Object value = variable.getValue();
+ Object existing = context.getVariable(key);
+ if (existing != null)
+ {
+ if (value instanceof String)
+ {
+ if (!(existing instanceof String))
+ {
+ LOG.error(
+ "cannot merge variable {} of type {} with a string",
+ key,
+ existing.getClass()
+ );
+ throw new RuntimeException(
+ "Type-Missmatch for variable " + key
+ );
+ }
+
+ String string = ((String)existing).concat((String) value);
+ LOG.info("appending variable to string {}", key);
+ handler.setLocalVariable(key, string);
+ }
+ else if (value instanceof Map)
+ {
+ if (!(existing instanceof Map))
+ {
+ LOG.error(
+ "cannot merge variable {} of type {} with a map",
+ key,
+ existing.getClass()
+ );
+ throw new RuntimeException(
+ "Type-Missmatch for variable " + key
+ );
+ }
+
+ Map map = ((Map)existing);
+ map.putAll((Map) value);
+ LOG.info("merging variable with map {}", key);
+ handler.setLocalVariable(key, map);
+ }
+ else if (value instanceof List)
+ {
+ if (!(existing instanceof List))
+ {
+ LOG.error(
+ "cannot merge variable {} of type {} with a list",
+ key,
+ existing.getClass()
+ );
+ throw new RuntimeException(
+ "Type-Missmatch for variable " + key
+ );
+ }
+
+ List list = ((List)existing);
+ list.addAll((List) value);
+ LOG.info("appending contents of variable to list {}", key);
+ handler.setLocalVariable(key, list);
+ }
+ else
+ {
+ LOG.error(
+ "variable {} is of unexpected type {}", key, value.getClass()
+ );
+ throw new RuntimeException(
+ "Found variable of unexpected type: " + key
+ );
+ }
+ }
+ else
+ {
+ LOG.info("adding new variable {}", key);
+ handler.setLocalVariable(key, value);
+ }
+ }
+ }
+ else
+ while(it.hasNext())
+ {
+ Entry<String, Object> variable = it.next();
+ String key = variable.getKey();
+ Object value = variable.getValue();
+ LOG.info("adding variable {}", key);
+ variables.put(key, value);
+ handler.setLocalVariable(key, value);
+ }
+ }
+ catch (IllegalArgumentException e)
+ {
+ LOG.error("cannot parse {} as JSON: {}", location, e.getMessage());
+ throw new RuntimeException(e);
+ }
+
+ handler.removeAttribute(name);
+ }
+
+
+ Map<String, Object> getVariables(ITemplateContext context)
+ {
+ Map<String, Object> variables = new HashMap<>();
+ return variables;
+ }
+
+
+ static Object convert(JsonParser parser) throws IOException
+ {
+ JsonToken token = parser.getCurrentToken();
+ if (token == null)
+ fail(parser, "unexpected EOF");
+
+ switch (token)
+ {
+ case VALUE_STRING: return parser.getText();
+ case VALUE_NUMBER_INT: return parser.getIntValue();
+ case VALUE_NUMBER_FLOAT: return parser.getDoubleValue();
+ case START_OBJECT: return convertObject(parser);
+ case START_ARRAY: return convertArray(parser);
+ case VALUE_TRUE: return Boolean.TRUE;
+ case VALUE_FALSE: return Boolean.FALSE;
+ case VALUE_NULL: return null;
+ }
+
+ fail(parser, "unexpected token " + token.toString());
+ return null; // << Will never be reached, because fail always throws an exception
+ }
+
+ static Map<String, Object> convertObject(JsonParser parser) throws IOException
+ {
+ JsonToken token = parser.nextToken();
+ if (token == null)
+ fail(parser, "unexpected EOF");
+
+ Map<String, Object> map = new LinkedHashMap<>();
+
+ while (!JsonToken.END_OBJECT.equals(token))
+ {
+ if (!JsonToken.FIELD_NAME.equals(token))
+ fail(parser, "expected a field-name");
+
+ String name = parser.getText();
+ parser.nextToken();
+ Object value = convert(parser);
+ map.put(name, value);
+
+ token = parser.nextToken();
+ if (token == null)
+ fail(parser, "unexpected EOF");
+ }
+
+ return map;
+ }
+
+ static List<Object> convertArray(JsonParser parser) throws IOException
+ {
+ JsonToken token = parser.nextToken();
+ if (token == null)
+ fail(parser, "unexpected EOF");
+
+ List<Object> list = new LinkedList<>();
+
+ while (!JsonToken.END_ARRAY.equals(token))
+ {
+ list.add(convert(parser));
+
+ token = parser.nextToken();
+ if (token == null)
+ fail(parser, "unexpected EOF");
+ }
+
+ return list;
+ }
+
+ static void fail(JsonParser parser, String message)
+ {
+ JsonLocation location = parser.getCurrentLocation();
+ LOG.error(
+ "{} at char-offset {} (line {}, column {})",
+ message,
+ location.getCharOffset(),
+ location.getLineNr(),
+ location.getColumnNr()
+ );
+ throw new RuntimeException("Cannot parse JSON: " + message);
+ }
+}