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}