    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.internal.services.javascript;
    014
    015import org.apache.tapestry5.SymbolConstants;
    016import org.apache.tapestry5.commons.Messages;
    017import org.apache.tapestry5.commons.Resource;
    018import org.apache.tapestry5.commons.util.CollectionFactory;
    019import org.apache.tapestry5.dom.Element;
    020import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
    021import org.apache.tapestry5.http.services.ResponseCompressionAnalyzer;
    022import org.apache.tapestry5.internal.InternalConstants;
    023import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
    024import org.apache.tapestry5.ioc.annotations.PostInjection;
    025import org.apache.tapestry5.ioc.annotations.Symbol;
    026import org.apache.tapestry5.json.JSONArray;
    027import org.apache.tapestry5.json.JSONLiteral;
    028import org.apache.tapestry5.json.JSONObject;
    029import org.apache.tapestry5.services.AssetSource;
    030import org.apache.tapestry5.services.PathConstructor;
    031import org.apache.tapestry5.services.assets.StreamableResourceSource;
    032import org.apache.tapestry5.services.javascript.JavaScriptModuleConfiguration;
    033import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback;
    034import org.apache.tapestry5.services.javascript.ModuleManager;
    035
    036import java.util.List;
    037import java.util.Map;
    038import java.util.Set;
    039
    040public class ModuleManagerImpl implements ModuleManager
    041{
    042
    043    private final ResponseCompressionAnalyzer compressionAnalyzer;
    044
    045    private final Messages globalMessages;
    046
    047    private final boolean compactJSON;
    048
    049    private final Map<String, Resource> shimModuleNameToResource = CollectionFactory.newMap();
    050
    051    private final Resource classpathRoot;
    052
    053    private final Set<String> extensions;
    054
    055    // Note: ConcurrentHashMap does not support null as a value, alas. We use classpathRoot as a null.
    056    private final Map<String, Resource> cache = CollectionFactory.newConcurrentMap();
    057
    058    private final JSONObject baseConfig;
    059
    060    private final String basePath, compressedBasePath;
    061
    062    public ModuleManagerImpl(ResponseCompressionAnalyzer compressionAnalyzer,
    063                             AssetSource assetSource,
    064                             Map<String, JavaScriptModuleConfiguration> configuration,
    065                             Messages globalMessages,
    066                             StreamableResourceSource streamableResourceSource,
    067                             @Symbol(SymbolConstants.COMPACT_JSON)
    068                             boolean compactJSON,
    069                             @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
    070                             boolean productionMode,
    071                             @Symbol(SymbolConstants.MODULE_PATH_PREFIX)
    072                             String modulePathPrefix,
    073                             PathConstructor pathConstructor)
    074    {
    075        this.compressionAnalyzer = compressionAnalyzer;
    076        this.globalMessages = globalMessages;
    077        this.compactJSON = compactJSON;
    078
    079        basePath = pathConstructor.constructClientPath(modulePathPrefix);
    080        compressedBasePath = pathConstructor.constructClientPath(modulePathPrefix + ".gz");
    081
    082        classpathRoot = assetSource.resourceForPath("");
    083        extensions = CollectionFactory.newSet("js");
    084
    085        extensions.addAll(streamableResourceSource.fileExtensionsForContentType(InternalConstants.JAVASCRIPT_CONTENT_TYPE));
    086
    087        baseConfig = buildBaseConfig(configuration, !productionMode);
    088    }
    089
    090    private String buildRequireJSConfig(List<ModuleConfigurationCallback> callbacks)
    091    {
    092        // This is the part that can vary from one request to another, based on the capabilities of the client.
    093        JSONObject config = baseConfig.copy().put("baseUrl", getBaseURL());
    094
    095        // TAP5-2196: allow changes to the configuration in a per-request basis.
    096        for (ModuleConfigurationCallback callback : callbacks)
    097        {
    098            config = callback.configure(config);
    099            assert config != null;
    100        }
    101
    102        // This part gets written out before any libraries are loaded (including RequireJS).
    103        return String.format("var require = %s;\n", config.toString(compactJSON));
    104    }
    105
    106    private JSONObject buildBaseConfig(Map<String, JavaScriptModuleConfiguration> configuration, boolean devMode)
    107    {
    108        JSONObject config = new JSONObject();
    109
    110        // In DevMode, wait up to five minutes for a script, as the developer may be using the debugger.
    111        if (devMode)
    112        {
    113            config.put("waitSeconds", 300);
    114        }
    115
    116        for (String name : configuration.keySet())
    117        {
    118            JavaScriptModuleConfiguration module = configuration.get(name);
    119
    120            shimModuleNameToResource.put(name, module.resource);
    121
    122            // Some modules (particularly overrides) just need an alternate location for their content
    123            // on the server.
    124            if (module.getNeedsConfiguration())
    125            {
    126                // Others are libraries being shimmed as AMD modules, and need some configuration
    127                // to ensure that everything hooks up properly on the client.
    128                addModuleToConfig(config, name, module);
    129            }
    130        }
    131        return config;
    132    }
    133
    134    private String getBaseURL()
    135    {
    136        return compressionAnalyzer.isGZipSupported() ? compressedBasePath : basePath;
    137    }
    138
    139    private void addModuleToConfig(JSONObject config, String name, JavaScriptModuleConfiguration module)
    140    {
    141        JSONObject shimConfig = config.in("shim");
    142
    143        boolean nestDependencies = false;
    144
    145        String exports = module.getExports();
    146
    147        if (exports != null)
    148        {
    149            shimConfig.in(name).put("exports", exports);
    150            nestDependencies = true;
    151        }
    152
    153        String initExpression = module.getInitExpression();
    154
    155        if (initExpression != null)
    156        {
    157            String function = String.format("function() { return %s; }", initExpression);
    158            shimConfig.in(name).put("init", new JSONLiteral(function));
    159            nestDependencies = true;
    160        }
    161
    162        List<String> dependencies = module.getDependencies();
    163
    164        if (dependencies != null)
    165        {
    166            JSONObject container = nestDependencies ? shimConfig.in(name) : shimConfig;
    167            String key = nestDependencies ? "deps" : name;
    168
    169            for (String dep : dependencies)
    170            {
    171                container.append(key, dep);
    172            }
    173        }
    174    }
    175
    176    @PostInjection
    177    public void setupInvalidation(ResourceChangeTracker tracker)
    178    {
    179        tracker.clearOnInvalidation(cache);
    180    }
    181
    182    public void writeConfiguration(Element body,
    183                                   List<ModuleConfigurationCallback> callbacks)
    184    {
    185        Element element = body.element("script", "type", "text/javascript");
    186
    187        // Build it each time because we don't know if the client supports GZip or not, and
    188        // (in development mode) URLs for some referenced assets could change (due to URLs
    189        // containing a checksum on the resource content).
    190        element.raw(buildRequireJSConfig(callbacks));
    191    }
    192
    193    public void writeInitialization(Element body, List<String> libraryURLs, List<?> inits)
    194    {
    195
    196        Element element = body.element("script", "type", "text/javascript");
    197
    198        element.raw(globalMessages.format("private-core-page-initialization-template",
    199                convert(libraryURLs),
    200                convert(inits)));
    201    }
    202
    203    private String convert(List<?> input)
    204    {
    205        return new JSONArray().putAll(input).toString(compactJSON);
    206    }
    207
    208    public Resource findResourceForModule(String moduleName)
    209    {
    210        Resource resource = cache.get(moduleName);
    211
    212        if (resource == null)
    213        {
    214            resource = resolveModuleNameToResource(moduleName);
    215            cache.put(moduleName, resource);
    216        }
    217
    218        // We're treating classpathRoot as a placeholder for null.
    219
    220        return resource == classpathRoot ? null : resource;
    221    }
    222
    223    private Resource resolveModuleNameToResource(String moduleName)
    224    {
    225        Resource resource = shimModuleNameToResource.get(moduleName);
    226
    227        if (resource != null)
    228        {
    229            return resource;
    230        }
    231
    232        // Tack on a fake extension; otherwise modules whose name includes a '.' get mangled
    233        // by Resource.withExtension().
    234        String baseName = String.format("/META-INF/modules/%s.EXT", moduleName);
    235
    236        Resource baseResource = classpathRoot.forFile(baseName);
    237
    238        for (String extension : extensions)
    239        {
    240            resource = baseResource.withExtension(extension);
    241
    242            if (resource.exists())
    243            {
    244                return resource;
    245            }
    246        }
    247
    248        // Return placeholder for null:
    249        return classpathRoot;
    250    }
    251}