Skip navigation.
Home

Local DTD catalog resolution woes

I attempted to deploy my newly created webapp onto a machine that had no access to the public internet. When the app attempted to start up, I was greeted with a nice friendly error that occurred when the xml parser attempted to validate dtd in one of my configuration files...

Background

The application is based upon the appfuse v1.9.3 application stack. In this case, I used Spring MVC on top of Hibernate. The application uses the commons-validator package to validate user input from web forms. Here's the stack that was thrown..


[ddf] INFO [Thread-1] [/ddf].log(646) | Loading Spring root WebApplicationContext
[ddf] INFO [Thread-1] DefaultValidatorFactory.setValidationConfigLocations(62) | Loading validation configurations from [ServletContext resource [/WEB-INF/validation.xml],ServletContext resource [/WEB-INF/validator-rules.xml],ServletContext resource [/WEB-INF/validator-rules-custom.xml]]
[ddf] ERROR [Thread-1] ContextLoader.initWebApplicationContext(211) | Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'validatorFactory' defined in ServletContext resource [/WEB-INF/applicationContext-validation.xml]: Error setting property values; nested exception is PropertyAccessExceptionsException (1 errors)
PropertyAccessExceptionsException (1 errors)
org.springframework.beans.MethodInvocationException: Property 'validationConfigLocations' threw exception; nested exception is org.springframework.beans.FatalBeanException: Unable to read validation configuration due to IOException.; nested exception is java.net.UnknownHostException: jakarta.apache.org
org.springframework.beans.FatalBeanException: Unable to read validation configuration due to IOException.; nested exception is java.net.UnknownHostException: jakarta.apache.org
java.net.UnknownHostException: jakarta.apache.org
	at java.net.PlainSocketImpl.connect(Unknown Source)
	at java.net.Socket.connect(Unknown Source)
	at java.net.Socket.connect(Unknown Source)
	at sun.net.NetworkClient.doConnect(Unknown Source)
	at sun.net.www.http.HttpClient.openServer(Unknown Source)
	at sun.net.www.http.HttpClient.openServer(Unknown Source)
	at sun.net.www.http.HttpClient.<init>(Unknown Source)
	at sun.net.www.http.HttpClient.New(Unknown Source)
	at sun.net.www.http.HttpClient.New(Unknown Source)
	at sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(Unknown Source)
	at sun.net.www.protocol.http.HttpURLConnection.plainConnect(Unknown Source)
	at sun.net.www.protocol.http.HttpURLConnection.connect(Unknown Source)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source)
	at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.setupCurrentEntity(Unknown Source)
	at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.startEntity(Unknown Source)
	at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.startDTDEntity(Unknown Source)
	at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.setInputSource(Unknown Source)
	at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$DTDDispatcher.dispatch(Unknown Source)
	at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source)
	at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)
	at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)
	at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(Unknown Source)
	at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(Unknown Source)
	at org.apache.commons.digester.Digester.parse(Digester.java:1591)
	at org.apache.commons.validator.ValidatorResources.<init>(ValidatorResources.java:152)
	at org.springmodules.validation.commons.DefaultValidatorFactory.setValidationConfigLocations(DefaultValidatorFactory.java:72)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.springframework.beans.BeanWrapperImpl.setPropertyValue(BeanWrapperImpl.java:707)
	at org.springframework.beans.BeanWrapperImpl.setPropertyValue(BeanWrapperImpl.java:572)
	at org.springframework.beans.BeanWrapperImpl.setPropertyValue(BeanWrapperImpl.java:737)
	at org.springframework.beans.BeanWrapperImpl.setPropertyValues(BeanWrapperImpl.java:764)
	at org.springframework.beans.BeanWrapperImpl.setPropertyValues(BeanWrapperImpl.java:753)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1057)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:857)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:378)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:145)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:283)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:313)
	at org.springframework.web.context.support.AbstractRefreshableWebApplicationContext.refresh(AbstractRefreshableWebApplicationContext.java:139)
	at org.springframework.web.context.ContextLoader.createWebApplicationContext(ContextLoader.java:252)
	at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:190)
	at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:49)
	at com.kraft.adf.ddf.webapp.listener.StartupListener.contextInitialized(StartupListener.java:51)
	at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:3729)
	at org.apache.catalina.core.StandardContext.start(StandardContext.java:4187)
	at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:759)
	at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:739)
	at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:524)
	at org.apache.catalina.startup.HostConfig.deployDescriptor(HostConfig.java:608)
	at org.apache.catalina.startup.HostConfig.deployDescriptors(HostConfig.java:535)
	at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:470)
	at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1122)
	at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:310)
	at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:119)
	at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1021)
	at org.apache.catalina.core.StandardHost.start(StandardHost.java:718)
	at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1013)
	at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:442)
	at org.apache.catalina.core.StandardService.start(StandardService.java:450)
	at org.apache.catalina.core.StandardServer.start(StandardServer.java:709)
	at org.apache.catalina.startup.Catalina.start(Catalina.java:551)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:294)
	at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:432)
[ddf] INFO [Thread-1] [/ddf].log(646) | Closing Spring root WebApplicationContext

The real problem

I have run into this nightmare before with dtds. It is pretty silly to have to have an internet connection to validate a dtd. Of course, I could change the dtd to point to something reachable by the machine (either local filesystem or local website) but that decreases my portability. XML Catalogs seems like the right way to solve this sort of thing, but it isn't in a java core package just yet. So what to do?

A solution

First thing I did was to make certain that tomcat wasn't attempting to validate xml documents.

server.xml


<Host name="localhost" appBase="webapps"
       unpackWARs="true" autoDeploy="true"
       xmlValidation="false" xmlNamespaceAware="false">

Check. The xmlValidation is set to false.

Okay then, why is a dtd being validated? When I looked at the stack closely I realized why. Here is the relevant part of the trace:


...
at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(Unknown Source)
at org.apache.commons.digester.Digester.parse(Digester.java:1591)
at org.apache.commons.validator.ValidatorResources.<init>(ValidatorResources.java:152)
...

Okay, the commons-validator package is using commons-digestor to parse the xml configuration files. I looked through the documentation on both packages and ended up downloading the source.

I looked inside the Digester class to see if there were anything fancy going on with the validate xml setting... Nope just a straight get/set method with a private instance variable. The problem must lie within the ValidatorResources class.


/**
 * The set of public identifiers, and corresponding resource names, for
 * the versions of the configuration file DTDs that we know about.  There
 * <strong>MUST</strong> be an even number of Strings in this list!
 */
private static final String REGISTRATIONS[] = {
    "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0//EN",
    "/org/apache/commons/validator/resources/validator_1_0.dtd",
    "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0.1//EN",
    "/org/apache/commons/validator/resources/validator_1_0_1.dtd",
    "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN",
    "/org/apache/commons/validator/resources/validator_1_1.dtd",
    "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN",
    "/org/apache/commons/validator/resources/validator_1_1_3.dtd",
    "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.2.0//EN",
    "/org/apache/commons/validator/resources/validator_1_2_0.dtd",
    "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.3.0//EN",
    "/org/apache/commons/validator/resources/validator_1_3_0.dtd"

/**
 *  Initialize the digester.
 */
private Digester initDigester() {
  URL rulesUrl = this.getClass().getResource("digester-rules.xml");
  Digester digester = DigesterLoader.createDigester(rulesUrl);
  digester.setNamespaceAware(true);
  digester.setValidating(true);
  digester.setUseContextClassLoader(true);

  // Add rules for arg0-arg3 elements
  addOldArgRules(digester);

  // register DTDs
  for (int i = 0; i < REGISTRATIONS.length; i += 2) {
    URL url = this.getClass().getResource(REGISTRATIONS[i + 1]);
    if (url != null) {
      digester.register(REGISTRATIONS[i], url.toString());
    }
  }
  return digester;
}    

Now we're getting somewhere.. the digester.setValidating(true) causes the validations to happen downstream. But, the class is also registering a set of dtds that will be resolved internally; no external resource lookup is required. So what's the deal?

After all of this, I realized that I was using a different version (lower) of the commons-validator.jar. The version I had, did not contain a registration for the validator_1_3_0 dtd and was attempting to fetch it from the internet. So I downloaded the latest version of commons-validator and voila! everything was working as expected.

It would have been nice if the java stack trace told me the full URI that it was trying to fetch instead of just the hostname but oh well, I figured it out in the end.