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