    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}