All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.adminfaces.template.util.WebXml Maven / Gradle / Ivy

/*
 * The MIT License
 *
 * Copyright 2018 rmpestano.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.adminfaces.template.util;

import static org.omnifaces.util.Faces.getServletContext;
import static org.omnifaces.util.Faces.hasContext;
import static org.omnifaces.util.Utils.isEmpty;
import static org.omnifaces.util.Utils.isNumber;
import static org.omnifaces.util.Xml.createDocument;
import static org.omnifaces.util.Xml.getNodeList;
import static org.omnifaces.util.Xml.getTextContent;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.faces.context.FacesContext;
import javax.faces.webapp.FacesServlet;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextListener;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * COPIED from OmniFaces because when users update to OmniFaces 3.x they get java.lang.IncompatibleClassChangeError: Found interface org.omnifaces.config.WebXml, but class was expected
 * because AdminTemplate depends on OmniFaces 2.1 and the method used by template (findErrorPages) has changed in OmniFaces 3.x
 * 
 * 

* This configuration enum parses the /WEB-INF/web.xml and all /META-INF/web-fragment files * found in the classpath and offers methods to obtain information from them which is not available by the standard * Servlet API. * *

Usage

*

* Some examples: *

 * // Get the <welcome-file-list> (which are essentially path-relative filenames which needs to be served when a folder is requested).
 * List<String> welcomeFiles = WebXml.INSTANCE.getWelcomeFiles();
 * 
*
 * // Get a mapping of all error page locations by exception type (a key of null represents the default error page location, if any).
 * Map<Class<Throwable>, String> errorPageLocations = WebXml.INSTANCE.getErrorPageLocations();
 * 
*
 * // Get the <form-login-page> (which is a context-relative URL to the login page of FORM based authentication).
 * String formLoginPage = WebXml.INSTANCE.getFormLoginPage();
 * 
*
 * // Get a mapping of all <security-constraint> URL patterns and associated roles.
 * Map<String, Set<String>> securityConstraints = WebXml.INSTANCE.getSecurityConstraints();
 * 
*
 * // Check if access to certain (context-relative) URL is allowed for the given role based on <security-constraint>.
 * boolean accessAllowed = WebXml.INSTANCE.isAccessAllowed("/admin.xhtml", "admin");
 * 
*
 * // Get web.xml configured session timeout (in seconds).
 * int sessionTimeout = WebXml.INSTANCE.getSessionTimeout();
 * 
* * @author Bauke Scholtz * @since 1.2 */ public enum WebXml { // Enum singleton ------------------------------------------------------------------------------------------------- /** * Returns the lazily loaded enum singleton instance. *

* Note: if this is needed in e.g. a {@link Filter} which is called before the {@link FacesServlet} is invoked, * then it won't work if the INSTANCE hasn't been referenced before. Since JSF installs a special * "init" {@link FacesContext} during startup, one option for doing this initial referencing is in a * {@link ServletContextListener}. The data this enum encapsulates will then be available even where there is no * {@link FacesContext} available. If there's no other option, then you need to manually invoke * {@link #init(ServletContext)} whereby you pass the desired {@link ServletContext}. */ INSTANCE; // Private constants ---------------------------------------------------------------------------------------------- private static final Logger logger = Logger.getLogger(WebXml.class.getName()); private static final String WEB_XML = "/WEB-INF/web.xml"; private static final String WEB_FRAGMENT_XML = "META-INF/web-fragment.xml"; private static final String XPATH_WELCOME_FILE = "welcome-file-list/welcome-file"; private static final String XPATH_EXCEPTION_TYPE = "error-page/exception-type"; private static final String XPATH_LOCATION = "location"; private static final String XPATH_ERROR_PAGE_500_LOCATION = "error-page[error-code=500]/location"; private static final String XPATH_ERROR_PAGE_DEFAULT_LOCATION = "error-page[not(error-code) and not(exception-type)]/location"; private static final String XPATH_FORM_LOGIN_PAGE = "login-config[auth-method='FORM']/form-login-config/form-login-page"; private static final String XPATH_FORM_ERROR_PAGE = "login-config[auth-method='FORM']/form-login-config/form-error-page"; private static final String XPATH_SECURITY_CONSTRAINT = "security-constraint"; private static final String XPATH_WEB_RESOURCE_URL_PATTERN = "web-resource-collection/url-pattern"; private static final String XPATH_AUTH_CONSTRAINT = "auth-constraint"; private static final String XPATH_AUTH_CONSTRAINT_ROLE_NAME = "auth-constraint/role-name"; private static final String XPATH_SESSION_TIMEOUT = "session-config/session-timeout"; private static final String ERROR_NOT_INITIALIZED = "WebXml is not initialized yet. Please use #init(ServletContext) method to manually initialize it."; private static final String ERROR_URL_MUST_START_WITH_SLASH = "URL must start with '/': '%s'"; private static final String LOG_INITIALIZATION_ERROR = "WebXml failed to initialize. Perhaps your web.xml contains a typo?"; // Properties ----------------------------------------------------------------------------------------------------- private final AtomicBoolean initialized = new AtomicBoolean(); private List welcomeFiles; private Map, String> errorPageLocations; private String formLoginPage; private String formErrorPage; private Map> securityConstraints; private int sessionTimeout; // Init ----------------------------------------------------------------------------------------------------------- /** * Perform automatic initialization whereby the servlet context is obtained from the faces context. */ private void init() { if (!initialized.get() && hasContext()) { init(getServletContext()); } } /** * Perform manual initialization with the given servlet context, if not null and not already initialized yet. * @param servletContext The servlet context to obtain the web.xml from. * @return The current {@link WebXml} instance, initialized and all. */ public WebXml init(ServletContext servletContext) { if (servletContext != null && !initialized.getAndSet(true)) { try { Element webXml = loadWebXml(servletContext).getDocumentElement(); XPath xpath = XPathFactory.newInstance().newXPath(); welcomeFiles = parseWelcomeFiles(webXml, xpath); errorPageLocations = parseErrorPageLocations(webXml, xpath); formLoginPage = parseFormLoginPage(webXml, xpath); formErrorPage = parseFormErrorPage(webXml, xpath); securityConstraints = parseSecurityConstraints(webXml, xpath); sessionTimeout = parseSessionTimeout(webXml, xpath); } catch (Exception e) { initialized.set(false); logger.log(Level.SEVERE, LOG_INITIALIZATION_ERROR, e); throw new UnsupportedOperationException(e); } } return this; } // Actions -------------------------------------------------------------------------------------------------------- /** * Find for the given exception the right error page location. Exception types are matched as per Servlet 3.0 * specification 10.9.2: *

    *
  • Make a first pass through all specific exception types. If an exact match is found, use its location. *
  • Else make a second pass through all specific exception types in the order as they are declared in * web.xml. If the current exception is an instance of it, then use its location. *
  • Else use the default error page location, which can be either the java.lang.Throwable or HTTP 500 or * default one. *
* @param exception The exception to find the error page location for. * @return The right error page location for the given exception. */ public String findErrorPageLocation(Throwable exception) { checkInitialized(); for (Entry, String> entry : errorPageLocations.entrySet()) { if (entry.getKey() == exception.getClass()) { return entry.getValue(); } } for (Entry, String> entry : errorPageLocations.entrySet()) { if (entry.getKey() != null && entry.getKey().isInstance(exception)) { return entry.getValue(); } } return errorPageLocations.get(null); } /** * Returns true if access to the given URL is allowed for the given role. URL patterns are matched as * per Servlet 3.0 specification 12.1: *
    *
  • Make a first pass through all URL patterns. If an exact match is found, then check the role on it. *
  • Else make a recursive pass through all prefix URL patterns, stepping down the URL one directory at a time, * trying to find the longest path match. If it is found, then check the role on it. *
  • Else if the last segment in the URL path contains an extension, then make a last pass through all suffix * URL patterns. If a match is found, then check the role on it. *
  • Else assume it as unprotected resource and return true. *
* @param url URL to be checked for access by the given role. It must start with '/' and be context-relative. * @param role Role to be checked for access to the given URL. * @return true if access to the given URL is allowed for the given role, otherwise false. * @throws NullPointerException If given URL is null. * @throws IllegalArgumentException If given URL does not start with '/'. * @since 1.4 */ public boolean isAccessAllowed(String url, String role) { checkInitialized(); if (url.charAt(0) != ('/')) { throw new IllegalArgumentException(String.format(ERROR_URL_MUST_START_WITH_SLASH, url)); } String uri = url; if (url.length() > 1 && url.charAt(url.length() - 1) == '/') { uri = url.substring(0, url.length() - 1); // Trim trailing slash. } Set roles = findExactMatchRoles(uri); if (roles == null) { roles = findPrefixMatchRoles(uri); } if (roles == null) { roles = findSuffixMatchRoles(uri); } return isRoleMatch(roles, role); } private Set findExactMatchRoles(String url) { for (Entry> entry : securityConstraints.entrySet()) { if (isExactMatch(entry.getKey(), url)) { return entry.getValue(); } } return null; } private Set findPrefixMatchRoles(String url) { for (String path = url, urlMatch = ""; !path.isEmpty(); path = path.substring(0, path.lastIndexOf('/'))) { Set roles = null; for (Entry> entry : securityConstraints.entrySet()) { if (urlMatch.length() < entry.getKey().length() && isPrefixMatch(entry.getKey(), path)) { urlMatch = entry.getKey(); roles = entry.getValue(); } } if (roles != null) { return roles; } } return null; } private Set findSuffixMatchRoles(String url) { if (url.contains(".")) { for (Entry> entry : securityConstraints.entrySet()) { if (isSuffixMatch(url, entry.getKey())) { return entry.getValue(); } } } return null; } private static boolean isExactMatch(String urlPattern, String url) { return url.equals(urlPattern.endsWith("/*") ? urlPattern.substring(0, urlPattern.length() - 2) : urlPattern); } private static boolean isPrefixMatch(String urlPattern, String url) { return urlPattern.endsWith("/*") ? url.startsWith(urlPattern.substring(0, urlPattern.length() - 2)) : false; } private static boolean isSuffixMatch(String urlPattern, String url) { return urlPattern.startsWith("*.") ? url.endsWith(urlPattern.substring(1)) : false; } private static boolean isRoleMatch(Set roles, String role) { return roles == null || roles.contains(role) || (role != null && roles.contains("*")); } // Getters -------------------------------------------------------------------------------------------------------- /** * Returns a list of all welcome files. * @return A list of all welcome files. * @since 1.4 */ public List getWelcomeFiles() { checkInitialized(); return welcomeFiles; } /** * Returns a mapping of all error page locations by exception type. The default location is identified by * null key. * @return A mapping of all error page locations by exception type. */ public Map, String> getErrorPageLocations() { checkInitialized(); return errorPageLocations; } /** * Returns the location of the FORM authentication login page, or null if it is not defined. * @return The location of the FORM authentication login page, or null if it is not defined. */ public String getFormLoginPage() { checkInitialized(); return formLoginPage; } /** * Returns the location of the FORM authentication error page, or null if it is not defined. * @return The location of the FORM authentication error page, or null if it is not defined. * @since 1.8 */ public String getFormErrorPage() { checkInitialized(); return formErrorPage; } /** * Returns a mapping of all security constraint URL patterns and the associated roles in the declared order. If the * roles is null, then it means that no auth constraint is been set (i.e. the resource is publicly * accessible). If the roles is empty, then it means that an empty auth constraint is been set (i.e. the resource * is in no way accessible). * @return A mapping of all security constraint URL patterns and the associated roles in the declared order. * @since 1.4 */ public Map> getSecurityConstraints() { checkInitialized(); return securityConstraints; } /** * Returns the configured session timeout in minutes, or -1 if it is not defined. * @return The configured session timeout in minutes, or -1 if it is not defined. * @since 1.7 */ public int getSessionTimeout() { checkInitialized(); return sessionTimeout; } private void checkInitialized() { // This init() call is performed here instead of in constructor, because WebLogic loads this enum as a CDI // managed bean (in spite of having a VetoAnnotatedTypeExtension) which in turn implicitly invokes the enum // constructor and thus causes an init while JSF context isn't fully initialized and thus the faces context // isn't available yet. Perhaps it's fixed in newer WebLogic versions. init(); if (!initialized.get()) { throw new IllegalStateException(ERROR_NOT_INITIALIZED); } } // Helpers -------------------------------------------------------------------------------------------------------- /** * Load, merge and return all web.xml and web-fragment.xml files found in the classpath * into a single {@link Document}. */ private static Document loadWebXml(ServletContext context) throws IOException, SAXException { List webXmlURLs = new ArrayList<>(); webXmlURLs.add(context.getResource(WEB_XML)); webXmlURLs.addAll(Collections.list(Thread.currentThread().getContextClassLoader().getResources(WEB_FRAGMENT_XML))); return createDocument(webXmlURLs); } /** * Create and return a list of all welcome files. */ private static List parseWelcomeFiles(Element webXml, XPath xpath) throws XPathExpressionException { NodeList welcomeFileList = getNodeList(webXml, xpath, XPATH_WELCOME_FILE); List welcomeFiles = new ArrayList<>(welcomeFileList.getLength()); for (int i = 0; i < welcomeFileList.getLength(); i++) { welcomeFiles.add(getTextContent(welcomeFileList.item(i))); } return Collections.unmodifiableList(welcomeFiles); } /** * Create and return a mapping of all error page locations by exception type found in the given document. * @throws ClassNotFoundException */ @SuppressWarnings("unchecked") // For the cast on Class. private static Map, String> parseErrorPageLocations(Element webXml, XPath xpath) throws XPathExpressionException, ClassNotFoundException { Map, String> errorPageLocations = new LinkedHashMap<>(); NodeList exceptionTypes = getNodeList(webXml, xpath, XPATH_EXCEPTION_TYPE); for (int i = 0; i < exceptionTypes.getLength(); i++) { Node node = exceptionTypes.item(i); Class exceptionClass = (Class) Class.forName(getTextContent(node)); String exceptionLocation = xpath.compile(XPATH_LOCATION).evaluate(node.getParentNode()).trim(); Class key = (exceptionClass == Throwable.class) ? null : exceptionClass; if (!errorPageLocations.containsKey(key)) { errorPageLocations.put(key, exceptionLocation); } } if (!errorPageLocations.containsKey(null)) { String defaultLocation = xpath.compile(XPATH_ERROR_PAGE_500_LOCATION).evaluate(webXml).trim(); if (isEmpty(defaultLocation)) { defaultLocation = xpath.compile(XPATH_ERROR_PAGE_DEFAULT_LOCATION).evaluate(webXml).trim(); } if (!isEmpty(defaultLocation)) { errorPageLocations.put(null, defaultLocation); } } return Collections.unmodifiableMap(errorPageLocations); } /** * Return the location of the FORM authentication login page. */ private static String parseFormLoginPage(Element webXml, XPath xpath) throws XPathExpressionException { String formLoginPage = xpath.compile(XPATH_FORM_LOGIN_PAGE).evaluate(webXml).trim(); return isEmpty(formLoginPage) ? null : formLoginPage; } /** * Return the location of the FORM authentication error page. */ private static String parseFormErrorPage(Element webXml, XPath xpath) throws XPathExpressionException { String formErrorPage = xpath.compile(XPATH_FORM_ERROR_PAGE).evaluate(webXml).trim(); return isEmpty(formErrorPage) ? null : formErrorPage; } /** * Create and return a mapping of all security constraint URL patterns and the associated roles. */ private static Map> parseSecurityConstraints(Element webXml, XPath xpath) throws XPathExpressionException { Map> securityConstraints = new LinkedHashMap<>(); NodeList constraints = getNodeList(webXml, xpath, XPATH_SECURITY_CONSTRAINT); for (int i = 0; i < constraints.getLength(); i++) { Node constraint = constraints.item(i); Set roles = null; NodeList auth = getNodeList(constraint, xpath, XPATH_AUTH_CONSTRAINT); if (auth.getLength() > 0) { NodeList authRoles = getNodeList(constraint, xpath, XPATH_AUTH_CONSTRAINT_ROLE_NAME); roles = new HashSet<>(authRoles.getLength()); for (int j = 0; j < authRoles.getLength(); j++) { roles.add(getTextContent(authRoles.item(j))); } roles = Collections.unmodifiableSet(roles); } NodeList urlPatterns = getNodeList(constraint, xpath, XPATH_WEB_RESOURCE_URL_PATTERN); for (int j = 0; j < urlPatterns.getLength(); j++) { String urlPattern = getTextContent(urlPatterns.item(j)); securityConstraints.put(urlPattern, roles); } } return Collections.unmodifiableMap(securityConstraints); } /** * Return the configured session timeout in minutes, or -1 if it is not defined. */ private static int parseSessionTimeout(Element webXml, XPath xpath) throws XPathExpressionException { String sessionTimeout = xpath.compile(XPATH_SESSION_TIMEOUT).evaluate(webXml).trim(); return isNumber(sessionTimeout) ? Integer.parseInt(sessionTimeout) : -1; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy