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 // make a rough estimate of the size to which this StringBuilder will grow 350 int domReadyLength = domReadyItemsToBeRendered.size() * 256; 351 StringBuilder domReadScript = new StringBuilder(domReadyLength); 352 for (HeaderItem curItem : domReadyItemsToBeRendered) 353 { 354 if (markItemRendered(curItem)) 355 { 356 domReadScript.append('\n'); 357 if (curItem instanceof OnDomReadyHeaderItem) 358 { 359 domReadScript.append(((OnDomReadyHeaderItem)curItem).getJavaScript()); 360 } else if (curItem instanceof OnEventHeaderItem) 361 { 362 domReadScript.append(((OnEventHeaderItem)curItem).getCompleteJavaScript()); 363 } 364 domReadScript.append(';'); 365 } 366 } 367 if (domReadScript.length() > 0) 368 { 369 domReadScript.append("\nWicket.Event.publish(Wicket.Event.Topic.AJAX_HANDLERS_BOUND);\n"); 370 getRealResponse().render(OnDomReadyHeaderItem.forScript(domReadScript)); 371 } 372 373 int onLoadLength = loadItemsToBeRendered.size() * 256; 374 StringBuilder onLoadScript = new StringBuilder(onLoadLength); 375 376 for (OnLoadHeaderItem curItem : loadItemsToBeRendered) 377 { 378 if (markItemRendered(curItem)) 379 { 380 onLoadScript.append('\n'); 381 onLoadScript.append(curItem.getJavaScript()); 382 onLoadScript.append(';'); 383 } 384 } 385 if (onLoadScript.length() > 0) 386 { 387 getRealResponse().render( 388 OnLoadHeaderItem.forScript(onLoadScript.append('\n'))); 389 } 390 } 391 392 /** 393 * Renders the DOM ready and onLoad scripts as separate tags. 394 */ 395 private void renderSeparateEventScripts() 396 { 397 for (HeaderItem curItem : domReadyItemsToBeRendered) 398 { 399 if (markItemRendered(curItem)) 400 { 401 getRealResponse().render(curItem); 402 } 403 } 404 405 for (OnLoadHeaderItem curItem : loadItemsToBeRendered) 406 { 407 if (markItemRendered(curItem)) 408 { 409 getRealResponse().render(curItem); 410 } 411 } 412 } 413 414 private boolean markItemRendered(HeaderItem item) 415 { 416 if (wasRendered(item)) 417 return false; 418 419 if (item instanceof IWrappedHeaderItem) 420 { 421 getRealResponse().markRendered(((IWrappedHeaderItem)item).getWrapped()); 422 } 423 getRealResponse().markRendered(item); 424 for (HeaderItem curProvided : item.getProvidedResources()) 425 { 426 getRealResponse().markRendered(curProvided); 427 } 428 return true; 429 } 430 431 /** 432 * Resolves the actual item that needs to be rendered for the given item. This can be a 433 * {@link NoHeaderItem} when the item was already rendered. It can also be a bundle or the item 434 * itself, when it is not part of a bundle. 435 * 436 * @param item 437 * @return The item to be rendered 438 */ 439 private HeaderItem getItemToBeRendered(HeaderItem item) 440 { 441 HeaderItem innerItem = item; 442 while (innerItem instanceof IWrappedHeaderItem) 443 { 444 innerItem = ((IWrappedHeaderItem)innerItem).getWrapped(); 445 } 446 if (getRealResponse().wasRendered(innerItem)) 447 { 448 return NoHeaderItem.get(); 449 } 450 451 HeaderItem bundle = Application.get().getResourceBundles().findBundle(innerItem); 452 if (bundle == null) 453 { 454 return item; 455 } 456 457 bundle = preserveDetails(item, bundle); 458 459 if (item instanceof IWrappedHeaderItem) 460 { 461 bundle = ((IWrappedHeaderItem)item).wrap(bundle); 462 } 463 return bundle; 464 } 465 466 /** 467 * Preserves the resource reference details for resource replacements. 468 * 469 * For example if CSS resource with media <em>screen</em> is replaced with 470 * {@link org.apache.wicket.protocol.http.WebApplication#addResourceReplacement(org.apache.wicket.request.resource.CssResourceReference, org.apache.wicket.request.resource.ResourceReference)} then the replacement will 471 * will inherit the media attribute 472 * 473 * @param item The replaced header item 474 * @param bundle The bundle that represents the replacement 475 * @return the bundle with the preserved details 476 */ 477 protected HeaderItem preserveDetails(HeaderItem item, HeaderItem bundle) 478 { 479 HeaderItem resultBundle; 480 if (item instanceof CssReferenceHeaderItem && bundle instanceof CssReferenceHeaderItem) 481 { 482 CssReferenceHeaderItem originalHeaderItem = (CssReferenceHeaderItem) item; 483 resultBundle = preserveCssDetails(originalHeaderItem, (CssReferenceHeaderItem) bundle); 484 } 485 else if (item instanceof JavaScriptReferenceHeaderItem && bundle instanceof JavaScriptReferenceHeaderItem) 486 { 487 JavaScriptReferenceHeaderItem originalHeaderItem = (JavaScriptReferenceHeaderItem) item; 488 resultBundle = preserveJavaScriptDetails(originalHeaderItem, (JavaScriptReferenceHeaderItem) bundle); 489 } 490 else 491 { 492 resultBundle = bundle; 493 } 494 495 return resultBundle; 496 } 497 498 /** 499 * Preserves the resource reference details for JavaScript resource replacements. 500 * 501 * For example if CSS resource with media <em>screen</em> is replaced with 502 * {@link org.apache.wicket.protocol.http.WebApplication#addResourceReplacement(org.apache.wicket.request.resource.JavaScriptResourceReference, org.apache.wicket.request.resource.ResourceReference)} then the replacement will 503 * will inherit the media attribute 504 * 505 * @param item The replaced header item 506 * @param bundle The bundle that represents the replacement 507 * @return the bundle with the preserved details 508 */ 509 private HeaderItem preserveJavaScriptDetails(JavaScriptReferenceHeaderItem item, JavaScriptReferenceHeaderItem bundle) 510 { 511 HeaderItem resultBundle; 512 ResourceReference bundleReference = bundle.getReference(); 513 if (bundleReference instanceof ReplacementResourceBundleReference) 514 { 515 resultBundle = JavaScriptHeaderItem.forReference(bundleReference, 516 item.getPageParameters(), 517 item.getId() 518 ).setCharset(item.getCharset()).setDefer(item.isDefer()).setAsync(item.isAsync()).setNonce(item.getNonce()); 519 } 520 else 521 { 522 resultBundle = bundle; 523 } 524 return resultBundle; 525 } 526 527 /** 528 * Preserves the resource reference details for CSS resource replacements. 529 * 530 * For example if CSS resource with media <em>screen</em> is replaced with 531 * {@link org.apache.wicket.protocol.http.WebApplication#addResourceReplacement(org.apache.wicket.request.resource.CssResourceReference, org.apache.wicket.request.resource.ResourceReference)} then the replacement will 532 * will inherit the media attribute 533 * 534 * @param item The replaced header item 535 * @param bundle The bundle that represents the replacement 536 * @return the bundle with the preserved details 537 */ 538 protected HeaderItem preserveCssDetails(CssReferenceHeaderItem item, CssReferenceHeaderItem bundle) 539 { 540 HeaderItem resultBundle; 541 ResourceReference bundleReference = bundle.getReference(); 542 if (bundleReference instanceof ReplacementResourceBundleReference) 543 { 544 resultBundle = CssHeaderItem.forReference(bundleReference, 545 item.getPageParameters(), 546 item.getMedia()); 547 } 548 else 549 { 550 resultBundle = bundle; 551 } 552 return resultBundle; 553 } 554}