001// Copyright 2012, 2013 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.internal.services.javascript;
016
017import org.apache.tapestry5.internal.services.AssetDispatcher;
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.services.Dispatcher;
023import org.apache.tapestry5.services.PathConstructor;
024import org.apache.tapestry5.services.Request;
025import org.apache.tapestry5.services.Response;
026import org.apache.tapestry5.services.javascript.ModuleManager;
027
028import java.io.IOException;
029import java.util.EnumSet;
030import java.util.Set;
031
032/**
033 * Handler contributed to {@link AssetDispatcher} with key "modules". It interprets the extra path as a module name,
034 * and searches for the corresponding JavaScript module.  Unlike normal assets, modules do not include any kind of checksum
035 * in the URL, and do not set a far-future expires header.
036 *
037 * @see ModuleManager
038 */
039public class ModuleDispatcher implements Dispatcher
040{
041    private final ModuleManager moduleManager;
042
043    private final ResourceStreamer streamer;
044
045    private final OperationTracker tracker;
046
047    private final String requestPrefix;
048
049    private final boolean compress;
050
051    private final Set<ResourceStreamer.Options> omitExpiration = EnumSet.of(ResourceStreamer.Options.OMIT_EXPIRATION);
052
053    public ModuleDispatcher(ModuleManager moduleManager,
054                            ResourceStreamer streamer,
055                            OperationTracker tracker,
056                            PathConstructor pathConstructor,
057                            String prefix,
058                            boolean compress)
059    {
060        this.moduleManager = moduleManager;
061        this.streamer = streamer;
062        this.tracker = tracker;
063        this.compress = compress;
064
065        requestPrefix = pathConstructor.constructDispatchPath(compress ? prefix + ".gz" : prefix) + "/";
066    }
067
068    public boolean dispatch(Request request, Response response) throws IOException
069    {
070        String path = request.getPath();
071
072        return path.startsWith(requestPrefix) &&
073                handleModuleRequest(path.substring(requestPrefix.length()));
074
075    }
076
077    private boolean handleModuleRequest(String extraPath) throws IOException
078    {
079        // Ensure request ends with '.js'.  That's the extension tacked on by RequireJS because it expects there
080        // to be a hierarchy of static JavaScript files here. In reality, we may be cross-compiling CoffeeScript to
081        // JavaScript, or generating modules on-the-fly, or exposing arbitrary Resources from somewhere on the classpath
082        // as a module.
083
084        int dotx = extraPath.lastIndexOf('.');
085
086        if (dotx < 0)
087        {
088            return false;
089        }
090
091        if (!extraPath.substring(dotx + 1).equals("js"))
092        {
093            return false;
094        }
095
096        final String moduleName = extraPath.substring(0, dotx);
097
098        return tracker.perform(String.format("Streaming %s %s",
099                compress ? "compressed module" : "module",
100                moduleName), new IOOperation<Boolean>()
101        {
102            public Boolean perform() throws IOException
103            {
104                Resource resource = moduleManager.findResourceForModule(moduleName);
105
106                if (resource != null)
107                {
108                    // Slightly hacky way of informing the streamer whether to supply the
109                    // compressed or default stream. May need to iterate the API on this a bit.
110                    return streamer.streamResource(resource, compress ? "z" : "", omitExpiration);
111                }
112
113                return false;
114            }
115        });
116    }
117}