Ported app to Thymeleaf 3.0.x
[juplo-dialect] / src / main / java / de / juplo / thymeleaf / ImportVariablesAttributeProcessor.java
diff --git a/src/main/java/de/juplo/thymeleaf/ImportVariablesAttributeProcessor.java b/src/main/java/de/juplo/thymeleaf/ImportVariablesAttributeProcessor.java
new file mode 100644 (file)
index 0000000..4190767
--- /dev/null
@@ -0,0 +1,382 @@
+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);
+  }
+}