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