/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2014 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.faces.config;
import javax.xml.parsers.ParserConfigurationException;
import java.net.URL;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.digester.Digester;
import org.apache.commons.logging.impl.NoOpLog;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import com.sun.faces.util.ToolsUtil;
/**
* <p>
* A simple factory to hide <code>Digester</code> configuration details.</p>
*/
public class DigesterFactory {
private static final Logger logger = ToolsUtil.getLogger(ToolsUtil.FACES_LOGGER
+ ToolsUtil.CONFIG_LOGGER);
/**
* <p>
* <code>Xerces</code> specific feature to enable both DTD and Schema
* validation.</p>
*/
private static final String XERCES_VALIDATION
= "http://xml.org/sax/features/validation";
/**
* <p>
* <code>Xerces</code> specific feature to enable both DTD and Schema
* validation.</p>
*/
private static final String XERCES_SCHEMA_VALIDATION
= "http://apache.org/xml/features/validation/schema";
/**
* <p>
* <code>Xerces</code> specific feature to enabled constraint
* validation.</p>
*/
private static final String XERCES_SCHEMA_CONSTRAINT_VALIDATION
= "http://apache.org/xml/features/validation/schema-full-checking";
/**
* <p>
* Custom <code>EntityResolver</code>.</p>
*/
private static final JsfEntityResolver RESOLVER = new JsfEntityResolver();
/**
* <p>
* Custom <code>ErrorHandler</code>.</p>
*/
private static final JsfErrorHandler ERROR_HANDLER = new JsfErrorHandler();
/**
* <p>
* Indicates whether or not document validation is requested or not.</p>
*/
private boolean validating;
/**
* <p>
* The <code>ThreadLocal</code> variable used to record the VersionListener
* instance for each processing thread.</p>
*/
private static ThreadLocal versionListener = new ThreadLocal() {
@Override
protected Object initialValue() {
return (null);
}
};
// ------------------------------------------------------------ Constructors
/**
* <p>
* Creates a new DigesterFactory instance.</p>
*
* @param isValidating - <code>true</code> if the <code>Digester</code>
* instance that is ultimately returned should be configured (if possible)
* for document validation. If validation is not desired, pass
* <code>false</code>.
*/
private DigesterFactory(boolean isValidating) {
validating = isValidating;
} // END DigesterFactory
// ---------------------------------------------------------- Public Methods
/**
* <p>
* Returns a new <code>DigesterFactory</code> instance that will create a
* non-validating <code>Digester</code> instance.</p>
* @return
*/
public static DigesterFactory newInstance() {
return DigesterFactory.newInstance(false);
} // END newInstance
/**
* <p>
* Creates a new <code>DigesterFactory</code> instance that will create a
* <code>Digester</code> instance where validation depends on the value of
* <code>isValidating</code>.</p>
*
* @param isValidating - <code>true</code> if the <code>Digester</code>
* instance that is ultimately returned should be configured (if possible)
* for document validation. If validation is not desired, pass
* <code>false</code>.
* @return a new <code>DigesterFactory</code> capable of creating
* <code>Digester</code>instances
*/
public static DigesterFactory newInstance(boolean isValidating) {
DigesterFactory result = new DigesterFactory(isValidating);
return result;
} // END newInstance
/**
* <p>
* Creates a new <code>DigesterFactory</code> instance that will create a
* <code>Digester</code> instance where validation depends on the value of
* <code>isValidating</code>.</p>
*
* @param isValidating - <code>true</code> if the <code>Digester</code>
* instance that is ultimately returned should be configured (if possible)
* for document validation. If validation is not desired, pass
* <code>false</code>.
* @param listener
* @return a new <code>DigesterFactory</code> capable of creating
* <code>Digester</code>instances
*/
public static DigesterFactory newInstance(boolean isValidating,
VersionListener listener) {
DigesterFactory result = new DigesterFactory(isValidating);
if (null != listener) {
result.RESOLVER.setVersionListener(listener);
versionListener.set(listener);
}
return result;
} // END newInstance
public static VersionListener getVersionListener() {
return ((VersionListener) versionListener.get());
}
public static void releaseDigester(Digester toRelease) {
RESOLVER.setVersionListener(null);
versionListener.set(null);
}
/**
* <p>
* Creates a new <code>Digester</code> instance configured for use with
* JSF.</p>
* @return
*/
public Digester createDigester() {
Digester digester = new Digester();
configureDigester(digester);
return digester;
} // END getDigester
/**
* <p>
* Implemented by a class that wants to be called as a particular
* configuration file is parsed.</p>
*
* <p>
* This interface is implemented as an anonymous inner class inside
* ConfigureListener.digester().</p>
*/
public interface VersionListener {
/**
* <p>
* Called from the EntityResolver when we know one of the XML Grammar
* elements to which this config file conforms.</p>
* @param grammar */
public void takeActionOnGrammar(String grammar);
/**
* <p>
* Called from the individual digester beans to cause the artifact to be
* associated with the current JSF spec level of the file being
* parsed.</p>
* @param artifactName
*/
public void takeActionOnArtifact(String artifactName);
}
// --------------------------------------------------------- Private Methods
/**
* <p>
* Configures the provided <code>Digester</code> instance appropriate for
* use with JSF.</p>
*
* @param digester - the <code>Digester</code> instance to configure
*/
private void configureDigester(Digester digester) {
digester.setNamespaceAware(true);
digester.setUseContextClassLoader(true);
digester.setEntityResolver(RESOLVER);
digester.setErrorHandler(ERROR_HANDLER);
// disable digester log messages
digester.setLogger(new NoOpLog());
if (validating) {
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Attempting to configure Digester to perform"
+ " document validation.");
}
// In order to validate using *both* DTD and Schema, certain
// Xerces specific features are required. Try to set these
// features. If an exception is thrown trying to set these
// features, then disable validation.
try {
digester.setFeature(XERCES_VALIDATION, true);
digester.setFeature(XERCES_SCHEMA_VALIDATION, true);
digester.setFeature(XERCES_SCHEMA_CONSTRAINT_VALIDATION, true);
digester.setValidating(true);
} catch (SAXNotSupportedException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.log(Level.WARNING, "Attempt to set supported feature on XMLReader, "
+ "but the value provided was not accepted. "
+ "Validation will be disabledb.");
}
digester.setValidating(false);
} catch (SAXNotRecognizedException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.log(Level.WARNING, "Attempt to set unsupported feature on XMLReader"
+ " necessary for validation. Validation will be"
+ "disabled.");
}
digester.setValidating(false);
} catch (ParserConfigurationException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.log(Level.WARNING, "Digester unable to configure underlying parser."
+ " Validation will be disabled.");
}
digester.setValidating(false);
}
} else {
digester.setValidating(false);
}
} // END configureDigester
// ----------------------------------------------------------- Inner Classes
private static class JsfEntityResolver extends DefaultHandler {
/**
* <p>
* Contains associations between grammar name and the physical
* resource.</p>
*/
private static final String[][] DTD_SCHEMA_INFO = {
{
"web-facesconfig_1_0.dtd",
"/com/sun/faces/web-facesconfig_1_0.dtd"
},
{
"web-facesconfig_1_1.dtd",
"/com/sun/faces/web-facesconfig_1_1.dtd"
},
{
"web-facesconfig_1_2.xsd",
"/com/sun/faces/web-facesconfig_1_2.xsd"
},
{
"web-facesconfig_2_0.xsd",
"/com/sun/faces/web-facesconfig_2_0.xsd"
},
{
"web-facesconfig_2_2.xsd",
"/com/sun/faces/web-facesconfig_2_2.xsd"
},
{
"javaee_5.xsd",
"/com/sun/faces/javaee_5.xsd"
},
{
"javaee_7.xsd",
"/com/sun/faces/javaee_7.xsd"
},
{
"javaee_web_services_client_1_2.xsd",
"/com/sun/faces/javaee_web_services_client_1_2.xsd"
},
{
"javaee_web_services_client_1_4.xsd",
"/com/sun/faces/javaee_web_services_client_1_4.xsd"
},
{
"xml.xsd",
"/com/sun/faces/xml.xsd"
}
};
/**
* <p>
* Contains mapping between grammar name and the local URL to the
* physical resource.</p>
*/
private HashMap<String, String> entities = new HashMap<String, String>();
// -------------------------------------------------------- Constructors
public JsfEntityResolver() {
// Add mappings between last segment of system ID and
// the expected local physical resource. If the resource
// cannot be found, then rely on default entity resolution
// and hope a firewall isn't in the way or a proxy has
// been configured
for (String[] aDTD_SCHEMA_INFO : DTD_SCHEMA_INFO) {
URL url = this.getClass().getResource(aDTD_SCHEMA_INFO[1]);
if (url == null) {
if (logger.isLoggable(Level.WARNING)) {
logger.log(Level.WARNING,
"Unable to locate local resource '"
+ aDTD_SCHEMA_INFO[1]
+ "'. Standard entity "
+ "resolution will be used when request "
+ "is present for '"
+ aDTD_SCHEMA_INFO[0] + '\'');
}
} else {
entities.put(aDTD_SCHEMA_INFO[0], url.toString());
}
}
} // END JsfEntityResolver
private VersionListener versionListener;
public void setVersionListener(VersionListener listener) {
versionListener = listener;
}
public VersionListener getVersionListener() {
return versionListener;
}
// ----------------------------------------- Methods from DefaultHandler
/**
* <p>
* Resolves the physical resource using the last segment of the
* <code>systemId</code> (e.g.
* http://java.sun.com/dtds/web-facesconfig_1_1.dtd, the last segment
* would be web-facesconfig_1_1.dtd). If a mapping cannot be found for
* the segment, then defer to the <code>DefaultHandler</code> for
* resolution.</p>
*/
@Override
public InputSource resolveEntity(String publicId, String systemId)
throws SAXException {
// publicId is ignored. Resolution performed using
// the systemId.
// If no system ID, defer to superclass
if (systemId == null) {
InputSource result;
try {
result = super.resolveEntity(publicId, systemId);
} catch (Exception e) {
throw new SAXException(e);
}
return result;
}
String grammarName
= systemId.substring(systemId.lastIndexOf('/') + 1);
if (null != getVersionListener()) {
getVersionListener().takeActionOnGrammar(grammarName);
}
String entityURL = entities.get(grammarName);
InputSource source;
if (entityURL == null) {
// we don't have a registered mapping, so defer to our
// superclass for resolution
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Unknown entity, deferring to superclass.");
}
try {
source = super.resolveEntity(publicId, systemId);
} catch (Exception e) {
throw new SAXException(e);
}
} else {
try {
source = new InputSource(new URL(entityURL).openStream());
} catch (Exception e) {
if (logger.isLoggable(Level.WARNING)) {
logger.log(Level.WARNING, "Unable to create InputSource for URL '"
+ entityURL + "'");
}
source = null;
}
}
// Set the System ID of the InputSource with the URL of the local
// resource - necessary to prevent parsing errors
if (source != null) {
source.setSystemId(entityURL);
if (publicId != null) {
source.setPublicId(publicId);
}
}
return source;
} // END resolveEntity
} // END JsfEntityResolver
private static class JsfErrorHandler implements ErrorHandler {
@Override
public void warning(SAXParseException exception) throws SAXException {
// do nothing
}
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
}
}