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