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}