/*
* 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);
}
}