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.commons.Messages; 017import org.apache.tapestry5.commons.Resource; 018import org.apache.tapestry5.commons.util.CollectionFactory; 019import org.apache.tapestry5.dom.Element; 020import org.apache.tapestry5.http.TapestryHttpSymbolConstants; 021import org.apache.tapestry5.http.services.ResponseCompressionAnalyzer; 022import org.apache.tapestry5.internal.InternalConstants; 023import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; 024import org.apache.tapestry5.ioc.annotations.PostInjection; 025import org.apache.tapestry5.ioc.annotations.Symbol; 026import org.apache.tapestry5.json.JSONArray; 027import org.apache.tapestry5.json.JSONLiteral; 028import org.apache.tapestry5.json.JSONObject; 029import org.apache.tapestry5.services.AssetSource; 030import org.apache.tapestry5.services.PathConstructor; 031import org.apache.tapestry5.services.assets.StreamableResourceSource; 032import org.apache.tapestry5.services.javascript.JavaScriptModuleConfiguration; 033import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback; 034import org.apache.tapestry5.services.javascript.ModuleManager; 035 036import java.util.List; 037import java.util.Map; 038import java.util.Set; 039 040public class ModuleManagerImpl implements ModuleManager 041{ 042 043 private final ResponseCompressionAnalyzer compressionAnalyzer; 044 045 private final Messages globalMessages; 046 047 private final boolean compactJSON; 048 049 private final Map<String, Resource> shimModuleNameToResource = CollectionFactory.newMap(); 050 051 private final Resource classpathRoot; 052 053 private final Set<String> extensions; 054 055 // Note: ConcurrentHashMap does not support null as a value, alas. We use classpathRoot as a null. 056 private final Map<String, Resource> cache = CollectionFactory.newConcurrentMap(); 057 058 private final JSONObject baseConfig; 059 060 private final String basePath, compressedBasePath; 061 062 public ModuleManagerImpl(ResponseCompressionAnalyzer compressionAnalyzer, 063 AssetSource assetSource, 064 Map<String, JavaScriptModuleConfiguration> configuration, 065 Messages globalMessages, 066 StreamableResourceSource streamableResourceSource, 067 @Symbol(SymbolConstants.COMPACT_JSON) 068 boolean compactJSON, 069 @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) 070 boolean productionMode, 071 @Symbol(SymbolConstants.MODULE_PATH_PREFIX) 072 String modulePathPrefix, 073 PathConstructor pathConstructor) 074 { 075 this.compressionAnalyzer = compressionAnalyzer; 076 this.globalMessages = globalMessages; 077 this.compactJSON = compactJSON; 078 079 basePath = pathConstructor.constructClientPath(modulePathPrefix); 080 compressedBasePath = pathConstructor.constructClientPath(modulePathPrefix + ".gz"); 081 082 classpathRoot = assetSource.resourceForPath(""); 083 extensions = CollectionFactory.newSet("js"); 084 085 extensions.addAll(streamableResourceSource.fileExtensionsForContentType(InternalConstants.JAVASCRIPT_CONTENT_TYPE)); 086 087 baseConfig = buildBaseConfig(configuration, !productionMode); 088 } 089 090 private String buildRequireJSConfig(List<ModuleConfigurationCallback> callbacks) 091 { 092 // This is the part that can vary from one request to another, based on the capabilities of the client. 093 JSONObject config = baseConfig.copy().put("baseUrl", getBaseURL()); 094 095 // TAP5-2196: allow changes to the configuration in a per-request basis. 096 for (ModuleConfigurationCallback callback : callbacks) 097 { 098 config = callback.configure(config); 099 assert config != null; 100 } 101 102 // This part gets written out before any libraries are loaded (including RequireJS). 103 return String.format("var require = %s;\n", config.toString(compactJSON)); 104 } 105 106 private JSONObject buildBaseConfig(Map<String, JavaScriptModuleConfiguration> configuration, boolean devMode) 107 { 108 JSONObject config = new JSONObject(); 109 110 // In DevMode, wait up to five minutes for a script, as the developer may be using the debugger. 111 if (devMode) 112 { 113 config.put("waitSeconds", 300); 114 } 115 116 for (String name : configuration.keySet()) 117 { 118 JavaScriptModuleConfiguration module = configuration.get(name); 119 120 shimModuleNameToResource.put(name, module.resource); 121 122 // Some modules (particularly overrides) just need an alternate location for their content 123 // on the server. 124 if (module.getNeedsConfiguration()) 125 { 126 // Others are libraries being shimmed as AMD modules, and need some configuration 127 // to ensure that everything hooks up properly on the client. 128 addModuleToConfig(config, name, module); 129 } 130 } 131 return config; 132 } 133 134 private String getBaseURL() 135 { 136 return compressionAnalyzer.isGZipSupported() ? compressedBasePath : basePath; 137 } 138 139 private void addModuleToConfig(JSONObject config, String name, JavaScriptModuleConfiguration module) 140 { 141 JSONObject shimConfig = config.in("shim"); 142 143 boolean nestDependencies = false; 144 145 String exports = module.getExports(); 146 147 if (exports != null) 148 { 149 shimConfig.in(name).put("exports", exports); 150 nestDependencies = true; 151 } 152 153 String initExpression = module.getInitExpression(); 154 155 if (initExpression != null) 156 { 157 String function = String.format("function() { return %s; }", initExpression); 158 shimConfig.in(name).put("init", new JSONLiteral(function)); 159 nestDependencies = true; 160 } 161 162 List<String> dependencies = module.getDependencies(); 163 164 if (dependencies != null) 165 { 166 JSONObject container = nestDependencies ? shimConfig.in(name) : shimConfig; 167 String key = nestDependencies ? "deps" : name; 168 169 for (String dep : dependencies) 170 { 171 container.append(key, dep); 172 } 173 } 174 } 175 176 @PostInjection 177 public void setupInvalidation(ResourceChangeTracker tracker) 178 { 179 tracker.clearOnInvalidation(cache); 180 } 181 182 public void writeConfiguration(Element body, 183 List<ModuleConfigurationCallback> callbacks) 184 { 185 Element element = body.element("script", "type", "text/javascript"); 186 187 // Build it each time because we don't know if the client supports GZip or not, and 188 // (in development mode) URLs for some referenced assets could change (due to URLs 189 // containing a checksum on the resource content). 190 element.raw(buildRequireJSConfig(callbacks)); 191 } 192 193 public void writeInitialization(Element body, List<String> libraryURLs, List<?> inits) 194 { 195 196 Element element = body.element("script", "type", "text/javascript"); 197 198 element.raw(globalMessages.format("private-core-page-initialization-template", 199 convert(libraryURLs), 200 convert(inits))); 201 } 202 203 private String convert(List<?> input) 204 { 205 return new JSONArray().putAll(input).toString(compactJSON); 206 } 207 208 public Resource findResourceForModule(String moduleName) 209 { 210 Resource resource = cache.get(moduleName); 211 212 if (resource == null) 213 { 214 resource = resolveModuleNameToResource(moduleName); 215 cache.put(moduleName, resource); 216 } 217 218 // We're treating classpathRoot as a placeholder for null. 219 220 return resource == classpathRoot ? null : resource; 221 } 222 223 private Resource resolveModuleNameToResource(String moduleName) 224 { 225 Resource resource = shimModuleNameToResource.get(moduleName); 226 227 if (resource != null) 228 { 229 return resource; 230 } 231 232 // Tack on a fake extension; otherwise modules whose name includes a '.' get mangled 233 // by Resource.withExtension(). 234 String baseName = String.format("/META-INF/modules/%s.EXT", moduleName); 235 236 Resource baseResource = classpathRoot.forFile(baseName); 237 238 for (String extension : extensions) 239 { 240 resource = baseResource.withExtension(extension); 241 242 if (resource.exists()) 243 { 244 return resource; 245 } 246 } 247 248 // Return placeholder for null: 249 return classpathRoot; 250 } 251}