001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.markup.head; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.Comparator; 022import java.util.LinkedHashMap; 023import java.util.LinkedHashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027 028import org.apache.wicket.Application; 029import org.apache.wicket.Component; 030import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; 031import org.apache.wicket.markup.html.DecoratingHeaderResponse; 032import org.apache.wicket.request.cycle.RequestCycle; 033import org.apache.wicket.request.resource.ResourceReference; 034import org.apache.wicket.resource.CircularDependencyException; 035import org.apache.wicket.resource.bundles.ReplacementResourceBundleReference; 036import org.apache.wicket.util.lang.Classes; 037 038/** 039 * {@code ResourceAggregator} implements resource dependencies, resource bundles and sorting of 040 * resources. During the rendering of components, all {@link HeaderItem}s are 041 * {@linkplain RecordedHeaderItem recorded} and processed at the end. 042 * 043 * @author papegaaij 044 */ 045public class ResourceAggregator extends DecoratingHeaderResponse 046{ 047 048 /** 049 * The location in which a {@link HeaderItem} is added, consisting of the component/behavior 050 * that added the item, the index in the list for that component/behavior at which the item was 051 * added and the index in the request. 052 * 053 * @author papegaaij 054 */ 055 public static class RecordedHeaderItemLocation 056 { 057 private final Component renderBase; 058 059 private int indexInRequest; 060 061 private int depth = -1; 062 063 /** 064 * Construct. 065 * 066 * @param renderBase 067 * The component that added the item. 068 */ 069 public RecordedHeaderItemLocation(Component renderBase, int indexInRequest) 070 { 071 this.renderBase = renderBase; 072 073 this.indexInRequest = indexInRequest; 074 } 075 076 /** 077 * @return the component or behavior that added the item. 078 */ 079 public Object getRenderBase() 080 { 081 return renderBase; 082 } 083 084 /** 085 * @return the number of items added before this one in the same request. 086 */ 087 public int getIndexInRequest() 088 { 089 return indexInRequest; 090 } 091 092 public int getDepth() 093 { 094 if (depth == -1) { 095 Component component = renderBase; 096 while (component != null) { 097 depth++; 098 099 component = component.getParent(); 100 } 101 102 } 103 return depth; 104 } 105 106 @Override 107 public String toString() 108 { 109 return Classes.simpleName(renderBase.getClass()); 110 } 111 } 112 113 /** 114 * Contains information about an {@link HeaderItem} that must be rendered. 115 * 116 * @author papegaaij 117 */ 118 public static class RecordedHeaderItem 119 { 120 private final HeaderItem item; 121 122 private final List<RecordedHeaderItemLocation> locations; 123 124 private int minDepth = Integer.MAX_VALUE; 125 126 /** 127 * Construct. 128 * 129 * @param item 130 */ 131 public RecordedHeaderItem(HeaderItem item) 132 { 133 this.item = item; 134 locations = new ArrayList<>(); 135 } 136 137 /** 138 * Records a location at which the item was added. 139 * 140 * @param renderBase 141 * The component or behavior that added the item. 142 * @param indexInRequest 143 * Indicates the number of items added before this one in this request. 144 */ 145 void addLocation(Component renderBase, int indexInRequest) 146 { 147 locations.add(new RecordedHeaderItemLocation(renderBase, indexInRequest)); 148 149 minDepth = Integer.MAX_VALUE; 150 } 151 152 /** 153 * @return the actual item 154 */ 155 public HeaderItem getItem() 156 { 157 return item; 158 } 159 160 /** 161 * @return The locations at which the item was added. 162 */ 163 public List<RecordedHeaderItemLocation> getLocations() 164 { 165 return locations; 166 } 167 168 /** 169 * Get the minimum depth in the component tree. 170 * 171 * @return depth 172 */ 173 public int getMinDepth() 174 { 175 if (minDepth == Integer.MAX_VALUE) { 176 for (RecordedHeaderItemLocation location : locations) { 177 minDepth = Math.min(minDepth, location.getDepth()); 178 } 179 } 180 181 return minDepth; 182 } 183 184 185 @Override 186 public String toString() 187 { 188 return locations + ":" + item; 189 } 190 } 191 192 private final Map<HeaderItem, RecordedHeaderItem> itemsToBeRendered; 193 194 /** 195 * Header items which should be executed once the DOM is ready. 196 * Collects OnDomReadyHeaderItems and OnEventHeaderItems 197 */ 198 private final List<HeaderItem> domReadyItemsToBeRendered; 199 private final List<OnLoadHeaderItem> loadItemsToBeRendered; 200 201 /** 202 * The currently rendered component 203 */ 204 private Component renderBase; 205 206 private int indexInRequest; 207 208 /** 209 * Construct. 210 * 211 * @param real 212 */ 213 public ResourceAggregator(IHeaderResponse real) 214 { 215 super(real); 216 217 itemsToBeRendered = new LinkedHashMap<>(); 218 domReadyItemsToBeRendered = new ArrayList<>(); 219 loadItemsToBeRendered = new ArrayList<>(); 220 } 221 222 /** 223 * Overridden to keep track of the currently rendered component. 224 * 225 * @see Component#internalRenderHead(org.apache.wicket.markup.html.internal.HtmlHeaderContainer) 226 */ 227 @Override 228 public boolean wasRendered(Object object) 229 { 230 boolean ret = super.wasRendered(object); 231 if (!ret && object instanceof Component) 232 { 233 renderBase = (Component)object; 234 } 235 return ret; 236 } 237 238 /** 239 * Overridden to keep track of the currently rendered component. 240 * 241 * @see Component#internalRenderHead(org.apache.wicket.markup.html.internal.HtmlHeaderContainer) 242 */ 243 @Override 244 public void markRendered(Object object) 245 { 246 super.markRendered(object); 247 if (object instanceof Component) 248 { 249 renderBase = null; 250 } 251 } 252 253 private void recordHeaderItem(HeaderItem item, Set<HeaderItem> depsDone) 254 { 255 renderDependencies(item, depsDone); 256 RecordedHeaderItem recordedItem = itemsToBeRendered.get(item); 257 if (recordedItem == null) 258 { 259 recordedItem = new RecordedHeaderItem(item); 260 itemsToBeRendered.put(item, recordedItem); 261 } 262 recordedItem.addLocation(renderBase, indexInRequest); 263 indexInRequest++; 264 } 265 266 private void renderDependencies(HeaderItem item, Set<HeaderItem> depsDone) 267 { 268 for (HeaderItem curDependency : item.getDependencies()) 269 { 270 curDependency = getItemToBeRendered(curDependency); 271 if (depsDone.add(curDependency)) 272 { 273 recordHeaderItem(curDependency, depsDone); 274 } 275 else 276 { 277 throw new CircularDependencyException(depsDone, curDependency); 278 } 279 depsDone.remove(curDependency); 280 } 281 } 282 283 @Override 284 public void render(HeaderItem item) 285 { 286 item = getItemToBeRendered(item); 287 if (item instanceof OnDomReadyHeaderItem || item instanceof OnEventHeaderItem) 288 { 289 renderDependencies(item, new LinkedHashSet<HeaderItem>()); 290 domReadyItemsToBeRendered.add(item); 291 } 292 else if (item instanceof OnLoadHeaderItem) 293 { 294 renderDependencies(item, new LinkedHashSet<HeaderItem>()); 295 loadItemsToBeRendered.add((OnLoadHeaderItem)item); 296 } 297 else 298 { 299 Set<HeaderItem> depsDone = new LinkedHashSet<>(); 300 depsDone.add(item); 301 recordHeaderItem(item, depsDone); 302 } 303 } 304 305 @Override 306 public void close() 307 { 308 renderHeaderItems(); 309 310 if (RequestCycle.get().find(IPartialPageRequestHandler.class).isPresent()) 311 { 312 renderSeparateEventScripts(); 313 } 314 else 315 { 316 renderCombinedEventScripts(); 317 } 318 super.close(); 319 } 320 321 /** 322 * Renders all normal header items, sorting them and taking bundles into account. 323 */ 324 private void renderHeaderItems() 325 { 326 List<RecordedHeaderItem> sortedItemsToBeRendered = new ArrayList<>( 327 itemsToBeRendered.values()); 328 Comparator<? super RecordedHeaderItem> headerItemComparator = Application.get() 329 .getResourceSettings() 330 .getHeaderItemComparator(); 331 if (headerItemComparator != null) 332 { 333 Collections.sort(sortedItemsToBeRendered, headerItemComparator); 334 } 335 for (RecordedHeaderItem curRenderItem : sortedItemsToBeRendered) 336 { 337 if (markItemRendered(curRenderItem.getItem())) 338 { 339 getRealResponse().render(curRenderItem.getItem()); 340 } 341 } 342 } 343 344 /** 345 * Combines all DOM ready and onLoad scripts and renders them as 2 script tags. 346 */ 347 private void renderCombinedEventScripts() 348 { 349 StringBuilder combinedScript = new StringBuilder(); 350 for (HeaderItem curItem : domReadyItemsToBeRendered) 351 { 352 if (markItemRendered(curItem)) 353 { 354 combinedScript.append('\n'); 355 if (curItem instanceof OnDomReadyHeaderItem) 356 { 357 combinedScript.append(((OnDomReadyHeaderItem)curItem).getJavaScript()); 358 } else if (curItem instanceof OnEventHeaderItem) 359 { 360 combinedScript.append(((OnEventHeaderItem)curItem).getCompleteJavaScript()); 361 } 362 combinedScript.append(';'); 363 } 364 } 365 if (combinedScript.length() > 0) 366 { 367 combinedScript.append("\nWicket.Event.publish(Wicket.Event.Topic.AJAX_HANDLERS_BOUND);"); 368 getRealResponse().render( 369 OnDomReadyHeaderItem.forScript(combinedScript.append('\n').toString())); 370 } 371 372 combinedScript.setLength(0); 373 for (OnLoadHeaderItem curItem : loadItemsToBeRendered) 374 { 375 if (markItemRendered(curItem)) 376 { 377 combinedScript.append('\n'); 378 combinedScript.append(curItem.getJavaScript()); 379 combinedScript.append(';'); 380 } 381 } 382 if (combinedScript.length() > 0) 383 { 384 getRealResponse().render( 385 OnLoadHeaderItem.forScript(combinedScript.append('\n').toString())); 386 } 387 } 388 389 /** 390 * Renders the DOM ready and onLoad scripts as separate tags. 391 */ 392 private void renderSeparateEventScripts() 393 { 394 for (HeaderItem curItem : domReadyItemsToBeRendered) 395 { 396 if (markItemRendered(curItem)) 397 { 398 getRealResponse().render(curItem); 399 } 400 } 401 402 for (OnLoadHeaderItem curItem : loadItemsToBeRendered) 403 { 404 if (markItemRendered(curItem)) 405 { 406 getRealResponse().render(curItem); 407 } 408 } 409 } 410 411 private boolean markItemRendered(HeaderItem item) 412 { 413 if (wasRendered(item)) 414 return false; 415 416 if (item instanceof IWrappedHeaderItem) 417 { 418 getRealResponse().markRendered(((IWrappedHeaderItem)item).getWrapped()); 419 } 420 getRealResponse().markRendered(item); 421 for (HeaderItem curProvided : item.getProvidedResources()) 422 { 423 getRealResponse().markRendered(curProvided); 424 } 425 return true; 426 } 427 428 /** 429 * Resolves the actual item that needs to be rendered for the given item. This can be a 430 * {@link NoHeaderItem} when the item was already rendered. It can also be a bundle or the item 431 * itself, when it is not part of a bundle. 432 * 433 * @param item 434 * @return The item to be rendered 435 */ 436 private HeaderItem getItemToBeRendered(HeaderItem item) 437 { 438 HeaderItem innerItem = item; 439 while (innerItem instanceof IWrappedHeaderItem) 440 { 441 innerItem = ((IWrappedHeaderItem)innerItem).getWrapped(); 442 } 443 if (getRealResponse().wasRendered(innerItem)) 444 { 445 return NoHeaderItem.get(); 446 } 447 448 HeaderItem bundle = Application.get().getResourceBundles().findBundle(innerItem); 449 if (bundle == null) 450 { 451 return item; 452 } 453 454 bundle = preserveDetails(item, bundle); 455 456 if (item instanceof IWrappedHeaderItem) 457 { 458 bundle = ((IWrappedHeaderItem)item).wrap(bundle); 459 } 460 return bundle; 461 } 462 463 /** 464 * Preserves the resource reference details for resource replacements. 465 * 466 * For example if CSS resource with media <em>screen</em> is replaced with 467 * {@link org.apache.wicket.protocol.http.WebApplication#addResourceReplacement(org.apache.wicket.request.resource.CssResourceReference, org.apache.wicket.request.resource.ResourceReference)} then the replacement will 468 * will inherit the media attribute 469 * 470 * @param item The replaced header item 471 * @param bundle The bundle that represents the replacement 472 * @return the bundle with the preserved details 473 */ 474 protected HeaderItem preserveDetails(HeaderItem item, HeaderItem bundle) 475 { 476 HeaderItem resultBundle; 477 if (item instanceof CssReferenceHeaderItem && bundle instanceof CssReferenceHeaderItem) 478 { 479 CssReferenceHeaderItem originalHeaderItem = (CssReferenceHeaderItem) item; 480 resultBundle = preserveCssDetails(originalHeaderItem, (CssReferenceHeaderItem) bundle); 481 } 482 else if (item instanceof JavaScriptReferenceHeaderItem && bundle instanceof JavaScriptReferenceHeaderItem) 483 { 484 JavaScriptReferenceHeaderItem originalHeaderItem = (JavaScriptReferenceHeaderItem) item; 485 resultBundle = preserveJavaScriptDetails(originalHeaderItem, (JavaScriptReferenceHeaderItem) bundle); 486 } 487 else 488 { 489 resultBundle = bundle; 490 } 491 492 return resultBundle; 493 } 494 495 /** 496 * Preserves the resource reference details for JavaScript resource replacements. 497 * 498 * For example if CSS resource with media <em>screen</em> is replaced with 499 * {@link org.apache.wicket.protocol.http.WebApplication#addResourceReplacement(org.apache.wicket.request.resource.JavaScriptResourceReference, org.apache.wicket.request.resource.ResourceReference)} then the replacement will 500 * will inherit the media attribute 501 * 502 * @param item The replaced header item 503 * @param bundle The bundle that represents the replacement 504 * @return the bundle with the preserved details 505 */ 506 private HeaderItem preserveJavaScriptDetails(JavaScriptReferenceHeaderItem item, JavaScriptReferenceHeaderItem bundle) 507 { 508 HeaderItem resultBundle; 509 ResourceReference bundleReference = bundle.getReference(); 510 if (bundleReference instanceof ReplacementResourceBundleReference) 511 { 512 resultBundle = JavaScriptHeaderItem.forReference(bundleReference, 513 item.getPageParameters(), 514 item.getId() 515 ).setCharset(item.getCharset()).setDefer(item.isDefer()).setAsync(item.isAsync()).setNonce(item.getNonce()); 516 } 517 else 518 { 519 resultBundle = bundle; 520 } 521 return resultBundle; 522 } 523 524 /** 525 * Preserves the resource reference details for CSS resource replacements. 526 * 527 * For example if CSS resource with media <em>screen</em> is replaced with 528 * {@link org.apache.wicket.protocol.http.WebApplication#addResourceReplacement(org.apache.wicket.request.resource.CssResourceReference, org.apache.wicket.request.resource.ResourceReference)} then the replacement will 529 * will inherit the media attribute 530 * 531 * @param item The replaced header item 532 * @param bundle The bundle that represents the replacement 533 * @return the bundle with the preserved details 534 */ 535 protected HeaderItem preserveCssDetails(CssReferenceHeaderItem item, CssReferenceHeaderItem bundle) 536 { 537 HeaderItem resultBundle; 538 ResourceReference bundleReference = bundle.getReference(); 539 if (bundleReference instanceof ReplacementResourceBundleReference) 540 { 541 resultBundle = CssHeaderItem.forReference(bundleReference, 542 item.getPageParameters(), 543 item.getMedia()); 544 } 545 else 546 { 547 resultBundle = bundle; 548 } 549 return resultBundle; 550 } 551}