001// Copyright 2014 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.services.javascript;
016
017import java.io.ByteArrayInputStream;
018import java.io.IOException;
019import java.io.InputStream;
020import java.io.SequenceInputStream;
021import java.net.URL;
022import java.util.LinkedHashMap;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Vector;
026
027import org.apache.tapestry5.func.F;
028import org.apache.tapestry5.func.Flow;
029import org.apache.tapestry5.func.Mapper;
030import org.apache.tapestry5.func.Predicate;
031import org.apache.tapestry5.internal.util.VirtualResource;
032import org.apache.tapestry5.ioc.Resource;
033import org.apache.tapestry5.ioc.internal.util.InternalUtils;
034
035/**
036 * Used to wrap plain JavaScript libraries as AMD modules. The underlying
037 * resource is transformed before it is sent to the client.
038 * <p>
039 * This is an alternative to configuring RequireJS module shims for the
040 * libraries. As opposed to shimmed libraries, the modules created using the
041 * AMDWrapper can be added to JavaScript stacks.
042 * <p>
043 * If the library depends on global variables, these can be added as module
044 * dependencies. For a library that expects jQuery to be available as
045 * <code>$<code>, the wrapper should be setup calling <code>require("jQuery", "$")<code>
046 * on the respective wrapper.
047 *
048 * @since 5.4
049 * @see JavaScriptModuleConfiguration
050 * @see ModuleManager
051 */
052public class AMDWrapper {
053
054    /**
055     * The underlying resource, usually a JavaScript library
056     */
057    private final Resource resource;
058
059    /**
060     * The modules that this module requires, the keys being module names and
061     * the values being the respective parameter names for the module's factory
062     * function.
063     */
064    private final Map<String, String> requireConfig = new LinkedHashMap<String, String>();
065
066    /**
067     * The expression that determines what is returned from the factory function
068     */
069    private String returnExpression;
070
071    public AMDWrapper(final Resource resource) {
072        this.resource = resource;
073    }
074
075    /**
076     * Add a dependency on another module. The module will be passed into the
077     * generated factory function as a parameter.
078     *
079     * @param moduleName
080     *            the name of the required module, e.g. <code>jQuery</code>
081     * @param parameterName
082     *            the module's corresponding parameter name of the factory
083     *            function, e.g. <code>$</code>
084     * @return this AMDWrapper for further configuration
085     */
086    public AMDWrapper require(final String moduleName,
087            final String parameterName) {
088        requireConfig.put(moduleName, parameterName);
089        return this;
090    }
091
092    /**
093     * Add a dependency on another module. The module will be loaded but not
094     * passed to the factory function. This is useful for dependencies on other
095     * modules that do not actually return a value.
096     *
097     * @param moduleName
098     *            the name of the required module, e.g.
099     *            <code>bootstrap/transition</code>
100     * @return this AMDWrapper for further configuration
101     */
102    public AMDWrapper require(final String moduleName) {
103        requireConfig.put(moduleName, null);
104        return this;
105    }
106
107    /**
108     * Optionally sets a return expression for this module. If the underlying
109     * library creates a global variable, this is usually what is returned here.
110     *
111     * @param returnExpression
112     *            the expression that is returned from this module (e.g.
113     *            <code>Raphael</code>)
114     * @return this AMDWrapper for further configuration
115     */
116    public AMDWrapper setReturnExpression(final String returnExpression) {
117        this.returnExpression = returnExpression;
118        return this;
119    }
120
121    /**
122     * Return this wrapper instance as a {@link JavaScriptModuleConfiguration},
123     * so it can be contributed to the {@link ModuleManager}'s configuration.
124     * The resulting {@link JavaScriptModuleConfiguration} should not be
125     * changed.
126     *
127     * @return a {@link JavaScriptModuleConfiguration} for this AMD wrapper
128     */
129    public JavaScriptModuleConfiguration asJavaScriptModuleConfiguration() {
130        return new JavaScriptModuleConfiguration(transformResource());
131    }
132
133    private Resource transformResource() {
134        return new AMDModuleWrapperResource(resource, requireConfig,
135                returnExpression);
136    }
137
138    /**
139     * A virtual resource that wraps a plain JavaScript library as an AMD
140     * module.
141     *
142     */
143    private final static class AMDModuleWrapperResource extends VirtualResource {
144        private final Resource resource;
145        private final Map<String, String> requireConfig;
146        private final String returnExpression;
147
148        public AMDModuleWrapperResource(final Resource resource,
149                final Map<String, String> requireConfig,
150                final String returnExpression) {
151            this.resource = resource;
152            this.requireConfig = requireConfig;
153            this.returnExpression = returnExpression;
154
155        }
156
157        @Override
158        public InputStream openStream() throws IOException {
159            InputStream leaderStream;
160            InputStream trailerStream;
161
162            StringBuilder sb = new StringBuilder();
163
164            // create a Flow of map entries (module name to factory function
165            // parameter name)
166            Flow<Entry<String, String>> requiredModulesToNames = F
167                    .flow(requireConfig.entrySet());
168
169            // some of the modules are not passed to the factory, sort them last
170            Flow<Entry<String, String>> requiredModulesToNamesNamedFirst = requiredModulesToNames
171                    .remove(VALUE_IS_NULL).concat(
172                            requiredModulesToNames.filter(VALUE_IS_NULL));
173
174            sb.append("define([");
175            sb.append(InternalUtils.join(requiredModulesToNamesNamedFirst
176                    .map(GET_KEY).map(QUOTE).toList()));
177            sb.append("], function(");
178
179            // append only the modules that should be passed to the factory
180            // function, i.e. those whose map entry value is not null
181            sb.append(InternalUtils.join(F.flow(requireConfig.values())
182                    .filter(F.notNull()).toList()));
183            sb.append("){\n");
184            leaderStream = toInputStream(sb);
185            sb.setLength(0);
186
187            if (returnExpression != null)
188            {
189                sb.append("\nreturn ");
190                sb.append(returnExpression);
191                sb.append(";");
192            }
193            sb.append("\n});");
194            trailerStream = toInputStream(sb);
195
196            Vector<InputStream> v = new Vector<InputStream>(3);
197            v.add(leaderStream);
198            v.add(resource.openStream());
199            v.add(trailerStream);
200
201            return new SequenceInputStream(v.elements());
202        }
203
204        @Override
205        public String getFile() {
206            return "generated-module-for-" + resource.getFile();
207        }
208
209        @Override
210        public URL toURL() {
211            return null;
212        }
213
214        @Override
215        public String toString() {
216            return "AMD module wrapper for " + resource.toString();
217        }
218
219        private static InputStream toInputStream(final StringBuilder sb) {
220            return new ByteArrayInputStream(sb.toString().getBytes(UTF8));
221
222        }
223    }
224
225    private final static Mapper<Entry<String, String>, String> GET_KEY = new Mapper<Entry<String, String>, String>() {
226
227        @Override
228        public String map(final Entry<String, String> element) {
229            return element.getKey();
230        }
231
232    };
233
234    private final static Predicate<Entry<String, String>> VALUE_IS_NULL = new Predicate<Entry<String, String>>() {
235
236        @Override
237        public boolean accept(final Entry<String, String> element) {
238            return element.getValue() == null;
239        }
240
241    };
242
243    private final static Mapper<String, String> QUOTE = new Mapper<String, String>() {
244
245        @Override
246        public String map(final String element) {
247            StringBuilder sb = new StringBuilder(element.length() + 2);
248            sb.append('"');
249            sb.append(element);
250            sb.append('"');
251            return sb.toString();
252        }
253    };
254
255}