1 package de.juplo.thymeproxy;
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;
36 public class ProxyTemplateResolver implements ITemplateResolver
38 private final static Logger LOG =
39 LoggerFactory.getLogger(ProxyTemplateResolver.class);
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);
48 public final static Long DEFAULT_TTL = 300000l; /** 5 minutes */
50 private final String name;
51 private final Integer order;
53 private final CloseableHttpClient client;
54 private final String origin;
55 private final Long ttl;
56 private final Clock clock;
59 public ProxyTemplateResolver(
62 CloseableHttpClient client,
77 public ProxyTemplateResolver(
80 CloseableHttpClient client,
85 this(name, order, client, origin, ttl, Clock.systemDefaultZone());
88 public ProxyTemplateResolver(
91 CloseableHttpClient client,
95 this(name, order, client, origin, DEFAULT_TTL, Clock.systemDefaultZone());
100 public String getName()
106 public Integer getOrder()
111 public String getOrigin()
116 public Long getDefaultTTL()
122 public TemplateResolution resolveTemplate(TemplateProcessingParameters params)
124 StringBuilder builder = new StringBuilder();
125 builder.append(origin);
126 builder.append(params.getTemplateName());
128 String resource = builder.toString();
132 HttpGet request = new HttpGet(resource);
133 CloseableHttpResponse response = client.execute(request);
135 switch (response.getStatusLine().getStatusCode())
137 case HttpServletResponse.SC_FOUND:
138 case HttpServletResponse.SC_OK:
139 /** OK. Continue as normal... */
142 case HttpServletResponse.SC_NOT_FOUND:
143 case HttpServletResponse.SC_GONE:
144 /** The resource can not be resolved through this origin */
147 case HttpServletResponse.SC_MOVED_PERMANENTLY:
150 case HttpServletResponse.SC_SEE_OTHER:
153 case HttpServletResponse.SC_TEMPORARY_REDIRECT:
157 LOG.error("{} -- {}", response.getStatusLine(), resource);
158 // TODO: throw sensible exceptions, to communicate resolving-errors
159 throw new RuntimeException(response.getStatusLine().toString() + " -- " + resource);
162 HttpEntity entity = response.getEntity();
163 ContentType content = ContentType.getOrDefault(entity);
165 return new TemplateResolution(
166 params.getTemplateName(),
168 new ProxyResourceResolver(resource, response, entity),
169 content.getCharset().displayName(),
170 ProxyTemplateResolver.computeTemplateMode(content),
171 computeValidity(response)
174 catch (IOException e)
176 LOG.error("unexpected error while resolving {}: {}", resource, e.getMessage());
177 // TODO: throw sensible exceptions, to communicate resolving-errors
178 throw new RuntimeException(e);
183 public void initialize()
188 public static String computeTemplateMode(ContentType content)
190 String type = content.getMimeType();
192 if (HTML.matcher(type).matches())
193 return StandardTemplateModeHandlers.LEGACYHTML5.getTemplateModeName();
195 if (XML.matcher(type).find())
197 if (XHTML.matcher(type).matches())
198 return StandardTemplateModeHandlers.XHTML.getTemplateModeName();
200 return StandardTemplateModeHandlers.XML.getTemplateModeName();
203 throw new RuntimeException("Cannot handle mime-type " + type);
206 public ITemplateResolutionValidity computeValidity(HttpResponse response)
209 /** Caching is disabled! */
210 return NonCacheableTemplateResolutionValidity.INSTANCE;
212 boolean cacheable = true;
213 Integer max_age = null;
215 for (Header header : response.getHeaders("Cache-Control"))
217 for (HeaderElement element : header.getElements())
219 switch (element.getName())
223 case "must-revalidate":
230 max_age = Integer.parseInt(element.getValue());
232 catch (NumberFormatException e)
235 "invalid header \"Cache-Control: max-age={}\"",
244 if (max_age != null && max_age < 1)
248 return NonCacheableTemplateResolutionValidity.INSTANCE;
252 long millis = max_age;
253 if (millis >= Long.MAX_VALUE / 1000l )
254 millis = Long.MAX_VALUE;
256 millis = millis * 1000l;
257 return new TTLTemplateResolutionValidity(millis);
260 Header header = response.getLastHeader("Expires");
262 /** No TTL specified in response-headers: use configured default */
263 return new TTLTemplateResolutionValidity(ttl);
267 OffsetDateTime expires =
268 OffsetDateTime.parse(
270 DateTimeFormatter.RFC_1123_DATE_TIME
273 Duration delta = Duration.between(OffsetDateTime.now(clock), expires);
274 if (delta.isNegative() || delta.isZero())
275 return NonCacheableTemplateResolutionValidity.INSTANCE;
277 long millis = delta.getSeconds();
278 if (millis >= Long.MAX_VALUE / 1000l)
279 millis = Long.MAX_VALUE;
282 millis = millis * 1000;
283 millis = millis + (long)(delta.getNano() / 1000000);
285 return new TTLTemplateResolutionValidity(millis);
287 catch (DateTimeParseException e)
289 LOG.warn("invalid header \"Expires: {}\"", header.getValue());
291 * No TTL specified in response-headers: assume expired
292 * (see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21)
294 return NonCacheableTemplateResolutionValidity.INSTANCE;