/*
* 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.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Iterator;
import java.util.ArrayList;
import javax.xml.namespace.QName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
/**
* Basic SAML Attribute implementation that handles rudimentary attribute value
* types
*
* @author Scott Cantor
* @created May 9, 2002
*/
public class SAMLAttribute extends SAMLObject implements Cloneable
{
private final static Logger sLogger = LoggerFactory.getLogger(SAMLAttribute.class);
/** Custom implementation hook for building attributes. */
private static String factoryClass = null;
/**
* Registers a class name to handle SAML attribute construction from XML
*
* @param className The Java class to register
*/
public static synchronized String setFactory(String className) {
String temp = factoryClass;
factoryClass = className;
return temp;
}
/**
* Obtains a class name to handle SAML attribute construction from XML
*
* @param className The registered Java class
*/
public static synchronized String getFactory() {
return factoryClass;
}
/**
* Locates an implementation class for an attribute and constructs it based
* on the DOM provided.
*
* @param e The root of a DOM containing the SAML attribute
* @return SAMLAttribute A constructed attribute object
*
* @throws SAMLException Thrown if an error occurs while constructing the object
*/
public static SAMLAttribute getInstance(Element e) throws SAMLException {
if (e == null)
throw new MalformedException(SAMLException.RESPONDER, "SAMLAttribute.getInstance() given an empty DOM");
// Look for a custom factory. If not, use the default class (this one).
String className = getFactory();
if (className == null)
return new SAMLAttribute(e);
try {
Class implementation = Class.forName(className);
Class[] paramtypes = {Element.class};
Object[] params = {e};
Constructor ctor = implementation.getDeclaredConstructor(paramtypes);
return (SAMLAttribute)ctor.newInstance(params);
}
catch (ClassNotFoundException ex) {
throw new SAMLException(SAMLException.REQUESTER, "SAMLAttribute.getInstance() unable to locate attribute factory class (" + className + ")", ex);
}
catch (NoSuchMethodException ex) {
throw new SAMLException(SAMLException.REQUESTER, "SAMLAttribute.getInstance() unable to bind to constructor for attribute", ex);
}
catch (InstantiationException ex) {
throw new SAMLException(SAMLException.REQUESTER, "SAMLAttribute.getInstance() unable to build implementation object for attribute", ex);
}
catch (IllegalAccessException ex) {
throw new SAMLException(SAMLException.REQUESTER, "SAMLAttribute.getInstance() unable to access attribute factory", ex);
}
catch (java.lang.reflect.InvocationTargetException ex) {
ex.printStackTrace();
Throwable e2 = ex.getTargetException();
if (e2 instanceof SAMLException)
throw (SAMLException)e2;
else
throw new SAMLException(SAMLException.REQUESTER, "SAMLAttribute.getInstance() caught unknown exception while building attribute object: " + e2.getMessage());
}
}
/**
* Locates an implementation class for an attribute and constructs it based
* on the stream provided.
*
* @param in The stream to deserialize from
* @return SAMLAttribute A constructed attribute object
*
* @throws SAMLException Thrown if an error occurs while constructing the object
*/
public static SAMLAttribute getInstance(InputStream in) throws SAMLException {
try {
Document doc = XML.parserPool.parse(in);
return getInstance(doc.getDocumentElement());
}
catch (SAXException e) {
sLogger.error("SAMLAttribute.getInstance() caught exception while parsing a stream",e);
throw new MalformedException("SAMLAttribute.getInstance() caught exception while parsing a stream",e);
}
catch (IOException e) {
sLogger.error("SAMLAttribute.getInstance() caught exception while parsing a stream",e);
throw new MalformedException("SAMLAttribute.getInstance() caught exception while parsing a stream",e);
}
}
/** Name of attribute */
protected String name = null;
/** Namespace/qualifier of attribute */
protected String namespace = null;
/** The schema type of attribute value(s) */
protected QName type = null;
/** Effective lifetime of attribute's value(s) in seconds (0 means infinite) */
protected long lifetime = 0;
/** An array of attribute values */
protected ArrayList values = new ArrayList();
/**
* Default constructor
*/
public SAMLAttribute() {
}
/**
* Builds an Attribute out of its component parts
*
* @param name Name of attribute
* @param namespace Namespace/qualifier of attribute
* @param type The schema type of attribute value(s)
* @param lifetime Effective lifetime of attribute's value(s) in
* seconds (0 means infinite)
* @param values An array of attribute values
* @exception SAMLException Thrown if attribute cannot be built from the
* supplied information
*/
public SAMLAttribute(String name, String namespace, QName type, long lifetime, Collection values) throws SAMLException {
this.name = XML.assign(name);
this.namespace = XML.assign(namespace);
this.type = type;
this.lifetime = lifetime;
if (values != null)
this.values.addAll(values);
}
/**
* Reconstructs an attribute from a DOM tree
*
* @param e The root of a DOM tree
* @exception SAMLException Thrown if the object cannot be constructed
*/
public SAMLAttribute(Element e) throws SAMLException {
fromDOM(e);
}
/**
* Reconstructs an attribute from a stream
*
* @param in A stream containing XML
* @exception SAMLException Raised if an exception occurs while constructing
* the object.
*/
public SAMLAttribute(InputStream in) throws SAMLException {
fromDOM(fromStream(in));
}
/**
* Initialization of attribute from a DOM element.<P>
*
* Checks the attribute's syntactic validity. An exception
* is thrown if any problems are detected. The exception will contain a
* message describing the problem, and may wrap another exception.<P>
*
* Because attributes are generalized, this base method only handles
* attributes whose values are of uniform schema type. The
* attribute's schema type is set by the first xsi:type attribute found in
* the value list, if any.<P>
*
* The addValue method is used to actually process the values, and can be
* overridden to handle more complex values
*
* @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 {
super.fromDOM(e);
if (config.getBooleanProperty("gov.nih.nci.cagrid.opensaml.strict-dom-checking") && !XML.isElementNamed(e,XML.SAML_NS,"Attribute"))
throw new MalformedException("SAMLAttribute.fromDOM() requires saml:Attribute at root");
name = XML.assign(e.getAttributeNS(null, "AttributeName"));
namespace = XML.assign(e.getAttributeNS(null, "AttributeNamespace"));
// Iterate over AttributeValues.
Element n = XML.getFirstChildElement(e, XML.SAML_NS, "AttributeValue");
while (n != null) {
if (type == null)
type = XML.getQNameAttribute(n, XML.XSI_NS, "type");
valueFromDOM(n);
n = XML.getNextSiblingElement(n);
}
checkValidity();
}
/**
* Creates the internal representation of an attribute value from the specified element.<P>
*
* The base implementation handles simple string values by extracting a single Text node.
* Override this method in your subclass to perform more advanced processing.
*
* @param e The AttributeValue element to read from
* @exception SAMLException Raised if an error occurs while parsing the DOM
*/
protected void valueFromDOM(Element e) throws SAMLException {
Node val = e.getFirstChild();
if (val != null && val.getNodeType() == Node.TEXT_NODE)
values.add(val.getNodeValue());
else if (val == null)
values.add("");
else {
values.add("");
log.warn("skipping AttributeValue element without a simple text node");
}
}
/**
* Gets the AttributeName attribute of the SAML Attribute
*
* @return The name value
*/
public String getName() {
return name;
}
/**
* Sets the AttributeName attribute of the SAML Attribute
*
* @param name The name value
*/
public void setName(String name) {
if (XML.isEmpty(name))
throw new IllegalArgumentException("name cannot be null");
this.name = name;
setDirty(true);
}
/**
* Gets the AttributeNamespace attribute of the SAML Attribute
*
* @return The namespace value
*/
public String getNamespace() {
return namespace;
}
/**
* Sets the AttributeNamespace attribute of the SAML Attribute
*
* @param namespace The name value
*/
public void setNamespace(String namespace) {
if (XML.isEmpty(namespace))
throw new IllegalArgumentException("namespace cannot be null");
this.namespace = namespace;
setDirty(true);
}
/**
* Gets the value of the xsi:type attribute, if any, of the SAML Attribute
*
* @return The schema type value
*/
public QName getType() {
return type;
}
/**
* Sets the value of the xsi:type attribute, if any, of the SAML Attribute
*
* @param type The schema type value
*/
public void setType(QName type) {
this.type = type;
setDirty(true);
}
/**
* Gets the value's lifetime, in seconds
*
* @return The effective lifetime of the attribute value, in seconds (0
* means infinite)
*/
public long getLifetime() {
return lifetime;
}
/**
* Sets the value's lifetime, in seconds
*
* @param lifetime The effective lifetime of the attribute value, in seconds (0
* means infinite)
*/
public void setLifetime(long lifetime) {
this.lifetime = lifetime;
}
/**
* Gets the values of the SAML Attribute
*
* @return An iterator over the values
*/
public Iterator getValues() {
return values.iterator();
}
/**
* Gets the set of existing AttributeValue elements, if the DOM exists.
*
* @return A NodeList containing the elements, or null
*/
public NodeList getValueElements() {
return (!dirty && root != null) ? ((Element)root).getElementsByTagNameNS(XML.SAML_NS,"AttributeValue") : null;
}
/**
* Sets the values of the attribute
*
* @param values The values to use
* @throws SAMLException Raised if the value cannot be added to the attribute
*/
public void setValues(Collection values) throws SAMLException {
this.values.clear();
if (values != null) {
this.values.addAll(values);
}
setDirty(true);
}
/**
* Adds a value to the attribute
*
* @param value The value to add
* @exception SAMLException Raised if the value cannot be properly added
*/
public void addValue(Object value) throws SAMLException {
if (value != null)
values.add(value);
else
values.add("");
setDirty(true);
}
/**
* Removes a value by position (zero-based)
*
* @param index The position of the value to remove
*/
public void removeValue(int index) throws IndexOutOfBoundsException {
values.remove(index);
setDirty(true);
}
/**
* Computes the xsi:type attribute on each AttributeValue element with any supporting
* declarations created.
*
* @param e The root element of the attribute
* @return The xsi:type value to use
*/
protected String computeTypeDecl(Element e) {
String xsitype = null;
e.removeAttributeNS(XML.XMLNS_NS, "xmlns:typens");
if (type != null) {
String prefix;
if (XML.XSD_NS.equals(type.getNamespaceURI())) {
prefix = "xsd";
}
else {
e.setAttributeNS(XML.XMLNS_NS, "xmlns:typens", type.getNamespaceURI());
prefix = "typens";
}
xsitype = prefix + ":" + type.getLocalPart();
}
return xsitype;
}
/**
* @see gov.nih.gov.nih.nci.cagrid.opensaml.SAMLObject#buildRoot(org.w3c.dom.Document,boolean)
*/
protected Element buildRoot(Document doc, boolean xmlns) {
Element a = doc.createElementNS(XML.SAML_NS, "Attribute");
if (xmlns)
a.setAttributeNS(XML.XMLNS_NS, "xmlns", XML.SAML_NS);
a.setAttributeNS(XML.XMLNS_NS, "xmlns:xsd", XML.XSD_NS);
a.setAttributeNS(XML.XMLNS_NS, "xmlns:xsi", XML.XSI_NS);
return a;
}
/**
* Creates the DOM representation of an attribute value in the specified element.<P>
*
* The base implementation handles string values by creating a single Text node.
* Override this method in your subclass to perform more advanced processing.
*
* @param index The position of the attribute value to DOM-ify
* @param e The AttributeValue element to write into
* @exception SAMLException Raised if an error occurs while creating the DOM
*/
protected void valueToDOM(int index, Element e) throws SAMLException {
String val = values.get(index).toString();
if (!XML.isEmpty(val))
e.appendChild(e.getOwnerDocument().createTextNode(val));
}
/**
* @see gov.nih.gov.nih.nci.cagrid.opensaml.SAMLObject#toDOM(org.w3c.dom.Document,boolean)
*/
public Node toDOM(Document doc, boolean xmlns) throws SAMLException {
// Let the base build/verify the DOM root.
super.toDOM(doc, xmlns);
Element a = (Element)root;
if (dirty) {
a.setAttributeNS(null, "AttributeName", name);
a.setAttributeNS(null, "AttributeNamespace", namespace);
String xsitype = computeTypeDecl(a);
for (int i = 0; i < values.size(); i++) {
Element v = doc.createElementNS(XML.SAML_NS, "AttributeValue");
if (xsitype != null)
v.setAttributeNS(XML.XSI_NS, "xsi:type", xsitype);
valueToDOM(i, v);
a.appendChild(v);
}
setDirty(false);
}
else if (xmlns) {
a.setAttributeNS(XML.XMLNS_NS, "xmlns", XML.SAML_NS);
a.setAttributeNS(XML.XMLNS_NS, "xmlns:xsi", XML.XSI_NS);
a.setAttributeNS(XML.XMLNS_NS, "xmlns:xsd", XML.XSD_NS);
}
return root;
}
/**
* @see gov.nih.gov.nih.nci.cagrid.opensaml.SAMLObject#checkValidity()
*/
public void checkValidity() throws SAMLException {
if (XML.isEmpty(name) || XML.isEmpty(namespace) || values.size() == 0)
throw new MalformedException(SAMLException.RESPONDER, "Attribute invalid, requires name and namespace, and at least one value");
}
/**
* Copies a SAML object such that no dependencies exist between the original
* and the copy. Does not clone values.
*
* @return The new object
* @see java.lang.Object#clone()
*/
public Object clone() throws CloneNotSupportedException {
SAMLAttribute dup=(SAMLAttribute)super.clone();
dup.values = (ArrayList)values.clone();
return dup;
}
}