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.filter; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.function.Predicate; 025 026import org.apache.wicket.MetaDataKey; 027import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; 028import org.apache.wicket.markup.head.HeaderItem; 029import org.apache.wicket.markup.head.IHeaderResponse; 030import org.apache.wicket.markup.head.internal.HeaderResponse; 031import org.apache.wicket.markup.html.DecoratingHeaderResponse; 032import org.apache.wicket.request.Response; 033import org.apache.wicket.request.cycle.RequestCycle; 034import org.apache.wicket.response.LazyStringResponse; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038/** 039 * This header response allows you to separate things that are added to the IHeaderResponse into 040 * different buckets. Then, you can render those different buckets in separate areas of the page 041 * based on your filter logic. A typical use case for this header response is to move the loading of 042 * JavaScript files (and inline script tags) to the footer of the page. 043 * 044 * @see HeaderResponseContainer 045 * @see CssAcceptingHeaderResponseFilter 046 * @see JavaScriptAcceptingHeaderResponseFilter 047 * @author Jeremy Thomerson 048 * @author Emond Papegaaij 049 */ 050public class FilteringHeaderResponse extends DecoratingHeaderResponse 051{ 052 053 private static final Logger log = LoggerFactory.getLogger(FilteringHeaderResponse.class); 054 055 /** 056 * The default name of the filter that will collect contributions which should be rendered 057 * in the page's <head> 058 */ 059 public static final String DEFAULT_HEADER_FILTER_NAME = "wicket-default-header-filter"; 060 061 /** 062 * A filter used to bucket your resources, inline scripts, etc, into different responses. The 063 * bucketed resources are then rendered by a {@link HeaderResponseContainer}, using the name of 064 * the filter to get the correct bucket. 065 * 066 * @author Jeremy Thomerson 067 */ 068 public interface IHeaderResponseFilter extends Predicate<HeaderItem> 069 { 070 /** 071 * @return name of the filter (used by the container that renders these resources) 072 */ 073 String getName(); 074 075 /** 076 * Determines whether a given HeaderItem should be rendered in the bucket represented by 077 * this filter. 078 * 079 * @param item 080 * the item to be rendered 081 * @return true if it should be bucketed with other things in this filter 082 */ 083 boolean accepts(HeaderItem item); 084 085 @Override 086 default boolean test(HeaderItem item) { 087 return accepts(item); 088 } 089 } 090 091 /** 092 * we store this FilteringHeaderResponse in the RequestCycle so that the containers can access 093 * it to render their bucket of stuff 094 */ 095 private static final MetaDataKey<FilteringHeaderResponse> RESPONSE_KEY = new MetaDataKey<>() 096 { 097 private static final long serialVersionUID = 1L; 098 }; 099 100 private final Map<String, List<HeaderItem>> responseFilterMap = new HashMap<String, List<HeaderItem>>(); 101 private Iterable<? extends IHeaderResponseFilter> filters; 102 private final String headerFilterName; 103 104 /** 105 * Constructor without explicit filters. 106 * 107 * Generates filters automatically for any FilteredHeaderItem. 108 * Any other contribution is rendered in the page's <head> 109 * 110 * @param response 111 * the wrapped IHeaderResponse 112 * @see HeaderResponseContainer 113 */ 114 public FilteringHeaderResponse(IHeaderResponse response) 115 { 116 this(response, DEFAULT_HEADER_FILTER_NAME, Collections.<IHeaderResponseFilter>emptyList()); 117 } 118 119 /** 120 * Construct. 121 * 122 * @param response 123 * the wrapped IHeaderResponse 124 * @param headerFilterName 125 * the name that the filter for things that should appear in the head (default Wicket 126 * location) uses 127 * @param filters 128 * the filters to use to bucket things. There will be a bucket created for each 129 * filter, by name. There should typically be at least one filter with the same name 130 * as your headerFilterName 131 */ 132 public FilteringHeaderResponse(IHeaderResponse response, String headerFilterName, 133 Iterable<? extends IHeaderResponseFilter> filters) 134 { 135 super(response); 136 this.headerFilterName = headerFilterName; 137 138 setFilters(filters); 139 140 RequestCycle.get().setMetaData(RESPONSE_KEY, this); 141 } 142 143 protected void setFilters(Iterable<? extends IHeaderResponseFilter> filters) 144 { 145 this.filters = filters; 146 if (filters == null) 147 { 148 return; 149 } 150 for (IHeaderResponseFilter filter : filters) 151 { 152 responseFilterMap.put(filter.getName(), new ArrayList<HeaderItem>()); 153 } 154 } 155 156 /** 157 * @return the FilteringHeaderResponse being used in this RequestCycle 158 */ 159 public static FilteringHeaderResponse get() 160 { 161 RequestCycle requestCycle = RequestCycle.get(); 162 if (requestCycle == null) 163 { 164 throw new IllegalStateException( 165 "You can only get the FilteringHeaderResponse when there is a RequestCycle present"); 166 } 167 FilteringHeaderResponse response = requestCycle.getMetaData(RESPONSE_KEY); 168 if (response == null) 169 { 170 throw new IllegalStateException( 171 "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"); 172 } 173 return response; 174 } 175 176 @Override 177 public void render(HeaderItem item) 178 { 179 if (item instanceof FilteredHeaderItem) 180 { 181 String filterName = ((FilteredHeaderItem)item).getFilterName(); 182 183 if (responseFilterMap.containsKey(filterName) == false) 184 { 185 responseFilterMap.put(filterName, new ArrayList<HeaderItem>()); 186 } 187 188 render(item, filterName); 189 } 190 else 191 { 192 if (filters != null) 193 { 194 for (IHeaderResponseFilter filter : filters) 195 { 196 if (filter.accepts(item)) 197 { 198 render(item, filter.getName()); 199 return; 200 } 201 } 202 } 203 204 // none of the configured filters accepted it so put it in the header 205 if (responseFilterMap.containsKey(headerFilterName) == false) 206 { 207 responseFilterMap.put(headerFilterName, new ArrayList<HeaderItem>()); 208 } 209 render(item, headerFilterName); 210 log.debug("A HeaderItem '{}' was rendered to the filtering header response, but did not match any filters, so it put in the <head>.", 211 item); 212 } 213 } 214 215 @Override 216 public void close() 217 { 218 // write the stuff that was actually supposed to be in the header to the 219 // response, which is used by the built-in HtmlHeaderContainer to get 220 // its contents 221 222 CharSequence headerContent = getContent(headerFilterName); 223 RequestCycle.get().getResponse().write(headerContent); 224 // must make sure our super (and with it, the wrapped response) get closed: 225 super.close(); 226 } 227 228 /** 229 * Gets the content that was rendered to this header response and matched the filter with the 230 * given name. 231 * 232 * @param filterName 233 * the name of the filter to get the bucket for 234 * @return the content that was accepted by the filter with this name 235 */ 236 @SuppressWarnings("resource") 237 public final CharSequence getContent(String filterName) 238 { 239 if (filterName == null || !responseFilterMap.containsKey(filterName)) 240 { 241 return ""; 242 } 243 List<HeaderItem> resp = responseFilterMap.get(filterName); 244 final LazyStringResponse strResponse = new LazyStringResponse(); 245 IHeaderResponse headerRenderer = new HeaderResponse() 246 { 247 @Override 248 protected Response getRealResponse() 249 { 250 return strResponse; 251 } 252 253 @Override 254 public boolean wasRendered(Object object) 255 { 256 return FilteringHeaderResponse.this.getRealResponse().wasRendered(object); 257 } 258 259 @Override 260 public void markRendered(Object object) 261 { 262 FilteringHeaderResponse.this.getRealResponse().markRendered(object); 263 } 264 }; 265 266 headerRenderer = decorate(headerRenderer); 267 268 for (HeaderItem curItem : resp) 269 { 270 headerRenderer.render(curItem); 271 } 272 273 headerRenderer.close(); 274 275 return strResponse.getBuffer(); 276 } 277 278 /** 279 * Decorate the given response used to get contents. 280 * 281 * @param response 282 * response to decorate 283 * @return default implementation just returns the response 284 */ 285 protected IHeaderResponse decorate(IHeaderResponse response) 286 { 287 return response; 288 } 289 290 private void render(HeaderItem item, String filterName) 291 { 292 if (responseFilterMap.containsKey(filterName) == false) 293 { 294 throw new IllegalArgumentException("No filter named '" + filterName + 295 "', known filter names are: " + responseFilterMap.keySet()); 296 } 297 render(item, responseFilterMap.get(filterName)); 298 } 299 300 protected void render(HeaderItem item, List<HeaderItem> filteredItems) 301 { 302 if (RequestCycle.get().find(IPartialPageRequestHandler.class).isPresent()) 303 { 304 // we're in an ajax request, so we don't filter and separate stuff.... 305 getRealResponse().render(item); 306 return; 307 } 308 filteredItems.add(item); 309 } 310}