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}