SLING-7785: made virtual resources cacheable
diff --git a/src/main/java/org/apache/sling/dynamicinclude/CacheControlFilter.java b/src/main/java/org/apache/sling/dynamicinclude/CacheControlFilter.java
index a4883ca..984d0c8 100644
--- a/src/main/java/org/apache/sling/dynamicinclude/CacheControlFilter.java
+++ b/src/main/java/org/apache/sling/dynamicinclude/CacheControlFilter.java
@@ -39,6 +39,8 @@
 @SlingFilter(scope = SlingFilterScope.REQUEST, order = 0)
 public class CacheControlFilter implements Filter {
 
+    private static final String HEADER_DATE = "Date";
+
     private static final String HEADER_CACHE_CONTROL = "Cache-Control";
 
     private static final Logger LOG = LoggerFactory.getLogger(CacheControlFilter.class);
@@ -57,6 +59,9 @@
             SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) response;
             slingResponse.setHeader(HEADER_CACHE_CONTROL, "max-age=" + config.getTtl());
             LOG.debug("set \"{}: max-age={}\" to {}", HEADER_CACHE_CONTROL, config.getTtl(), resourceType);
+            if (!slingResponse.containsHeader(HEADER_DATE)) {
+                slingResponse.setDateHeader(HEADER_DATE, System.currentTimeMillis());
+            }
         }
 
         chain.doFilter(request, response);
diff --git a/src/main/java/org/apache/sling/dynamicinclude/Configuration.java b/src/main/java/org/apache/sling/dynamicinclude/Configuration.java
index 9cb6695..027dae6 100755
--- a/src/main/java/org/apache/sling/dynamicinclude/Configuration.java
+++ b/src/main/java/org/apache/sling/dynamicinclude/Configuration.java
@@ -57,6 +57,7 @@
         @PropertyOption(name = "JSI", value = "Javascript")}),
     @Property(name = Configuration.PROPERTY_ADD_COMMENT, boolValue = Configuration.DEFAULT_ADD_COMMENT, label = "Add comment", description = "Add comment to included components"),
     @Property(name = Configuration.PROPERTY_FILTER_SELECTOR, value = Configuration.DEFAULT_FILTER_SELECTOR, label = "Filter selector", description = "Selector used to mark included resources"),
+    @Property(name = Configuration.PROPERTY_EXTENSION, value = Configuration.DEFAULT_EXTENSION, label = "Extension", description = "Extension to append to virtual resources to make caching possible"),
     @Property(name = Configuration.PROPERTY_COMPONENT_TTL, label = "Component TTL", description = "\"Time to live\" cache header for rendered component (in seconds)"),
     @Property(name = Configuration.PROPERTY_REQUIRED_HEADER, value = Configuration.DEFAULT_REQUIRED_HEADER, label = "Required header", description = "SDI will work only for requests with given header"),
     @Property(name = Configuration.PROPERTY_IGNORE_URL_PARAMS, cardinality = Integer.MAX_VALUE, label = "Ignore URL params", description = "SDI will process the request even if it contains configured GET parameters"),
@@ -80,6 +81,10 @@
 
   static final String DEFAULT_FILTER_SELECTOR = "nocache";
 
+  static final String PROPERTY_EXTENSION = "include-filter.config.extension";
+
+  static final String DEFAULT_EXTENSION = "";
+
   static final String PROPERTY_COMPONENT_TTL = "include-filter.config.ttl";
 
   static final String PROPERTY_INCLUDE_TYPE = "include-filter.config.include-type";
@@ -111,6 +116,8 @@
 
   private String includeSelector;
 
+  private String extension;
+
   private int ttl;
 
   private List<String> resourceTypes;
@@ -140,6 +147,7 @@
     this.resourceTypes = Arrays.asList(resourceTypeList);
 
     includeSelector = PropertiesUtil.toString(properties.get(PROPERTY_FILTER_SELECTOR), DEFAULT_FILTER_SELECTOR);
+    extension = PropertiesUtil.toString(properties.get(PROPERTY_EXTENSION), DEFAULT_EXTENSION);
     ttl = PropertiesUtil.toInteger(properties.get(PROPERTY_COMPONENT_TTL), -1);
     addComment = PropertiesUtil.toBoolean(properties.get(PROPERTY_ADD_COMMENT), DEFAULT_ADD_COMMENT);
     includeTypeName = PropertiesUtil.toString(properties.get(PROPERTY_INCLUDE_TYPE), DEFAULT_INCLUDE_TYPE);
@@ -173,6 +181,19 @@
     return includeSelector;
   }
 
+  public boolean hasExtension(final SlingHttpServletRequest request) {
+    final String suffix = request.getRequestPathInfo().getSuffix();
+    return suffix.endsWith("." + this.extension);
+  }
+
+  public boolean hasExtensionSet() {
+    return StringUtils.isNotBlank(this.extension);
+  }
+
+  public String getExtension() {
+    return this.extension;
+  }
+
   public boolean hasTtlSet() {
     return ttl >= 0;
   }
diff --git a/src/main/java/org/apache/sling/dynamicinclude/IncludeTagFilter.java b/src/main/java/org/apache/sling/dynamicinclude/IncludeTagFilter.java
index 62b64c3..ccbf2bd 100644
--- a/src/main/java/org/apache/sling/dynamicinclude/IncludeTagFilter.java
+++ b/src/main/java/org/apache/sling/dynamicinclude/IncludeTagFilter.java
@@ -161,8 +161,9 @@
 
     private String buildUrl(Configuration config, SlingHttpServletRequest request) {
         final Resource resource = request.getResource();
+
         final boolean synthetic = ResourceUtil.isSyntheticResource(request.getResource());
-        return UrlBuilder.buildUrl(config.getIncludeSelector(), resource.getResourceType(), synthetic, request.getRequestPathInfo());
+        return UrlBuilder.buildUrl(config.getIncludeSelector(), resource.getResourceType(), synthetic, config, request.getRequestPathInfo());
     }
 
     private static String sanitize(String path) {
diff --git a/src/main/java/org/apache/sling/dynamicinclude/SyntheticResourceFilter.java b/src/main/java/org/apache/sling/dynamicinclude/SyntheticResourceFilter.java
index d2e2725..0f2ac1b 100644
--- a/src/main/java/org/apache/sling/dynamicinclude/SyntheticResourceFilter.java
+++ b/src/main/java/org/apache/sling/dynamicinclude/SyntheticResourceFilter.java
@@ -37,6 +37,7 @@
 import org.apache.felix.scr.annotations.Service;
 import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.request.RequestDispatcherOptions;
+import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceUtil;
 import org.apache.sling.engine.EngineConstants;
 import org.osgi.framework.Constants;
@@ -60,19 +61,23 @@
         final Configuration config = configurationWhiteboard.getConfiguration(slingRequest, resourceType);
 
         if (config == null || !config.hasIncludeSelector(slingRequest)
-                || !ResourceUtil.isSyntheticResource(slingRequest.getResource())) {
+                || !ResourceUtil.isSyntheticResource(slingRequest.getResource())
+                || (config.hasExtensionSet() && !config.hasExtension(slingRequest))) {
             chain.doFilter(request, response);
             return;
         }
 
         final RequestDispatcherOptions options = new RequestDispatcherOptions();
         options.setForceResourceType(resourceType);
-        final RequestDispatcher dispatcher = slingRequest.getRequestDispatcher(slingRequest.getResource(), options);
+        String resourcePath = StringUtils.substringBefore(slingRequest.getRequestPathInfo().getResourcePath(), ".");
+        Resource resource = slingRequest.getResourceResolver().resolve(resourcePath);
+        final RequestDispatcher dispatcher = slingRequest.getRequestDispatcher(resource, options);
         dispatcher.forward(request, response);
     }
 
-    private static String getResourceTypeFromSuffix(SlingHttpServletRequest request) {
-        final String suffix = request.getRequestPathInfo().getSuffix();
+    private static String getResourceTypeFromSuffix(final SlingHttpServletRequest request) {
+        String suffix = request.getRequestPathInfo().getSuffix();
+        suffix = StringUtils.substringBeforeLast(suffix, ".");
         return StringUtils.removeStart(suffix, "/");
     }
 
diff --git a/src/main/java/org/apache/sling/dynamicinclude/impl/UrlBuilder.java b/src/main/java/org/apache/sling/dynamicinclude/impl/UrlBuilder.java
index 79338b9..9077cae 100644
--- a/src/main/java/org/apache/sling/dynamicinclude/impl/UrlBuilder.java
+++ b/src/main/java/org/apache/sling/dynamicinclude/impl/UrlBuilder.java
@@ -19,15 +19,16 @@
 
 package org.apache.sling.dynamicinclude.impl;
 
+import java.util.Arrays;
+
 import org.apache.commons.lang.StringUtils;
 import org.apache.sling.api.request.RequestPathInfo;
-
-import java.util.Arrays;
+import org.apache.sling.dynamicinclude.Configuration;
 
 public final class UrlBuilder {
 
 
-    public static String buildUrl(String includeSelector, String resourceType, boolean synthetic, RequestPathInfo pathInfo) {
+    public static String buildUrl(String includeSelector, String resourceType, boolean synthetic, Configuration config, RequestPathInfo pathInfo) {
         final StringBuilder builder = new StringBuilder();
 
         final String resourcePath = pathInfo.getResourcePath();
@@ -42,6 +43,9 @@
         builder.append('.').append(pathInfo.getExtension());
         if (synthetic) {
             builder.append('/').append(resourceType);
+            if (config.hasExtensionSet()) {
+                builder.append('.').append(config.getExtension());
+            }
         } else {
             builder.append(StringUtils.defaultString(pathInfo.getSuffix()));
         }
diff --git a/src/main/java/org/apache/sling/dynamicinclude/package-info.java b/src/main/java/org/apache/sling/dynamicinclude/package-info.java
index 6eed06e..78fc7ba 100644
--- a/src/main/java/org/apache/sling/dynamicinclude/package-info.java
+++ b/src/main/java/org/apache/sling/dynamicinclude/package-info.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-@Version("4.0.0")
+@Version("4.1.0")
 package org.apache.sling.dynamicinclude;
 import aQute.bnd.annotation.Version;
 
diff --git a/src/test/java/org/apache/sling/dynamicinclude/impl/UrlBuilderTest.java b/src/test/java/org/apache/sling/dynamicinclude/impl/UrlBuilderTest.java
index 3184bb7..ccf6e67 100644
--- a/src/test/java/org/apache/sling/dynamicinclude/impl/UrlBuilderTest.java
+++ b/src/test/java/org/apache/sling/dynamicinclude/impl/UrlBuilderTest.java
@@ -21,6 +21,8 @@
 
 import org.apache.commons.lang.StringUtils;
 import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.dynamicinclude.Configuration;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -29,6 +31,7 @@
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.reset;
 
 @RunWith(MockitoJUnitRunner.class)
 public class UrlBuilderTest {
@@ -36,13 +39,16 @@
     @Mock
     private RequestPathInfo requestPathInfo;
 
+    @Mock
+    private Configuration config;
+
     @Test
     public void shouldAppendTheIncludeSelectorToUrlWithNoSelectors() {
         givenAnHtmlRequestForResource("/resource/path");
         withSelectorString(null);
         boolean isSyntheticResource = false;
 
-        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, requestPathInfo);
+        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, config, requestPathInfo);
 
         assertThat(actualResult, is("/resource/path.include.html"));
     }
@@ -53,7 +59,7 @@
         withSelectorString("foo.bar.baz");
         boolean isSyntheticResource = false;
 
-        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, requestPathInfo);
+        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, config, requestPathInfo);
 
         assertThat(actualResult, is("/resource/path.foo.bar.baz.include.html"));
     }
@@ -64,7 +70,7 @@
         withSelectorString("foo.2.31");
         boolean isSyntheticResource = false;
 
-        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, requestPathInfo);
+        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, config, requestPathInfo);
 
         assertThat(actualResult, is("/resource/path.foo.2.31.include.html"));
     }
@@ -75,7 +81,7 @@
         withSelectorString("foo.include");
         boolean isSyntheticResource = false;
 
-        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, requestPathInfo);
+        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, config, requestPathInfo);
 
         assertThat(actualResult, is("/resource/path.foo.include.html"));
     }
@@ -86,7 +92,7 @@
         withSelectorString("longerSelectorThatHappensToContainTheIncludeSelector");
         boolean isSyntheticResource = false;
 
-        String actualResult = UrlBuilder.buildUrl("IncludeSelector", "apps/example/resource/type", isSyntheticResource, requestPathInfo);
+        String actualResult = UrlBuilder.buildUrl("IncludeSelector", "apps/example/resource/type", isSyntheticResource, config, requestPathInfo);
 
         assertThat(actualResult, is("/resource/path.longerSelectorThatHappensToContainTheIncludeSelector.IncludeSelector.html"));
     }
@@ -97,10 +103,25 @@
         withSelectorString("foo.include");
         boolean isSyntheticResource = true;
 
-        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, requestPathInfo);
+        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, config, requestPathInfo);
 
         assertThat(actualResult, is("/resource/path.foo.include.html/apps/example/resource/type"));
     }
+    
+    @Test
+    public void shouldAppendExtensionForSyntheticResources() {
+        givenAnHtmlRequestForResource("/resource/path");
+        withSelectorString("foo.include");
+
+        when(config.hasExtensionSet()).thenReturn(true);
+        when(config.getExtension()).thenReturn("sdi");
+
+        boolean isSyntheticResource = true;
+
+        String actualResult = UrlBuilder.buildUrl("include", "apps/example/resource/type", isSyntheticResource, config, requestPathInfo);
+
+        assertThat(actualResult, is("/resource/path.foo.include.html/apps/example/resource/type.sdi"));
+    }
 
     private void givenAnHtmlRequestForResource(String resourcePath) {
         when(requestPathInfo.getExtension()).thenReturn("html");