Commits

mrdon  committed 6c495c4

Adding line-precise error reporting by processing DOM with SAX filters that
record the location of each element and store it in special attributes. The
location information is then transformed into an Object and stored with each
configuration object. The next step will be to use this new information in
exceptions thrown by xwork. The bulk of this code comes from Apache Cocoon,
which is why the license is still Apache.

Issue number: XW-379

git-svn-id: http://svn.opensymphony.com/svn/xwork/trunk/src@983e221344d-f017-0410-9bd5-d282ab1896d7

  • Participants
  • Parent commits f159fa6

Comments (0)

Files changed (22)

File java/com/opensymphony/xwork/config/entities/ActionConfig.java

 
 import com.opensymphony.xwork.interceptor.Interceptor;
 
+import com.opensymphony.xwork.util.location.Located;
 import java.io.Serializable;
 import java.util.*;
 
  * @author Rainer Hermanns
  * @version $Revision$
  */
-public class ActionConfig implements InterceptorListHolder, Parameterizable, Serializable {
+public class ActionConfig extends Located implements InterceptorListHolder, Parameterizable, Serializable {
 
     protected List externalRefs;
     protected List interceptors;

File java/com/opensymphony/xwork/config/entities/ExceptionMappingConfig.java

 import java.util.LinkedHashMap;
 import java.io.Serializable;
 
+import com.opensymphony.xwork.util.location.Located;
+
 /**
  * Configuration for exception mapping.
  *
  * @author Rainer Hermanns
  * @author Matthew E. Porter (matthew dot porter at metissian dot com)
  */
-public class ExceptionMappingConfig implements Serializable {
+public class ExceptionMappingConfig extends Located implements Serializable {
 
     private String name;
     private String exceptionClassName;

File java/com/opensymphony/xwork/config/entities/InterceptorConfig.java

 import java.util.LinkedHashMap;
 import java.io.Serializable;
 
+import com.opensymphony.xwork.util.location.Located;
 
 /**
  * Configuration for Interceptors.
  *
  * @author Mike
  */
-public class InterceptorConfig implements Parameterizable, Serializable {
+public class InterceptorConfig extends Located implements Parameterizable, Serializable {
 
     Map params;
     String className;

File java/com/opensymphony/xwork/config/entities/InterceptorStackConfig.java

 import java.util.List;
 import java.io.Serializable;
 
+import com.opensymphony.xwork.util.location.Located;
+
 
 /**
  * Configuration for InterceptorStack.
  * @author Mike
  * @author Rainer Hermanns
  */
-public class InterceptorStackConfig implements InterceptorListHolder, Serializable {
+public class InterceptorStackConfig extends Located implements InterceptorListHolder, Serializable {
 
     private List interceptors;
     private String name;

File java/com/opensymphony/xwork/config/entities/PackageConfig.java

 
 import com.opensymphony.util.TextUtils;
 import com.opensymphony.xwork.config.ExternalReferenceResolver;
+import com.opensymphony.xwork.util.location.Located;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
  * @author Rainer Hermanns
  * @version $Revision$
  */
-public class PackageConfig implements Comparable, Serializable {
+public class PackageConfig extends Located implements Comparable, Serializable {
 
     private static final Log LOG = LogFactory.getLog(PackageConfig.class);
 

File java/com/opensymphony/xwork/config/entities/ResultConfig.java

 import java.util.LinkedHashMap;
 import java.io.Serializable;
 
+import com.opensymphony.xwork.util.location.Located;
+
 
 /**
  * Configuration for Result.
  * 
  * @author Mike
  */
-public class ResultConfig implements Parameterizable, Serializable {
+public class ResultConfig extends Located implements Parameterizable, Serializable {
 
     private Map params;
     private String className;

File java/com/opensymphony/xwork/config/entities/ResultTypeConfig.java

 import java.util.LinkedHashMap;
 import java.io.Serializable;
 
+import com.opensymphony.xwork.util.location.Located;
+
 
 /**
  * Configuration class for result types.
  * @author Rainer Hermanns
  * @author Neo
  */
-public class ResultTypeConfig implements Serializable {
+public class ResultTypeConfig extends Located implements Serializable {
 
     private Class clazz;
     private String name;

File java/com/opensymphony/xwork/config/providers/XmlConfigurationProvider.java

 import com.opensymphony.xwork.ObjectFactory;
 import com.opensymphony.xwork.config.*;
 import com.opensymphony.xwork.config.entities.*;
+import com.opensymphony.xwork.util.DomHelper;
+
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.w3c.dom.Document;
         this.configuration = configuration;
         includedFileNames.clear();
 
-        DocumentBuilder db;
 
         try {
-            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-            dbf.setValidating(true);
-            dbf.setNamespaceAware(true);
-
-            db = dbf.newDocumentBuilder();
-            db.setEntityResolver(new EntityResolver() {
-                public InputSource resolveEntity(String publicId, String systemId) {
-                    if ("-//OpenSymphony Group//XWork 1.1.1//EN".equals(publicId)) {
-                        return new InputSource(ClassLoaderUtil.getResourceAsStream("xwork-1.1.1.dtd", XmlConfigurationProvider.class));
-                    }
-                    else if ("-//OpenSymphony Group//XWork 1.1//EN".equals(publicId)) {
-                        return new InputSource(ClassLoaderUtil.getResourceAsStream("xwork-1.1.dtd", XmlConfigurationProvider.class));
-                    }
-                    else if ("-//OpenSymphony Group//XWork 1.0//EN".equals(publicId)) {
-                        return new InputSource(ClassLoaderUtil.getResourceAsStream("xwork-1.0.dtd", XmlConfigurationProvider.class));
-                    }
-
-                    return null;
-                }
-            });
-            db.setErrorHandler(new ErrorHandler() {
-                public void warning(SAXParseException exception) {
-                }
-
-                public void error(SAXParseException exception) throws SAXException {
-                    LOG.error(exception.getMessage() + " at (" + exception.getLineNumber() + ":" + exception.getColumnNumber() + ")");
-                    throw exception;
-                }
-
-                public void fatalError(SAXParseException exception) throws SAXException {
-                    LOG.fatal(exception.getMessage() + " at (" + exception.getLineNumber() + ":" + exception.getColumnNumber() + ")");
-                    throw exception;
-                }
-            });
-            loadConfigurationFile(configFileName, db);
+            loadConfigurationFile(configFileName);
         } catch (Exception e) {
             LOG.fatal("Could not load XWork configuration file, failing", e);
             throw new ConfigurationException("Error loading configuration file " + configFileName, e);
 
         List exceptionMappings = buildExceptionMappings(actionElement, packageContext);
 
-        ActionConfig actionConfig = new ActionConfig(methodName, className, actionParams, results, interceptorList, externalrefs, exceptionMappings, packageContext.getName());
+        ActionConfig actionConfig = new ActionConfig(methodName, className, actionParams, results, interceptorList, externalrefs, exceptionMappings,
+        packageContext.getName());
+        actionConfig.setLocation(DomHelper.getLocationObject(actionElement));
         packageContext.addActionConfig(name, actionConfig);
 
         if (LOG.isDebugEnabled()) {
             }
 
             ResultTypeConfig resultType = new ResultTypeConfig(name, clazz);
+            resultType.setLocation(DomHelper.getLocationObject(resultTypeElement));
+            
             Map params = XmlHelper.getParams(resultTypeElement);
 
             if (!params.isEmpty()) {
 
         String externalReferenceResolver = TextUtils.noNull(packageElement.getAttribute("externalReferenceResolver"));
 
+        PackageConfig cfg = null;
         if (!("".equals(externalReferenceResolver))) {
             try {
                 erResolver = (ExternalReferenceResolver) ObjectFactory.getObjectFactory().buildBean(externalReferenceResolver, null);
 
         if (!TextUtils.stringSet(TextUtils.noNull(parent))) { // no parents
 
-            return new PackageConfig(name, namespace, isAbstract, erResolver);
+            cfg = new PackageConfig(name, namespace, isAbstract, erResolver);
         } else { // has parents, let's look it up
 
             List parents = ConfigurationUtil.buildParentsFromString(configuration, parent);
             if (parents.size() <= 0) {
                 LOG.error("Unable to find parent packages " + parent);
 
-                return new PackageConfig(name, namespace, isAbstract, erResolver);
+                cfg = new PackageConfig(name, namespace, isAbstract, erResolver);
             } else {
-                return new PackageConfig(name, namespace, isAbstract, erResolver, parents);
+                cfg = new PackageConfig(name, namespace, isAbstract, erResolver, parents);
             }
         }
+        
+        if (cfg != null) {
+            cfg.setLocation(DomHelper.getLocationObject(packageElement));
+        }
+        return cfg;
     }
 
     /**
                 params.putAll(resultParams);
 
                 ResultConfig resultConfig = new ResultConfig(resultName, resultClass, params);
+                resultConfig.setLocation(DomHelper.getLocationObject(element));
                 results.put(resultConfig.getName(), resultConfig);
             }
         }
                 }
 
                 ExceptionMappingConfig ehConfig = new ExceptionMappingConfig(emName, exceptionClassName, exceptionResult, params);
+                ehConfig.setLocation(DomHelper.getLocationObject(ehElement));
                 exceptionMappings.add(ehConfig);
             }
         }
         String name = element.getAttribute("name");
 
         InterceptorStackConfig config = new InterceptorStackConfig(name);
+        config.setLocation(DomHelper.getLocationObject(element));
         NodeList interceptorRefList = element.getElementsByTagName("interceptor-ref");
 
         for (int j = 0; j < interceptorRefList.getLength(); j++) {
 
             Map params = XmlHelper.getParams(interceptorElement);
             InterceptorConfig config = new InterceptorConfig(name, className, params);
+            config.setLocation(DomHelper.getLocationObject(interceptorElement));
 
             context.addInterceptorConfig(config);
         }
     //            addPackage(packageElement);
     //        }
     //    }
-    private void loadConfigurationFile(String fileName, DocumentBuilder db) {
+    private void loadConfigurationFile(String fileName) {
         if (!includedFileNames.contains(fileName)) {
             if (LOG.isDebugEnabled()) {
                 LOG.debug("Loading xwork configuration from: " + fileName);
                     throw new Exception("Could not open file " + fileName);
                 }
 
-                doc = db.parse(is);
+                InputSource in = new InputSource(is);
+                in.setSystemId(fileName);
+                
+                doc = DomHelper.parse(in);
             } catch (Exception e) {
                 final String s = "Caught exception while loading file " + fileName;
                 LOG.error(s, e);
                         addPackage(child);
                     } else if (nodeName.equals("include")) {
                         String includeFileName = child.getAttribute("file");
-                        loadConfigurationFile(includeFileName, db);
+                        loadConfigurationFile(includeFileName);
                     }
                 }
             }

File java/com/opensymphony/xwork/util/DomHelper.java

+/*
+ * Copyright 1999-2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.opensymphony.xwork.util;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import com.opensymphony.util.ClassLoaderUtil;
+
+import com.opensymphony.xwork.util.location.Location;
+import com.opensymphony.xwork.util.location.LocationAttributes;
+import com.opensymphony.xwork.XworkException;
+import org.w3c.dom.Attr;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.SAXNotSupportedException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.parsers.SAXParser;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
+
+/**
+ * Helper class to create and retrieve information from location-enabled
+ * DOM-trees.
+ */
+public class DomHelper {
+
+    private static final Log LOG = LogFactory.getLog(DomHelper.class);
+    
+    public static final String XMLNS_URI = "http://www.w3.org/2000/xmlns/";
+
+    public static Location getLocationObject(Element element) {
+        return LocationAttributes.getLocation(element);
+    }
+
+    /**
+     * Creates a W3C Document that remembers the location of each element in
+     * the source file. The location of element nodes can then be retrieved
+     * using the {@link #getLocation(Element)} method.
+     *
+     * @param inputSource the inputSource to read the document from
+     */
+    public static Document parse(InputSource inputSource)
+            throws SAXException, SAXNotSupportedException, IOException {
+                
+        SAXParserFactory factory = SAXParserFactory.newInstance();
+        factory.setValidating(true);
+        factory.setNamespaceAware(true);
+        
+        SAXParser parser = null;
+        try {
+            parser = factory.newSAXParser();
+        } catch (javax.xml.parsers.ParserConfigurationException ex) {
+            throw new XworkException("Unable to create SAX parser", ex);
+        }
+        
+        
+        DOMBuilder builder = new DOMBuilder();
+        
+        // Enhance the sax stream with location information
+        ContentHandler locationHandler = new LocationAttributes.Pipe(builder);
+        
+        parser.parse(inputSource, new StartHandler(locationHandler));
+        
+        return builder.getDocument();
+    }
+    
+    /**
+     * The <code>DOMBuilder</code> is a utility class that will generate a W3C
+     * DOM Document from SAX events.
+     *
+     * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
+     */
+    static public class DOMBuilder implements ContentHandler {
+    
+        /** The default transformer factory shared by all instances */
+        protected static final SAXTransformerFactory FACTORY = (SAXTransformerFactory) TransformerFactory.newInstance();
+    
+        /** The transformer factory */
+        protected SAXTransformerFactory factory;
+    
+        /** The result */
+        protected DOMResult result;
+    
+        /** The parentNode */
+        protected Node parentNode;
+        
+        protected ContentHandler nextHandler;
+    
+        /**
+         * Construct a new instance of this DOMBuilder.
+         */
+        public DOMBuilder() {
+            this((Node) null);
+        }
+    
+        /**
+         * Construct a new instance of this DOMBuilder.
+         */
+        public DOMBuilder(SAXTransformerFactory factory) {
+            this(factory, null);
+        }
+    
+        /**
+         * Constructs a new instance that appends nodes to the given parent node.
+         */
+        public DOMBuilder(Node parentNode) {
+            this(null, parentNode);
+        }
+    
+        /**
+         * Construct a new instance of this DOMBuilder.
+         */
+        public DOMBuilder(SAXTransformerFactory factory, Node parentNode) {
+            this.factory = factory == null? FACTORY: factory;
+            this.parentNode = parentNode;
+            setup();
+        }
+    
+        /**
+         * Setup this instance transformer and result objects.
+         */
+        private void setup() {
+            try {
+                TransformerHandler handler = this.factory.newTransformerHandler();
+                nextHandler = handler;
+                if (this.parentNode != null) {
+                    this.result = new DOMResult(this.parentNode);
+                } else {
+                    this.result = new DOMResult();
+                }
+                handler.setResult(this.result);
+            } catch (javax.xml.transform.TransformerException local) {
+                throw new XworkException("Fatal-Error: Unable to get transformer handler", local);
+            }
+        }
+    
+        /**
+         * Return the newly built Document.
+         */
+        public Document getDocument() {
+            if (this.result == null || this.result.getNode() == null) {
+                return null;
+            } else if (this.result.getNode().getNodeType() == Node.DOCUMENT_NODE) {
+                return (Document) this.result.getNode();
+            } else {
+                return this.result.getNode().getOwnerDocument();
+            }
+        }
+    
+        public void setDocumentLocator(Locator locator) {
+            nextHandler.setDocumentLocator(locator);
+        }
+        
+        public void startDocument() throws SAXException {
+            nextHandler.startDocument();
+        }
+        
+        public void endDocument() throws SAXException {
+            nextHandler.endDocument();
+        }
+    
+        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException {
+            nextHandler.startElement(uri, loc, raw, attrs);
+        }
+    
+        public void endElement(String arg0, String arg1, String arg2) throws SAXException {
+            nextHandler.endElement(arg0, arg1, arg2);
+        }
+    
+        public void startPrefixMapping(String arg0, String arg1) throws SAXException {
+            nextHandler.startPrefixMapping(arg0, arg1);
+        }
+    
+        public void endPrefixMapping(String arg0) throws SAXException {
+            nextHandler.endPrefixMapping(arg0);
+        }
+    
+        public void characters(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.characters(arg0, arg1, arg2);
+        }
+    
+        public void ignorableWhitespace(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.ignorableWhitespace(arg0, arg1, arg2);
+        }
+    
+        public void processingInstruction(String arg0, String arg1) throws SAXException {
+            nextHandler.processingInstruction(arg0, arg1);
+        }
+    
+        public void skippedEntity(String arg0) throws SAXException {
+            nextHandler.skippedEntity(arg0);
+        }
+    }
+    
+    public static class StartHandler extends DefaultHandler {
+        
+        private ContentHandler nextHandler;
+        
+        /**
+         * Create a filter that is chained to another handler.
+         * @param next the next handler in the chain.
+         */
+        public StartHandler(ContentHandler next) {
+            nextHandler = next;
+        }
+
+        public void setDocumentLocator(Locator locator) {
+            nextHandler.setDocumentLocator(locator);
+        }
+        
+        public void startDocument() throws SAXException {
+            nextHandler.startDocument();
+        }
+        
+        public void endDocument() throws SAXException {
+            nextHandler.endDocument();
+        }
+
+        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException {
+            nextHandler.startElement(uri, loc, raw, attrs);
+        }
+
+        public void endElement(String arg0, String arg1, String arg2) throws SAXException {
+            nextHandler.endElement(arg0, arg1, arg2);
+        }
+
+        public void startPrefixMapping(String arg0, String arg1) throws SAXException {
+            nextHandler.startPrefixMapping(arg0, arg1);
+        }
+
+        public void endPrefixMapping(String arg0) throws SAXException {
+            nextHandler.endPrefixMapping(arg0);
+        }
+
+        public void characters(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.characters(arg0, arg1, arg2);
+        }
+
+        public void ignorableWhitespace(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.ignorableWhitespace(arg0, arg1, arg2);
+        }
+
+        public void processingInstruction(String arg0, String arg1) throws SAXException {
+            nextHandler.processingInstruction(arg0, arg1);
+        }
+
+        public void skippedEntity(String arg0) throws SAXException {
+            nextHandler.skippedEntity(arg0);
+        }
+        
+        public InputSource resolveEntity(String publicId, String systemId) {
+            if ("-//OpenSymphony Group//XWork 1.1.1//EN".equals(publicId)) {
+                return new InputSource(ClassLoaderUtil.getResourceAsStream("xwork-1.1.1.dtd", DomHelper.class));
+            }
+            else if ("-//OpenSymphony Group//XWork 1.1//EN".equals(publicId)) {
+                return new InputSource(ClassLoaderUtil.getResourceAsStream("xwork-1.1.dtd", DomHelper.class));
+            }
+            else if ("-//OpenSymphony Group//XWork 1.0//EN".equals(publicId)) {
+                return new InputSource(ClassLoaderUtil.getResourceAsStream("xwork-1.0.dtd", DomHelper.class));
+            }
+    
+            return null;
+        }
+        
+        public void warning(SAXParseException exception) {
+        }
+
+        public void error(SAXParseException exception) throws SAXException {
+            LOG.error(exception.getMessage() + " at (" + exception.getLineNumber() + ":" + exception.getColumnNumber() + ")");
+            throw exception;
+        }
+
+        public void fatalError(SAXParseException exception) throws SAXException {
+            LOG.fatal(exception.getMessage() + " at (" + exception.getLineNumber() + ":" + exception.getColumnNumber() + ")");
+            throw exception;
+        }
+    }
+
+}

File java/com/opensymphony/xwork/util/location/Locatable.java

+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.opensymphony.xwork.util.location;
+
+/**
+ * A interface that should be implemented by objects knowning their location (i.e. where they
+ * have been created from).
+ */
+public interface Locatable {
+    /**
+     * Get the location of this object
+     * 
+     * @return the location
+     */
+    public Location getLocation();
+}

File java/com/opensymphony/xwork/util/location/Located.java

+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.opensymphony.xwork.util.location;
+
+/**
+ * Base class for location aware objects
+ */
+public abstract class Located implements Locatable {
+    
+    protected Location location;
+    
+    /**
+     * Get the location of this object
+     * 
+     * @return the location
+     */
+    public Location getLocation() {
+        return location;
+    }
+    
+    /**
+     * Set the location of this object
+     * 
+     * @param loc the location
+     */
+    public void setLocation(Location loc) {
+        this.location = loc;
+    }
+}

File java/com/opensymphony/xwork/util/location/Location.java

+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.opensymphony.xwork.util.location;
+
+
+/**
+ * A location in a resource. The location is composed of the URI of the resource, and 
+ * the line and column numbers within that resource (when available), along with a description.
+ * <p>
+ * Locations are mostly provided by {@link Locatable}s objects.
+ */
+public interface Location {
+    
+    /**
+     * Constant for unknown locations.
+     */
+    public static final Location UNKNOWN = LocationImpl.UNKNOWN;
+    
+    /**
+     * Get the description of this location
+     * 
+     * @return the description (can be <code>null</code>)
+     */
+    public String getDescription();
+    
+    /**
+     * Get the URI of this location
+     * 
+     * @return the URI (<code>null</code> if unknown).
+     */
+    public String getURI();
+    /**
+     * Get the line number of this location
+     * 
+     * @return the line number (<code>-1</code> if unknown)
+     */
+    public int getLineNumber();
+    
+    /**
+     * Get the column number of this location
+     * 
+     * @return the column number (<code>-1</code> if unknown)
+     */
+    public int getColumnNumber();
+
+}

File java/com/opensymphony/xwork/util/location/LocationAttributes.java

+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.opensymphony.xwork.util.location;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+/**
+ * A class to handle location information stored in attributes.
+ * These attributes are typically setup using {@link com.opensymphony.xwork.util.location.LocationAttributes.Pipe}
+ * which augments the SAX stream with additional attributes, e.g.:
+ * <pre>
+ * &lt;root xmlns:loc="http://opensymphony.com/xwork/location"
+ *       loc:src="file://path/to/file.xml"
+ *       loc:line="1" loc:column="1"&gt;
+ *   &lt;foo loc:src="file://path/to/file.xml" loc:line="2" loc:column="3"/&gt;
+ * &lt;/root&gt;
+ * </pre>
+ * 
+ * @see com.opensymphony.xwork.util.location.LocationAttributes.Pipe
+ * @since 2.1.8
+ * @version $Id$
+ */
+public class LocationAttributes {
+    /** Prefix for the location namespace */
+    public static final String PREFIX = "loc";
+    /** Namespace URI for location attributes */
+    public static final String URI = "http://opensymphony.com/xwork/location";
+
+    /** Attribute name for the location URI */
+    public static final String SRC_ATTR  = "src";
+    /** Attribute name for the line number */
+    public static final String LINE_ATTR = "line";
+    /** Attribute name for the column number */
+    public static final String COL_ATTR  = "column";
+
+    /** Attribute qualified name for the location URI */
+    public static final String Q_SRC_ATTR  = "loc:src";
+    /** Attribute qualified name for the line number */
+    public static final String Q_LINE_ATTR = "loc:line";
+    /** Attribute qualified name for the column number */
+    public static final String Q_COL_ATTR  = "loc:column";
+    
+    // Private constructor, we only have static methods
+    private LocationAttributes() {
+        // Nothing
+    }
+    
+    /**
+     * Add location attributes to a set of SAX attributes.
+     * 
+     * @param locator the <code>Locator</code> (can be null)
+     * @param attrs the <code>Attributes</code> where locator information should be added
+     * @return
+     */
+    public static Attributes addLocationAttributes(Locator locator, Attributes attrs) {
+        if (locator == null || attrs.getIndex(URI, SRC_ATTR) != -1) {
+            // No location information known, or already has it
+            return attrs;
+        }
+        
+        // Get an AttributeImpl so that we can add new attributes.
+        AttributesImpl newAttrs = attrs instanceof AttributesImpl ?
+            (AttributesImpl)attrs : new AttributesImpl(attrs);
+
+        newAttrs.addAttribute(URI, SRC_ATTR, Q_SRC_ATTR, "CDATA", locator.getSystemId());
+        newAttrs.addAttribute(URI, LINE_ATTR, Q_LINE_ATTR, "CDATA", Integer.toString(locator.getLineNumber()));
+        newAttrs.addAttribute(URI, COL_ATTR, Q_COL_ATTR, "CDATA", Integer.toString(locator.getColumnNumber()));
+        
+        return newAttrs;
+    }
+    
+    /**
+     * Returns the {@link Location} of an element (SAX flavor).
+     * 
+     * @param attrs the element's attributes that hold the location information
+     * @param description a description for the location (can be null)
+     * @return a {@link Location} object
+     */
+    public static Location getLocation(Attributes attrs, String description) {
+        String src = attrs.getValue(URI, SRC_ATTR);
+        if (src == null) {
+            return Location.UNKNOWN;
+        }
+        
+        return new LocationImpl(description, src, getLine(attrs), getColumn(attrs));
+    }
+
+    /**
+     * Returns the location of an element (SAX flavor). If the location is to be kept
+     * into an object built from this element, consider using {@link #getLocation(Attributes)}
+     * and the {@link Locatable} interface.
+     * 
+     * @param attrs the element's attributes that hold the location information
+     * @return a location string as defined by {@link Location#toString()}.
+     */
+    public static String getLocationString(Attributes attrs) {
+        String src = attrs.getValue(URI, SRC_ATTR);
+        if (src == null) {
+            return LocationUtils.UNKNOWN_STRING;
+        }
+        
+        return src + ":" + attrs.getValue(URI, LINE_ATTR) + ":" + attrs.getValue(URI, COL_ATTR);
+    }
+    
+    /**
+     * Returns the URI of an element (SAX flavor)
+     * 
+     * @param attrs the element's attributes that hold the location information
+     * @return the element's URI or "<code>[unknown location]</code>" if <code>attrs</code>
+     *         has no location information.
+     */
+    public static String getURI(Attributes attrs) {
+        String src = attrs.getValue(URI, SRC_ATTR);
+        return src != null ? src : LocationUtils.UNKNOWN_STRING;
+    }
+    
+    /**
+     * Returns the line number of an element (SAX flavor)
+     * 
+     * @param attrs the element's attributes that hold the location information
+     * @return the element's line number or <code>-1</code> if <code>attrs</code>
+     *         has no location information.
+     */
+    public static int getLine(Attributes attrs) {
+        String line = attrs.getValue(URI, LINE_ATTR);
+        return line != null ? Integer.parseInt(line) : -1;
+    }
+    
+    /**
+     * Returns the column number of an element (SAX flavor)
+     * 
+     * @param attrs the element's attributes that hold the location information
+     * @return the element's column number or <code>-1</code> if <code>attrs</code>
+     *         has no location information.
+     */
+    public static int getColumn(Attributes attrs) {
+        String col = attrs.getValue(URI, COL_ATTR);
+        return col != null ? Integer.parseInt(col) : -1;
+    }
+    
+    /**
+     * Returns the {@link Location} of an element (DOM flavor).
+     * 
+     * @param attrs the element that holds the location information
+     * @param description a description for the location (if <code>null</code>, the element's name is used)
+     * @return a {@link Location} object
+     */
+    public static Location getLocation(Element elem, String description) {
+        Attr srcAttr = elem.getAttributeNodeNS(URI, SRC_ATTR);
+        if (srcAttr == null) {
+            return Location.UNKNOWN;
+        }
+
+        return new LocationImpl(description == null ? elem.getNodeName() : description,
+                srcAttr.getValue(), getLine(elem), getColumn(elem));
+    }
+    
+    /**
+     * Same as <code>getLocation(elem, null)</code>.
+     */
+    public static Location getLocation(Element elem) {
+        return getLocation(elem, null);
+    }
+   
+
+    /**
+     * Returns the location of an element that has been processed by this pipe (DOM flavor).
+     * If the location is to be kept into an object built from this element, consider using
+     * {@link #getLocation(Element)} and the {@link Locatable} interface.
+     * 
+     * @param elem the element that holds the location information
+     * @return a location string as defined by {@link Location#toString()}.
+     */
+    public static String getLocationString(Element elem) {
+        Attr srcAttr = elem.getAttributeNodeNS(URI, SRC_ATTR);
+        if (srcAttr == null) {
+            return LocationUtils.UNKNOWN_STRING;
+        }
+        
+        return srcAttr.getValue() + ":" + elem.getAttributeNS(URI, LINE_ATTR) + ":" + elem.getAttributeNS(URI, COL_ATTR);
+    }
+    
+    /**
+     * Returns the URI of an element (DOM flavor)
+     * 
+     * @param elem the element that holds the location information
+     * @return the element's URI or "<code>[unknown location]</code>" if <code>elem</code>
+     *         has no location information.
+     */
+    public static String getURI(Element elem) {
+        Attr attr = elem.getAttributeNodeNS(URI, SRC_ATTR);
+        return attr != null ? attr.getValue() : LocationUtils.UNKNOWN_STRING;
+    }
+
+    /**
+     * Returns the line number of an element (DOM flavor)
+     * 
+     * @param elem the element that holds the location information
+     * @return the element's line number or <code>-1</code> if <code>elem</code>
+     *         has no location information.
+     */
+    public static int getLine(Element elem) {
+        Attr attr = elem.getAttributeNodeNS(URI, LINE_ATTR);
+        return attr != null ? Integer.parseInt(attr.getValue()) : -1;
+    }
+
+    /**
+     * Returns the column number of an element (DOM flavor)
+     * 
+     * @param elem the element that holds the location information
+     * @return the element's column number or <code>-1</code> if <code>elem</code>
+     *         has no location information.
+     */
+    public static int getColumn(Element elem) {
+        Attr attr = elem.getAttributeNodeNS(URI, COL_ATTR);
+        return attr != null ? Integer.parseInt(attr.getValue()) : -1;
+    }
+    
+    /**
+     * Remove the location attributes from a DOM element.
+     * 
+     * @param elem the element to remove the location attributes from.
+     * @param recurse if <code>true</code>, also remove location attributes on descendant elements.
+     */
+    public static void remove(Element elem, boolean recurse) {
+        elem.removeAttributeNS(URI, SRC_ATTR);
+        elem.removeAttributeNS(URI, LINE_ATTR);
+        elem.removeAttributeNS(URI, COL_ATTR);
+        if (recurse) {
+            NodeList children = elem.getChildNodes();
+            for (int i = 0; i < children.getLength(); i++) {
+                Node child = children.item(i);
+                if (child.getNodeType() == Node.ELEMENT_NODE) {
+                    remove((Element)child, recurse);
+                }
+            }
+        }
+    }
+
+    /**
+     * A SAX filter that adds the information available from the <code>Locator</code> as attributes.
+     * The purpose of having location as attributes is to allow this information to survive transformations
+     * of the document (an XSL could copy these attributes over) or conversion of SAX events to a DOM.
+     * <p>
+     * The location is added as 3 attributes in a specific namespace to each element.
+     * <pre>
+     * &lt;root xmlns:loc="http://opensymphony.com/xwork/location"
+     *       loc:src="file://path/to/file.xml"
+     *       loc:line="1" loc:column="1"&gt;
+     *   &lt;foo loc:src="file://path/to/file.xml" loc:line="2" loc:column="3"/&gt;
+     * &lt;/root&gt;
+     * </pre>
+     * <strong>Note:</strong> Although this adds a lot of information to the serialized form of the document,
+     * the overhead in SAX events is not that big, as attribute names are interned, and all <code>src</code>
+     * attributes point to the same string.
+     * 
+     * @see com.opensymphony.xwork.util.location.LocationAttributes
+     */
+    public static class Pipe implements ContentHandler {
+        
+        private Locator locator;
+        
+        private ContentHandler nextHandler;
+        
+        /**
+         * Create a filter. It has to be chained to another handler to be really useful.
+         */
+        public Pipe() {
+        }
+
+        /**
+         * Create a filter that is chained to another handler.
+         * @param next the next handler in the chain.
+         */
+        public Pipe(ContentHandler next) {
+            nextHandler = next;
+        }
+
+        public void setDocumentLocator(Locator locator) {
+            this.locator = locator;
+            nextHandler.setDocumentLocator(locator);
+        }
+        
+        public void startDocument() throws SAXException {
+            nextHandler.startDocument();
+            nextHandler.startPrefixMapping(LocationAttributes.PREFIX, LocationAttributes.URI);
+        }
+        
+        public void endDocument() throws SAXException {
+            endPrefixMapping(LocationAttributes.PREFIX);
+            nextHandler.endDocument();
+        }
+
+        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException {
+            // Add location attributes to the element
+            nextHandler.startElement(uri, loc, raw, LocationAttributes.addLocationAttributes(locator, attrs));
+        }
+
+        public void endElement(String arg0, String arg1, String arg2) throws SAXException {
+            nextHandler.endElement(arg0, arg1, arg2);
+        }
+
+        public void startPrefixMapping(String arg0, String arg1) throws SAXException {
+            nextHandler.startPrefixMapping(arg0, arg1);
+        }
+
+        public void endPrefixMapping(String arg0) throws SAXException {
+            nextHandler.endPrefixMapping(arg0);
+        }
+
+        public void characters(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.characters(arg0, arg1, arg2);
+        }
+
+        public void ignorableWhitespace(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.ignorableWhitespace(arg0, arg1, arg2);
+        }
+
+        public void processingInstruction(String arg0, String arg1) throws SAXException {
+            nextHandler.processingInstruction(arg0, arg1);
+        }
+
+        public void skippedEntity(String arg0) throws SAXException {
+            nextHandler.skippedEntity(arg0);
+        }
+    }
+}

File java/com/opensymphony/xwork/util/location/LocationImpl.java

+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.opensymphony.xwork.util.location;
+
+import java.io.Serializable;
+
+/**
+ * A simple immutable and serializable implementation of {@link Location}.
+ */
+public class LocationImpl implements Location, Serializable {
+    private final String uri;
+    private final int line;
+    private final int column;
+    private final String description;
+    
+    // Package private: outside this package, use Location.UNKNOWN.
+    static final LocationImpl UNKNOWN = new LocationImpl(null, null, -1, -1);
+
+    /**
+     * Build a location for a given URI, with unknown line and column numbers.
+     * 
+     * @param uri the resource URI
+     */
+    public LocationImpl(String description, String uri) {
+        this(description, uri, -1, -1);
+    }
+
+    /**
+     * Build a location for a given URI and line and column numbers.
+     * 
+     * @param uri the resource URI
+     * @param line the line number (starts at 1)
+     * @param column the column number (starts at 1)
+     */
+    public LocationImpl(String description, String uri, int line, int column) {
+        if (uri == null || uri.length() == 0) {
+            this.uri = null;
+            this.line = -1;
+            this.column = -1;
+        } else {
+            this.uri = uri;
+            this.line = line;
+            this.column = column;
+        }
+        
+        if (description != null && description.length() == 0) {
+            description = null;
+        }
+        this.description = description;
+    }
+    
+    /**
+     * Copy constructor.
+     * 
+     * @param location the location to be copied
+     */
+    public LocationImpl(Location location) {
+        this(location.getDescription(), location.getURI(), location.getLineNumber(), location.getColumnNumber());
+    }
+    
+    /**
+     * Create a location from an existing one, but with a different description
+     */
+    public LocationImpl(String description, Location location) {
+        this(description, location.getURI(), location.getLineNumber(), location.getColumnNumber());
+    }
+    
+    /**
+     * Obtain a <code>LocationImpl</code> from a {@link Location}. If <code>location</code> is
+     * already a <code>LocationImpl</code>, it is returned, otherwise it is copied.
+     * <p>
+     * This method is useful when an immutable and serializable location is needed, such as in locatable
+     * exceptions.
+     * 
+     * @param location the location
+     * @return an immutable and serializable version of <code>location</code>
+     */
+    public static LocationImpl get(Location location) {
+        if (location instanceof LocationImpl) {
+            return (LocationImpl)location;
+        } else if (location == null) {
+            return UNKNOWN;
+        } else {
+            return new LocationImpl(location);
+        }
+    }
+    
+    /**
+     * Get the description of this location
+     * 
+     * @return the description (can be <code>null</code>)
+     */
+    public String getDescription() {
+        return this.description;
+    }
+    
+    /**
+     * Get the URI of this location
+     * 
+     * @return the URI (<code>null</code> if unknown).
+     */
+    public String getURI() {
+        return this.uri;
+    }
+
+    /**
+     * Get the line number of this location
+     * 
+     * @return the line number (<code>-1</code> if unknown)
+     */
+    public int getLineNumber() {
+        return this.line;
+    }
+    
+    /**
+     * Get the column number of this location
+     * 
+     * @return the column number (<code>-1</code> if unknown)
+     */
+    public int getColumnNumber() {
+        return this.column;
+    }
+
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (obj instanceof Location) {
+            Location other = (Location)obj;
+            return this.line == other.getLineNumber() && this.column == other.getColumnNumber()
+                   && testEquals(this.uri, other.getURI())
+                   && testEquals(this.description, other.getDescription());
+        }
+        
+        return false;
+    }
+    
+    public int hashCode() {
+        int hash = line ^ column;
+        if (uri != null) hash ^= uri.hashCode();
+        if (description != null) hash ^= description.hashCode();
+        
+        return hash;
+    }
+    
+    public String toString() {
+        return LocationUtils.toString(this);
+    }
+    
+    /**
+     * Ensure serialized unknown location resolve to {@link Location#UNKNOWN}.
+     */
+    private Object readResolve() {
+        return this.equals(Location.UNKNOWN) ? Location.UNKNOWN : this;
+    }
+    
+    private boolean testEquals(Object object1, Object object2) {
+        if (object1 == object2) {
+            return true;
+        }
+        if ((object1 == null) || (object2 == null)) {
+            return false;
+        }
+        return object1.equals(object2);
+    }
+}

File java/com/opensymphony/xwork/util/location/LocationUtils.java

+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.opensymphony.xwork.util.location;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.transform.SourceLocator;
+import javax.xml.transform.TransformerException;
+
+import org.xml.sax.Locator;
+import org.xml.sax.SAXParseException;
+
+/**
+ * Location-related utility methods.
+ */
+public class LocationUtils {
+    
+    /**
+     * The string representation of an unknown location: "<code>[unknown location]</code>".
+     */
+    public static final String UNKNOWN_STRING = "[unknown location]";
+    
+    private static List finders = new ArrayList();
+    
+    /**
+     * An finder or object locations
+     */
+    public interface LocationFinder {
+        /**
+         * Get the location of an object
+         * @param obj the object for which to find a location
+         * @param description and optional description to be added to the object's location
+         * @return the object's location or <code>null</code> if object's class isn't handled
+         *         by this finder.
+         */
+        Location getLocation(Object obj, String description);
+    }
+
+    private LocationUtils() {
+        // Forbid instanciation
+    }
+    
+    /**
+     * Builds a string representation of a location, in the
+     * "<code><em>descripton</em> - <em>uri</em>:<em>line</em>:<em>column</em></code>"
+     * format (e.g. "<code>foo - file://path/to/file.xml:3:40</code>"). For {@link Location#UNKNOWN an unknown location}, returns
+     * {@link #UNKNOWN_STRING}.
+     * 
+     * @return the string representation
+     */
+    public static String toString(Location location) {
+        StringBuffer result = new StringBuffer();
+
+        String description = location.getDescription();
+        if (description != null) {
+            result.append(description).append(" - ");
+        }
+
+        String uri = location.getURI();
+        if (uri != null) {
+            result.append(uri).append(':').append(location.getLineNumber()).append(':').append(location.getColumnNumber());
+        } else {
+            result.append(UNKNOWN_STRING);
+        }
+        
+        return result.toString();
+    }
+
+    /**
+     * Parse a location string of the form "<code><em>uri</em>:<em>line</em>:<em>column</em></code>" (e.g.
+     * "<code>path/to/file.xml:3:40</code>") to a Location object. Additionally, a description may
+     * also optionally be present, separated with an hyphen (e.g. "<code>foo - path/to/file.xml:3.40</code>").
+     * 
+     * @param text the text to parse
+     * @return the location (possibly <code>null</code> if text was null or in an incorrect format)
+     */
+    public static LocationImpl parse(String text) throws IllegalArgumentException {
+        if (text == null || text.length() == 0) {
+            return null;
+        }
+
+        // Do we have a description?
+        String description;
+        int uriStart = text.lastIndexOf(" - "); // lastIndexOf to allow the separator to be in the description
+        if (uriStart > -1) {
+            description = text.substring(0, uriStart);
+            uriStart += 3; // strip " - "
+        } else {
+            description = null;
+            uriStart = 0;
+        }
+        
+        try {
+            int colSep = text.lastIndexOf(':');
+            if (colSep > -1) {
+                int column = Integer.parseInt(text.substring(colSep + 1));
+                
+                int lineSep = text.lastIndexOf(':', colSep - 1);
+                if (lineSep > -1) {
+                    int line = Integer.parseInt(text.substring(lineSep + 1, colSep));
+                    return new LocationImpl(description, text.substring(uriStart, lineSep), line, column);
+                }
+            } else {
+                // unkonwn?
+                if (text.endsWith(UNKNOWN_STRING)) {
+                    return LocationImpl.UNKNOWN;
+                }
+            }
+        } catch(Exception e) {
+            // Ignore: handled below
+        }
+        
+        return LocationImpl.UNKNOWN;
+    }
+
+    /**
+     * Checks if a location is known, i.e. it is not null nor equal to {@link Location#UNKNOWN}.
+     * 
+     * @param location the location to check
+     * @return <code>true</code> if the location is known
+     */
+    public static boolean isKnown(Location location) {
+        return location != null && !Location.UNKNOWN.equals(location);
+    }
+
+    /**
+     * Checks if a location is unknown, i.e. it is either null or equal to {@link Location#UNKNOWN}.
+     * 
+     * @param location the location to check
+     * @return <code>true</code> if the location is unknown
+     */
+    public static boolean isUnknown(Location location) {
+        return location == null || Location.UNKNOWN.equals(location);
+    }
+
+    /**
+     * Add a {@link LocationFinder} to the list of finders that will be queried for an object's
+     * location by {@link #getLocation(Object, String)}.
+     * <p>
+     * <b>Important:</b> LocationUtils internally stores a weak reference to the finder. This
+     * avoids creating strong links between the classloader holding this class and the finder's
+     * classloader, which can cause some weird memory leaks if the finder's classloader is to
+     * be reloaded. Therefore, you <em>have</em> to keep a strong reference to the finder in the
+     * calling code, e.g.:
+     * <pre>
+     *   private static LocationUtils.LocationFinder myFinder =
+     *       new LocationUtils.LocationFinder() {
+     *           public Location getLocation(Object obj, String desc) {
+     *               ...
+     *           }
+     *       };
+     *
+     *   static {
+     *       LocationUtils.addFinder(myFinder);
+     *   }
+     * </pre>
+     * 
+     * @param finder the location finder to add
+     */
+    public static void addFinder(LocationFinder finder) {
+        if (finder == null) {
+            return;
+        }
+
+        synchronized(LocationFinder.class) {
+            // Update a clone of the current finder list to avoid breaking
+            // any iteration occuring in another thread.
+            List newFinders = new ArrayList(finders);
+            newFinders.add(new WeakReference(finder));
+            finders = newFinders;
+        }
+    }
+    
+    /**
+     * Get the location of an object. Some well-known located classes built in the JDK are handled
+     * by this method. Handling of other located classes can be handled by adding new location finders.
+     * 
+     * @param obj the object of which to get the location
+     * @return the object's location, or {@link Location#UNKNOWN} if no location could be found
+     */
+    public static Location getLocation(Object obj) {
+        return getLocation(obj, null);
+    }
+    
+    /**
+     * Get the location of an object. Some well-known located classes built in the JDK are handled
+     * by this method. Handling of other located classes can be handled by adding new location finders.
+     * 
+     * @param obj the object of which to get the location
+     * @param description an optional description of the object's location, used if a Location object
+     *        has to be created.
+     * @return the object's location, or {@link Location#UNKNOWN} if no location could be found
+     */
+    public static Location getLocation(Object obj, String description) {
+        if (obj instanceof Locatable) {
+            return ((Locatable)obj).getLocation();
+        }
+        
+        // Check some well-known locatable exceptions
+        if (obj instanceof SAXParseException) {
+            SAXParseException spe = (SAXParseException)obj;
+            if (spe.getSystemId() != null) {
+                return new LocationImpl(description, spe.getSystemId(), spe.getLineNumber(), spe.getColumnNumber());
+            } else {
+                return Location.UNKNOWN;
+            }
+        }
+        
+        if (obj instanceof TransformerException) {
+            TransformerException ex = (TransformerException)obj;
+            SourceLocator locator = ex.getLocator();
+            if (locator != null && locator.getSystemId() != null) {
+                return new LocationImpl(description, locator.getSystemId(), locator.getLineNumber(), locator.getColumnNumber());
+            } else {
+                return Location.UNKNOWN;
+            }
+        }
+        
+        if (obj instanceof Locator) {
+            Locator locator = (Locator)obj;
+            if (locator.getSystemId() != null) {
+                return new LocationImpl(description, locator.getSystemId(), locator.getLineNumber(), locator.getColumnNumber());
+            } else {
+                return Location.UNKNOWN;
+            }
+        }
+
+        List currentFinders = finders; // Keep the current list
+        int size = currentFinders.size();
+        for (int i = 0; i < size; i++) {
+            WeakReference ref = (WeakReference)currentFinders.get(i);
+            LocationFinder finder = (LocationFinder)ref.get();
+            if (finder == null) {
+                // This finder was garbage collected: update finders
+                synchronized(LocationFinder.class) {
+                    // Update a clone of the current list to avoid breaking current iterations
+                    List newFinders = new ArrayList(finders);
+                    newFinders.remove(ref);
+                    finders = newFinders;
+                }
+            }
+            
+            Location result = finder.getLocation(obj, description);
+            if (result != null) {
+                return result;
+            }
+        }
+
+        return Location.UNKNOWN;
+    }
+}

File java/com/opensymphony/xwork/util/location/package.html

+<html>
+  <body>Classes and utilities used to track location information.</body>
+</html>

File test/com/opensymphony/xwork/interceptor/TimerInterceptorTest.java

 
     public void testLogCategoryLevel() throws Exception {
         interceptor.setLogCategory("com.mycompany.myapp.actiontiming");
-        interceptor.setLogLevel("debug");
+        interceptor.setLogLevel("error");
         interceptor.intercept(mai);
         assertTrue(interceptor.message.startsWith("Executed action [myApp/myAction!execute] took "));
         assertNotSame(interceptor.logger, TimerInterceptor.log);

File test/com/opensymphony/xwork/util/DomHelperTest.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork.util;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import org.w3c.dom.*;
+import org.xml.sax.*;
+import java.io.*;
+import com.opensymphony.xwork.util.location.*;
+
+import javax.xml.transform.*;
+import javax.xml.transform.stream.*;
+import javax.xml.transform.dom.*;
+
+
+/**
+ * Test cases for {@link DomHelper}.
+ */
+public class DomHelperTest extends TestCase {
+
+    private String xml = "<!DOCTYPE foo [\n" +
+                         "<!ELEMENT foo (bar)>\n" +
+                         "<!ELEMENT bar (#PCDATA)>\n" +
+                         "]>\n" +
+                         "<foo>\n" +
+                         " <bar/>\n" +
+                         "</foo>\n";
+    
+    public void testParse() throws Exception {
+        InputSource in = new InputSource(new StringReader(xml));
+        in.setSystemId("foo://bar");
+        
+        Document doc = DomHelper.parse(in);
+        assertNotNull(doc);
+        assertTrue("Wrong root node",
+            "foo".equals(doc.getDocumentElement().getNodeName()));
+        
+        NodeList nl = doc.getElementsByTagName("bar");
+        assertTrue(nl.getLength() == 1);
+        
+        
+        
+    }
+    
+    public void testGetLocationObject() throws Exception {
+        InputSource in = new InputSource(new StringReader(xml));
+        in.setSystemId("foo://bar");
+        
+        Document doc = DomHelper.parse(in);
+        
+        NodeList nl = doc.getElementsByTagName("bar");
+        
+        Location loc = DomHelper.getLocationObject((Element)nl.item(0));
+        
+        assertNotNull(loc);
+        assertTrue("Should be line 6, was "+loc.getLineNumber(), 
+            6==loc.getLineNumber());
+    }
+}

File test/com/opensymphony/xwork/util/location/LocationAttributesTest.java

+/*
+ * Copyright (c) 2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork.util.location;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.DocumentBuilder;
+import org.w3c.dom.Document;
+
+
+import junit.framework.TestCase;
+
+public class LocationAttributesTest extends TestCase {
+    
+    public LocationAttributesTest(String name) {
+        super(name);
+    }
+    
+    public void testAddLocationAttributes() throws Exception {
+        AttributesImpl attrs = new AttributesImpl();
+        LocationAttributes.addLocationAttributes(new Locator() {
+            public int getColumnNumber() { return 40; }
+            public int getLineNumber() { return 1; }
+            public String getSystemId() { return "path/to/file.xml"; }
+            public String getPublicId() { return "path/to/file.xml"; }
+        }, attrs);
+
+        assertTrue("path/to/file.xml".equals(attrs.getValue("loc:src")));
+        assertTrue("1".equals(attrs.getValue("loc:line")));
+        assertTrue("40".equals(attrs.getValue("loc:column")));
+    }
+ 
+    public void testRecursiveRemove() throws Exception {
+        Document doc = getDoc("xml-with-location.xml");
+
+        Element root = doc.getDocumentElement();
+        LocationAttributes.remove(root, true);
+
+        assertNull(root.getAttributeNode("loc:line"));
+        assertNull(root.getAttributeNode("loc:column"));
+        assertNull(root.getAttributeNode("loc:src"));
+
+        Element kid = (Element)doc.getElementsByTagName("bar").item(0);
+        assertNull(kid.getAttributeNode("loc:line"));
+        assertNull(kid.getAttributeNode("loc:column"));
+        assertNull(kid.getAttributeNode("loc:src"));
+    }    
+
+    public void testNonRecursiveRemove() throws Exception {
+        Document doc = getDoc("xml-with-location.xml");
+
+        Element root = doc.getDocumentElement();
+        LocationAttributes.remove(root, false);
+
+        assertNull(root.getAttributeNode("loc:line"));
+        assertNull(root.getAttributeNode("loc:column"));
+        assertNull(root.getAttributeNode("loc:src"));
+
+        Element kid = (Element)doc.getElementsByTagName("bar").item(0);
+        assertNotNull(kid.getAttributeNode("loc:line"));
+        assertNotNull(kid.getAttributeNode("loc:column"));
+        assertNotNull(kid.getAttributeNode("loc:src"));
+    }    
+
+    private Document getDoc(String path) throws Exception {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        return builder.parse(LocationAttributesTest.class.getResourceAsStream(path));
+
+
+    }
+}

File test/com/opensymphony/xwork/util/location/LocationImplTest.java

+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.opensymphony.xwork.util.location;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import junit.framework.TestCase;
+
+public class LocationImplTest extends TestCase {
+    
+    public LocationImplTest(String name) {
+        super(name);
+    }
+    
+    static final String str = "path/to/file.xml:1:40";
+
+    public void testEquals() throws Exception {
+        Location loc1 = LocationUtils.parse(str);
+        Location loc2 = new LocationImpl(null, "path/to/file.xml", 1, 40);
+        
+        assertEquals("locations", loc1, loc2);
+        assertEquals("hashcode", loc1.hashCode(), loc2.hashCode());
+        assertEquals("string representation", loc1.toString(), loc2.toString());
+    }
+    
+    /**
+     * Test that Location.UNKNOWN is kept identical on deserialization