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