juplo:variables parses its parameter as JSON and can merge variable-defs
[juplo-dialect] / src / main / java / de / juplo / thymeleaf / ImportVariablesAttrProcessor.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.io.InputStream;
8 import java.util.HashMap;
9 import java.util.Iterator;
10 import java.util.List;
11 import java.util.Locale;
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.Arguments;
19 import org.thymeleaf.Configuration;
20 import org.thymeleaf.TemplateProcessingParameters;
21 import org.thymeleaf.context.IContext;
22 import org.thymeleaf.context.VariablesMap;
23 import org.thymeleaf.dom.Element;
24 import org.thymeleaf.dom.Node;
25 import org.thymeleaf.processor.ProcessorResult;
26 import org.thymeleaf.processor.attr.AbstractAttrProcessor;
27 import org.thymeleaf.resourceresolver.IResourceResolver;
28 import org.thymeleaf.templateresolver.ITemplateResolver;
29 import org.thymeleaf.templateresolver.TemplateResolution;
30
31
32
33 /**
34  * Retrievs and parses JSON-data and imports the parsed variables as node-local
35  * variables.
36  * @author Kai Moritz
37  */
38 public class ImportVariablesAttrProcessor extends AbstractAttrProcessor
39 {
40   private static final Logger LOG =
41       LoggerFactory.getLogger(ImportVariablesAttrProcessor.class);
42   private static final JsonFactory FACTORY = new JsonFactory();
43   private static final String PROPERTY_NAME =
44       ImportVariablesAttrProcessor.class.getCanonicalName() + "_VARIABLES";
45
46   public static final Pattern PATTERN =
47       Pattern.compile(
48           "^\\s*(?:(?:(merge)|replace):)?\\s*(?:(\\{.*\\})|(.*))\\s*$",
49           Pattern.DOTALL | Pattern.CASE_INSENSITIVE
50           );
51   public static final int ATTR_PRECEDENCE = 200;
52
53
54   public ImportVariablesAttrProcessor()
55   {
56     super("variables");
57   }
58
59
60   @Override
61   public final ProcessorResult processAttribute(
62       final Arguments arguments,
63       final Element element,
64       final String name
65       )
66   {
67     Configuration config = arguments.getConfiguration();
68     String parameter = element.getAttributeValue(name);
69
70     Iterator<Entry<String, Object>> it = null;
71
72     Matcher matcher = PATTERN.matcher(parameter);
73     boolean merge = false;
74     String json = null;
75     String resource = parameter;
76
77     if (matcher.matches())
78     {
79       merge = matcher.group(1) != null;
80       json = matcher.group(2);
81       resource = matcher.group(3);
82     }
83
84     if (json != null)
85     {
86       LOG.info("parsing parameter as JSON");
87       LOG.debug("parameter: {}", json);
88       try
89       {
90         it = SimpleMapper.getObjectIterator(FACTORY.createParser(json));
91       }
92       catch (IOException e)
93       {
94         LOG.error("cannot parse parameter as JSON: {}", json, e.getMessage());
95         throw new RuntimeException(e);
96       }
97     }
98     else
99     {
100       LOG.info("retriving {} as JSON-template", resource);
101       TemplateProcessingParameters params =
102           new TemplateProcessingParameters(
103               config,
104               resource,
105               new IContext() // << We will not execute the template, hence we need no context
106               {
107                 @Override
108                 public VariablesMap<String, Object> getVariables()
109                 {
110                   return new VariablesMap<>();
111                 }
112
113                 @Override
114                 public Locale getLocale()
115                 {
116                   return Locale.getDefault();
117                 }
118
119                 @Override
120                 public void addContextExecutionInfo(String templateName)
121                 {
122                 }
123               });
124
125       for (ITemplateResolver t_resolver : config.getTemplateResolvers())
126       {
127         TemplateResolution resolution = t_resolver.resolveTemplate(params);
128         if (resolution == null)
129           continue;
130         if (!"JSON".equals(resolution.getTemplateMode()))
131           continue;
132         IResourceResolver r_resolver = resolution.getResourceResolver();
133         InputStream is =
134             r_resolver.getResourceAsStream(params, resolution.getResourceName());
135         if (is == null)
136           continue;
137
138         try
139         {
140           it = SimpleMapper.getObjectIterator(FACTORY.createParser(is));
141           break;
142         }
143         catch (IOException | IllegalArgumentException e)
144         {
145           LOG.error("cannot retreive {} as JSON: {}", parameter, e.getMessage());
146           throw new RuntimeException(e);
147         }
148       }
149
150       if (it == null)
151       {
152         LOG.error("cannot resolve {} as JSON (not found)!", parameter);
153         throw new RuntimeException("Template not found: " + parameter);
154       }
155     }
156
157     try
158     {
159       Map<String, Object> variables = getVariables(element);
160       if (merge)
161       {
162         while(it.hasNext())
163         {
164           Entry<String, Object> variable = it.next();
165           String key = variable.getKey();
166           Object value = variable.getValue();
167           Object existing = variables.get(key);
168           if (existing != null)
169           {
170             if (value instanceof String)
171             {
172               if (!(existing instanceof String))
173               {
174                 LOG.error(
175                     "cannot merge variable {} of type {} with a string",
176                     key,
177                     existing.getClass()
178                     );
179                 throw new RuntimeException(
180                     "Type-Missmatch for variable  " + key
181                     );
182               }
183
184               String string = ((String)existing).concat((String) value);
185               LOG.info("appending variable to string {}", key);
186               element.setNodeLocalVariable(key, string);
187             }
188             else if (value instanceof Map)
189             {
190               if (!(existing instanceof Map))
191               {
192                 LOG.error(
193                     "cannot merge variable {} of type {} with a map",
194                     key,
195                     existing.getClass()
196                     );
197                 throw new RuntimeException(
198                     "Type-Missmatch for variable  " + key
199                     );
200               }
201
202               Map map = ((Map)existing);
203               map.putAll((Map) value);
204               LOG.info("merging variable with map {}", key);
205               element.setNodeLocalVariable(key, map);
206             }
207             else if (value instanceof List)
208             {
209               if (!(existing instanceof List))
210               {
211                 LOG.error(
212                     "cannot merge variable {} of type {} with a list",
213                     key,
214                     existing.getClass()
215                     );
216                 throw new RuntimeException(
217                     "Type-Missmatch for variable  " + key
218                     );
219               }
220
221               List list = ((List)existing);
222               list.addAll((List) value);
223               LOG.info("appending contents of variable to list {}", key);
224               element.setNodeLocalVariable(key, list);
225             }
226             else
227             {
228               LOG.error(
229                   "variable {} is of unexpected type {}", key, value.getClass()
230                   );
231               throw new RuntimeException(
232                   "Found variable of unexpected type: " + key
233                   );
234             }
235           }
236           else
237           {
238             LOG.info("adding new variable {}", key);
239             element.setNodeLocalVariable(key, value);
240           }
241         }
242       }
243       else
244         while(it.hasNext())
245         {
246           Entry<String, Object> variable = it.next();
247           String key = variable.getKey();
248           Object value = variable.getValue();
249           LOG.info("adding variable {}", key);
250           variables.put(key, value);
251           element.setNodeLocalVariable(key, value);
252         }
253     }
254     catch (IllegalArgumentException e)
255     {
256       LOG.error("cannot parse {} as JSON: {}", parameter, e.getMessage());
257       throw new RuntimeException(e);
258     }
259
260     element.removeAttribute(name);
261
262     return ProcessorResult.OK;
263   }
264
265
266   Map<String, Object> getVariables(Node node)
267   {
268     Node parent = node;
269     do
270     {
271       Map<String, Object> variables =
272           (Map<String, Object>)parent.getNodeProperty(PROPERTY_NAME);
273
274       if (variables != null)
275         return variables;
276
277       parent = parent.getParent();
278     }
279     while (parent != null);
280
281     Map<String, Object> variables = new HashMap<>();
282     node.setNodeProperty(PROPERTY_NAME, variables);
283     return variables;
284   }
285
286
287   @Override
288   public int getPrecedence()
289   {
290     return ATTR_PRECEDENCE;
291   }
292 }