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}