autoconfigure
authorKai Moritz <kai@juplo.de>
Mon, 13 Jun 2016 12:52:51 +0000 (14:52 +0200)
committerKai Moritz <kai@juplo.de>
Thu, 16 Jun 2016 05:06:43 +0000 (07:06 +0200)
.gitignore [new file with mode: 0644]
pom.xml [new file with mode: 0644]
src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java [new file with mode: 0644]
src/main/java/de/juplo/autoconfigure/ThymeproxyProperties.java [new file with mode: 0644]
src/main/java/de/juplo/thymeproxy/ProxyResourceResolver.java [new file with mode: 0644]
src/main/java/de/juplo/thymeproxy/ProxyTemplateResolver.java [new file with mode: 0644]
src/main/resources/META-INF/spring.factories [new file with mode: 0644]
src/main/resources/application.properties [new file with mode: 0644]
src/main/resources/log4j.xml [new file with mode: 0644]
src/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java [new file with mode: 0644]
src/test/resources/logback.xml [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..a6f89c2
--- /dev/null
@@ -0,0 +1 @@
+/target/
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644 (file)
index 0000000..031167a
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,115 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-parent</artifactId>
+    <version>1.3.5.RELEASE</version>
+  </parent>
+
+  <groupId>de.juplo</groupId>
+  <artifactId>thymeproxy</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <properties>
+
+    <!-- settings for Spring-Boot -->
+    <java.version>1.8</java.version>
+
+    <!-- other usefull settings -->
+    <encoding>UTF-8</encoding>
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+
+    <!-- application-settings -->
+    <thymeproxy.name>${project.name}</thymeproxy.name>
+    <thymeproxy.origin>http://localhost:8080</thymeproxy.origin>
+    <thymeproxy.port>80</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>
+
+  </properties>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-web</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-thymeleaf</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>de.juplo</groupId>
+      <artifactId>httpclient-spring-boot-starter</artifactId>
+      <version>${httpclient-spring-boot-starter.version}</version>
+    </dependency>
+
+    <!-- Httpclient -->
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+    </dependency>
+
+    <!-- Needed to parse HTML5 with Thymeleaf -->
+    <dependency>
+      <groupId>net.sourceforge.nekohtml</groupId>
+      <artifactId>nekohtml</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>
+    </dependency>
+
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-maven-plugin</artifactId>
+        <!-- Needed, because otherwise, filtering of maven properties won't work -->
+        <configuration>
+          <addResources>false</addResources>
+        </configuration>
+        <dependencies>
+          <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>springloaded</artifactId>
+            <version>1.2.4.RELEASE</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java b/src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java
new file mode 100644 (file)
index 0000000..d81b31d
--- /dev/null
@@ -0,0 +1,77 @@
+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
new file mode 100644 (file)
index 0000000..34a204b
--- /dev/null
@@ -0,0 +1,67 @@
+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/ProxyResourceResolver.java b/src/main/java/de/juplo/thymeproxy/ProxyResourceResolver.java
new file mode 100644 (file)
index 0000000..5e24034
--- /dev/null
@@ -0,0 +1,123 @@
+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
new file mode 100644 (file)
index 0000000..c6601a6
--- /dev/null
@@ -0,0 +1,297 @@
+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/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories
new file mode 100644 (file)
index 0000000..f6c92b4
--- /dev/null
@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=de.juplo.autoconfiguration.ThymeproxyAutoConfiguration
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644 (file)
index 0000000..dd442f2
--- /dev/null
@@ -0,0 +1,4 @@
+thymeproxy.name=@thymeproxy.name@
+thymeproxy.origin=@thymeproxy.origin@
+server.port=@thymeproxy.port@
+thymeproxy.ttl=@thymeproxy.ttl@
diff --git a/src/main/resources/log4j.xml b/src/main/resources/log4j.xml
new file mode 100644 (file)
index 0000000..a3d7d44
--- /dev/null
@@ -0,0 +1,28 @@
+<?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/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java b/src/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java
new file mode 100644 (file)
index 0000000..9360b7c
--- /dev/null
@@ -0,0 +1,230 @@
+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
new file mode 100644 (file)
index 0000000..e6f6c58
--- /dev/null
@@ -0,0 +1,23 @@
+<?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>