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 &lt;head&gt;
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}