You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by jk...@apache.org on 2014/06/18 17:12:52 UTC

git commit: TAP5-2350: add an AMDWrapper class that can be used to wrap plain JavaScript libraries as AMD modules

Repository: tapestry-5
Updated Branches:
  refs/heads/master 3ac532438 -> a08fd3bd1


TAP5-2350: add an AMDWrapper class that can be used to wrap plain JavaScript libraries as AMD modules


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/a08fd3bd
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/a08fd3bd
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/a08fd3bd

Branch: refs/heads/master
Commit: a08fd3bd19dfece2b818a0746af828f091277265
Parents: 3ac5324
Author: Jochen Kemnade <jo...@eddyson.de>
Authored: Wed Jun 18 16:59:47 2014 +0200
Committer: Jochen Kemnade <jo...@eddyson.de>
Committed: Wed Jun 18 17:12:35 2014 +0200

----------------------------------------------------------------------
 .../services/javascript/AMDWrapper.java         | 255 +++++++++++++++++++
 .../JavaScriptModuleConfiguration.java          |   1 +
 .../services/javascript/ModuleManager.java      |   1 +
 .../services/javascript/AMDWrapperSpec.groovy   |  65 +++++
 4 files changed, 322 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a08fd3bd/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AMDWrapper.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AMDWrapper.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AMDWrapper.java
new file mode 100644
index 0000000..3783a65
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AMDWrapper.java
@@ -0,0 +1,255 @@
+// Copyright 2014 The 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.tapestry5.services.javascript;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.SequenceInputStream;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Vector;
+
+import org.apache.tapestry5.func.F;
+import org.apache.tapestry5.func.Flow;
+import org.apache.tapestry5.func.Mapper;
+import org.apache.tapestry5.func.Predicate;
+import org.apache.tapestry5.internal.util.VirtualResource;
+import org.apache.tapestry5.ioc.Resource;
+import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+
+/**
+ * Used to wrap plain JavaScript libraries as AMD modules. The underlying
+ * resource is transformed before it is sent to the client.
+ * <p>
+ * This is an alternative to configuring RequireJS module shims for the
+ * libraries. As opposed to shimmed libraries, the modules created using the
+ * AMDWrapper can be added to JavaScript stacks.
+ * <p>
+ * If the library depends on global variables, these can be added as module
+ * dependencies. For a library that expects jQuery to be available as
+ * <code>$<code>, the wrapper should be setup calling <code>require("jQuery", "$")<code>
+ * on the respective wrapper.
+ *
+ * @since 5.4
+ * @see JavaScriptModuleConfiguration
+ * @see ModuleManager
+ */
+public class AMDWrapper {
+
+    /**
+     * The underlying resource, usually a JavaScript library
+     */
+    private final Resource resource;
+
+    /**
+     * The modules that this module requires, the keys being module names and
+     * the values being the respective parameter names for the module's factory
+     * function.
+     */
+    private final Map<String, String> requireConfig = new LinkedHashMap<String, String>();
+
+    /**
+     * The expression that determines what is returned from the factory function
+     */
+    private String returnExpression;
+
+    public AMDWrapper(final Resource resource) {
+        this.resource = resource;
+    }
+
+    /**
+     * Add a dependency on another module. The module will be passed into the
+     * generated factory function as a parameter.
+     *
+     * @param moduleName
+     *            the name of the required module, e.g. <code>jQuery</code>
+     * @param parameterName
+     *            the module's corresponding parameter name of the factory
+     *            function, e.g. <code>$</code>
+     * @return this AMDWrapper for further configuration
+     */
+    public AMDWrapper require(final String moduleName,
+            final String parameterName) {
+        requireConfig.put(moduleName, parameterName);
+        return this;
+    }
+
+    /**
+     * Add a dependency on another module. The module will be loaded but not
+     * passed to the factory function. This is useful for dependencies on other
+     * modules that do not actually return a value.
+     *
+     * @param moduleName
+     *            the name of the required module, e.g.
+     *            <code>bootstrap/transition</code>
+     * @return this AMDWrapper for further configuration
+     */
+    public AMDWrapper require(final String moduleName) {
+        requireConfig.put(moduleName, null);
+        return this;
+    }
+
+    /**
+     * Optionally sets a return expression for this module. If the underlying
+     * library creates a global variable, this is usually what is returned here.
+     *
+     * @param returnExpression
+     *            the expression that is returned from this module (e.g.
+     *            <code>Raphael</code>)
+     * @return this AMDWrapper for further configuration
+     */
+    public AMDWrapper setReturnExpression(final String returnExpression) {
+        this.returnExpression = returnExpression;
+        return this;
+    }
+
+    /**
+     * Return this wrapper instance as a {@link JavaScriptModuleConfiguration},
+     * so it can be contributed to the {@link ModuleManager}'s configuration.
+     * The resulting {@link JavaScriptModuleConfiguration} should not be
+     * changed.
+     *
+     * @return a {@link JavaScriptModuleConfiguration} for this AMD wrapper
+     */
+    public JavaScriptModuleConfiguration asJavaScriptModuleConfiguration() {
+        return new JavaScriptModuleConfiguration(transformResource());
+    }
+
+    private Resource transformResource() {
+        return new AMDModuleWrapperResource(resource, requireConfig,
+                returnExpression);
+    }
+
+    /**
+     * A virtual resource that wraps a plain JavaScript library as an AMD
+     * module.
+     *
+     */
+    private final static class AMDModuleWrapperResource extends VirtualResource {
+        private final Resource resource;
+        private final Map<String, String> requireConfig;
+        private final String returnExpression;
+
+        public AMDModuleWrapperResource(final Resource resource,
+                final Map<String, String> requireConfig,
+                final String returnExpression) {
+            this.resource = resource;
+            this.requireConfig = requireConfig;
+            this.returnExpression = returnExpression;
+
+        }
+
+        @Override
+        public InputStream openStream() throws IOException {
+            InputStream leaderStream;
+            InputStream trailerStream;
+
+            StringBuilder sb = new StringBuilder();
+
+            // create a Flow of map entries (module name to factory function
+            // parameter name)
+            Flow<Entry<String, String>> requiredModulesToNames = F
+                    .flow(requireConfig.entrySet());
+
+            // some of the modules are not passed to the factory, sort them last
+            Flow<Entry<String, String>> requiredModulesToNamesNamedFirst = requiredModulesToNames
+                    .remove(VALUE_IS_NULL).concat(
+                            requiredModulesToNames.filter(VALUE_IS_NULL));
+
+            sb.append("define([");
+            sb.append(InternalUtils.join(requiredModulesToNamesNamedFirst
+                    .map(GET_KEY).map(QUOTE).toList()));
+            sb.append("], function(");
+
+            // append only the modules that should be passed to the factory
+            // function, i.e. those whose map entry value is not null
+            sb.append(InternalUtils.join(F.flow(requireConfig.values())
+                    .filter(F.notNull()).toList()));
+            sb.append("){\n");
+            leaderStream = toInputStream(sb);
+            sb.setLength(0);
+
+            if (returnExpression != null)
+            {
+                sb.append("\nreturn ");
+                sb.append(returnExpression);
+                sb.append(";");
+            }
+            sb.append("\n});");
+            trailerStream = toInputStream(sb);
+
+            Vector<InputStream> v = new Vector<InputStream>(3);
+            v.add(leaderStream);
+            v.add(resource.openStream());
+            v.add(trailerStream);
+
+            return new SequenceInputStream(v.elements());
+        }
+
+        @Override
+        public String getFile() {
+            return "generated-module-for-" + resource.getFile();
+        }
+
+        @Override
+        public URL toURL() {
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            return "AMD module wrapper for " + resource.toString();
+        }
+
+        private static InputStream toInputStream(final StringBuilder sb) {
+            return new ByteArrayInputStream(sb.toString().getBytes(UTF8));
+
+        }
+    }
+
+    private final static Mapper<Entry<String, String>, String> GET_KEY = new Mapper<Entry<String, String>, String>() {
+
+        @Override
+        public String map(final Entry<String, String> element) {
+            return element.getKey();
+        }
+
+    };
+
+    private final static Predicate<Entry<String, String>> VALUE_IS_NULL = new Predicate<Entry<String, String>>() {
+
+        @Override
+        public boolean accept(final Entry<String, String> element) {
+            return element.getValue() == null;
+        }
+
+    };
+
+    private final static Mapper<String, String> QUOTE = new Mapper<String, String>() {
+
+        @Override
+        public String map(final String element) {
+            StringBuilder sb = new StringBuilder(element.length() + 2);
+            sb.append('"');
+            sb.append(element);
+            sb.append('"');
+            return sb.toString();
+        }
+    };
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a08fd3bd/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptModuleConfiguration.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptModuleConfiguration.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptModuleConfiguration.java
index f719b87..2fb1478 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptModuleConfiguration.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptModuleConfiguration.java
@@ -33,6 +33,7 @@ import java.util.List;
  * module will be satisfied by the resource.'
  *
  * @since 5.4
+ * @see AMDWrapper
  */
 public final class JavaScriptModuleConfiguration
 {

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a08fd3bd/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleManager.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleManager.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleManager.java
index f7d5487..5496133 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleManager.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleManager.java
@@ -29,6 +29,7 @@ import java.util.List;
  *
  * @since 5.4
  * @see ModuleConfigurationCallback
+ * @see AMDWrapper
  */
 @UsesMappedConfiguration(JavaScriptModuleConfiguration.class)
 public interface ModuleManager

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a08fd3bd/tapestry-core/src/test/groovy/org/apache/tapestry5/services/javascript/AMDWrapperSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-core/src/test/groovy/org/apache/tapestry5/services/javascript/AMDWrapperSpec.groovy b/tapestry-core/src/test/groovy/org/apache/tapestry5/services/javascript/AMDWrapperSpec.groovy
new file mode 100644
index 0000000..802f932
--- /dev/null
+++ b/tapestry-core/src/test/groovy/org/apache/tapestry5/services/javascript/AMDWrapperSpec.groovy
@@ -0,0 +1,65 @@
+package org.apache.tapestry5.services.javascript
+
+import org.apache.tapestry5.ioc.Resource
+
+import spock.lang.Specification
+
+class AMDWrapperSpec extends Specification {
+
+    def "AMD wrapper without dependencies"(){
+        setup:
+        Resource resource = Mock()
+        when:
+        def wrapper = new AMDWrapper(resource)
+        def moduleConfiguration = wrapper.asJavaScriptModuleConfiguration()
+        then:
+        !moduleConfiguration.needsConfiguration
+        when:
+        def amdModuleContent = moduleConfiguration.resource.openStream().text
+        then:
+        amdModuleContent ==
+                """define([], function(){
+alert('Hello World!');
+});"""
+        1 * resource.openStream() >> new ByteArrayInputStream("alert('Hello World!');".bytes)
+    }
+
+    def "AMD wrapper with named dependencies"(){
+        setup:
+        Resource resource = Mock()
+        when:
+        def wrapper = new AMDWrapper(resource)
+        wrapper.require("jquery", '$')
+        def moduleConfiguration = wrapper.asJavaScriptModuleConfiguration()
+        then:
+        !moduleConfiguration.needsConfiguration
+        when:
+        def amdModuleContent = moduleConfiguration.resource.openStream().text
+        then:
+        amdModuleContent ==
+                '''define(["jquery"], function($){
+$("body").css("background-color", "pink");
+});'''
+        1 * resource.openStream() >> new ByteArrayInputStream('$("body").css("background-color", "pink");'.bytes)
+    }
+
+    def "AMD wrapper with return expression"(){
+        setup:
+        Resource resource = Mock()
+        when:
+        def wrapper = new AMDWrapper(resource)
+        wrapper.setReturnExpression("myImportantVar")
+        def moduleConfiguration = wrapper.asJavaScriptModuleConfiguration()
+        then:
+        !moduleConfiguration.needsConfiguration
+        when:
+        def amdModuleContent = moduleConfiguration.resource.openStream().text
+        then:
+        amdModuleContent ==
+                '''define([], function(){
+var myImportantVar = 42;
+return myImportantVar;
+});'''
+        1 * resource.openStream() >> new ByteArrayInputStream('var myImportantVar = 42;'.bytes)
+    }
+}