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