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.ajax; 014 015import org.apache.tapestry5.Asset; 016import org.apache.tapestry5.BooleanHook; 017import org.apache.tapestry5.ComponentResources; 018import org.apache.tapestry5.FieldFocusPriority; 019import org.apache.tapestry5.func.F; 020import org.apache.tapestry5.func.Worker; 021import org.apache.tapestry5.internal.InternalConstants; 022import org.apache.tapestry5.internal.services.DocumentLinker; 023import org.apache.tapestry5.internal.services.javascript.JavaScriptStackPathConstructor; 024import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 025import org.apache.tapestry5.ioc.internal.util.InternalUtils; 026import org.apache.tapestry5.ioc.util.IdAllocator; 027import org.apache.tapestry5.json.JSONArray; 028import org.apache.tapestry5.json.JSONObject; 029import org.apache.tapestry5.services.javascript.*; 030 031import java.util.*; 032 033public class JavaScriptSupportImpl implements JavaScriptSupport 034{ 035 private final IdAllocator idAllocator; 036 037 private final DocumentLinker linker; 038 039 // Using a Map as a case-insensitive set of stack names. 040 041 private final Map<String, Boolean> addedStacks = CollectionFactory.newCaseInsensitiveMap(); 042 043 private final Set<String> otherLibraries = CollectionFactory.newSet(); 044 045 private final Set<String> importedStylesheetURLs = CollectionFactory.newSet(); 046 047 private final List<StylesheetLink> stylesheetLinks = CollectionFactory.newList(); 048 049 private final List<InitializationImpl> inits = CollectionFactory.newList(); 050 051 private final JavaScriptStackSource javascriptStackSource; 052 053 private final JavaScriptStackPathConstructor stackPathConstructor; 054 055 private final boolean partialMode; 056 057 private final BooleanHook suppressCoreStylesheetsHook; 058 059 private FieldFocusPriority focusPriority; 060 061 private String focusFieldId; 062 063 private Map<String, String> libraryURLToStackName, moduleNameToStackName; 064 065 class InitializationImpl implements Initialization 066 { 067 InitializationPriority priority = InitializationPriority.NORMAL; 068 069 final String moduleName; 070 071 String functionName; 072 073 JSONArray arguments; 074 075 InitializationImpl(String moduleName) 076 { 077 this.moduleName = moduleName; 078 } 079 080 public Initialization invoke(String functionName) 081 { 082 assert InternalUtils.isNonBlank(functionName); 083 084 this.functionName = functionName; 085 086 return this; 087 } 088 089 public Initialization priority(InitializationPriority priority) 090 { 091 assert priority != null; 092 093 this.priority = priority; 094 095 return this; 096 } 097 098 public void with(Object... arguments) 099 { 100 assert arguments != null; 101 102 this.arguments = new JSONArray(arguments); 103 } 104 } 105 106 public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource, 107 JavaScriptStackPathConstructor stackPathConstructor, BooleanHook suppressCoreStylesheetsHook) 108 { 109 this(linker, javascriptStackSource, stackPathConstructor, new IdAllocator(), false, suppressCoreStylesheetsHook); 110 } 111 112 /** 113 * @param linker 114 * responsible for assembling all the information gathered by JavaScriptSupport and 115 * attaching it to the Document (for a full page render) or to the JSON response (in a partial render) 116 * @param javascriptStackSource 117 * source of information about {@link org.apache.tapestry5.services.javascript.JavaScriptStack}s, used when handling the import 118 * of libraries and stacks (often, to handle transitive dependencies) 119 * @param stackPathConstructor 120 * encapsulates the knowledge of how to represent a stack (which may be converted 121 * from a series of JavaScript libraries into a single virtual JavaScript library) 122 * @param idAllocator 123 * used when allocating unique ids (it is usually pre-initialized in an Ajax request to ensure 124 * that newly allocated ids do not conflict with previous renders and partial updates) 125 * @param partialMode 126 * if true, then the JSS configures itself for a partial page render (part of an Ajax request) 127 * which automatically assumes the "core" library has been added (to the original page render) 128 * @param suppressCoreStylesheetsHook 129 * a hook that enables ignoring CSS files on the core stack 130 */ 131 public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource, 132 JavaScriptStackPathConstructor stackPathConstructor, IdAllocator idAllocator, boolean partialMode, 133 BooleanHook suppressCoreStylesheetsHook) 134 { 135 this.linker = linker; 136 this.idAllocator = idAllocator; 137 this.javascriptStackSource = javascriptStackSource; 138 this.stackPathConstructor = stackPathConstructor; 139 this.partialMode = partialMode; 140 this.suppressCoreStylesheetsHook = suppressCoreStylesheetsHook; 141 142 // In partial mode, assume that the infrastructure stack is already present 143 // (from the original page render). 144 145 if (partialMode) 146 { 147 addedStacks.put(InternalConstants.CORE_STACK_NAME, true); 148 } 149 } 150 151 public void commit() 152 { 153 if (focusFieldId != null) 154 { 155 require("t5/core/pageinit").invoke("focus").with(focusFieldId); 156 } 157 158 F.flow(stylesheetLinks).each(new Worker<StylesheetLink>() 159 { 160 public void work(StylesheetLink value) 161 { 162 linker.addStylesheetLink(value); 163 } 164 }); 165 166 F.flow(inits).sort(new Comparator<InitializationImpl>() 167 { 168 public int compare(InitializationImpl o1, InitializationImpl o2) 169 { 170 return o1.priority.compareTo(o2.priority); 171 } 172 }).each(new Worker<InitializationImpl>() 173 { 174 public void work(InitializationImpl element) 175 { 176 linker.addInitialization(element.priority, element.moduleName, element.functionName, element.arguments); 177 } 178 }); 179 } 180 181 public void addInitializerCall(InitializationPriority priority, String functionName, JSONObject parameter) 182 { 183 createInitializer(priority).with(functionName, parameter); 184 } 185 186 public void addInitializerCall(String functionName, JSONArray parameter) 187 { 188 addInitializerCall(InitializationPriority.NORMAL, functionName, parameter); 189 } 190 191 public void addInitializerCall(InitializationPriority priority, String functionName, 192 JSONArray parameter) 193 { 194 // TAP5-2300: In 5.3, a JSONArray implied an array of method arguments, so unwrap and add 195 // functionName to the arguments 196 197 List parameterList = new ArrayList(parameter.length() + 1); 198 parameterList.add(functionName); 199 parameterList.addAll(parameter.toList()); 200 createInitializer(priority).with(parameterList.toArray()); 201 } 202 203 private Initialization createInitializer(InitializationPriority priority) 204 { 205 assert priority != null; 206 207 importCoreStack(); 208 209 return require("t5/core/init").priority(priority); 210 } 211 212 public void addInitializerCall(String functionName, JSONObject parameter) 213 { 214 addInitializerCall(InitializationPriority.NORMAL, functionName, parameter); 215 } 216 217 public void addInitializerCall(InitializationPriority priority, String functionName, String parameter) 218 { 219 createInitializer(priority).with(functionName, parameter); 220 } 221 222 public void addInitializerCall(String functionName, String parameter) 223 { 224 addInitializerCall(InitializationPriority.NORMAL, functionName, parameter); 225 } 226 227 public void addScript(InitializationPriority priority, String format, Object... arguments) 228 { 229 assert priority != null; 230 assert InternalUtils.isNonBlank(format); 231 232 importCoreStack(); 233 234 String newScript = arguments.length == 0 ? format : String.format(format, arguments); 235 236 if (partialMode) 237 { 238 require("t5/core/pageinit").invoke("evalJavaScript").with(newScript); 239 } else 240 { 241 linker.addScript(priority, newScript); 242 } 243 } 244 245 public void addScript(String format, Object... arguments) 246 { 247 addScript(InitializationPriority.NORMAL, format, arguments); 248 } 249 250 public void addModuleConfigurationCallback(ModuleConfigurationCallback callback) 251 { 252 linker.addModuleConfigurationCallback(callback); 253 } 254 255 public String allocateClientId(ComponentResources resources) 256 { 257 return allocateClientId(resources.getId()); 258 } 259 260 public String allocateClientId(String id) 261 { 262 return idAllocator.allocateId(id); 263 } 264 265 public JavaScriptSupport importJavaScriptLibrary(Asset asset) 266 { 267 assert asset != null; 268 269 return importJavaScriptLibrary(asset.toClientURL()); 270 } 271 272 public JavaScriptSupport importJavaScriptLibrary(String libraryURL) 273 { 274 importCoreStack(); 275 276 String stackName = findStackForLibrary(libraryURL); 277 278 if (stackName != null) 279 { 280 return importStack(stackName); 281 } 282 283 if (!otherLibraries.contains(libraryURL)) 284 { 285 linker.addLibrary(libraryURL); 286 287 otherLibraries.add(libraryURL); 288 } 289 290 return this; 291 } 292 293 private void importCoreStack() 294 { 295 addAssetsFromStack(InternalConstants.CORE_STACK_NAME); 296 } 297 298 /** 299 * Locates the name of the stack that includes the library URL. Returns the stack, 300 * or null if the library is free-standing. 301 */ 302 private String findStackForLibrary(String libraryURL) 303 { 304 return getLibraryURLToStackName().get(libraryURL); 305 } 306 307 308 private Map<String, String> getLibraryURLToStackName() 309 { 310 if (libraryURLToStackName == null) 311 { 312 libraryURLToStackName = CollectionFactory.newMap(); 313 314 for (String stackName : javascriptStackSource.getStackNames()) 315 { 316 for (Asset library : javascriptStackSource.getStack(stackName).getJavaScriptLibraries()) 317 { 318 libraryURLToStackName.put(library.toClientURL(), stackName); 319 } 320 } 321 } 322 323 return libraryURLToStackName; 324 } 325 326 private String findStackForModule(String moduleName) 327 { 328 return getModuleNameToStackName().get(moduleName); 329 } 330 331 private Map<String, String> getModuleNameToStackName() 332 { 333 334 if (moduleNameToStackName == null) 335 { 336 moduleNameToStackName = CollectionFactory.newMap(); 337 338 for (String stackName : javascriptStackSource.getStackNames()) 339 { 340 for (String moduleName : javascriptStackSource.getStack(stackName).getModules()) 341 { 342 moduleNameToStackName.put(moduleName, stackName); 343 } 344 } 345 } 346 347 return moduleNameToStackName; 348 } 349 350 351 private void addAssetsFromStack(String stackName) 352 { 353 if (addedStacks.containsKey(stackName)) 354 { 355 return; 356 } 357 358 JavaScriptStack stack = javascriptStackSource.getStack(stackName); 359 360 for (String dependentStackname : stack.getStacks()) 361 { 362 addAssetsFromStack(dependentStackname); 363 } 364 365 addedStacks.put(stackName, true); 366 367 boolean addAsCoreLibrary = stackName.equals(InternalConstants.CORE_STACK_NAME); 368 369 List<String> libraryURLs = stackPathConstructor.constructPathsForJavaScriptStack(stackName); 370 371 for (String libraryURL : libraryURLs) 372 { 373 if (addAsCoreLibrary) 374 { 375 linker.addCoreLibrary(libraryURL); 376 } else 377 { 378 linker.addLibrary(libraryURL); 379 } 380 } 381 382 if (!(addAsCoreLibrary && suppressCoreStylesheetsHook.checkHook())) 383 { 384 stylesheetLinks.addAll(stack.getStylesheets()); 385 } 386 387 String initialization = stack.getInitialization(); 388 389 if (initialization != null) 390 { 391 addScript(InitializationPriority.IMMEDIATE, initialization); 392 } 393 } 394 395 public JavaScriptSupport importStylesheet(Asset stylesheet) 396 { 397 assert stylesheet != null; 398 399 return importStylesheet(new StylesheetLink(stylesheet)); 400 } 401 402 public JavaScriptSupport importStylesheet(StylesheetLink stylesheetLink) 403 { 404 assert stylesheetLink != null; 405 406 importCoreStack(); 407 408 String stylesheetURL = stylesheetLink.getURL(); 409 410 if (!importedStylesheetURLs.contains(stylesheetURL)) 411 { 412 importedStylesheetURLs.add(stylesheetURL); 413 414 stylesheetLinks.add(stylesheetLink); 415 } 416 417 return this; 418 } 419 420 public JavaScriptSupport importStack(String stackName) 421 { 422 assert InternalUtils.isNonBlank(stackName); 423 424 importCoreStack(); 425 426 addAssetsFromStack(stackName); 427 428 return this; 429 } 430 431 public JavaScriptSupport autofocus(FieldFocusPriority priority, String fieldId) 432 { 433 assert priority != null; 434 assert InternalUtils.isNonBlank(fieldId); 435 436 if (focusFieldId == null || priority.compareTo(focusPriority) > 0) 437 { 438 this.focusPriority = priority; 439 focusFieldId = fieldId; 440 } 441 442 return this; 443 } 444 445 public Initialization require(String moduleName) 446 { 447 assert InternalUtils.isNonBlank(moduleName); 448 449 importCoreStack(); 450 451 String stackName = findStackForModule(moduleName); 452 453 if (stackName != null) 454 { 455 importStack(stackName); 456 } 457 458 InitializationImpl init = new InitializationImpl(moduleName); 459 460 inits.add(init); 461 462 return init; 463 } 464 465}