WIP: variables-dialect
[maven-thymeleaf-skin] / src / main / java / de / juplo / thymeleaf / ImportVariablesAttrProcessor.java
1 package de.juplo.thymeleaf;
2
3
4 import com.fasterxml.jackson.core.JsonFactory;
5 import com.fasterxml.jackson.core.JsonLocation;
6 import com.fasterxml.jackson.core.JsonParser;
7 import com.fasterxml.jackson.core.JsonToken;
8 import com.fasterxml.jackson.databind.ObjectMapper;
9 import java.io.IOException;
10 import java.io.InputStream;
11 import java.time.format.DateTimeFormatter;
12 import java.util.LinkedHashMap;
13 import java.util.LinkedList;
14 import java.util.List;
15 import java.util.Locale;
16 import java.util.Map;
17 import org.slf4j.Logger;
18 import org.slf4j.LoggerFactory;
19 import org.springframework.beans.factory.annotation.Autowired;
20 import org.springframework.beans.factory.annotation.Configurable;
21 import org.springframework.context.MessageSource;
22 import org.thymeleaf.Arguments;
23 import org.thymeleaf.Configuration;
24 import org.thymeleaf.TemplateProcessingParameters;
25 import org.thymeleaf.context.IContext;
26 import org.thymeleaf.context.VariablesMap;
27 import org.thymeleaf.dom.Element;
28 import org.thymeleaf.processor.ProcessorResult;
29 import org.thymeleaf.processor.attr.AbstractAttrProcessor;
30 import org.thymeleaf.resourceresolver.IResourceResolver;
31 import org.thymeleaf.templateresolver.ITemplateResolver;
32 import org.thymeleaf.templateresolver.TemplateResolution;
33
34
35
36 /**
37  *
38  * @author kai
39  */
40 @Configurable
41 public class ImportVariablesAttrProcessor extends AbstractAttrProcessor
42 {
43   public static final int ATTR_PRECEDENCE = 200;
44   public static final String ATTR_VAR_NAME =
45       JuploDialect.DIALECT_PREFIX + ":var";
46   public static final String ATTR_LOCALE_NAME =
47       JuploDialect.DIALECT_PREFIX + ":locale";
48   public static final String DEFAULT_VAR_NAME = "crumb";
49
50   private static final Logger LOG =
51       LoggerFactory.getLogger(ImportVariablesAttrProcessor.class);
52   private static final DateTimeFormatter FORMATTER =
53       DateTimeFormatter.ofPattern("dd.MM");
54   private static final ObjectMapper MAPPER = new ObjectMapper();
55   private static final JsonFactory FACTORY = new JsonFactory();
56
57
58   @Autowired
59   MessageSource messageSource;
60   @Autowired
61   Locale defaultLocale;
62
63
64   public ImportVariablesAttrProcessor()
65   {
66     super("variables");
67   }
68
69
70   @Override
71   public final ProcessorResult processAttribute(
72       final Arguments arguments,
73       final Element element,
74       final String name
75       )
76   {
77     Configuration config = arguments.getConfiguration();
78     String templateName = element.getAttributeValue(name);
79
80     TemplateProcessingParameters params =
81         new TemplateProcessingParameters(
82             config,
83             templateName,
84             new IContext() // << We will not execute the template, hence we need no context
85             {
86               @Override
87               public VariablesMap<String, Object> getVariables()
88               {
89                 return new VariablesMap<>();
90               }
91
92               @Override
93               public Locale getLocale()
94               {
95                 return Locale.getDefault();
96               }
97
98               @Override
99               public void addContextExecutionInfo(String templateName)
100               {
101               }
102             });
103
104     for (ITemplateResolver t_resolver : config.getTemplateResolvers())
105     {
106       TemplateResolution resolution = t_resolver.resolveTemplate(params);
107       if (resolution == null)
108         continue;
109       if (!"JSON".equals(resolution.getTemplateMode()))
110         continue;
111       IResourceResolver r_resolver = resolution.getResourceResolver();
112       InputStream is =
113           r_resolver.getResourceAsStream(params, resolution.getResourceName());
114       if (is == null)
115         continue;
116
117       try
118       {
119         /** Read the JSON and create the variables */
120         JsonParser parser = FACTORY.createParser(is);
121
122         JsonToken token = parser.nextToken();
123
124         if (token == null)
125         {
126           LOG.warn("found empty content for {}", templateName);
127           break;
128         }
129
130         if (!JsonToken.START_OBJECT.equals(token))
131         {
132           LOG.error("{} must contain an object as root-element", templateName);
133           throw new RuntimeException(
134               "The root-element of " +
135               templateName +
136               " has to be an object, that contains the variable-definitions!"
137               );
138         }
139
140         token = parser.nextToken();
141         if (token == null)
142           fail(parser, "unexpected EOF");
143         if (JsonToken.END_OBJECT.equals(token))
144         {
145           LOG.warn("found empty object for {}", templateName);
146           break;
147         }
148
149         do
150         {
151           if (!JsonToken.FIELD_NAME.equals(token))
152             fail(parser, "expected a field-name");
153
154           String var_name = parser.getText();
155           parser.nextToken();
156           Object var_value = convert(parser);
157
158           LOG.debug(
159               "defining variable {} of type {}",
160               var_name,
161               var_value == null ? "NULL" : var_value.getClass().getSimpleName()
162               );
163           element.setNodeLocalVariable(var_name, var_value);
164
165           token = parser.nextToken();
166           if (token == null)
167             fail(parser, "unexpected EOF");
168         }
169         while (!JsonToken.END_OBJECT.equals(token));
170
171         if (parser.nextToken() != null)
172           fail(parser, "unexpected data after parsed variables");
173       }
174       catch (IOException e)
175       {
176         LOG.error("cannot parse {} as JSON: {}", templateName, e.getMessage());
177         throw new RuntimeException(e);
178       }
179     }
180
181     element.removeAttribute(name);
182
183     return ProcessorResult.OK;
184   }
185
186
187   @Override
188   public int getPrecedence()
189   {
190     return ATTR_PRECEDENCE;
191   }
192
193
194   static Object convert(JsonParser parser) throws IOException
195   {
196     JsonToken token = parser.getCurrentToken();
197     if (token == null)
198       fail(parser, "unexpected EOF");
199
200     switch (token)
201     {
202       case VALUE_STRING:       return parser.getText();
203       case VALUE_NUMBER_INT:   return parser.getIntValue();
204       case VALUE_NUMBER_FLOAT: return parser.getDoubleValue();
205       case START_OBJECT:       return convertObject(parser);
206       case START_ARRAY:        return convertArray(parser);
207       case VALUE_TRUE:         return Boolean.TRUE;
208       case VALUE_FALSE:        return Boolean.FALSE;
209       case VALUE_NULL:         return null;
210     }
211
212     fail(parser, "unexpected token " + token.toString());
213     return null; // << Will never be reached, because fail always throws an exception
214   }
215
216   static Map<String, Object> convertObject(JsonParser parser) throws IOException
217   {
218     JsonToken token = parser.nextToken();
219     if (token == null)
220       fail(parser, "unexpected EOF");
221
222     Map<String, Object> map = new LinkedHashMap<>();
223
224     while (!JsonToken.END_OBJECT.equals(token))
225     {
226       if (!JsonToken.FIELD_NAME.equals(token))
227         fail(parser, "expected a field-name");
228
229       String name = parser.getText();
230       parser.nextToken();
231       Object value = convert(parser);
232       map.put(name, value);
233
234       token = parser.nextToken();
235       if (token == null)
236         fail(parser, "unexpected EOF");
237     }
238
239     return map;
240   }
241
242   static List<Object> convertArray(JsonParser parser) throws IOException
243   {
244     JsonToken token = parser.nextToken();
245     if (token == null)
246       fail(parser, "unexpected EOF");
247
248     List<Object> list = new LinkedList<>();
249
250     while (!JsonToken.END_ARRAY.equals(token))
251     {
252       list.add(convert(parser));
253
254       token = parser.nextToken();
255       if (token == null)
256         fail(parser, "unexpected EOF");
257     }
258
259     return list;
260   }
261
262   static void fail(JsonParser parser, String message)
263   {
264     JsonLocation location = parser.getCurrentLocation();
265     LOG.error(
266         "{} at char-offset {} (line {}, column {})",
267         message,
268         location.getCharOffset(),
269         location.getLineNr(),
270         location.getColumnNr()
271         );
272     throw new RuntimeException("Cannot parse JSON: " + message);
273   }
274 }