419076712678ff7af4325dd05d3f93f378a0a709
[juplo-dialect] / src / main / java / de / juplo / thymeleaf / ImportVariablesAttributeProcessor.java
1 package de.juplo.thymeleaf;
2
3
4 import com.fasterxml.jackson.core.JsonFactory;
5 import de.juplo.jackson.SimpleMapper;
6 import java.io.IOException;
7 import java.util.HashMap;
8 import java.util.Iterator;
9 import java.util.List;
10 import java.util.Map;
11 import java.util.Map.Entry;
12 import java.util.regex.Matcher;
13 import java.util.regex.Pattern;
14 import org.slf4j.Logger;
15 import org.slf4j.LoggerFactory;
16 import org.thymeleaf.Arguments;
17 import org.thymeleaf.Configuration;
18 import org.thymeleaf.TemplateProcessingParameters;
19 import org.thymeleaf.context.IContext;
20 import org.thymeleaf.context.VariablesMap;
21 import org.thymeleaf.dom.Element;
22 import org.thymeleaf.dom.Node;
23 import org.thymeleaf.IEngineConfiguration;
24 import org.thymeleaf.context.ITemplateContext;
25 import org.thymeleaf.engine.AttributeName;
26 import org.thymeleaf.engine.EngineEventUtils;
27 import org.thymeleaf.exceptions.TemplateProcessingException;
28 import org.thymeleaf.model.IProcessableElementTag;
29 import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
30 import org.thymeleaf.processor.element.IElementTagStructureHandler;
31 import org.thymeleaf.standard.expression.FragmentExpression;
32 import org.thymeleaf.standard.expression.FragmentExpression.ExecutedFragmentExpression;
33 import org.thymeleaf.standard.expression.IStandardExpression;
34 import org.thymeleaf.standard.expression.NoOpToken;
35 import org.thymeleaf.standard.expression.StandardExpressionExecutionContext;
36 import org.thymeleaf.templatemode.TemplateMode;
37 import org.thymeleaf.templateresolver.ITemplateResolver;
38 import org.thymeleaf.templateresolver.TemplateResolution;
39
40
41
42 /**
43  * Retrievs and parses JSON-data and imports the parsed variables as node-local
44  * variables.
45  * @author Kai Moritz
46  */
47 public class ImportVariablesAttributeProcessor extends AbstractAttributeTagProcessor
48 {
49   private static final Logger LOG =
50       LoggerFactory.getLogger(ImportVariablesAttributeProcessor.class);
51   private static final JsonFactory FACTORY = new JsonFactory();
52   private static final String PROPERTY_NAME =
53       ImportVariablesAttributeProcessor.class.getCanonicalName() + "_VARIABLES";
54
55   public static final Pattern PATTERN =
56       Pattern.compile(
57           "^\\s*(?:(?:(merge)|replace):)?\\s*(?:(\\{.*\\})|(.*))\\s*$",
58           Pattern.DOTALL | Pattern.CASE_INSENSITIVE
59           );
60
61   public static final String ATTR_NAME = "variables";
62   public static final int ATTR_PRECEDENCE = 200;
63
64
65   public ImportVariablesAttributeProcessor(String prefix)
66   {
67     super(TemplateMode.HTML, prefix, null, false, ATTR_NAME, true, ATTR_PRECEDENCE, true);
68   }
69
70
71   @Override
72   protected void doProcess(
73       final ITemplateContext context,
74       final IProcessableElementTag element,
75       final AttributeName name,
76       final String attribute,
77       final IElementTagStructureHandler handler
78       )
79   {
80     if (attribute == null)
81       return;
82
83     String location;
84     try
85     {
86       final Object result;
87       final IStandardExpression expression =
88          EngineEventUtils.computeAttributeExpression(
89              context,
90              element,
91              name,
92              attribute
93              );
94
95       if (expression != null && expression instanceof FragmentExpression)
96       {
97         final ExecutedFragmentExpression executedFragmentExpression =
98             FragmentExpression.createExecutedFragmentExpression(
99                 context,
100                 (FragmentExpression) expression,
101                 StandardExpressionExecutionContext.NORMAL
102                 );
103         result =
104             FragmentExpression.resolveExecutedFragmentExpression(
105                 context,
106                 executedFragmentExpression,
107                 true
108                 );
109       }
110       else
111       {
112         result = expression.execute(context);
113       }
114
115       // If the result of this expression is NO-OP, there is nothing to execute
116       if (result == NoOpToken.VALUE)
117       {
118         handler.removeAttribute(name);
119         return;
120       }
121
122       location = result.toString();
123     }
124     catch (final TemplateProcessingException e)
125     {
126       location = attribute;
127     }
128
129
130     Iterator<Entry<String, Object>> it = null;
131
132     Matcher matcher = PATTERN.matcher(location);
133     boolean merge = false;
134     String json = null;
135
136     if (matcher.matches())
137     {
138       merge = matcher.group(1) != null;
139       json = matcher.group(2);
140       location = matcher.group(3);
141     }
142
143     if (json != null)
144     {
145       LOG.info("parsing parameter as JSON");
146       LOG.debug("parameter: {}", json);
147       try
148       {
149         it = SimpleMapper.getObjectIterator(FACTORY.createParser(json));
150       }
151       catch (IOException e)
152       {
153         LOG.error("cannot parse parameter as JSON: {}", json, e.getMessage());
154         return;
155       }
156     }
157     else
158     {
159       LOG.info("retriving {} as Spring-resource", location);
160       IEngineConfiguration config = context.getConfiguration();
161       for (ITemplateResolver resolver : config.getTemplateResolvers())
162       {
163         TemplateResolution resolution =
164             resolver.resolveTemplate(
165                 config,
166                 context.getTemplateData().getTemplate(),
167                 location,
168                 null
169                 );
170         if (resolution != null)
171         {
172           try
173           {
174             it = SimpleMapper.getObjectIterator(FACTORY.createParser(resolution.getTemplateResource().reader()));
175             break;
176           }
177           catch (IOException e) {}
178         }
179       }
180
181       if (it == null)
182       {
183         LOG.error("cannot resolve {} as JSON (not found)!", location);
184         return;
185       }
186     }
187
188     try
189     {
190       Map<String, Object> variables = getVariables(context);
191       if (merge)
192       {
193         while(it.hasNext())
194         {
195           Entry<String, Object> variable = it.next();
196           String key = variable.getKey();
197           Object value = variable.getValue();
198           Object existing = context.getVariable(key);
199           if (existing != null)
200           {
201             if (value instanceof String)
202             {
203               if (!(existing instanceof String))
204               {
205                 LOG.error(
206                     "cannot merge variable {} of type {} with a string",
207                     key,
208                     existing.getClass()
209                     );
210                 throw new RuntimeException(
211                     "Type-Missmatch for variable  " + key
212                     );
213               }
214
215               String string = ((String)existing).concat((String) value);
216               LOG.info("appending variable to string {}", key);
217               handler.setLocalVariable(key, string);
218             }
219             else if (value instanceof Map)
220             {
221               if (!(existing instanceof Map))
222               {
223                 LOG.error(
224                     "cannot merge variable {} of type {} with a map",
225                     key,
226                     existing.getClass()
227                     );
228                 throw new RuntimeException(
229                     "Type-Missmatch for variable  " + key
230                     );
231               }
232
233               Map map = ((Map)existing);
234               map.putAll((Map) value);
235               LOG.info("merging variable with map {}", key);
236               handler.setLocalVariable(key, map);
237             }
238             else if (value instanceof List)
239             {
240               if (!(existing instanceof List))
241               {
242                 LOG.error(
243                     "cannot merge variable {} of type {} with a list",
244                     key,
245                     existing.getClass()
246                     );
247                 throw new RuntimeException(
248                     "Type-Missmatch for variable  " + key
249                     );
250               }
251
252               List list = ((List)existing);
253               list.addAll((List) value);
254               LOG.info("appending contents of variable to list {}", key);
255               handler.setLocalVariable(key, list);
256             }
257             else
258             {
259               LOG.error(
260                   "variable {} is of unexpected type {}", key, value.getClass()
261                   );
262               throw new RuntimeException(
263                   "Found variable of unexpected type: " + key
264                   );
265             }
266           }
267           else
268           {
269             LOG.info("adding new variable {}", key);
270             handler.setLocalVariable(key, value);
271           }
272         }
273       }
274       else
275         while(it.hasNext())
276         {
277           Entry<String, Object> variable = it.next();
278           String key = variable.getKey();
279           Object value = variable.getValue();
280           LOG.info("adding variable {}", key);
281           variables.put(key, value);
282           handler.setLocalVariable(key, value);
283         }
284     }
285     catch (IllegalArgumentException e)
286     {
287       LOG.error("cannot parse {} as JSON: {}", location, e.getMessage());
288       throw new RuntimeException(e);
289     }
290
291     handler.removeAttribute(name);
292   }
293
294
295   Map<String, Object> getVariables(ITemplateContext context)
296   {
297     Map<String, Object> variables = new HashMap<>();
298     return variables;
299   }
300
301
302   static Object convert(JsonParser parser) throws IOException
303   {
304     JsonToken token = parser.getCurrentToken();
305     if (token == null)
306       fail(parser, "unexpected EOF");
307
308     switch (token)
309     {
310       case VALUE_STRING:       return parser.getText();
311       case VALUE_NUMBER_INT:   return parser.getIntValue();
312       case VALUE_NUMBER_FLOAT: return parser.getDoubleValue();
313       case START_OBJECT:       return convertObject(parser);
314       case START_ARRAY:        return convertArray(parser);
315       case VALUE_TRUE:         return Boolean.TRUE;
316       case VALUE_FALSE:        return Boolean.FALSE;
317       case VALUE_NULL:         return null;
318     }
319
320     fail(parser, "unexpected token " + token.toString());
321     return null; // << Will never be reached, because fail always throws an exception
322   }
323
324   static Map<String, Object> convertObject(JsonParser parser) throws IOException
325   {
326     JsonToken token = parser.nextToken();
327     if (token == null)
328       fail(parser, "unexpected EOF");
329
330     Map<String, Object> map = new LinkedHashMap<>();
331
332     while (!JsonToken.END_OBJECT.equals(token))
333     {
334       if (!JsonToken.FIELD_NAME.equals(token))
335         fail(parser, "expected a field-name");
336
337       String name = parser.getText();
338       parser.nextToken();
339       Object value = convert(parser);
340       map.put(name, value);
341
342       token = parser.nextToken();
343       if (token == null)
344         fail(parser, "unexpected EOF");
345     }
346
347     return map;
348   }
349
350   static List<Object> convertArray(JsonParser parser) throws IOException
351   {
352     JsonToken token = parser.nextToken();
353     if (token == null)
354       fail(parser, "unexpected EOF");
355
356     List<Object> list = new LinkedList<>();
357
358     while (!JsonToken.END_ARRAY.equals(token))
359     {
360       list.add(convert(parser));
361
362       token = parser.nextToken();
363       if (token == null)
364         fail(parser, "unexpected EOF");
365     }
366
367     return list;
368   }
369
370   static void fail(JsonParser parser, String message)
371   {
372     JsonLocation location = parser.getCurrentLocation();
373     LOG.error(
374         "{} at char-offset {} (line {}, column {})",
375         message,
376         location.getCharOffset(),
377         location.getLineNr(),
378         location.getColumnNr()
379         );
380     throw new RuntimeException("Cannot parse JSON: " + message);
381   }
382 }