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}