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}