org.apache.wicket.markup.head.ResourceAggregator.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.markup.head.ResourceAggregator.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.markup.head;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.markup.html.DecoratingHeaderResponse;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.resource.CircularDependencyException;
import org.apache.wicket.resource.bundles.ReplacementResourceBundleReference;
import org.apache.wicket.util.lang.Classes;

/**
 * {@code ResourceAggregator} implements resource dependencies, resource bundles and sorting of
 * resources. During the rendering of components, all {@link HeaderItem}s are
 * {@linkplain RecordedHeaderItem recorded} and processed at the end.
 * 
 * @author papegaaij
 */
public class ResourceAggregator extends DecoratingHeaderResponse {
    /**
     * The location in which a {@link HeaderItem} is added, consisting of the component/behavior
     * that added the item, the index in the list for that component/behavior at which the item was
     * added and the index in the request.
     * 
     * @author papegaaij
     */
    public static class RecordedHeaderItemLocation {
        private final Object renderBase;
        private final int indexInRenderBase;
        private final int indexInRequest;

        /**
         * Construct.
         * 
         * @param renderBase
         *            The component or behavior that added the item.
         * @param indexInRenderBase
         *            Indicates the number of items added before this one on the same component or
         *            behavior.
         * @param indexInRequest
         *            Indicates the number of items added before this one in the same request.
         */
        public RecordedHeaderItemLocation(Object renderBase, int indexInRenderBase, int indexInRequest) {
            this.renderBase = renderBase;
            this.indexInRenderBase = indexInRenderBase;
            this.indexInRequest = indexInRequest;
        }

        /**
         * @return the component or behavior that added the item.
         */
        public Object getRenderBase() {
            return renderBase;
        }

        /**
         * @return the number of items added before this one on the same component or behavior.
         */
        public int getIndexInRenderBase() {
            return indexInRenderBase;
        }

        /**
         * @return the number of items added before this one in the same request.
         */
        public int getIndexInRequest() {
            return indexInRequest;
        }

        @Override
        public String toString() {
            return (renderBase == null ? "null" : Classes.simpleName(renderBase.getClass())) + '@'
                    + indexInRenderBase;
        }
    }

    /**
     * Contains information about an {@link HeaderItem} that must be rendered.
     * 
     * @author papegaaij
     */
    public static class RecordedHeaderItem {
        private final HeaderItem item;

        private final List<RecordedHeaderItemLocation> locations;

        /**
         * Construct.
         * 
         * @param item
         */
        public RecordedHeaderItem(HeaderItem item) {
            this.item = item;
            locations = new ArrayList<>();
        }

        /**
         * Records a location at which the item was added.
         * 
         * @param renderBase
         *            The component or behavior that added the item.
         * @param indexInRenderBase
         *            Indicates the number of items added before this one on the same component or
         *            behavior.
         * @param indexInRequest
         *            Indicates the number of items added before this one in this request.
         */
        public void addLocation(Object renderBase, int indexInRenderBase, int indexInRequest) {
            locations.add(new RecordedHeaderItemLocation(renderBase, indexInRenderBase, indexInRequest));
        }

        /**
         * @return the actual item
         */
        public HeaderItem getItem() {
            return item;
        }

        /**
         * @return The locations at which the item was added.
         */
        public List<RecordedHeaderItemLocation> getLocations() {
            return locations;
        }

        @Override
        public String toString() {
            return locations + ":" + item;
        }
    }

    private final Map<HeaderItem, RecordedHeaderItem> itemsToBeRendered;

    /**
     * Header items which should be executed once the DOM is ready.
     * Collects OnDomReadyHeaderItems and OnEventHeaderItems
     */
    private final List<HeaderItem> domReadyItemsToBeRendered;
    private final List<OnLoadHeaderItem> loadItemsToBeRendered;

    private Object renderBase;
    private int indexInRenderBase;
    private int indexInRequest;

    /**
     * Construct.
     * 
     * @param real
     */
    public ResourceAggregator(IHeaderResponse real) {
        super(real);

        itemsToBeRendered = new LinkedHashMap<>();
        domReadyItemsToBeRendered = new ArrayList<>();
        loadItemsToBeRendered = new ArrayList<>();
    }

    @Override
    public void markRendered(Object object) {
        super.markRendered(object);
        if (object instanceof Component || object instanceof Behavior) {
            renderBase = null;
            indexInRenderBase = 0;
        }
    }

    @Override
    public boolean wasRendered(Object object) {
        boolean ret = super.wasRendered(object);
        if (!ret && object instanceof Component || object instanceof Behavior) {
            renderBase = object;
            indexInRenderBase = 0;
        }
        return ret;
    }

    private void recordHeaderItem(HeaderItem item, Set<HeaderItem> depsDone) {
        renderDependencies(item, depsDone);
        RecordedHeaderItem recordedItem = itemsToBeRendered.get(item);
        if (recordedItem == null) {
            recordedItem = new RecordedHeaderItem(item);
            itemsToBeRendered.put(item, recordedItem);
        }
        recordedItem.addLocation(renderBase, indexInRenderBase, indexInRequest);
        indexInRenderBase++;
        indexInRequest++;
    }

    private void renderDependencies(HeaderItem item, Set<HeaderItem> depsDone) {
        for (HeaderItem curDependency : item.getDependencies()) {
            curDependency = getItemToBeRendered(curDependency);
            if (depsDone.add(curDependency)) {
                recordHeaderItem(curDependency, depsDone);
            } else {
                throw new CircularDependencyException(depsDone, curDependency);
            }
            depsDone.remove(curDependency);
        }
    }

    @Override
    public void render(HeaderItem item) {
        item = getItemToBeRendered(item);
        if (item instanceof OnDomReadyHeaderItem || item instanceof OnEventHeaderItem) {
            renderDependencies(item, new LinkedHashSet<HeaderItem>());
            domReadyItemsToBeRendered.add(item);
        } else if (item instanceof OnLoadHeaderItem) {
            renderDependencies(item, new LinkedHashSet<HeaderItem>());
            loadItemsToBeRendered.add((OnLoadHeaderItem) item);
        } else {
            Set<HeaderItem> depsDone = new LinkedHashSet<>();
            depsDone.add(item);
            recordHeaderItem(item, depsDone);
        }
    }

    @Override
    public void close() {
        renderHeaderItems();

        if (RequestCycle.get().find(IPartialPageRequestHandler.class).isPresent()) {
            renderSeparateEventScripts();
        } else {
            renderCombinedEventScripts();
        }
        super.close();
    }

    /**
     * Renders all normal header items, sorting them and taking bundles into account.
     */
    private void renderHeaderItems() {
        List<RecordedHeaderItem> sortedItemsToBeRendered = new ArrayList<>(itemsToBeRendered.values());
        Comparator<? super RecordedHeaderItem> headerItemComparator = Application.get().getResourceSettings()
                .getHeaderItemComparator();
        if (headerItemComparator != null) {
            Collections.sort(sortedItemsToBeRendered, headerItemComparator);
        }
        for (RecordedHeaderItem curRenderItem : sortedItemsToBeRendered) {
            if (markItemRendered(curRenderItem.getItem())) {
                getRealResponse().render(curRenderItem.getItem());
            }
        }
    }

    /**
     * Combines all DOM ready and onLoad scripts and renders them as 2 script tags.
     */
    private void renderCombinedEventScripts() {
        StringBuilder combinedScript = new StringBuilder();
        for (HeaderItem curItem : domReadyItemsToBeRendered) {
            if (markItemRendered(curItem)) {
                combinedScript.append('\n');
                if (curItem instanceof OnDomReadyHeaderItem) {
                    combinedScript.append(((OnDomReadyHeaderItem) curItem).getJavaScript());
                } else if (curItem instanceof OnEventHeaderItem) {
                    combinedScript.append(((OnEventHeaderItem) curItem).getCompleteJavaScript());
                }
                combinedScript.append(';');
            }
        }
        if (combinedScript.length() > 0) {
            combinedScript.append("\nWicket.Event.publish(Wicket.Event.Topic.AJAX_HANDLERS_BOUND);");
            getRealResponse().render(OnDomReadyHeaderItem.forScript(combinedScript.append('\n').toString()));
        }

        combinedScript.setLength(0);
        for (OnLoadHeaderItem curItem : loadItemsToBeRendered) {
            if (markItemRendered(curItem)) {
                combinedScript.append('\n');
                combinedScript.append(curItem.getJavaScript());
                combinedScript.append(';');
            }
        }
        if (combinedScript.length() > 0) {
            getRealResponse().render(OnLoadHeaderItem.forScript(combinedScript.append('\n').toString()));
        }
    }

    /**
     * Renders the DOM ready and onLoad scripts as separate tags.
     */
    private void renderSeparateEventScripts() {
        for (HeaderItem curItem : domReadyItemsToBeRendered) {
            if (markItemRendered(curItem)) {
                getRealResponse().render(curItem);
            }
        }

        for (OnLoadHeaderItem curItem : loadItemsToBeRendered) {
            if (markItemRendered(curItem)) {
                getRealResponse().render(curItem);
            }
        }
    }

    private boolean markItemRendered(HeaderItem item) {
        if (wasRendered(item))
            return false;

        if (item instanceof IWrappedHeaderItem) {
            getRealResponse().markRendered(((IWrappedHeaderItem) item).getWrapped());
        }
        getRealResponse().markRendered(item);
        for (HeaderItem curProvided : item.getProvidedResources()) {
            getRealResponse().markRendered(curProvided);
        }
        return true;
    }

    /**
     * Resolves the actual item that needs to be rendered for the given item. This can be a
     * {@link NoHeaderItem} when the item was already rendered. It can also be a bundle or the item
     * itself, when it is not part of a bundle.
     * 
     * @param item
     * @return The item to be rendered
     */
    private HeaderItem getItemToBeRendered(HeaderItem item) {
        HeaderItem innerItem = item;
        while (innerItem instanceof IWrappedHeaderItem) {
            innerItem = ((IWrappedHeaderItem) innerItem).getWrapped();
        }
        if (getRealResponse().wasRendered(innerItem)) {
            return NoHeaderItem.get();
        }

        HeaderItem bundle = Application.get().getResourceBundles().findBundle(innerItem);
        if (bundle == null) {
            return item;
        }

        bundle = preserveDetails(item, bundle);

        if (item instanceof IWrappedHeaderItem) {
            bundle = ((IWrappedHeaderItem) item).wrap(bundle);
        }
        return bundle;
    }

    /**
     * Preserves the resource reference details for resource replacements.
     *
     * For example if CSS resource with media <em>screen</em> is replaced with
     * {@link org.apache.wicket.protocol.http.WebApplication#addResourceReplacement(org.apache.wicket.request.resource.CssResourceReference, org.apache.wicket.request.resource.ResourceReference)} then the replacement will
     * will inherit the media attribute
     *
     * @param item   The replaced header item
     * @param bundle The bundle that represents the replacement
     * @return the bundle with the preserved details
     */
    protected HeaderItem preserveDetails(HeaderItem item, HeaderItem bundle) {
        HeaderItem resultBundle;
        if (item instanceof CssReferenceHeaderItem && bundle instanceof CssReferenceHeaderItem) {
            CssReferenceHeaderItem originalHeaderItem = (CssReferenceHeaderItem) item;
            resultBundle = preserveCssDetails(originalHeaderItem, (CssReferenceHeaderItem) bundle);
        } else if (item instanceof JavaScriptReferenceHeaderItem
                && bundle instanceof JavaScriptReferenceHeaderItem) {
            JavaScriptReferenceHeaderItem originalHeaderItem = (JavaScriptReferenceHeaderItem) item;
            resultBundle = preserveJavaScriptDetails(originalHeaderItem, (JavaScriptReferenceHeaderItem) bundle);
        } else {
            resultBundle = bundle;
        }

        return resultBundle;
    }

    /**
     * Preserves the resource reference details for JavaScript resource replacements.
     *
     * For example if CSS resource with media <em>screen</em> is replaced with
     * {@link org.apache.wicket.protocol.http.WebApplication#addResourceReplacement(org.apache.wicket.request.resource.JavaScriptResourceReference, org.apache.wicket.request.resource.ResourceReference)} then the replacement will
     * will inherit the media attribute
     *
     * @param item   The replaced header item
     * @param bundle The bundle that represents the replacement
     * @return the bundle with the preserved details
     */
    private HeaderItem preserveJavaScriptDetails(JavaScriptReferenceHeaderItem item,
            JavaScriptReferenceHeaderItem bundle) {
        HeaderItem resultBundle;
        ResourceReference bundleReference = bundle.getReference();
        if (bundleReference instanceof ReplacementResourceBundleReference) {
            resultBundle = JavaScriptHeaderItem.forReference(bundleReference, item.getPageParameters(),
                    item.getId(), item.isDefer(), item.getCharset(), item.getCondition());
        } else {
            resultBundle = bundle;
        }
        return resultBundle;
    }

    /**
     * Preserves the resource reference details for CSS resource replacements.
     *
     * For example if CSS resource with media <em>screen</em> is replaced with
     * {@link org.apache.wicket.protocol.http.WebApplication#addResourceReplacement(org.apache.wicket.request.resource.CssResourceReference, org.apache.wicket.request.resource.ResourceReference)} then the replacement will
     * will inherit the media attribute
     *
     * @param item   The replaced header item
     * @param bundle The bundle that represents the replacement
     * @return the bundle with the preserved details
     */
    protected HeaderItem preserveCssDetails(CssReferenceHeaderItem item, CssReferenceHeaderItem bundle) {
        HeaderItem resultBundle;
        ResourceReference bundleReference = bundle.getReference();
        if (bundleReference instanceof ReplacementResourceBundleReference) {
            resultBundle = CssHeaderItem.forReference(bundleReference, item.getPageParameters(), item.getMedia(),
                    item.getCondition());
        } else {
            resultBundle = bundle;
        }
        return resultBundle;
    }
}