/* * Copyright 2001-2005 Internet2 * * 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 gov.nih.nci.cagrid.opensaml; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; import org.apache.log4j.NDC; import org.apache.xml.security.c14n.CanonicalizationException; import org.apache.xml.security.c14n.Canonicalizer; import org.apache.xml.security.c14n.InvalidCanonicalizerException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.InputSource; /** * Abstract base class for all SAML constructs * * @author Scott Cantor * @created November 17, 2001 */ public abstract class SAMLObject implements Cloneable { /** OpenSAML configuration */ protected SAMLConfig config = SAMLConfig.instance(); /** Root node of a DOM tree capturing the object */ protected Node root = null; /** Class-specific logging object */ protected Logger log = Logger.getLogger(this.getClass()); /** Back pointer to SAML "parent" to allow back-walking and prevent double-containment */ protected SAMLObject parentObject = null; /** Dirty bit triggers recreation of DOM */ protected boolean dirty = true; /** * Sets or clears the object's dirty bit. When set, serialization will flush * an existing DOM. All parent objects will also be flagged. * * @param flag The new value of the dirty bit */ protected void setDirty(boolean flag) { dirty = flag; if (flag && parentObject != null) parentObject.setDirty(flag); } /** * Allows parsing of objects from a stream of XML * * @param in A stream containing XML * @return The root of the XML document instance * @exception SAMLException Raised if an exception occurs while constructing * the object */ static protected Element fromStream(InputStream in) throws SAMLException { try { Document doc = XML.parserPool.parse(in); return doc.getDocumentElement(); } catch (Exception e) { NDC.push("fromStream"); Logger.getLogger(SAMLObject.class.getName()).error("caught an exception while parsing a stream:\n" + e.getMessage()); NDC.pop(); throw new MalformedException("SAMLObject.fromStream() caught exception while parsing a stream",e); } } /** * Allows parsing of objects of a particular minor version from a stream of XML * * @param in A stream containing XML * @param minor The minor version of the incoming object * @return The root of the XML document instance * @exception SAMLException Raised if an exception occurs while constructing * the object */ static protected Element fromStream(InputStream in, int minor) throws SAMLException { try { Document doc = XML.parserPool.parse( new InputSource(in), (minor==1) ? XML.parserPool.getSchemaSAML11() : XML.parserPool.getSchemaSAML10() ); return doc.getDocumentElement(); } catch (Exception e) { NDC.push("fromStream"); Logger.getLogger(SAMLObject.class.getName()).error("caught an exception while parsing a stream:\n" + e.getMessage()); NDC.pop(); throw new MalformedException("SAMLObject.fromStream() caught exception while parsing a stream",e); } } /** * Installs the root node of this DOM as the document element * * @return The root node so planted */ protected Node plantRoot() { if (root!=null) { Node domroot=root; while (domroot.getParentNode()!=null && domroot.getParentNode().getNodeType() != Node.DOCUMENT_NODE) domroot=domroot.getParentNode(); Element e=root.getOwnerDocument().getDocumentElement(); if (e!=null && e!=domroot) root.getOwnerDocument().replaceChild(domroot,e); else if (e==null) root.getOwnerDocument().appendChild(domroot); } return root; } /** * Delegates the process of building the root element of an object and * inserting appropriate namespaces. * * @param doc The document context to use * @param xmlns Include namespace(s) on root element? * @return A new root element for the object */ protected abstract Element buildRoot(Document doc, boolean xmlns); /** * Evaluates the object's content to see if it is currently valid if serialized. * Does not evaluate embedded objects except on the basis of whether they exist. * For example, an Assertion must have a Statement, but if an invalid statement * is added, SAMLAssertion.checkValidity() would succeed, while SAMLStatement.checkValidity * would raise an exception. * * @exception SAMLException Raised if the serialized object would be invalid SAML, * excluding any embedded objects */ public abstract void checkValidity() throws SAMLException; /** * Informs the object that it is being inserted into a composite structure, allowing * it to check for existing containment and throw an exception, preventing unexplained * errors due to multiple object containment. * * @param parent The object into which this object is being inserted * @return A reference to the object being inserted (allows for cleaner insertion) * @throws SAMLException Raised if this object already has a parent */ public SAMLObject setParent(SAMLObject parent) throws SAMLException { if (parentObject != null) throw new SAMLException("SAMLObject.setParent() called on an already-contained object"); if (parent == null) throw new IllegalArgumentException("SAMLObject.setParent() called with null parameter"); parentObject = parent; return this; } /** * Returns the containing object, if any. Multiple containment of a single object * is prohibited! You must clone() to add an owned object to another parent. * * @return The parent SAML object, or null if stand-alone */ public SAMLObject getParent() { return parentObject; } /** * Initialization of an object from a DOM element * * @param e Root element of a DOM tree * @exception SAMLException Raised if an exception occurs while constructing * the object */ public void fromDOM(Element e) throws SAMLException { if (e==null) throw new MalformedException("SAMLObject.fromDOM() given an empty DOM"); root = e; setDirty(false); // we have to be clean if we're starting from existing XML } /** * Serializes the XML representation of the SAML object to a stream * * @param out Stream to use for output * @exception java.io.IOException Raised if an I/O problem is detected * @exception SAMLException Raised if the object is incompletely defined */ public void toStream(OutputStream out) throws java.io.IOException, SAMLException { try { toDOM(); plantRoot(); Canonicalizer c = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); out.write(c.canonicalizeSubtree(root, config.getProperty("gov.nih.nci.cagrid.opensaml.inclusive-namespace-prefixes"))); } catch (InvalidCanonicalizerException e) { throw new java.io.IOException(e.getMessage()); } catch (CanonicalizationException e) { throw new java.io.IOException(e.getMessage()); } } /** * Returns a base64-encoded XML representation of the SAML object * * @return A byte array containing the encoded data * @exception java.io.IOException Raised if an I/O problem is detected * @exception SAMLException Raised if the object is incompletely defined */ public byte[] toBase64() throws java.io.IOException, SAMLException { ByteArrayOutputStream out = new ByteArrayOutputStream(); toStream(out); return Base64.encodeBase64Chunked(out.toByteArray()); } /** * Transforms the object into a DOM tree using an existing document context * * @param doc A Document object to use in manufacturing the tree * @param xmlns Include namespace(s) on root element? * @return Root element node of the DOM tree capturing the object * @exception SAMLException Raised if the object is incompletely defined */ public Node toDOM(Document doc, boolean xmlns) throws SAMLException { checkValidity(); if (root != null) { if (!dirty) { // If the DOM tree is already generated, compare the Documents. if (root.getOwnerDocument() != doc) { // We already built a tree. Just adopt it into the new document. return root = doc.adoptNode(root); } return root; } // Dirty, so we need a new root element. log.debug("toDOM() detected object changes, rebuilding tree"); } return root = buildRoot(doc,xmlns); } /** * Transforms the object into a DOM tree without an existing document context * * @param xmlns Include namespace(s) on root element? * @return Root element node of the DOM tree capturing the object * @exception SAMLException Raised if the object is incompletely defined */ public Node toDOM(boolean xmlns) throws SAMLException { // Reuse document if possible. if (root != null) return toDOM(root.getOwnerDocument(), xmlns); // No existing document object, so we have to create one. return toDOM(XML.parserPool.newDocument(), xmlns); } /** * Transforms the object into a DOM tree using an existing document context, * including namespace declarations * * @param doc A Document object to use in manufacturing the tree * @return Root element node of the DOM tree capturing the object * @exception SAMLException Raised if the object is incompletely defined */ public Node toDOM(Document doc) throws SAMLException { return toDOM(doc, true); } /** * Transforms the object into a DOM tree without an existing document context, * including namespace declarations * * @return Root element node of the DOM tree capturing the object * @exception SAMLException Raised if the object is incompletely defined */ public Node toDOM() throws SAMLException { return toDOM(true); } /** * Copies a SAML object such that no dependencies exist between the original * and the copy. * * @return The new object * @see java.lang.Object#clone() */ protected Object clone() throws CloneNotSupportedException { SAMLObject dup=(SAMLObject)super.clone(); // Clear the DOM and parent before returning the copy. dup.root = null; dup.parentObject = null; dup.dirty = true; return dup; } /** * Serializes a SAML object to a string in exclusive canonical form. * * @return The canonicalized output * @see java.lang.Object#toString() */ public String toString() { try { // We already support serialization to streams, but note that c14n XML is always in UTF-8. ByteArrayOutputStream os= new ByteArrayOutputStream(); toStream(os); return os.toString("UTF8"); } catch (java.io.IOException e) { NDC.push("toString"); log.error("caught an I/O exception while serializing XML: " + e); NDC.pop(); return ""; } catch (SAMLException e) { NDC.push("toString"); log.error("caught a SAML exception while serializing XML: " + e); NDC.pop(); return ""; } } }