TREQ-11: Adding Mustache support to Tiles

git-svn-id: https://svn.apache.org/repos/asf/tiles/framework/trunk/tiles-request@1297703 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index e348d6f..04354f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,7 @@
 		<module>tiles-request-jsp</module>
 		<module>tiles-request-freemarker</module>
 		<module>tiles-request-velocity</module>
+        <module>tiles-request-mustache</module>
 	</modules>
 	<build>
 		<pluginManagement>
diff --git a/tiles-request-mustache/pom.xml b/tiles-request-mustache/pom.xml
new file mode 100644
index 0000000..bf5ccb5
--- /dev/null
+++ b/tiles-request-mustache/pom.xml
@@ -0,0 +1,69 @@
+<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/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <artifactId>tiles-request</artifactId>
+    <groupId>org.apache.tiles</groupId>
+    <version>1.0-SNAPSHOT</version>
+  </parent>
+  <groupId>org.apache.tiles</groupId>
+  <artifactId>tiles-request-mustache</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <name>Tiles request - Mustache support</name>
+  <description>Tiles request implementation for Mustache templates.</description>
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.tiles</groupId>
+      <artifactId>tiles-request-api</artifactId>
+      <version>1.0-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.tiles</groupId>
+      <artifactId>tiles-request-servlet</artifactId>
+      <version>1.0-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+        <groupId>com.github.spullara.mustache.java</groupId>
+        <artifactId>core</artifactId>
+        <version>0.6.2</version>
+    </dependency>
+    <dependency>
+        <groupId>com.github.spullara.mustache.java</groupId>
+        <artifactId>builder</artifactId>
+        <version>0.6.2</version>
+    </dependency>
+    <dependency>
+    	<groupId>javax.servlet</groupId>
+    	<artifactId>servlet-api</artifactId>
+    	<version>2.5</version>
+    	<scope>provided</scope>
+    </dependency>
+    <dependency>
+    	<groupId>junit</groupId>
+    	<artifactId>junit</artifactId>
+    	<version>4.7</version>
+    	<scope>test</scope>
+    </dependency>
+    <dependency>
+    	<groupId>org.easymock</groupId>
+    	<artifactId>easymock</artifactId>
+    	<version>2.5.2</version>
+    	<scope>test</scope>
+    </dependency>
+    <dependency>
+    	<groupId>org.easymock</groupId>
+    	<artifactId>easymockclassextension</artifactId>
+    	<version>2.4</version>
+    	<scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <repositories>
+    <repository>
+      <id>sonatype-nexus</id>
+      <name>sonatype-nexus</name>
+      <url>https://oss.sonatype.org/content/repositories/releases/</url>
+    </repository>
+  </repositories>
+</project>
diff --git a/tiles-request-mustache/src/main/java/org/apache/tiles/request/MustacheScopeMap.java b/tiles-request-mustache/src/main/java/org/apache/tiles/request/MustacheScopeMap.java
new file mode 100644
index 0000000..e47d11e
--- /dev/null
+++ b/tiles-request-mustache/src/main/java/org/apache/tiles/request/MustacheScopeMap.java
@@ -0,0 +1,76 @@
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tiles.request;
+
+import java.util.Set;
+
+import com.sampullara.mustache.Scope;
+import org.apache.tiles.request.collection.ScopeMap;
+import org.apache.tiles.request.extractor.MustacheScopeExtractor;
+
+
+final class MustacheScopeMap extends ScopeMap {
+
+    /**
+     * The request object to use.
+     */
+    private Scope scope = null;
+
+    /**
+     * Constructor.
+     *
+     * @param request The request object to use.
+     */
+    public MustacheScopeMap(Scope request) {
+        super(new MustacheScopeExtractor(request));
+        this.scope = request;
+    }
+
+    @Override
+    public Object remove(Object key) {
+        return scope.remove(key);
+    }
+
+    @Override
+    public Object put(String key, Object value) {
+        return scope.put(key, value);
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return scope.containsKey(key);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return size() < 1;
+    }
+
+    @Override
+    public Set<String> keySet() {
+        return (Set<String>)(Set<?>)scope.keySet();
+    }
+
+    @Override
+    public int size() {
+        return scope.keySet().size();
+    }
+}
diff --git a/tiles-request-mustache/src/main/java/org/apache/tiles/request/extractor/MustacheScopeExtractor.java b/tiles-request-mustache/src/main/java/org/apache/tiles/request/extractor/MustacheScopeExtractor.java
new file mode 100644
index 0000000..6d9abab
--- /dev/null
+++ b/tiles-request-mustache/src/main/java/org/apache/tiles/request/extractor/MustacheScopeExtractor.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2012 Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tiles.request.extractor;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Set;
+
+import com.sampullara.mustache.Scope;
+import org.apache.tiles.request.attribute.AttributeExtractor;
+
+
+public final class MustacheScopeExtractor  implements AttributeExtractor {
+    private final Scope scope;
+
+    public MustacheScopeExtractor(Scope scope){
+        this.scope = scope;
+    }
+
+    @Override
+    public void removeValue(String key) {
+        scope.remove(key);
+    }
+
+    @Override
+    public Enumeration<String> getKeys() {
+        return (Enumeration<String>) Collections.enumeration((Set<?>)scope.keySet());
+    }
+
+    @Override
+    public Object getValue(String key) {
+        return scope.get(key);
+    }
+
+    @Override
+    public void setValue(String key, Object value) {
+        scope.put(value, value);
+    }
+}
\ No newline at end of file
diff --git a/tiles-request-mustache/src/main/java/org/apache/tiles/request/render/MustacheRenderer.java b/tiles-request-mustache/src/main/java/org/apache/tiles/request/render/MustacheRenderer.java
new file mode 100644
index 0000000..0e60173
--- /dev/null
+++ b/tiles-request-mustache/src/main/java/org/apache/tiles/request/render/MustacheRenderer.java
@@ -0,0 +1,97 @@
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.tiles.request.render;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.sampullara.mustache.MustacheBuilder;
+import com.sampullara.mustache.MustacheException;
+import com.sampullara.mustache.Scope;
+import org.apache.tiles.request.ApplicationContext;
+import org.apache.tiles.request.Request;
+
+/**
+ * The Mustache-specific renderer.
+ *
+ * @version $Rev: 1215006 $ $Date: 2011-12-16 01:30:41 +0100 (Fri, 16 Dec 2011) $
+ * @since 3.0
+ */
+public final class MustacheRenderer implements Renderer {
+
+    private Pattern acceptPattern;
+
+    @Override
+    public void render(String path, Request request) throws IOException {
+        if (path == null) {
+            throw new CannotRenderException("Cannot dispatch a null path");
+        }
+
+        try{
+            new MustacheBuilder()
+                    .build(new BufferedReader(new InputStreamReader(getResourceStream(request, path))), path)
+                    .execute(request.getWriter(), buildScope(request));
+
+        }catch(MustacheException ex){
+            throw new IOException("failed to MustacheRenderer.render(" + path + ",request)", ex);
+        }
+    }
+
+    private static InputStream getResourceStream(Request request, String path) throws IOException {
+        final ApplicationContext applicationContext = request.getApplicationContext();
+        final URL resource = applicationContext.getResource(path);
+        return resource.openStream();
+    }
+
+    private static Scope buildScope(Request request){
+        Scope scope = null;
+        List<String> availableScopes = request.getAvailableScopes();
+        for(int i = availableScopes.size() -1; i >= 0; --i){
+            scope = null == scope
+                    ? new Scope(request.getContext(availableScopes.get(i)))
+                    : new Scope(request.getContext(availableScopes.get(i)), scope);
+        }
+        return scope;
+    }
+
+    //@Override
+    public boolean isRenderable(String path, Request request) {
+        if (path == null) {
+            return false;
+        }
+        if (acceptPattern != null) {
+            final Matcher matcher = acceptPattern.matcher(path);
+            return matcher.matches();
+        }
+        return true;
+    }
+
+    public void setAcceptPattern(Pattern acceptPattern) {
+        this.acceptPattern = acceptPattern;
+    }
+}
diff --git a/tiles-request-mustache/src/test/java/org/apache/tiles/request/render/MustacheRendererTest.java b/tiles-request-mustache/src/test/java/org/apache/tiles/request/render/MustacheRendererTest.java
new file mode 100644
index 0000000..8de2298
--- /dev/null
+++ b/tiles-request-mustache/src/test/java/org/apache/tiles/request/render/MustacheRendererTest.java
@@ -0,0 +1,108 @@
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tiles.request.render;
+
+
+import java.io.IOException;
+import java.io.Writer;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.tiles.request.ApplicationContext;
+import org.apache.tiles.request.servlet.ServletRequest;
+import org.junit.Test;
+
+import static org.easymock.classextension.EasyMock.*;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests {@link MustacheRenderer}.
+ *
+ * @version $Rev: 1066788 $ $Date: 2011-02-03 11:49:11 +0000 (Thu, 03 Feb 2011) $
+ */
+public final class MustacheRendererTest {
+
+    /**
+     * Tests {@link MustacheRenderer#render(String, org.apache.tiles.request.Request)}.
+     * @throws IOException If something goes wrong.
+     */
+    @Test
+    public void testRender() throws IOException {
+        ServletRequest request = createMock(ServletRequest.class);
+        Writer writer = createMock(Writer.class);
+        ApplicationContext applicationContext = createMock(ApplicationContext.class);
+        URL resource = getClass().getResource("/test.html");
+
+        Map<String,Object> context = Collections.singletonMap("testKey", (Object)"test value");
+
+        expect(request.getApplicationContext()).andReturn(applicationContext);
+        expect(applicationContext.getResource(isA(String.class))).andReturn(resource).anyTimes();
+        expect(request.getAvailableScopes()).andReturn(Arrays.asList("request", "session", "application"));
+        expect(request.getContext("request")).andReturn(context);
+        expect(request.getContext("session")).andReturn(Collections.<String,Object>emptyMap());
+        expect(request.getContext("application")).andReturn(Collections.<String,Object>emptyMap());
+        expect(request.getWriter()).andReturn(writer);
+        writer.write("test template with test value");
+        writer.flush();
+
+        replay(request, applicationContext, writer);
+        Renderer renderer = new MustacheRenderer();
+        renderer.render("/test.html", request);
+        verify(request, applicationContext, writer);
+    }
+
+    /**
+     * Tests {@link MustacheRenderer#render(String, org.apache.tiles.request.Request)}.
+     * @throws IOException If something goes wrong.
+     */
+    @Test(expected = CannotRenderException.class)
+    public void testRenderException() throws IOException {
+        ServletRequest request = createMock(ServletRequest.class);
+        replay(request);
+        Renderer renderer = new MustacheRenderer();
+        try {
+            renderer.render(null, request);
+        } finally {
+            verify(request);
+        }
+    }
+
+    /**
+     * Test method for
+     * {@link MustacheRenderer#isRenderable(String, org.apache.tiles.request.Request)}
+     * .
+     */
+    @Test
+    public void testIsRenderable() {
+        MustacheRenderer renderer = new MustacheRenderer();
+        final Pattern pattern = Pattern.compile("/.*");
+        renderer.setAcceptPattern(pattern);
+
+        assertTrue(renderer.isRenderable("/my/template.html", null));
+        assertTrue(renderer.isRenderable("/my/template.any", null));
+        assertFalse(renderer.isRenderable("my/template.html", null));
+        assertFalse(renderer.isRenderable(null, null));
+    }
+}
diff --git a/tiles-request-mustache/src/test/resources/test.html b/tiles-request-mustache/src/test/resources/test.html
new file mode 100644
index 0000000..3de397c
--- /dev/null
+++ b/tiles-request-mustache/src/test/resources/test.html
@@ -0,0 +1 @@
+test template with {{testKey}}
\ No newline at end of file