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.util.file;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.Set;
024
025import javax.servlet.FilterConfig;
026import javax.servlet.ServletContext;
027import javax.xml.parsers.DocumentBuilder;
028import javax.xml.parsers.DocumentBuilderFactory;
029import javax.xml.parsers.ParserConfigurationException;
030
031import org.apache.wicket.util.xml.CustomEntityResolver;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034import org.w3c.dom.Document;
035import org.w3c.dom.Node;
036import org.w3c.dom.NodeList;
037import org.xml.sax.SAXException;
038
039/**
040 * A utility class providing helper methods in dealing with web.xml
041 * 
042 * @author jcompagner
043 * @author Juergen Donnerstag
044 */
045public class WebXmlFile
046{
047        private static final Logger log = LoggerFactory.getLogger(WebXmlFile.class);
048
049        /**
050         * Construct.
051         */
052        public WebXmlFile()
053        {
054        }
055
056        /**
057         * Gets unique Wicket filter path via FilterConfig
058         * 
059         * @param isServlet
060         *            true if Servlet, false if Filter
061         * @param filterConfig
062         * @return Filter path retrieved from "url-pattern". Null if not found or error occurred
063         */
064        public final String getUniqueFilterPath(final boolean isServlet, final FilterConfig filterConfig)
065        {
066                String filterName = filterConfig.getFilterName();
067                Set<String> paths = getFilterPath(isServlet, filterConfig.getServletContext(), filterName);
068                return uniquePath(paths, isServlet, filterName);
069        }
070
071        /**
072         * Gets Wicket filter path via ServletContext and the filter name
073         * 
074         * @param isServlet
075         *            true if Servlet, false if Filter
076         * @param servletContext
077         * @param filterName
078         * @return Filter paths retrieved from "url-pattern"
079         */
080        public final Set<String> getFilterPath(final boolean isServlet,
081                final ServletContext servletContext, final String filterName)
082        {
083                InputStream is = servletContext.getResourceAsStream("/WEB-INF/web.xml");
084                if (is != null)
085                {
086                        try
087                        {
088                                return getFilterPath(isServlet, filterName, is);
089                        }
090                        catch (ParserConfigurationException | SAXException | IOException ex)
091                        {
092                                log.error("Error reading servlet/filter path from web.xml", ex);
093                        }
094                        catch (SecurityException e)
095                        {
096                                // Swallow this at INFO.
097                                log.info("Couldn't read web.xml to automatically pick up servlet/filter path: " +
098                                        e.getMessage());
099                        }
100                }
101                return Collections.emptySet();
102        }
103
104        /**
105         * Gets unique filter path via filter name and InputStream.
106         * 
107         * @param isServlet
108         *            true if Servlet, false if Filter
109         * @param filterName
110         * @param is
111         *            The web.xml file
112         * @return Filter path retrieved from "url-pattern". Null if not found.
113         * @throws ParserConfigurationException
114         * @throws IOException
115         * @throws SAXException
116         * 
117         * @see #getFilterPath(boolean, String, java.io.InputStream)
118         */
119        public final String getUniqueFilterPath(final boolean isServlet, final String filterName,
120                final InputStream is) throws ParserConfigurationException, SAXException, IOException
121        {
122                return uniquePath(getFilterPath(isServlet, filterName, is), isServlet, filterName);
123        }
124
125        /**
126         * return unique path from set of paths
127         * 
128         * @param paths
129         *            <code>null</code> if set was empty, unique path if set was of length == 1
130         * @param isServlet
131         * @param filterName
132         * @return unique path
133         * @throws RuntimeException
134         *             in case length > 1
135         */
136        private String uniquePath(final Set<String> paths, final boolean isServlet,
137                final String filterName)
138        {
139                if (paths.size() > 1)
140                {
141                        StringBuilder err = new StringBuilder();
142                        err.append("web.xml: expected one ");
143                        err.append(isServlet ? "servlet" : "filter");
144                        err.append(" path for [");
145                        err.append(filterName);
146                        err.append("] but found multiple:");
147
148                        for (String path : paths)
149                        {
150                                err.append(" [").append(path).append(']');
151                        }
152                        throw new RuntimeException(err.toString());
153                }
154
155                if (paths.size() == 1)
156                {
157                        return paths.iterator().next();
158                }
159                return null;
160        }
161
162        /**
163         * Gets Wicket filter path via filter name and InputStream. The InputStream is assumed to be an
164         * web.xml file.
165         * <p>
166         * A typical Wicket web.xml entry looks like:
167         * 
168         * <pre>
169         * <code>
170         * &lt;filter&gt;
171         *   &lt;filter-name&gt;HelloWorldApplication&lt;/filter-name&gt;
172         *   &lt;filter-class&gt;org.apache.wicket.protocol.http.WicketFilter&lt;/filter-class&gt;
173         *   &lt;init-param&gt;
174         *     &lt;param-name&gt;applicationClassName&lt;/param-name&gt;
175         *     &lt;param-value&gt;org.apache.wicket.examples.helloworld.HelloWorldApplication&lt;/param-value&gt;
176         *   &lt;/init-param&gt;
177         * &lt;/filter&gt;
178         * 
179         * &lt;filter-mapping&gt;
180         *   &lt;filter-name&gt;HelloWorldApplication&lt;/filter-name&gt;
181         *   &lt;url-pattern&gt;/helloworld/*&lt;/url-pattern&gt;
182         *   &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
183         *   &lt;dispatcher&gt;INCLUDE&lt;/dispatcher&gt;
184         * &lt;/filter-mapping&gt;
185         * </code>
186         * </pre>
187         * 
188         * @param isServlet
189         *            true if Servlet, false if Filter
190         * @param filterName
191         * @param is
192         *            The web.xml file
193         * @return Filter paths retrieved from "url-pattern".
194         * @throws ParserConfigurationException
195         * @throws IOException
196         * @throws SAXException
197         */
198        public final Set<String> getFilterPath(final boolean isServlet, final String filterName,
199                final InputStream is) throws ParserConfigurationException, SAXException, IOException
200        {
201                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
202                DocumentBuilder builder = factory.newDocumentBuilder();
203
204                // try to pull DTD from local set of entities
205                builder.setEntityResolver(CustomEntityResolver.getPreloaded());
206                Document document = builder.parse(is);
207
208                String tag = (isServlet ? "servlet" : "filter");
209                String mapping = tag + "-mapping";
210                String name = tag + "-name";
211
212                Set<String> urlPatterns = getFilterPaths(filterName, mapping, name,
213                        document.getChildNodes());
214
215                if (urlPatterns.size() == 0)
216                {
217                        log.warn("web.xml: No url-pattern found for '{}' with name '{}'", tag, filterName);
218                }
219
220                if (log.isInfoEnabled())
221                {
222                        StringBuilder msg = new StringBuilder();
223                        msg.append("web.xml: url mapping found for ")
224                                .append(tag)
225                                .append(" with name ")
226                                .append(filterName)
227                                .append(':');
228
229                        for (String urlPattern : urlPatterns)
230                        {
231                                msg.append(" [");
232                                msg.append(urlPattern);
233                                msg.append(']');
234                        }
235                        log.info(msg.toString());
236                }
237                Set<String> stripped = new HashSet<>(urlPatterns.size());
238
239                for (String urlPattern : urlPatterns)
240                {
241                        stripped.add(urlPattern.substring(1, urlPattern.length() - 1));
242                }
243                return stripped;
244        }
245
246        /**
247         * Iterate through all children of 'node' and search for a node with name "filterName". Return
248         * the value of node "url-pattern" if "filterName" was found.
249         * 
250         * @param filterName
251         * @param name
252         * @param node
253         * @return value of node "url-pattern"
254         */
255        private Set<String> getFilterPaths(final String filterName, final String name, final Node node)
256        {
257                Set<String> paths = new HashSet<>();
258                String foundUrlPattern = null;
259                String foundFilterName = null;
260
261                for (int i = 0; i < node.getChildNodes().getLength(); ++i)
262                {
263                        Node n = node.getChildNodes().item(i);
264
265                        if (name.equals(n.getNodeName()))
266                        {
267                                foundFilterName = n.getTextContent();
268                        }
269                        else if ("url-pattern".equals(n.getNodeName()))
270                        {
271                                foundUrlPattern = n.getTextContent();
272                        }
273
274                        if (foundFilterName != null)
275                        {
276                                foundFilterName = foundFilterName.trim();
277                        }
278
279                        if (filterName.equals(foundFilterName))
280                        {
281                                if (foundUrlPattern != null)
282                                {
283                                        paths.add(foundUrlPattern.trim());
284                                }
285                        }
286                }
287                return paths;
288        }
289
290        /**
291         * Find a node with name 'mapping' within 'nodeList' and if found continue to search amongst its
292         * children for a node with 'filterName' and "url-pattern'
293         * 
294         * 
295         * @param filterName
296         * @param mapping
297         * @param name
298         * @param nodeList
299         * @return The value assigned to node "url-pattern"
300         */
301        private Set<String> getFilterPaths(final String filterName, final String mapping,
302                final String name, final NodeList nodeList)
303        {
304                Set<String> paths = new HashSet<>(1);
305
306                for (int i = 0; i < nodeList.getLength(); ++i)
307                {
308                        Node node = nodeList.item(i);
309
310                        if (mapping.equals(node.getNodeName()))
311                        {
312                                paths.addAll(getFilterPaths(filterName, name, node));
313                        }
314                        else
315                        {
316                                paths.addAll(getFilterPaths(filterName, mapping, name, node.getChildNodes()));
317                        }
318                }
319                return paths;
320        }
321}