WIP: proxy
authorKai Moritz <kai@juplo.de>
Thu, 16 Jun 2016 09:10:24 +0000 (11:10 +0200)
committerKai Moritz <kai@juplo.de>
Thu, 16 Jun 2016 09:10:24 +0000 (11:10 +0200)
15 files changed:
pom.xml
src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java [deleted file]
src/main/java/de/juplo/autoconfigure/ThymeproxyProperties.java [deleted file]
src/main/java/de/juplo/thymeproxy/Application.java [new file with mode: 0644]
src/main/java/de/juplo/thymeproxy/ProxyHttpRequestHandler.java [new file with mode: 0644]
src/main/java/de/juplo/thymeproxy/ProxyResourceResolver.java [deleted file]
src/main/java/de/juplo/thymeproxy/ProxyTemplateResolver.java [deleted file]
src/main/java/de/juplo/thymeproxy/RequestToProxyViewNameTranslator.java [new file with mode: 0644]
src/main/java/de/juplo/thymeproxy/UrlProxyViewController.java [new file with mode: 0644]
src/main/resources/META-INF/spring.factories [deleted file]
src/main/resources/application.properties
src/main/resources/log4j.xml [deleted file]
src/main/resources/logback.xml [new file with mode: 0644]
src/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java [deleted file]
src/test/resources/logback.xml [deleted file]

diff --git a/pom.xml b/pom.xml
index 031167a..d58173c 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
   </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>
diff --git a/src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java b/src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java
deleted file mode 100644 (file)
index d81b31d..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-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;
-  }
-}
diff --git a/src/main/java/de/juplo/autoconfigure/ThymeproxyProperties.java b/src/main/java/de/juplo/autoconfigure/ThymeproxyProperties.java
deleted file mode 100644 (file)
index 34a204b..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-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;
-    }
-  }
-}
diff --git a/src/main/java/de/juplo/thymeproxy/Application.java b/src/main/java/de/juplo/thymeproxy/Application.java
new file mode 100644 (file)
index 0000000..47a60c9
--- /dev/null
@@ -0,0 +1,45 @@
+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
diff --git a/src/main/java/de/juplo/thymeproxy/ProxyHttpRequestHandler.java b/src/main/java/de/juplo/thymeproxy/ProxyHttpRequestHandler.java
new file mode 100644 (file)
index 0000000..8320c7e
--- /dev/null
@@ -0,0 +1,384 @@
+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;
+  }
+}
diff --git a/src/main/java/de/juplo/thymeproxy/ProxyResourceResolver.java b/src/main/java/de/juplo/thymeproxy/ProxyResourceResolver.java
deleted file mode 100644 (file)
index 5e24034..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-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);
-      }
-    };
-  }
-}
diff --git a/src/main/java/de/juplo/thymeproxy/ProxyTemplateResolver.java b/src/main/java/de/juplo/thymeproxy/ProxyTemplateResolver.java
deleted file mode 100644 (file)
index c6601a6..0000000
+++ /dev/null
@@ -1,297 +0,0 @@
-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;
-    }
-  }
-}
diff --git a/src/main/java/de/juplo/thymeproxy/RequestToProxyViewNameTranslator.java b/src/main/java/de/juplo/thymeproxy/RequestToProxyViewNameTranslator.java
new file mode 100644 (file)
index 0000000..53c83fb
--- /dev/null
@@ -0,0 +1,28 @@
+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();
+  }
+}
diff --git a/src/main/java/de/juplo/thymeproxy/UrlProxyViewController.java b/src/main/java/de/juplo/thymeproxy/UrlProxyViewController.java
new file mode 100644 (file)
index 0000000..23ef9e3
--- /dev/null
@@ -0,0 +1,28 @@
+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();
+  }
+}
diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories
deleted file mode 100644 (file)
index f6c92b4..0000000
+++ /dev/null
@@ -1 +0,0 @@
-org.springframework.boot.autoconfigure.EnableAutoConfiguration=de.juplo.autoconfiguration.ThymeproxyAutoConfiguration
index dd442f2..1ada05c 100644 (file)
@@ -2,3 +2,4 @@ thymeproxy.name=@thymeproxy.name@
 thymeproxy.origin=@thymeproxy.origin@
 server.port=@thymeproxy.port@
 thymeproxy.ttl=@thymeproxy.ttl@
+thymeproxy.origins[0].uri=http://localhost:8080/thymeleaf/
diff --git a/src/main/resources/log4j.xml b/src/main/resources/log4j.xml
deleted file mode 100644 (file)
index a3d7d44..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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>
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644 (file)
index 0000000..793eaa1
--- /dev/null
@@ -0,0 +1,19 @@
+<?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>
diff --git a/src/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java b/src/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java
deleted file mode 100644 (file)
index 9360b7c..0000000
+++ /dev/null
@@ -1,230 +0,0 @@
-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;
-  }
-}
diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml
deleted file mode 100644 (file)
index e6f6c58..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<?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>