/* * 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.filter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; import org.apache.wicket.MetaDataKey; import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; import org.apache.wicket.markup.head.HeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.ResourceAggregator; import org.apache.wicket.markup.head.internal.HeaderResponse; import org.apache.wicket.markup.html.DecoratingHeaderResponse; import org.apache.wicket.request.Response; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.response.StringResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This header response allows you to separate things that are added to the IHeaderResponse into * different buckets. Then, you can render those different buckets in separate areas of the page * based on your filter logic. A typical use case for this header response is to move the loading of * JavaScript files (and inline script tags) to the footer of the page. * * @see HeaderResponseContainer * @see CssAcceptingHeaderResponseFilter * @see JavaScriptAcceptingHeaderResponseFilter * @author Jeremy Thomerson * @author Emond Papegaaij */ public class FilteringHeaderResponse extends DecoratingHeaderResponse { private static final Logger log = LoggerFactory.getLogger(FilteringHeaderResponse.class); /** * The default name of the filter that will collect contributions which should be rendered * in the page's <head> */ public static final String DEFAULT_HEADER_FILTER_NAME = "wicket-default-header-filter"; /** * A filter used to bucket your resources, inline scripts, etc, into different responses. The * bucketed resources are then rendered by a {@link HeaderResponseContainer}, using the name of * the filter to get the correct bucket. * * @author Jeremy Thomerson */ public interface IHeaderResponseFilter extends Predicate<HeaderItem> { /** * @return name of the filter (used by the container that renders these resources) */ String getName(); /** * Determines whether a given HeaderItem should be rendered in the bucket represented by * this filter. * * @param item * the item to be rendered * @return true if it should be bucketed with other things in this filter */ boolean accepts(HeaderItem item); @Override default boolean test(HeaderItem item) { return accepts(item); } } /** * we store this FilteringHeaderResponse in the RequestCycle so that the containers can access * it to render their bucket of stuff */ private static final MetaDataKey<FilteringHeaderResponse> RESPONSE_KEY = new MetaDataKey<FilteringHeaderResponse>() { private static final long serialVersionUID = 1L; }; private final Map<String, List<HeaderItem>> responseFilterMap = new HashMap<String, List<HeaderItem>>(); private Iterable<? extends IHeaderResponseFilter> filters; private final String headerFilterName; /** * Constructor without explicit filters. * * Generates filters automatically for any FilteredHeaderItem. * Any other contribution is rendered in the page's <head> * * @param response * the wrapped IHeaderResponse * @see HeaderResponseContainer */ public FilteringHeaderResponse(IHeaderResponse response) { this(response, DEFAULT_HEADER_FILTER_NAME, Collections.<IHeaderResponseFilter>emptyList()); } /** * Construct. * * @param response * the wrapped IHeaderResponse * @param headerFilterName * the name that the filter for things that should appear in the head (default Wicket * location) uses * @param filters * the filters to use to bucket things. There will be a bucket created for each * filter, by name. There should typically be at least one filter with the same name * as your headerFilterName */ public FilteringHeaderResponse(IHeaderResponse response, String headerFilterName, Iterable<? extends IHeaderResponseFilter> filters) { super(response); this.headerFilterName = headerFilterName; setFilters(filters); RequestCycle.get().setMetaData(RESPONSE_KEY, this); } protected void setFilters(Iterable<? extends IHeaderResponseFilter> filters) { this.filters = filters; if (filters == null) { return; } for (IHeaderResponseFilter filter : filters) { responseFilterMap.put(filter.getName(), new ArrayList<HeaderItem>()); } } /** * @return the FilteringHeaderResponse being used in this RequestCycle */ public static FilteringHeaderResponse get() { RequestCycle requestCycle = RequestCycle.get(); if (requestCycle == null) { throw new IllegalStateException( "You can only get the FilteringHeaderResponse when there is a RequestCycle present"); } FilteringHeaderResponse response = requestCycle.getMetaData(RESPONSE_KEY); if (response == null) { throw new IllegalStateException( "No FilteringHeaderResponse is present in the request cycle. This may mean that you have not decorated the header response with a FilteringHeaderResponse. Simply calling the FilteringHeaderResponse constructor sets itself on the request cycle"); } return response; } @Override public void render(HeaderItem item) { if (item instanceof FilteredHeaderItem) { String filterName = ((FilteredHeaderItem)item).getFilterName(); if (responseFilterMap.containsKey(filterName) == false) { responseFilterMap.put(filterName, new ArrayList<HeaderItem>()); } render(item, filterName); } else { if (filters != null) { for (IHeaderResponseFilter filter : filters) { if (filter.accepts(item)) { render(item, filter.getName()); return; } } } // none of the configured filters accepted it so put it in the header if (responseFilterMap.containsKey(headerFilterName) == false) { responseFilterMap.put(headerFilterName, new ArrayList<HeaderItem>()); } render(item, headerFilterName); log.debug("A HeaderItem '{}' was rendered to the filtering header response, but did not match any filters, so it put in the <head>.", item); } } @Override public void close() { // write the stuff that was actually supposed to be in the header to the // response, which is used by the built-in HtmlHeaderContainer to get // its contents CharSequence headerContent = getContent(headerFilterName); RequestCycle.get().getResponse().write(headerContent); // must make sure our super (and with it, the wrapped response) get closed: super.close(); } /** * Gets the content that was rendered to this header response and matched the filter with the * given name. * * @param filterName * the name of the filter to get the bucket for * @return the content that was accepted by the filter with this name */ public final CharSequence getContent(String filterName) { if (filterName == null || !responseFilterMap.containsKey(filterName)) { return ""; } List<HeaderItem> resp = responseFilterMap.get(filterName); final StringResponse strResponse = new StringResponse(); IHeaderResponse headerRenderer = new HeaderResponse() { @Override protected Response getRealResponse() { return strResponse; } @Override public boolean wasRendered(Object object) { return FilteringHeaderResponse.this.getRealResponse().wasRendered(object); } @Override public void markRendered(Object object) { FilteringHeaderResponse.this.getRealResponse().markRendered(object); } }; ResourceAggregator resourceAggregator = new ResourceAggregator(headerRenderer); for (HeaderItem curItem : resp) { resourceAggregator.render(curItem); } resourceAggregator.close(); return strResponse.getBuffer(); } private void render(HeaderItem item, String filterName) { if (responseFilterMap.containsKey(filterName) == false) { throw new IllegalArgumentException("No filter named '" + filterName + "', known filter names are: " + responseFilterMap.keySet()); } render(item, responseFilterMap.get(filterName)); } protected void render(HeaderItem item, List<HeaderItem> filteredItems) { if (RequestCycle.get().find(IPartialPageRequestHandler.class).isPresent()) { // we're in an ajax request, so we don't filter and separate stuff.... getRealResponse().render(item); return; } filteredItems.add(item); } }