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