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}