    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.internal.services.AssetDispatcher;
    017import org.apache.tapestry5.internal.services.RequestConstants;
    018import org.apache.tapestry5.internal.services.ResourceStreamer;
    019import org.apache.tapestry5.ioc.IOOperation;
    020import org.apache.tapestry5.ioc.OperationTracker;
    021import org.apache.tapestry5.ioc.Resource;
    022import org.apache.tapestry5.ioc.annotations.Symbol;
    023import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    024import org.apache.tapestry5.services.Dispatcher;
    025import org.apache.tapestry5.services.LocalizationSetter;
    026import org.apache.tapestry5.services.PathConstructor;
    027import org.apache.tapestry5.services.Request;
    028import org.apache.tapestry5.services.Response;
    029import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
    030import org.apache.tapestry5.services.javascript.ModuleManager;
    031
    032import javax.servlet.http.HttpServletResponse;
    033import java.io.IOException;
    034import java.util.EnumSet;
    035import java.util.List;
    036import java.util.Locale;
    037import java.util.Map;
    038import java.util.Set;
    039
    040/**
    041 * Handler contributed to {@link AssetDispatcher} with key "modules". It interprets the extra path as a module name,
    042 * and searches for the corresponding JavaScript module.  Unlike normal assets, modules do not include any kind of checksum
    043 * in the URL, and do not set a far-future expires header.
    044 *
    045 * @see ModuleManager
    046 */
    047public class ModuleDispatcher implements Dispatcher
    048{
    049    private final ModuleManager moduleManager;
    050
    051    private final ResourceStreamer streamer;
    052
    053    private final OperationTracker tracker;
    054
    055    private final JavaScriptStackSource javaScriptStackSource;
    056
    057    private final JavaScriptStackPathConstructor javaScriptStackPathConstructor;
    058
    059    private final LocalizationSetter localizationSetter;
    060
    061    private final String requestPrefix;
    062
    063    private final String stackPathPrefix;
    064
    065    private final boolean compress;
    066
    067    private final Set<ResourceStreamer.Options> omitExpiration = EnumSet.of(ResourceStreamer.Options.OMIT_EXPIRATION);
    068
    069    private Map<String, String> moduleNameToStackName;
    070
    071    public ModuleDispatcher(ModuleManager moduleManager,
    072                            ResourceStreamer streamer,
    073                            OperationTracker tracker,
    074                            PathConstructor pathConstructor,
    075                            JavaScriptStackSource javaScriptStackSource,
    076                            JavaScriptStackPathConstructor javaScriptStackPathConstructor,
    077                            LocalizationSetter localizationSetter,
    078                            String prefix,
    079                            @Symbol(SymbolConstants.ASSET_PATH_PREFIX)
    080                            String assetPrefix,
    081                            boolean compress)
    082    {
    083        this.moduleManager = moduleManager;
    084        this.streamer = streamer;
    085        this.tracker = tracker;
    086        this.javaScriptStackSource = javaScriptStackSource;
    087        this.javaScriptStackPathConstructor = javaScriptStackPathConstructor;
    088        this.localizationSetter = localizationSetter;
    089        this.compress = compress;
    090
    091        requestPrefix = pathConstructor.constructDispatchPath(compress ? prefix + ".gz" : prefix) + "/";
    092        stackPathPrefix = pathConstructor.constructClientPath(assetPrefix, RequestConstants.STACK_FOLDER) + "/";
    093    }
    094
    095    public boolean dispatch(Request request, Response response) throws IOException
    096    {
    097        String path = request.getPath();
    098
    099        if (path.startsWith(requestPrefix))
    100        {
    101            String extraPath = path.substring(requestPrefix.length());
    102
    103            Locale locale = request.getLocale();
    104
    105            if (!handleModuleRequest(locale, extraPath, response))
    106            {
    107                response.sendError(HttpServletResponse.SC_NOT_FOUND, String.format("No module for path '%s'.", extraPath));
    108            }
    109
    110            return true;
    111        }
    112
    113        return false;
    114
    115    }
    116
    117    private boolean handleModuleRequest(Locale locale, String extraPath, Response response) throws IOException
    118    {
    119        // Ensure request ends with '.js'.  That's the extension tacked on by RequireJS because it expects there
    120        // to be a hierarchy of static JavaScript files here. In reality, we may be cross-compiling CoffeeScript to
    121        // JavaScript, or generating modules on-the-fly, or exposing arbitrary Resources from somewhere on the classpath
    122        // as a module.
    123
    124        int dotx = extraPath.lastIndexOf('.');
    125
    126        if (dotx < 0)
    127        {
    128            return false;
    129        }
    130
    131        if (!extraPath.substring(dotx + 1).equals("js"))
    132        {
    133            return false;
    134        }
    135
    136        final String moduleName = extraPath.substring(0, dotx);
    137
    138        String stackName = findStackForModule(moduleName);
    139
    140        if (stackName != null)
    141        {
    142            localizationSetter.setNonPersistentLocaleFromLocaleName(locale.toString());
    143            List<String> libraryUrls = javaScriptStackPathConstructor.constructPathsForJavaScriptStack(stackName);
    144            if (libraryUrls.size() == 1)
    145            {
    146                String firstUrl = libraryUrls.get(0);
    147                if (firstUrl.startsWith(stackPathPrefix))
    148                {
    149                    response.sendRedirect(firstUrl);
    150                    return true;
    151                }
    152            }
    153        }
    154
    155        return tracker.perform(String.format("Streaming %s %s",
    156                compress ? "compressed module" : "module",
    157                moduleName), new IOOperation<Boolean>()
    158        {
    159            public Boolean perform() throws IOException
    160            {
    161                Resource resource = moduleManager.findResourceForModule(moduleName);
    162
    163                if (resource != null)
    164                {
    165                    // Slightly hacky way of informing the streamer whether to supply the
    166                    // compressed or default stream. May need to iterate the API on this a bit.
    167                    return streamer.streamResource(resource, compress ? "z" : "", omitExpiration);
    168                }
    169
    170                return false;
    171            }
    172        });
    173    }
    174
    175    private String findStackForModule(String moduleName)
    176    {
    177        return getModuleNameToStackName().get(moduleName);
    178    }
    179
    180    private Map<String, String> getModuleNameToStackName()
    181    {
    182
    183        if (moduleNameToStackName == null)
    184        {
    185            moduleNameToStackName = CollectionFactory.newMap();
    186
    187            for (String stackName : javaScriptStackSource.getStackNames())
    188            {
    189                for (String moduleName : javaScriptStackSource.getStack(stackName).getModules())
    190                {
    191                    moduleNameToStackName.put(moduleName, stackName);
    192                }
    193            }
    194        }
    195
    196        return moduleNameToStackName;
    197    }
    198}