    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.commons.Resource;
    016import org.apache.tapestry5.func.F;
    017import org.apache.tapestry5.func.Flow;
    018import org.apache.tapestry5.func.Mapper;
    019import org.apache.tapestry5.func.Predicate;
    020import org.apache.tapestry5.internal.util.VirtualResource;
    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}