| /* |
| * 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.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<>() |
| { |
| 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 |
| */ |
| @SuppressWarnings("resource") |
| 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); |
| } |
| }; |
| |
| headerRenderer = decorate(headerRenderer); |
| |
| for (HeaderItem curItem : resp) |
| { |
| headerRenderer.render(curItem); |
| } |
| |
| headerRenderer.close(); |
| |
| return strResponse.getBuffer(); |
| } |
| |
| /** |
| * Decorate the given response used to get contents. |
| * |
| * @param response |
| * response to decorate |
| * @return default implementation just returns the response |
| */ |
| protected IHeaderResponse decorate(IHeaderResponse response) |
| { |
| return response; |
| } |
| |
| 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); |
| } |
| } |