Use JAXP 1.3 for all XML services. Create Schema objects.
authorgilbert <gilbert@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Thu, 27 Jan 2005 19:15:01 +0000 (19:15 +0000)
committergilbert <gilbert@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Thu, 27 Jan 2005 19:15:01 +0000 (19:15 +0000)
git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/trunk@1224 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

src/edu/internet2/middleware/shibboleth/serviceprovider/ServiceProviderConfig.java
src/edu/internet2/middleware/shibboleth/xml/Parser.java [new file with mode: 0644]
src/edu/internet2/middleware/shibboleth/xml/Schemas.java [new file with mode: 0644]
src/edu/internet2/middleware/shibboleth/xml/SchemasDirectoryImpl.java [new file with mode: 0644]

index 385e238..17a4241 100644 (file)
 
 package edu.internet2.middleware.shibboleth.serviceprovider;
 
-import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
@@ -143,7 +142,6 @@ import java.util.Map;
 import java.util.TreeMap;
 
 import org.apache.log4j.Logger;
-import org.apache.xerces.parsers.DOMParser;
 import org.apache.xmlbeans.XmlException;
 import org.apache.xmlbeans.XmlOptions;
 import org.opensaml.SAMLAssertion;
@@ -153,10 +151,6 @@ import org.opensaml.SAMLObject;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
-import org.xml.sax.ErrorHandler;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-import org.xml.sax.SAXParseException;
 
 import x0.maceShibboleth1.AttributeAcceptancePolicyDocument;
 import x0.maceShibbolethTargetConfig1.ApplicationDocument;
@@ -172,7 +166,6 @@ import x0.maceShibbolethTargetConfig1.ShibbolethTargetConfigDocument.ShibbolethT
 import edu.internet2.middleware.shibboleth.common.AAP;
 import edu.internet2.middleware.shibboleth.common.AttributeRule;
 import edu.internet2.middleware.shibboleth.common.Credentials;
-import edu.internet2.middleware.shibboleth.common.ShibResource;
 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
 import edu.internet2.middleware.shibboleth.common.XML;
 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
@@ -180,6 +173,7 @@ import edu.internet2.middleware.shibboleth.metadata.EntityLocator;
 import edu.internet2.middleware.shibboleth.metadata.Metadata;
 import edu.internet2.middleware.shibboleth.metadata.Provider;
 import edu.internet2.middleware.shibboleth.metadata.ProviderRole;
+import edu.internet2.middleware.shibboleth.xml.Parser;
 
 /**
  * Load the configuration files into objects, index them, and return them on request.
@@ -296,7 +290,6 @@ public class ServiceProviderConfig {
        
        
        
-       private DOMParser parser = new DOMParser();
        
        /**
         * The constructor prepares for, but does not parse the configuration.
@@ -304,32 +297,7 @@ public class ServiceProviderConfig {
         * @throws ShibbolethConfigurationException
         *        if XML Parser cannot be initialized (Classpath problem)
         */
-       public ServiceProviderConfig() 
-               throws ShibbolethConfigurationException {
-               try {
-               parser.setFeature("http://xml.org/sax/features/validation", true);
-               parser.setFeature("http://apache.org/xml/features/validation/schema", true);
-        
-        
-               parser.setErrorHandler(new ErrorHandler() {
-        
-                       public void error(SAXParseException arg0) throws SAXException {
-                               throw new SAXException("Error parsing xml file: " + arg0);
-                       }
-        
-                       public void fatalError(SAXParseException arg0) throws SAXException {
-                               throw new SAXException("Error parsing xml file: " + arg0);
-                       }
-        
-                       public void warning(SAXParseException arg0) throws SAXException {
-                               throw new SAXException("Error parsing xml file: " + arg0);
-                       }
-               });
-        
-        } catch (SAXException e) {
-               log.error("Unable to setup a workable XML parser: " + e);
-               throw new ShibbolethConfigurationException("Unable to setup a workable XML parser.");
-        }
+       public ServiceProviderConfig() {
        }
 
        /**
@@ -352,9 +320,8 @@ public class ServiceProviderConfig {
                }
 
                Document configDoc;
-        try {
-            configDoc = loadDom(configFilePath, MAINSCHEMA);
-        } catch (InternalConfigurationException e) {
+        configDoc = Parser.loadDom(configFilePath, true);
+        if (configDoc==null) {
             throw new ShibbolethConfigurationException("XML error in "+configFilePath);
         }
         loadConfigBean(configDoc);
@@ -667,7 +634,9 @@ public class ServiceProviderConfig {
                 schemaname=tempname;
             
                try {
-                       Document extdoc = loadDom(uri,schemaname);
+                       Document extdoc = Parser.loadDom(uri,true);
+                       if (extdoc==null)
+                           return null;
                        impl.initialize(extdoc);
                } catch (Exception e) {
                        log.error("XML error " + e);
@@ -713,7 +682,9 @@ public class ServiceProviderConfig {
                    uri.startsWith(INLINEURN))
                return false;
                try {
-                       Document sitedoc = loadDom(uri,METADATASCHEMA);
+                       Document sitedoc = Parser.loadDom(uri,true);
+                       if (sitedoc==null)
+                           return false;
                        XMLMetadataImpl impl = new XMLMetadataImpl();
                        impl.initialize(sitedoc);
                        addOrReplaceMetadataImplementor(uri,impl);
@@ -759,7 +730,9 @@ public class ServiceProviderConfig {
                    uri.startsWith(INLINEURN))
                return false;
                try {
-                       Document aapdoc = loadDom(uri,AAPSCHEMA);
+                       Document aapdoc = Parser.loadDom(uri,true);
+                       if (aapdoc==null)
+                           return false;
                        AttributeAcceptancePolicyDocument aap = AttributeAcceptancePolicyDocument.Factory.parse(aapdoc);
                        XMLAAPImpl impl = new XMLAAPImpl();
                        impl.initialize(aapdoc);
@@ -814,7 +787,9 @@ public class ServiceProviderConfig {
                    uri.startsWith(INLINEURN))
                return false;
                try {
-                       Document trustdoc = loadDom(uri,TRUSTSCHEMA);
+                       Document trustdoc = Parser.loadDom(uri,true);
+                       if (trustdoc==null)
+                           return false;
                        XMLTrustImpl impl = new XMLTrustImpl();
                        impl.initialize(trustdoc);
                        addOrReplaceTrustImplementor(uri,impl);
@@ -859,7 +834,9 @@ public class ServiceProviderConfig {
                
            } else { // external file
                try {
-                   mapdoc = loadDom(uri,METADATASCHEMA);
+                   mapdoc = Parser.loadDom(uri,true);
+                   if (mapdoc==null)
+                       return true;
                    requestMapDoc = RequestMapDocument.Factory.parse(mapdoc);
                } catch (Exception e) {
                    log.error("Error while parsing RequestMap file "+uri);
@@ -874,80 +851,6 @@ public class ServiceProviderConfig {
 
        
        
-       /**
-        * Given a file URI, parse it (validated against a schema) to a DOM.
-        * 
-        * <p>Note: The exact format of the URI (absolute file path, resource
-        * path, URL) is interpreted here. Changes to URI conventions can be
-        * implemented here and apply throughout the configuration system.</p>
-        *
-        * @param configFilePath Path to the particular configuration file
-        * @param schemaFilePath Path to the schema for this type of file 
-        * @return DOM Document object created from the file
-        * @throws ShibbolethConfigurationException if file not found or invalid
-        */
-       private Document loadDom(String configFilePath, String schemaFilePath) 
-               throws InternalConfigurationException {
-               
-               log.debug("Loading Configuration from (" + configFilePath + ").");
-
-                       InputSource insrc;
-                       String schemaCannonicalFilePath;
-                       ShibResource configResource;
-                       ShibResource schemaResource;
-                       try {
-                               configResource = new ShibResource(configFilePath,this.getClass());
-                               insrc = new InputSource(configResource.getInputStream());
-                       } catch (Exception e1) {
-                               log.error("Configuration file "+configFilePath+" could not be located.");
-                               throw new InternalConfigurationException();
-                       }
-                       
-                       try {
-                               schemaResource = new ShibResource(schemaFilePath,this.getClass());
-                schemaCannonicalFilePath = schemaResource.getFile().getCanonicalPath();
-                       } catch (Exception e) {
-                               log.error("Schema file "+schemaFilePath+" could not be located.");
-                               throw new InternalConfigurationException();
-                       }
-                       
-                       
-               return loadDom(insrc,schemaCannonicalFilePath);
-               
-       }
-
-       
-       /**
-        * Parse an InputSource (maybe file, maybe buffer) into a DOM
-        * @param insrc XML InputSource object
-        * @param schemaFilePath
-        * @return DOM Document
-        * @throws ShibbolethConfigurationException
-        */
-       private Document loadDom(InputSource insrc, String schemaFilePath) 
-               throws InternalConfigurationException {
-       
-               try {   
-                       parser.setProperty("http://java.sun.com/xml/jaxp/properties/schemaSource",
-                                       schemaFilePath);
-                       
-                       parser.parse(insrc);
-                       
-               } catch (SAXException e) {
-                       log.error("Error while parsing shibboleth configuration: " + e);
-                       throw new InternalConfigurationException();
-               } catch (IOException e) {
-                       log.error("Could not load shibboleth configuration file: " + e);
-                       throw new InternalConfigurationException();
-               }
-       
-               return parser.getDocument();
-               
-       }
-
-       
-       
-
        private int inlinenum = 1;
        private String genDummyUri() {
                return INLINEURN+Integer.toString(inlinenum++);
diff --git a/src/edu/internet2/middleware/shibboleth/xml/Parser.java b/src/edu/internet2/middleware/shibboleth/xml/Parser.java
new file mode 100644 (file)
index 0000000..0fcff96
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * Parser.java
+ * 
+ * Validating and non-validating XML parsing using JAXP 1.3.
+ * 
+ * Previous versions of the code directly used the Xerces DOMParser
+ * class. This class has been hidden in the Sun XML stack, and the
+ * public interface is to use DocumentBuilderFactory. This module 
+ * requires the DOM 3 and JAXP 1.3 support built into J2SE 5.0 and
+ * distributed separately for earlier releases of Java from
+ * https://jaxp.dev.java.net/. It should also work with Xerces 2.7.0
+ * when that release becomes available.
+ * 
+ * The org.opensaml.XML class already has most of the parsing code,
+ * but it uses a subset of the required Schemas. Here we build a
+ * wider Schema object, set it as the default SAML schema (because
+ * some Shibboleth namespace fields appear in SAML statements), and
+ * demand that Schema for every parser (DocumentBuilder) we request.
+ * 
+ * Currently, this class exposes static methods. Should a real 
+ * framework be installed, it would become a singleton object.
+ */
+package edu.internet2.middleware.shibboleth.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.validation.Schema;
+
+import org.apache.log4j.Logger;
+import org.opensaml.SAMLException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * Obtain schema validating and non-validating XML parsers.
+ * 
+ * @author Howard Gilbert
+ */
+public class Parser {
+    private static Logger log = Logger.getLogger(Parser.class);
+    
+    
+    /**
+     * All the namespaces used by any part of Shibboleth
+     * 
+     * Note: The current Schema compiler requires that dependencies
+     * (imports) be listed before the namespace of the schema
+     * that imports them.
+     */
+    private static String[] namespaces = new String[]{
+            "http://www.w3.org/2000/09/xmldsig#",  
+            "http://www.w3.org/2001/04/xmlenc#",
+            "urn:oasis:names:tc:SAML:1.0:assertion",
+            "urn:oasis:names:tc:SAML:2.0:assertion",
+            "http://www.w3.org/XML/1998/namespace",
+            "http://schemas.xmlsoap.org/soap/envelope/",
+            "urn:mace:shibboleth:credentials:1.0",
+            "urn:oasis:names:tc:SAML:1.0:protocol",
+            "urn:oasis:names:tc:SAML:2.0:protocol",
+            "urn:mace:shibboleth:namemapper:1.0",
+            "urn:mace:shibboleth:origin:1.0",
+            "urn:mace:shibboleth:arp:1.0",
+            "urn:mace:shibboleth:resolver:1.0",
+            "urn:mace:shibboleth:target:config:1.0",
+            "urn:mace:shibboleth:trust:1.0",
+            "urn:mace:shibboleth:1.0",
+            "http://schemas.xmlsoap.org/soap/envelope/",
+            "urn:oasis:names:tc:SAML:2.0:metadata",
+            "http://www.w3.org/2002/06/xmldsig-filter2"
+      };
+    
+    // If there were a real Framework here (like Spring) then
+    // the schemaBuilder would be inserted 
+    private static Schemas schemaBuilder = new SchemasDirectoryImpl();
+    private static Schema schema = schemaBuilder.compileSchema(namespaces);
+    
+    /**
+     * Load a DOM from a wrapped byte stream.
+     * 
+     * @param ins InputSource The XML document
+     * @param validate If true, use Schema. Otherwise, its raw XML.
+     * @return A DOM 3 tree
+     */
+    public static Document loadDom(InputSource ins, boolean validate) {
+        Document doc=null;
+        log.info("Loading XML from (" + ins.getSystemId() + ")"
+                + (validate?" with Schema validation":"")
+                );
+        try {
+            if (validate)
+                doc = org.opensaml.XML.parserPool.parse(ins,schema);
+            else
+                doc = org.opensaml.XML.parserPool.parse(ins,null);
+            
+        } catch (SAXException e) {
+            log.error("Error in XML configuration file"+e);
+            return null;
+        } catch (IOException e) {
+            log.error("Error accessing XML configuration file"+e);
+            return null;
+        } catch (SAMLException e) {
+            log.error("Error with XML parser "+e);
+            return null;
+       }
+        
+        return doc;
+    }
+    
+    
+    /**
+     * A dummy class that pretends to be an old Xerces DOMParser
+     * to simplify conversion of existing code.
+     */
+    public static class DOMParser {
+        Document doc = null;
+        boolean validate = false;
+        
+        public DOMParser(boolean validate) {
+            this.validate=validate;
+        }
+        
+        public Document parse(InputSource ins) throws SAXException, IOException {
+            doc = loadDom(ins,true);
+            return doc;
+        }
+        
+        public Document getDocument() {
+            return doc;
+        }
+    }
+    
+    /**
+     * Write a DOM out to a character stream (for debugging and logging)
+     * 
+     * @param dom The DOM tree to write
+     * @return A string containing the XML in character form.
+     */
+    public static String serialize(Node dom) {
+        String ret = null;
+        
+        TransformerFactory factory = TransformerFactory.newInstance();
+        Transformer transformer = null;
+        DOMSource source = new DOMSource(dom);
+        try {
+            transformer = factory.newTransformer();
+            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        } catch (TransformerConfigurationException e) {
+            return null;
+        }
+        StringWriter stringWriter = new StringWriter();
+        StreamResult result = new StreamResult(stringWriter);
+        try {
+            transformer.transform(source, result);
+        } catch (TransformerException e1) {
+            return null;
+        }
+        return stringWriter.toString();
+    }
+    
+    /**
+     * Version of loadDom where the file is specified as a resource name
+     * 
+     * @param configFilePath input resource
+     * @param validate if true, use Schema
+     * @return DOM tree
+     */
+    public static Document loadDom(String configFilePath,boolean validate) 
+    {
+        InputSource insrc;
+        String schemaCannonicalFilePath;
+       try {
+            InputStream resourceAsStream = Parser.class.getResourceAsStream(configFilePath);
+            insrc = new InputSource(resourceAsStream);
+            insrc.setSystemId(configFilePath);
+        } catch (Exception e1) {
+            log.error("Configuration file "+configFilePath+" could not be located.");
+            return null;
+        }
+        
+        return loadDom(insrc,validate); // Now pass on to the main routine
+        
+    }
+    
+    /**
+     * Override the OpenSAML default schema from SAML 1.1 to 
+     * SAML 1.1 plus Shibboleth (and some SAML 2.0).
+     */
+    static {
+        org.opensaml.XML.parserPool.setDefaultSchema(schema);
+    }
+    
+}
\ No newline at end of file
diff --git a/src/edu/internet2/middleware/shibboleth/xml/Schemas.java b/src/edu/internet2/middleware/shibboleth/xml/Schemas.java
new file mode 100644 (file)
index 0000000..9f1e8ec
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Schemas.java
+ * 
+ * For now there is only one implementation of Schema store -
+ * a resource directory in the WAR file of the application.
+ * So there is only one current implementation of this interface,
+ * the SchemasDirectoryImpl class. But we define the interface
+ * and later on there may be other schema-store options.
+ */
+package edu.internet2.middleware.shibboleth.xml;
+
+import javax.xml.validation.Schema;
+
+/**
+ * @author Howard Gilbert
+ */
+public interface Schemas {
+    
+    Schema compileSchema(String[] namespaces);
+
+}
diff --git a/src/edu/internet2/middleware/shibboleth/xml/SchemasDirectoryImpl.java b/src/edu/internet2/middleware/shibboleth/xml/SchemasDirectoryImpl.java
new file mode 100644 (file)
index 0000000..a0ebc81
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * SchemasDirectoryImpl.java
+ * 
+ * Find Schemas in a Resource directory
+ * 
+ * 
+ */
+package edu.internet2.middleware.shibboleth.xml;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.XMLConstants;
+import javax.xml.transform.Source;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+
+import org.apache.log4j.Logger;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * @author Howard Gilbert
+ */
+public class SchemasDirectoryImpl implements Schemas {
+    
+    private static Logger log = Logger.getLogger(SchemasDirectoryImpl.class);
+    
+    // The default is the /schemas/ resource directory in the WAR file.
+    private String resourcedir = "/schemas/";
+
+    public String getResourcedir() {
+        return resourcedir;
+    }
+    
+    /**
+     * Allow alternate schema directories to be injected if xsd files
+     * cannot coexist within a single schema. For example, SAML 1.0 and
+     * 1.1 schemas use the same targetNamespace incompatibly. If you 
+     * upgrade from "list of file names" to "list of namespaces", then
+     * two incompatible uses of the same namespace have to be stored in
+     * two separate schema directories.
+     * 
+     * @param resourcedir Resource directory from which to load xsd files.
+     */
+    public void setResourcedir(String resourcedir) {
+        this.resourcedir = resourcedir;
+    }
+    /**
+     * Create JAXP 1.3 Schema object from list of namespaces and resource dir
+     * 
+     * <p>This is an alternate approach to the Schema building logic used in
+     * org.opensaml.XML. That module is driven off a list of file names. 
+     * This code reads in all the *.xsd files in a directory, indexes them 
+     * by the namespace the schema defines, and then is driven off a list
+     * of namespaces. This is more more indirect and requires a bit more
+     * code, but it is more in line with the actual XSD standard where files
+     * and filenames are incidental. It can also be quickly ported to some
+     * other schema storage medium (LDAP, Database, ...).</p>
+     * 
+     * @param namespaces Array of required XML namespaces for validation
+     * @param resourcedir Resource directory with schema files ("/schemas/")
+     * @return Schema object combining all namespaces.
+     */
+    public Schema compileSchema(String[] namespaces) {
+        
+        Schema schema = null;
+        
+        Map bucket = new HashMap();
+        
+        // Find a directory of schemas
+        // It is a resource in WEB-INF/classes or the same jar file
+        // from which this class was loaded.
+        URL resource = Parser.class.getResource(resourcedir);
+        String path = resource.getPath();
+        File dir = new File(path);
+        if (!dir.isDirectory()) {
+            log.error("Cannot find the schemas resource directory");
+            return null;
+        }
+        
+        // for each .xsd file in the directory
+        String[] filenames = dir.list();
+        int nextsource=0;
+        for (int i=0;i<filenames.length;i++) {
+            String filename = filenames[i];
+            if (!filename.endsWith(".xsd"))
+                continue;
+            InputStream inputStream =
+                    Parser.class.getResourceAsStream(
+                        "/schemas/" + filename);
+            InputSource insrc = new InputSource(inputStream);
+           
+            // Non-validating parse to DOM
+            Document xsddom = Parser.loadDom(insrc,false);
+            
+            // Get the target namespace from the root element
+            Element ele = xsddom.getDocumentElement();
+            if (!ele.getLocalName().equals("schema")) {
+                log.error("Schema file wrong root element:"+filename);
+                continue;
+            }
+            String targetNamespace = ele.getAttribute("targetNamespace");
+            if (targetNamespace==null) {
+                log.error("Schema has no targetNamespace: "+filename);
+                continue;
+            }
+            
+            // Put the DOM in the Bucket keyed by namespace
+            if (bucket.containsKey(targetNamespace)) {
+                log.error("Schema for already defined namespace: "+targetNamespace+" "+filename);
+                continue;
+            }
+            bucket.put(targetNamespace,xsddom);
+        }
+        // Ok, so now we have a bucket of DOM objects keyed by the 
+        // namespaces that they internally declare they define
+        
+        
+        // Now we have a list of Namespaces in the order we need 
+        // to process them (imported dependencies first)
+        Source[] sources = new Source[namespaces.length];
+        for (int i=0;i<namespaces.length;i++) {
+            Document doc = (Document) bucket.get(namespaces[i]);
+            if (doc==null) 
+                log.error("Schema missing for namespace "+namespaces[i]);
+            sources[i]= new DOMSource(doc);
+        }
+        
+        // Now compile all the XSD files into a single composite Schema object
+        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+        try {
+            schema = factory.newSchema(sources);
+        } catch (SAXException e) {
+            log.error("Schemas failed to compile, dependencies may have changed "+e);
+            System.out.println(e);
+        }
+        return schema;
+        
+    }
+
+}