</parent>
<groupId>de.juplo</groupId>
- <artifactId>thymeproxy</artifactId>
+ <artifactId>thymeproxy-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- application-settings -->
<thymeproxy.name>${project.name}</thymeproxy.name>
<thymeproxy.origin>http://localhost:8080</thymeproxy.origin>
- <thymeproxy.port>80</thymeproxy.port>
+ <thymeproxy.port>8888</thymeproxy.port>
<thymeproxy.ttl>300000</thymeproxy.ttl><!-- 5 minutes -->
<!-- used versions (not defined in spring-boot) -->
<httpclient-spring-boot-starter.version>1.0-SNAPSHOT</httpclient-spring-boot-starter.version>
+ <thymeproxy.version>1.0-SNAPSHOT</thymeproxy.version>
</properties>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
+ <dependency>
+ <groupId>de.juplo</groupId>
+ <artifactId>thymeproxy</artifactId>
+ <version>${thymeproxy.version}</version>
+ </dependency>
<dependency>
<groupId>de.juplo</groupId>
<artifactId>httpclient-spring-boot-starter</artifactId>
<version>${nekohtml.version}</version>
</dependency>
- <!-- Testing -->
- <dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-test</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-configuration-processor</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>jcl-over-slf4j</artifactId>
- <scope>test</scope>
- </dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
- <scope>test</scope>
+ <scope>runtime</scope>
</dependency>
</dependencies>
+++ /dev/null
-package de.juplo.autoconfigure;
-
-
-import de.juplo.autoconfigure.ThymeproxyProperties.Origin;
-import de.juplo.thymeproxy.ProxyTemplateResolver;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.boot.autoconfigure.AutoConfigureBefore;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-
-/**
- * Automatic configuration
- *
- * @author Kai Moritz
- */
-@Configuration
-@EnableConfigurationProperties(ThymeproxyProperties.class)
-@ConditionalOnClass(value = ThymeleafAutoConfiguration.class)
-@AutoConfigureBefore(ThymeleafAutoConfiguration.class)
-public class ThymeproxyAutoConfiguration
-{
- private static final Logger LOG =
- LoggerFactory.getLogger(ThymeproxyAutoConfiguration.class);
-
-
- @Bean
- @ConditionalOnProperty("thymeproxy.origins[0].uri")
- public ProxyTemplateResolver defaultTemplateResolver(
- CloseableHttpClient client,
- ThymeproxyProperties properties,
- ConfigurableApplicationContext context
- )
- {
- LOG.info("configuring {} proxies", properties.origins.size());
-
- Origin origin = properties.origins.get(0);
- String uri = origin.uri.toString();
- ProxyTemplateResolver defaultResolver =
- new ProxyTemplateResolver(
- "0: " + origin.uri.getHost(),
- 0,
- client,
- uri,
- origin.ttl
- );
- LOG.info("registering defaultTemplateResolver for {}", uri);
-
- for (int i=1; i<properties.origins.size(); i++)
- {
- origin = properties.origins.get(i);
-
- String name = "proxy" + i;
- uri = origin.uri.toString();
- ProxyTemplateResolver resolver =
- new ProxyTemplateResolver(
- i + ": " + origin.uri.getHost(),
- i,
- client,
- origin.uri.toString(),
- origin.ttl
- );
-
- LOG.info("registering {} for {}", name, uri);
- context.getBeanFactory().registerSingleton(name, resolver);
- }
-
- return defaultResolver;
- }
-}
+++ /dev/null
-package de.juplo.autoconfigure;
-
-
-import java.net.URI;
-import java.util.LinkedList;
-import java.util.List;
-import org.hibernate.validator.constraints.NotEmpty;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
-
-
-/**
- *
- * @author Kai Moritz
- */
-@ConfigurationProperties("thymeproxy")
-public class ThymeproxyProperties
-{
- String name;
- List<Origin> origins = new LinkedList<>();
-
-
- public void setName(String name)
- {
- this.name = name;
- }
-
- public List<Origin> getOrigins()
- {
- return origins;
- }
-
- public void setOrigins(List<Origin> origins)
- {
- this.origins = origins;
- }
-
-
- public static class Origin
- {
- @NotEmpty
- URI uri;
- List<String> patterns = new LinkedList<>();
- Long ttl;
-
-
- public void setUri(URI uri)
- {
- this.uri = uri;
- }
-
- public List<String> getPatterns()
- {
- return this.patterns;
- }
-
- public void setPatterns(List<String> patterns)
- {
- this.patterns = patterns;
- }
-
- public void setTtl(Long ttl)
- {
- this.ttl = ttl;
- }
- }
-}
--- /dev/null
+package de.juplo.thymeproxy;
+
+import java.util.Collections;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.Environment;
+import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
+
+
+@SpringBootApplication
+public class Application
+{
+ @Bean
+ public SimpleUrlHandlerMapping proxiedHandlerMapping(
+ ProxyHttpRequestHandler handler
+ )
+ {
+ SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
+ mapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
+ mapping.setUrlMap(Collections.singletonMap("*.html", handler));
+ return mapping;
+ }
+
+ @Bean
+ public ProxyHttpRequestHandler proxiedRequestHandler(
+ CloseableHttpClient client,
+ Environment env
+ )
+ {
+ ProxyHttpRequestHandler handler = new ProxyHttpRequestHandler();
+ handler.setClient(client);
+ handler.setOrigin(env.getProperty("thymeproxy.origin"));
+ handler.setTtl(30000l);
+ return handler;
+ }
+
+
+ public static void main(String[] args)
+ {
+ SpringApplication.run(Application.class, args);
+ }
+}
\ No newline at end of file
--- /dev/null
+package de.juplo.thymeproxy;
+
+
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.http.Header;
+import org.apache.http.HeaderElement;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.CacheControl;
+import org.springframework.util.Assert;
+import org.springframework.util.StreamUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.HttpRequestHandler;
+import org.springframework.web.servlet.HandlerMapping;
+import org.springframework.web.servlet.support.WebContentGenerator;
+import org.springframework.web.util.UrlPathHelper;
+
+
+
+/**
+ *
+ * @author kai
+ */
+public class ProxyHttpRequestHandler
+ extends
+ WebContentGenerator
+ implements
+ HttpRequestHandler
+{
+ private final static Logger LOG =
+ LoggerFactory.getLogger(ProxyHttpRequestHandler.class);
+
+
+ public final static Long DEFAULT_TTL = 300000l; /** 5 minutes */
+
+
+ private UrlPathHelper urlPathHelper = new UrlPathHelper();
+
+ private CloseableHttpClient client;
+ private String origin;
+ private Long ttl;
+
+ private final Clock clock;
+
+
+ public ProxyHttpRequestHandler()
+ {
+ clock = Clock.systemDefaultZone();
+ }
+
+ public ProxyHttpRequestHandler(Clock clock)
+ {
+ this.clock = clock;
+ }
+
+
+ @Override
+ public void handleRequest(
+ HttpServletRequest request,
+ HttpServletResponse response
+ )
+ throws
+ ServletException,
+ IOException
+ {
+ String path =
+ (String)
+ request.getAttribute(
+ HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
+ );
+
+ if (!StringUtils.hasText(path))
+ path = urlPathHelper.getLookupPathForRequest(request);
+
+ StringBuilder builder = new StringBuilder();
+ builder.append(origin);
+ builder.append(path);
+
+ String query = request.getQueryString();
+ if (query != null)
+ {
+ builder.append('?');
+ builder.append(query);
+ }
+
+ String resource = builder.toString();
+
+ try
+ {
+ HttpGet p_request = new HttpGet(resource);
+ CloseableHttpResponse p_response = client.execute(p_request);
+
+ setCacheControl(computeCacheControl(p_response));
+ prepareResponse(response);
+
+ Header length = p_response.getLastHeader("Content-Length");
+ if (length != null)
+ response.addHeader(length.getName(), length.getValue());
+
+ int status = p_response.getStatusLine().getStatusCode();
+ switch (status)
+ {
+ case HttpServletResponse.SC_FOUND:
+ case HttpServletResponse.SC_OK:
+ /** OK. Continue as normal... */
+ response.setStatus(status);
+
+ HttpEntity entity = p_response.getEntity();
+
+ StreamUtils.copy(entity.getContent(), response.getOutputStream());
+
+ /** Release the connection */
+ EntityUtils.consume(entity);
+ p_response.close();
+
+ break;
+
+ case HttpServletResponse.SC_NOT_FOUND:
+ case HttpServletResponse.SC_GONE:
+ /** The resource can not be resolved through this origin */
+ response.sendError(status, p_response.getStatusLine().getReasonPhrase());
+ return;
+
+ case HttpServletResponse.SC_MOVED_PERMANENTLY:
+ // TODO
+
+ case HttpServletResponse.SC_SEE_OTHER:
+ // TODO
+
+ case HttpServletResponse.SC_TEMPORARY_REDIRECT:
+ // TODO
+
+ default:
+ LOG.error("{} -- {}", p_response.getStatusLine(), resource);
+ // TODO: throw sensible exceptions, to communicate resolving-errors
+ throw new RuntimeException(p_response.getStatusLine().toString() + " -- " + resource);
+ }
+ }
+ catch (IOException e)
+ {
+ LOG.error("unexpected error while resolving {}: {}", resource, e.getMessage());
+ // TODO: throw sensible exceptions, to communicate resolving-errors
+ throw new RuntimeException(e);
+ }
+ }
+
+ public CacheControl computeCacheControl(HttpResponse response)
+ {
+ if (ttl == null)
+ /** Caching is disabled! */
+ return CacheControl.noStore();
+
+
+ boolean has_cache_control = false;
+ boolean is_public = false;
+ boolean is_private = false;
+ boolean no_cache = false;
+ boolean no_store = false;
+ boolean no_transform = false;
+ boolean must_revalidate = false;
+ boolean proxy_revalidate = false;
+ Long max_age = null;
+ Long s_maxage = null;
+
+ for (Header header : response.getHeaders("Cache-Control"))
+ {
+ has_cache_control = true;
+ for (HeaderElement element : header.getElements())
+ {
+ switch (element.getName())
+ {
+ case "public":
+ is_public = true;
+ break;
+ case "private":
+ is_private = true;
+ break;
+ case "no-cache":
+ no_cache = true;
+ break;
+ case "no-store":
+ no_store = true;
+ break;
+ case "no_transform":
+ no_transform = true;
+ break;
+ case "must-revalidate":
+ must_revalidate = true;
+ break;
+ case "proxy-revalidate":
+ proxy_revalidate = true;
+ case "max-age":
+ try
+ {
+ max_age = Long.parseLong(element.getValue());
+ }
+ catch (NumberFormatException e)
+ {
+ LOG.warn(
+ "invalid header \"Cache-Control: max-age={}\"",
+ element.getValue()
+ );
+ }
+ break;
+ case "s-maxage":
+ try
+ {
+ s_maxage = Long.parseLong(element.getValue());
+ }
+ catch (NumberFormatException e)
+ {
+ LOG.warn(
+ "invalid header \"Cache-Control: s-maxage={}\"",
+ element.getValue()
+ );
+ }
+ break;
+ default:
+ LOG.warn(
+ "invalid header \"Cache-Control: {}{}\"",
+ element.getName(),
+ element.getValue() == null ? "" : "=" + element.getValue()
+ );
+ }
+ }
+ }
+
+ if (!has_cache_control)
+ {
+ Header header = response.getLastHeader("Expires");
+ if (header == null)
+ /** No TTL specified in response-headers: use configured default */
+ max_age = ttl;
+ else
+ try
+ {
+ OffsetDateTime expires
+ = OffsetDateTime.parse(
+ header.getValue(),
+ DateTimeFormatter.RFC_1123_DATE_TIME
+ );
+
+ Duration delta = Duration.between(OffsetDateTime.now(clock), expires);
+ if (delta.isNegative())
+ no_store = true;
+ else
+ max_age = delta.getSeconds();
+ }
+ catch (DateTimeParseException e)
+ {
+ LOG.warn("invalid header \"Expires: {}\"", header.getValue());
+ /**
+ * No TTL specified in response-headers: assume expired
+ * see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
+ */
+ max_age = 0l;
+ }
+ }
+
+ CacheControl cache_control;
+
+ if (no_store)
+ cache_control = CacheControl.noStore();
+ else if (no_cache)
+ cache_control = CacheControl.noCache();
+ else if (max_age != null)
+ cache_control = CacheControl.maxAge(max_age, TimeUnit.SECONDS);
+ else
+ cache_control = CacheControl.empty();
+
+ if (is_private)
+ cache_control.cachePrivate();
+ if (is_public)
+ cache_control.cachePublic();
+ if (no_transform)
+ cache_control.noTransform();
+ if (must_revalidate)
+ cache_control.mustRevalidate();
+ if (proxy_revalidate)
+ cache_control.proxyRevalidate();
+ if (s_maxage != null)
+ cache_control.sMaxAge(s_maxage, TimeUnit.SECONDS);
+
+ return cache_control;
+ }
+
+
+ public ProxyHttpRequestHandler setClient(CloseableHttpClient client)
+ {
+ this.client = client;
+ return this;
+ }
+
+ public ProxyHttpRequestHandler setOrigin(String origin)
+ {
+ this.origin = origin;
+ return this;
+ }
+
+ public ProxyHttpRequestHandler setTtl(Long ttl)
+ {
+ this.ttl = ttl;
+ return this;
+ }
+
+ /**
+ * Set if URL lookup should always use full path within current servlet
+ * context. Else, the path within the current servlet mapping is used
+ * if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml).
+ * Default is "false".
+ *
+ * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
+ */
+ public void setAlwaysUseFullPath(boolean alwaysUseFullPath)
+ {
+ this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
+ }
+
+ /**
+ * Set if context path and request URI should be URL-decoded.
+ * Both are returned <i>undecoded</i> by the Servlet API,
+ * in contrast to the servlet path.
+ * <p>
+ * Uses either the request encoding or the default encoding according
+ * to the Servlet spec (ISO-8859-1).
+ *
+ * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
+ */
+ public void setUrlDecode(boolean urlDecode)
+ {
+ this.urlPathHelper.setUrlDecode(urlDecode);
+ }
+
+ /**
+ * Set if ";" (semicolon) content should be stripped from the request URI.
+ *
+ * @see
+ * org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
+ */
+ public void setRemoveSemicolonContent(boolean removeSemicolonContent)
+ {
+ this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
+ }
+
+ /**
+ * Set the UrlPathHelper to use for the resolution of lookup paths.
+ * <p>
+ * Use this to override the default UrlPathHelper with a custom subclass,
+ * or to share common UrlPathHelper settings across multiple
+ * MethodNameResolvers
+ * and HandlerMappings.
+ *
+ * @see
+ * org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper
+ */
+ public void setUrlPathHelper(UrlPathHelper urlPathHelper)
+ {
+ Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
+ this.urlPathHelper = urlPathHelper;
+ }
+
+ /**
+ * Return the UrlPathHelper to use for the resolution of lookup paths.
+ */
+ protected UrlPathHelper getUrlPathHelper()
+ {
+ return this.urlPathHelper;
+ }
+}
+++ /dev/null
-package de.juplo.thymeproxy;
-
-
-import java.io.IOException;
-import java.io.InputStream;
-import org.apache.http.HttpEntity;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.util.EntityUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.thymeleaf.TemplateProcessingParameters;
-import org.thymeleaf.resourceresolver.IResourceResolver;
-
-
-
-/**
- *
- * @author kai
- */
-public class ProxyResourceResolver implements IResourceResolver
-{
- private final static Logger LOG =
- LoggerFactory.getLogger(ProxyResourceResolver.class);
-
-
- private final String resource;
-
- private final CloseableHttpResponse response;
- private final HttpEntity entity;
-
-
- public ProxyResourceResolver(
- String resource,
- CloseableHttpResponse response,
- HttpEntity entity
- )
- {
- this.resource = resource;
- this.response = response;
- this.entity = entity;
- }
-
-
- @Override
- public String getName()
- {
- return resource;
- }
-
- @Override
- public InputStream getResourceAsStream(TemplateProcessingParameters templateProcessingParameters, String resourceName)
- {
- InputStream is;
- try
- {
- is = entity.getContent();
- }
- catch (IOException e)
- {
- LOG.error("unexpected error while retriving the response-body", e);
- return null;
- }
-
- return new InputStream()
- {
- @Override
- public boolean markSupported()
- {
- return is.markSupported();
- }
-
- @Override
- public synchronized void reset() throws IOException
- {
- is.reset();
- }
-
- @Override
- public synchronized void mark(int readlimit)
- {
- is.mark(readlimit);
- }
-
- @Override
- public void close() throws IOException
- {
- is.close();
- EntityUtils.consume(entity);
- response.close();
- }
-
- @Override
- public int available() throws IOException
- {
- return is.available();
- }
-
- @Override
- public long skip(long n) throws IOException
- {
- return is.skip(n);
- }
-
- @Override
- public int read() throws IOException
- {
- return is.read();
- }
-
- @Override
- public int read(byte[] b, int off, int len) throws IOException
- {
- return is.read(b, off, len);
- }
-
- @Override
- public int read(byte[] b) throws IOException
- {
- return is.read(b);
- }
- };
- }
-}
+++ /dev/null
-package de.juplo.thymeproxy;
-
-
-import java.io.IOException;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
-import java.util.regex.Pattern;
-import javax.servlet.http.HttpServletResponse;
-import org.apache.http.Header;
-import org.apache.http.HeaderElement;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.entity.ContentType;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.thymeleaf.TemplateProcessingParameters;
-import org.thymeleaf.templatemode.StandardTemplateModeHandlers;
-import org.thymeleaf.templateresolver.ITemplateResolutionValidity;
-import org.thymeleaf.templateresolver.ITemplateResolver;
-import org.thymeleaf.templateresolver.NonCacheableTemplateResolutionValidity;
-import org.thymeleaf.templateresolver.TTLTemplateResolutionValidity;
-import org.thymeleaf.templateresolver.TemplateResolution;
-
-
-
-/**
- *
- * @author Kai Moritz
- */
-public class ProxyTemplateResolver implements ITemplateResolver
-{
- private final static Logger LOG =
- LoggerFactory.getLogger(ProxyTemplateResolver.class);
-
- private final static Pattern HTML =
- Pattern.compile("text/html", Pattern.CASE_INSENSITIVE);
- private final static Pattern XHTML =
- Pattern.compile("application/xhtml+xml", Pattern.CASE_INSENSITIVE);
- private final static Pattern XML =
- Pattern.compile("(?:/|\\+)xml$", Pattern.CASE_INSENSITIVE);
-
- public final static Long DEFAULT_TTL = 300000l; /** 5 minutes */
-
- private final String name;
- private final Integer order;
-
- private final CloseableHttpClient client;
- private final String origin;
- private final Long ttl;
- private final Clock clock;
-
-
- public ProxyTemplateResolver(
- String name,
- Integer order,
- CloseableHttpClient client,
- String origin,
- Long ttl,
- Clock clock
- )
- {
- super();
- this.name = name;
- this.order = order;
- this.client = client;
- this.origin = origin;
- this.ttl = ttl;
- this.clock = clock;
- }
-
- public ProxyTemplateResolver(
- String name,
- Integer order,
- CloseableHttpClient client,
- String origin,
- Long ttl
- )
- {
- this(name, order, client, origin, ttl, Clock.systemDefaultZone());
- }
-
- public ProxyTemplateResolver(
- String name,
- Integer order,
- CloseableHttpClient client,
- String origin
- )
- {
- this(name, order, client, origin, DEFAULT_TTL, Clock.systemDefaultZone());
- }
-
-
- @Override
- public String getName()
- {
- return name;
- }
-
- @Override
- public Integer getOrder()
- {
- return order;
- }
-
- public String getOrigin()
- {
- return origin;
- }
-
- public Long getDefaultTTL()
- {
- return ttl;
- }
-
- @Override
- public TemplateResolution resolveTemplate(TemplateProcessingParameters params)
- {
- StringBuilder builder = new StringBuilder();
- builder.append(origin);
- builder.append(params.getTemplateName());
-
- String resource = builder.toString();
-
- try
- {
- HttpGet request = new HttpGet(resource);
- CloseableHttpResponse response = client.execute(request);
-
- switch (response.getStatusLine().getStatusCode())
- {
- case HttpServletResponse.SC_FOUND:
- case HttpServletResponse.SC_OK:
- /** OK. Continue as normal... */
- break;
-
- case HttpServletResponse.SC_NOT_FOUND:
- case HttpServletResponse.SC_GONE:
- /** The resource can not be resolved through this origin */
- return null;
-
- case HttpServletResponse.SC_MOVED_PERMANENTLY:
- // TODO
-
- case HttpServletResponse.SC_SEE_OTHER:
- // TODO
-
- case HttpServletResponse.SC_TEMPORARY_REDIRECT:
- // TODO
-
- default:
- LOG.error("{} -- {}", response.getStatusLine(), resource);
- // TODO: throw sensible exceptions, to communicate resolving-errors
- throw new RuntimeException(response.getStatusLine().toString() + " -- " + resource);
- }
-
- HttpEntity entity = response.getEntity();
- ContentType content = ContentType.getOrDefault(entity);
-
- return new TemplateResolution(
- params.getTemplateName(),
- resource,
- new ProxyResourceResolver(resource, response, entity),
- content.getCharset().displayName(),
- ProxyTemplateResolver.computeTemplateMode(content),
- computeValidity(response)
- );
- }
- catch (IOException e)
- {
- LOG.error("unexpected error while resolving {}: {}", resource, e.getMessage());
- // TODO: throw sensible exceptions, to communicate resolving-errors
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public void initialize()
- {
- }
-
-
- public static String computeTemplateMode(ContentType content)
- {
- String type = content.getMimeType();
-
- if (HTML.matcher(type).matches())
- return StandardTemplateModeHandlers.LEGACYHTML5.getTemplateModeName();
-
- if (XML.matcher(type).find())
- {
- if (XHTML.matcher(type).matches())
- return StandardTemplateModeHandlers.XHTML.getTemplateModeName();
- else
- return StandardTemplateModeHandlers.XML.getTemplateModeName();
- }
-
- throw new RuntimeException("Cannot handle mime-type " + type);
- }
-
- public ITemplateResolutionValidity computeValidity(HttpResponse response)
- {
- if (ttl == null)
- /** Caching is disabled! */
- return NonCacheableTemplateResolutionValidity.INSTANCE;
-
- boolean cacheable = true;
- Integer max_age = null;
-
- for (Header header : response.getHeaders("Cache-Control"))
- {
- for (HeaderElement element : header.getElements())
- {
- switch (element.getName())
- {
- case "no-cache":
- case "no-store":
- case "must-revalidate":
- cacheable = false;
- break;
-
- case "max-age":
- try
- {
- max_age = Integer.parseInt(element.getValue());
- }
- catch (NumberFormatException e)
- {
- LOG.warn(
- "invalid header \"Cache-Control: max-age={}\"",
- element.getValue()
- );
- }
- break;
- }
- }
- }
-
- if (max_age != null && max_age < 1)
- cacheable = false;
-
- if (!cacheable)
- return NonCacheableTemplateResolutionValidity.INSTANCE;
-
- if (max_age != null)
- {
- long millis = max_age;
- if (millis >= Long.MAX_VALUE / 1000l )
- millis = Long.MAX_VALUE;
- else
- millis = millis * 1000l;
- return new TTLTemplateResolutionValidity(millis);
- }
-
- Header header = response.getLastHeader("Expires");
- if (header == null)
- /** No TTL specified in response-headers: use configured default */
- return new TTLTemplateResolutionValidity(ttl);
-
- try
- {
- OffsetDateTime expires =
- OffsetDateTime.parse(
- header.getValue(),
- DateTimeFormatter.RFC_1123_DATE_TIME
- );
-
- Duration delta = Duration.between(OffsetDateTime.now(clock), expires);
- if (delta.isNegative() || delta.isZero())
- return NonCacheableTemplateResolutionValidity.INSTANCE;
-
- long millis = delta.getSeconds();
- if (millis >= Long.MAX_VALUE / 1000l)
- millis = Long.MAX_VALUE;
- else
- {
- millis = millis * 1000;
- millis = millis + (long)(delta.getNano() / 1000000);
- }
- return new TTLTemplateResolutionValidity(millis);
- }
- catch (DateTimeParseException e)
- {
- LOG.warn("invalid header \"Expires: {}\"", header.getValue());
- /**
- * No TTL specified in response-headers: assume expired
- * (see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21)
- */
- return NonCacheableTemplateResolutionValidity.INSTANCE;
- }
- }
-}
--- /dev/null
+package de.juplo.thymeproxy;
+
+
+import javax.servlet.http.HttpServletRequest;
+import org.springframework.web.servlet.RequestToViewNameTranslator;
+
+
+
+/**
+ *
+ * @author kai
+ */
+public class RequestToProxyViewNameTranslator implements RequestToViewNameTranslator
+{
+ @Override
+ public String getViewName(HttpServletRequest request) throws Exception
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append(request.getRequestURI());
+ String query = request.getQueryString();
+ if (query != null)
+ {
+ builder.append('?');
+ builder.append(query);
+ }
+ return builder.toString();
+ }
+}
--- /dev/null
+package de.juplo.thymeproxy;
+
+
+import javax.servlet.http.HttpServletRequest;
+import org.springframework.web.servlet.mvc.AbstractUrlViewController;
+
+
+
+/**
+ *
+ * @author kai
+ */
+public class UrlProxyViewController extends AbstractUrlViewController
+{
+ @Override
+ protected String getViewNameForRequest(HttpServletRequest request)
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append(request.getRequestURI());
+ String query = request.getQueryString();
+ if (query != null)
+ {
+ builder.append('?');
+ builder.append(query);
+ }
+ return builder.toString();
+ }
+}
+++ /dev/null
-org.springframework.boot.autoconfigure.EnableAutoConfiguration=de.juplo.autoconfiguration.ThymeproxyAutoConfiguration
thymeproxy.origin=@thymeproxy.origin@
server.port=@thymeproxy.port@
thymeproxy.ttl=@thymeproxy.ttl@
+thymeproxy.origins[0].uri=http://localhost:8080/thymeleaf/
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
-
-<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
-
- <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
- <layout class="org.apache.log4j.PatternLayout">
- <param name="ConversionPattern" value="%p - %C{1}.%M(%L) | %m%n"/>
- </layout>
- </appender>
-
- <logger name="de.juplo">
- <level value="trace"/>
- </logger>
-
- <logger name="org.springframework">
- <level value="debug" />
- </logger>
- <logger name="org.thymeleaf">
- <level value="debug" />
- </logger>
-
- <root>
- <level value="info"/>
- <appender-ref ref="CONSOLE"/>
- </root>
-
-</log4j:configuration>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%p - %c{0}.%M\(%L\) | %m%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="de.juplo">
+ <level value="trace"/>
+ </logger>
+
+ <root>
+ <level value="info"/>
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+</configuration>
+++ /dev/null
-package de.juplo.autoconfigure;
-
-
-import de.juplo.thymeproxy.ProxyTemplateResolver;
-import java.net.URI;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import org.junit.Test;
-import org.springframework.context.annotation.Configuration;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.BeansException;
-import org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer;
-import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
-import org.springframework.boot.test.EnvironmentTestUtils;
-import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.thymeleaf.templateresolver.ITemplateResolver;
-import org.thymeleaf.templateresolver.TemplateResolver;
-
-
-
-public class ThymeproxyAutoConfigurationTest
-{
- private final Logger LOG =
- LoggerFactory.getLogger(ThymeproxyAutoConfigurationTest.class);
-
-
- @Test
- public void propertyBinding() throws Exception
- {
- LOG.info("<-- Start Of New Test-Case!");
-
- ConfigurableApplicationContext context;
- ThymeproxyProperties properties;
-
- context = load(
- EmptyConfiguration.class,
- "thymeproxy.name=Thymeproxy",
- "thymeproxy.origins[0].uri=http://localhost:8080/test/",
- "thymeproxy.origins[0].patterns[0]=^/css/",
- "thymeproxy.origins[0].patterns[1]=^/img/",
- "thymeproxy.origins[0].patterns[2]=*\\.xml$",
- "thymeproxy.origins[1].uri=http://127.0.0.1:8081",
- "thymeproxy.origins[1].ttl=30000"
- );
- properties = context.getBean(ThymeproxyProperties.class);
- assertNotNull(properties);
- assertEquals("Thymeproxy", properties.name);
- assertNotNull(properties.origins);
- assertEquals(2, properties.origins.size());
- assertEquals(new URI("http://localhost:8080/test/"), properties.origins.get(0).uri);
- assertNotNull(properties.origins.get(0).patterns);
- assertEquals(3, properties.origins.get(0).patterns.size());
- assertEquals("^/css/", properties.origins.get(0).patterns.get(0));
- assertEquals("^/img/", properties.origins.get(0).patterns.get(1));
- assertEquals("*\\.xml$", properties.origins.get(0).patterns.get(2));
- assertNull(properties.origins.get(0).ttl);
- assertEquals(new URI("http://127.0.0.1:8081"), properties.origins.get(1).uri);
- assertNotNull(properties.origins.get(1).patterns);
- assertEquals(0, properties.origins.get(1).patterns.size());
- assertEquals(new Long(30000l), properties.origins.get(1).ttl);
- context.close();
- }
-
- @Test
- public void defaultConfiguration()
- {
- LOG.info("<-- Start Of New Test-Case!");
-
- ConfigurableApplicationContext context = load(EmptyConfiguration.class);
-
- ITemplateResolver resolver =
- (ITemplateResolver)context.getBean("defaultTemplateResolver");
- assertNotNull(resolver);
- assertTrue(
- "Expected an instance of type TemplateResolver",
- resolver instanceof TemplateResolver
- );
- assertNotEquals(DefaultTemplateResolverConfiguration.RESOLVER, resolver);
-
- context.close();
- }
-
- @Test
- public void defaultTemplateResolverConfigured()
- {
- LOG.info("<-- Start Of New Test-Case!");
-
- ConfigurableApplicationContext context =
- load(DefaultTemplateResolverConfiguration.class);
-
- ITemplateResolver resolver =
- (ITemplateResolver)context.getBean("defaultTemplateResolver");
- assertNotNull(resolver);
- assertTrue(
- "Expected an instance of type TemplateResolver",
- resolver instanceof TemplateResolver
- );
- assertEquals(DefaultTemplateResolverConfiguration.RESOLVER, resolver);
-
- context.close();
- }
-
- @Test
- public void proxiesConfigured()
- {
- LOG.info("<-- Start Of New Test-Case!");
-
- ConfigurableApplicationContext context = load(
- EmptyConfiguration.class,
- "thymeproxy.name=Thymeproxy",
- "thymeproxy.origins[0].uri=http://localhost:8080/test/",
- "thymeproxy.origins[0].patterns[0]=^/css/",
- "thymeproxy.origins[0].patterns[1]=^/img/",
- "thymeproxy.origins[0].patterns[2]=*\\.xml$",
- "thymeproxy.origins[1].uri=http://127.0.0.1:8081",
- "thymeproxy.origins[1].ttl=30000"
- );
-
- ITemplateResolver resolver;
- ProxyTemplateResolver proxy;
-
- resolver = (ITemplateResolver)context.getBean("defaultTemplateResolver");
- assertNotNull(resolver);
- assertTrue(
- "Expected an instance of type ProxyTemplateResolver",
- resolver instanceof ProxyTemplateResolver
- );
- proxy = (ProxyTemplateResolver)resolver;
- assertEquals("0: localhost", proxy.getName());
- assertEquals(new Integer(0), proxy.getOrder());
- assertEquals("http://localhost:8080/test/", proxy.getOrigin());
- assertNull(proxy.getDefaultTTL());
-
- resolver = (ITemplateResolver)context.getBean("proxy1");
- assertNotNull(resolver);
- assertTrue(
- "Expected an instance of type ProxyTemplateResolver",
- resolver instanceof ProxyTemplateResolver
- );
- proxy = (ProxyTemplateResolver)resolver;
- assertEquals("1: 127.0.0.1", proxy.getName());
- assertEquals(new Integer(1), proxy.getOrder());
- assertEquals("http://127.0.0.1:8081", proxy.getOrigin());
- assertEquals(new Long(30000l), proxy.getDefaultTTL());
-
- context.close();
- }
-
- @Test
- public void proxiesAndDefaultTemplateResolverConfigured()
- {
- LOG.info("<-- Start Of New Test-Case!");
-
- ConfigurableApplicationContext context = load(
- DefaultTemplateResolverConfiguration.class,
- "thymeproxy.name=Thymeproxy",
- "thymeproxy.origins[0].uri=http://localhost:8080/test/",
- "thymeproxy.origins[0].patterns[0]=^/css/",
- "thymeproxy.origins[0].patterns[1]=^/img/",
- "thymeproxy.origins[0].patterns[2]=*\\.xml$",
- "thymeproxy.origins[1].uri=http://127.0.0.1:8081",
- "thymeproxy.origins[1].ttl=30000"
- );
-
- ITemplateResolver resolver;
- ProxyTemplateResolver proxy;
-
- resolver = (ITemplateResolver)context.getBean("defaultTemplateResolver");
- assertNotNull(resolver);
- assertTrue(
- "Expected an instance of type TemplateResolver",
- resolver instanceof TemplateResolver
- );
- assertEquals(DefaultTemplateResolverConfiguration.RESOLVER, resolver);
-
- try
- {
- resolver = (ITemplateResolver)context.getBean("proxy1");
- fail("Found bean for name proxy1: " + resolver);
- }
- catch (BeansException e)
- {
- LOG.info(e.toString());
- }
-
- context.close();
- }
-
-
- @Configuration
- static class EmptyConfiguration
- {
- }
-
- @Configuration
- static class DefaultTemplateResolverConfiguration
- {
- public static TemplateResolver RESOLVER = new TemplateResolver();
-
- @Bean
- public TemplateResolver defaultTemplateResolver()
- {
- return RESOLVER;
- }
- }
-
-
- private ConfigurableApplicationContext load(Class<?> config, String... pairs)
- {
- AnnotationConfigApplicationContext ctx =
- new AnnotationConfigApplicationContext();
- EnvironmentTestUtils.addEnvironment(ctx, pairs);
- ctx.register(HttpClientAutoConfiguration.class);
- ctx.register(ThymeleafAutoConfiguration.class);
- ctx.register(ThymeproxyAutoConfiguration.class);
- ctx.register(config);
- AutoConfigurationReportLoggingInitializer report =
- new AutoConfigurationReportLoggingInitializer();
- report.initialize(ctx);
- ctx.refresh();
- return ctx;
- }
-}
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8" ?>
-<configuration>
-
- <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
- <encoder>
- <pattern>%p - %c{0}.%M\(%L\) | %m%n</pattern>
- </encoder>
- </appender>
-
- <logger name="de.juplo.facebook">
- <level value="trace"/>
- </logger>
-
- <logger name="org.springframework.boot.autoconfigure.logging">
- <level value="debug"/>
- </logger>
-
- <root>
- <level value="info"/>
- <appender-ref ref="CONSOLE"/>
- </root>
-
-</configuration>