autoconfigure
[maven-thymeleaf-skin] / src / main / java / de / juplo / thymeproxy / ProxyTemplateResolver.java
1 package de.juplo.thymeproxy;
2
3
4 import java.io.IOException;
5 import java.time.Clock;
6 import java.time.Duration;
7 import java.time.OffsetDateTime;
8 import java.time.format.DateTimeFormatter;
9 import java.time.format.DateTimeParseException;
10 import java.util.regex.Pattern;
11 import javax.servlet.http.HttpServletResponse;
12 import org.apache.http.Header;
13 import org.apache.http.HeaderElement;
14 import org.apache.http.HttpEntity;
15 import org.apache.http.HttpResponse;
16 import org.apache.http.client.methods.CloseableHttpResponse;
17 import org.apache.http.client.methods.HttpGet;
18 import org.apache.http.entity.ContentType;
19 import org.apache.http.impl.client.CloseableHttpClient;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22 import org.thymeleaf.TemplateProcessingParameters;
23 import org.thymeleaf.templatemode.StandardTemplateModeHandlers;
24 import org.thymeleaf.templateresolver.ITemplateResolutionValidity;
25 import org.thymeleaf.templateresolver.ITemplateResolver;
26 import org.thymeleaf.templateresolver.NonCacheableTemplateResolutionValidity;
27 import org.thymeleaf.templateresolver.TTLTemplateResolutionValidity;
28 import org.thymeleaf.templateresolver.TemplateResolution;
29
30
31
32 /**
33  *
34  * @author Kai Moritz
35  */
36 public class ProxyTemplateResolver implements ITemplateResolver
37 {
38   private final static Logger LOG =
39       LoggerFactory.getLogger(ProxyTemplateResolver.class);
40
41   private final static Pattern HTML =
42       Pattern.compile("text/html", Pattern.CASE_INSENSITIVE);
43   private final static Pattern XHTML =
44       Pattern.compile("application/xhtml+xml", Pattern.CASE_INSENSITIVE);
45   private final static Pattern XML =
46       Pattern.compile("(?:/|\\+)xml$", Pattern.CASE_INSENSITIVE);
47
48   public final static Long DEFAULT_TTL = 300000l; /** 5 minutes */
49
50   private final String name;
51   private final Integer order;
52
53   private final CloseableHttpClient client;
54   private final String origin;
55   private final Long ttl;
56   private final Clock clock;
57
58
59   public ProxyTemplateResolver(
60       String name,
61       Integer order,
62       CloseableHttpClient client,
63       String origin,
64       Long ttl,
65       Clock clock
66       )
67   {
68     super();
69     this.name = name;
70     this.order = order;
71     this.client = client;
72     this.origin = origin;
73     this.ttl = ttl;
74     this.clock = clock;
75   }
76
77   public ProxyTemplateResolver(
78       String name,
79       Integer order,
80       CloseableHttpClient client,
81       String origin,
82       Long ttl
83       )
84   {
85     this(name, order, client, origin, ttl, Clock.systemDefaultZone());
86   }
87
88   public ProxyTemplateResolver(
89       String name,
90       Integer order,
91       CloseableHttpClient client,
92       String origin
93       )
94   {
95     this(name, order, client, origin, DEFAULT_TTL, Clock.systemDefaultZone());
96   }
97
98
99   @Override
100   public String getName()
101   {
102     return name;
103   }
104
105   @Override
106   public Integer getOrder()
107   {
108     return order;
109   }
110
111   public String getOrigin()
112   {
113     return origin;
114   }
115
116   public Long getDefaultTTL()
117   {
118     return ttl;
119   }
120
121   @Override
122   public TemplateResolution resolveTemplate(TemplateProcessingParameters params)
123   {
124     StringBuilder builder = new StringBuilder();
125     builder.append(origin);
126     builder.append(params.getTemplateName());
127
128     String resource = builder.toString();
129
130     try
131     {
132       HttpGet request = new HttpGet(resource);
133       CloseableHttpResponse response = client.execute(request);
134
135       switch (response.getStatusLine().getStatusCode())
136       {
137         case HttpServletResponse.SC_FOUND:
138         case HttpServletResponse.SC_OK:
139           /** OK. Continue as normal... */
140           break;
141
142         case HttpServletResponse.SC_NOT_FOUND:
143         case HttpServletResponse.SC_GONE:
144           /** The resource can not be resolved through this origin */
145           return null;
146
147         case HttpServletResponse.SC_MOVED_PERMANENTLY:
148           // TODO
149
150         case HttpServletResponse.SC_SEE_OTHER:
151           // TODO
152
153         case HttpServletResponse.SC_TEMPORARY_REDIRECT:
154           // TODO
155
156         default:
157           LOG.error("{} -- {}", response.getStatusLine(), resource);
158           // TODO: throw sensible exceptions, to communicate resolving-errors
159           throw new RuntimeException(response.getStatusLine().toString() + " -- " + resource);
160       }
161
162       HttpEntity entity = response.getEntity();
163       ContentType content = ContentType.getOrDefault(entity);
164
165       return new TemplateResolution(
166           params.getTemplateName(),
167           resource,
168           new ProxyResourceResolver(resource, response, entity),
169           content.getCharset().displayName(),
170           ProxyTemplateResolver.computeTemplateMode(content),
171           computeValidity(response)
172           );
173     }
174     catch (IOException e)
175     {
176       LOG.error("unexpected error while resolving {}: {}", resource, e.getMessage());
177       // TODO: throw sensible exceptions, to communicate resolving-errors
178       throw new RuntimeException(e);
179     }
180   }
181
182   @Override
183   public void initialize()
184   {
185   }
186
187
188   public static String computeTemplateMode(ContentType content)
189   {
190     String type = content.getMimeType();
191
192     if (HTML.matcher(type).matches())
193       return StandardTemplateModeHandlers.LEGACYHTML5.getTemplateModeName();
194
195     if (XML.matcher(type).find())
196     {
197       if (XHTML.matcher(type).matches())
198         return StandardTemplateModeHandlers.XHTML.getTemplateModeName();
199       else
200         return StandardTemplateModeHandlers.XML.getTemplateModeName();
201     }
202
203     throw new RuntimeException("Cannot handle mime-type " + type);
204   }
205
206   public ITemplateResolutionValidity computeValidity(HttpResponse response)
207   {
208     if (ttl == null)
209       /** Caching is disabled! */
210       return NonCacheableTemplateResolutionValidity.INSTANCE;
211
212     boolean cacheable = true;
213     Integer max_age = null;
214
215     for (Header header : response.getHeaders("Cache-Control"))
216     {
217       for (HeaderElement element : header.getElements())
218       {
219         switch (element.getName())
220         {
221           case "no-cache":
222           case "no-store":
223           case "must-revalidate":
224             cacheable = false;
225             break;
226
227           case "max-age":
228             try
229             {
230               max_age = Integer.parseInt(element.getValue());
231             }
232             catch (NumberFormatException e)
233             {
234               LOG.warn(
235                   "invalid header \"Cache-Control: max-age={}\"",
236                   element.getValue()
237                   );
238             }
239             break;
240         }
241       }
242     }
243
244     if (max_age != null && max_age < 1)
245       cacheable = false;
246
247     if (!cacheable)
248       return NonCacheableTemplateResolutionValidity.INSTANCE;
249     
250     if (max_age != null)
251     {
252       long millis = max_age;
253       if (millis >= Long.MAX_VALUE / 1000l )
254         millis = Long.MAX_VALUE;
255       else
256         millis = millis * 1000l;
257       return new TTLTemplateResolutionValidity(millis);
258     }
259
260     Header header = response.getLastHeader("Expires");
261     if (header == null)
262       /** No TTL specified in response-headers: use configured default */
263       return new TTLTemplateResolutionValidity(ttl);
264
265     try
266     {
267       OffsetDateTime expires =
268           OffsetDateTime.parse(
269               header.getValue(),
270               DateTimeFormatter.RFC_1123_DATE_TIME
271               );
272
273       Duration delta = Duration.between(OffsetDateTime.now(clock), expires);
274       if (delta.isNegative() || delta.isZero())
275         return NonCacheableTemplateResolutionValidity.INSTANCE;
276
277       long millis = delta.getSeconds();
278       if (millis >= Long.MAX_VALUE / 1000l)
279         millis = Long.MAX_VALUE;
280       else
281       {
282         millis = millis * 1000;
283         millis = millis + (long)(delta.getNano() / 1000000);
284       }
285       return new TTLTemplateResolutionValidity(millis);
286     }
287     catch (DateTimeParseException e)
288     {
289       LOG.warn("invalid header \"Expires: {}\"", header.getValue());
290       /**
291        * No TTL specified in response-headers: assume expired
292        * (see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21)
293        */
294       return NonCacheableTemplateResolutionValidity.INSTANCE;
295     }
296   }
297 }