/*
* @(#)RequestElement.java
*
* Copyright 2005-2006 Swedish Institute of Computer Science All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistribution of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistribution in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of Swedish Institute of Computer Science or the names of
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* This software is provided "AS IS," without a warranty of any kind. ALL
* EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
* ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. THE SWEDISH INSTITUE OF COMPUTER
* SCIENCE ("SICS") AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES
* SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS
* SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SICS OR ITS LICENSORS BE
* LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT,
* SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED
* AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
* INABILITY TO USE THIS SOFTWARE, EVEN IF SICS HAS BEEN ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGES.
*
* You acknowledge that this software is not designed or intended for use in
* the design, construction, operation or maintenance of any nuclear facility.
*/
package com.sun.xacml.ctx;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.transform.Transformer;
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.transform.OutputKeys;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.sun.xacml.Constants;
import com.sun.xacml.Indenter;
import com.sun.xacml.ParsingException;
import com.sun.xacml.PolicyMetaData;
/**
* Represents an element of a request made to the PDP. This is the class contains
* for example the subject, resource, action, delegate and one of the indirect
* delegate objects. Its purpose is to serve as an extension point for generic
* request elements.
*
* @since 3.0
* @author Ludwig Seitz
*/
public class RequestElement implements Cloneable {
/**
* The map of attributes characterizing this request element
* keyed by attribute-id. Maps to <code>Set</code>s of
* <code>Attribute</code>s with the same id.
*/
private Map<URI, Set<Attribute>> attributes = null;
/**
* The set of attributes with the includeInResult flag.
*/
private Set<Attribute> includeAttributes = null;
/**
* This holds the optional Content/ResourceContent element,
* or null if there is no content.
*/
private Node content = null;
/**
* The category of the request element
*/
private URI category = null;
/**
* The XACML version.
*/
private int xacmlVersion = Constants.XACML_DEFAULT_VERSION;
/**
* Remember if this was a pre 3.0 request with the SubjectCategory
* attribute (for encoding).
*/
private boolean withSubjectCategory = false;
public static final Set<RequestElement> EMPTY_SET = Collections.<RequestElement>emptySet();
/**
* Creates a request element out of it's category and attributes
*
* @param category the cateogry, must not be null
* @param attributes must be a <code>Set</code> containing
* <code>Attribute</code> objects.
* Must not be null or empty.
*/
public RequestElement(URI category, Set<Attribute> attributes) {
this(category, attributes, null,
Constants.XACML_DEFAULT_VERSION, false);
}
/**
* Creates a request element without content.
*
* @param category the cateogry, must not be null
* @param attributes must be a <code>Set</code> containing
* <code>Attribute</code> objects.
* Must not be null or empty.
* @param xacmlVersion The version number of the XACML used (see
* <code>PolicyMetaData</code> for details).
* @param withSubjectCategory Remember if this was a pre 3.0
* request with attribute category.
*/
public RequestElement(URI category, Set<Attribute> attributes, int xacmlVersion,
boolean withSubjectCategory) {
this(category, attributes, null, xacmlVersion, withSubjectCategory);
}
/**
* Creates a request element out of it's category and attributes
*
* @param category the cateogry, must not be null
* @param attributes must be a <code>Set</code> containing
* <code>Attribute</code> objects.
* Must not be null or empty.
* @param content the content element, or null if there is no content.
* @param xacmlVersion The version number of the XACML used (see
* <code>PolicyMetaData</code> for details).
* @param withSubjectCategory Remember if this was a pre 3.0
* request with attribute category.
*/
public RequestElement(URI category, Set<Attribute> attributes,
Node content, int xacmlVersion,
boolean withSubjectCategory) {
this.attributes = new HashMap<URI, Set<Attribute>>();
this.includeAttributes = new HashSet<Attribute>();
// convert the set to a map for request optimization and
// make sure the contents are of correct type.
Iterator<Attribute> attrs = attributes.iterator();
while (attrs.hasNext()){
// Object thing = attrs.next();
// if (!(thing instanceof Attribute)) {
// throw new IllegalArgumentException(category.toString()
// + " input is not well formed (should be an attribute "
// + "but is a " + thing.getClass().getName() + ")");
//
// }
Attribute attr = attrs.next();
URI id = attr.getId();
if (this.attributes.containsKey(id)) {
Set<Attribute> set = this.attributes.get(id);
set.add(attr);
} else {
Set<Attribute> set = new HashSet<Attribute>();
set.add(attr);
this.attributes.put(id, set);
}
if (attr.includeInResult()) {
this.includeAttributes.add(attr);
}
}
if (content != null) {
if (xacmlVersion < Constants.XACML_VERSION_3_0
&& !category.equals(Constants.RESOURCE_CAT)) {
throw new IllegalArgumentException("Can't have Content"
+ " in this category for this XACML version");
}
this.content = content.cloneNode(true);
}
if (category == null) {
throw new IllegalArgumentException("Request elements " +
"require a category");
}
this.category = category;
this.xacmlVersion = xacmlVersion;
this.withSubjectCategory = withSubjectCategory;
}
/**
* Constructor. Creates a request element out of it's name and attributes
* (already in Map), without content.
*
* @param category the cateogry, must not be null
* @param attributes must be a <code>Map</code> of <code>Set</code>s
* containing <code>Attribute</code> objects, keyed
* by the attribute ids. Must not be null or empty.
*/
public RequestElement(URI category, Map<URI, Set<Attribute>> attributes) {
this(category, attributes, null);
}
/**
* Constructor. Creates a request element out of it's name and attributes
* (already in Map), with content.
*
* @param category the cateogry, must not be null
* @param attributes must be a <code>Map</code> of <code>Set</code>s
* containing <code>Attribute</code> objects, keyed
* by the attribute ids. Must not be null or empty.
* @param content the content element, or null if there is no content.
*/
public RequestElement(URI category, Map<URI, Set<Attribute>> attributes, Node content) {
this.includeAttributes = new HashSet<Attribute>();
if (attributes == null || attributes.isEmpty()) {
throw new IllegalArgumentException("Can't create RequestElement"
+ " with empty or null attributes map.");
}
Iterator<Map.Entry<URI, Set<Attribute>>> entries = attributes.entrySet().iterator();
while(entries.hasNext()) {
Map.Entry<URI, Set<Attribute>> foo = entries.next();
// if (!(foo.getKey() instanceof URI)) {
// throw new IllegalArgumentException("Can't create a Request"
// + "Element with non URI keys in the attributes"
// + " Map");
// }
// Object bar = foo.getValue();
// if (!(bar instanceof Set)) {
// throw new IllegalArgumentException("Can't create a Request"
// + "Element with non-Set objects in the attributes"
// + " Map");
// }
// Set attrSet = (Set)bar;
Set<Attribute> attrSet = foo.getValue();
Iterator<Attribute> iter = attrSet.iterator();
while(iter.hasNext()) {
Object foobar = iter.next();
if (!(foobar instanceof Attribute)) {
throw new IllegalArgumentException("Can't create a"
+ " RequestElement with non-Attribute objects in"
+ " the sets in the attributes Map");
}
Attribute attr = (Attribute)foobar;
if (!attr.getId().equals(foo.getKey())) {
throw new IllegalArgumentException("Key mapped"
+ " to the wrong attribute in attributes map");
}
if (attr.includeInResult()) {
this.includeAttributes.add(attr);
}
}
}
this.attributes = new HashMap<URI, Set<Attribute>>(attributes);
if (content != null) {
this.content = content.cloneNode(true);
}
this.category = category;
}
/**
* The clone method.
*
* @return a copy of this object.
*/
public Object clone() {
try {
RequestElement clone = (RequestElement)super.clone();
// deep copy of the attributes
clone.attributes = new HashMap<URI, Set<Attribute>>();
clone.includeAttributes = new HashSet<Attribute>();
Iterator<URI> iter = this.attributes.keySet().iterator();
while(iter.hasNext()) {
URI key = URI.create(iter.next().toString());
Set<Attribute> attrs = new HashSet<Attribute>();
Iterator<Attribute> iter2 = this.attributes.get(key).iterator();
while(iter2.hasNext()) {
attrs.add( (Attribute) iter2.next().clone());
}
clone.attributes.put(key, attrs);
}
Iterator<Attribute> iter3 = this.includeAttributes.iterator();
while(iter3.hasNext()) {
clone.includeAttributes.add( (Attribute) iter3.next().clone());
}
if (this.content != null) {
clone.content = this.content.cloneNode(true);
}
clone.category = this.category;
clone.xacmlVersion = this.xacmlVersion;
clone.withSubjectCategory = this.withSubjectCategory;
return clone;
} catch (CloneNotSupportedException e) {//this should never happen
throw new RuntimeException("Couldn't clone RequestElement");
}
}
/**
* Create a new <code>RequestElement</code> by parsing a node in a
* request. This node should be created by schema-verified parsing of an
* <code>XML</code> document.
*
* @param root the node to parse for the <code>RequestElement</code>
* @param metaData the meta-data associated with the containing request.
*
* @return a new <code>RequestElement</code> constructed by parsing
*
* @throws ParsingException if the DOM node is invalid
*/
public static RequestElement getInstance(Element root,
PolicyMetaData metaData) throws ParsingException {
URI category = null;
boolean withSubjectCategory = false;
if (metaData.getXACMLVersion() == Constants.XACML_VERSION_3_0) {
NamedNodeMap xmlAttrs = root.getAttributes();
if (xmlAttrs == null) {
throw new ParsingException("No XML attributes found,"
+ " where Category attribute was expected");
}
Node catNode = xmlAttrs.getNamedItem("Category");
if (catNode == null) {
throw new ParsingException("'Category' XML attribute " +
"not found");
}
String categoryStr = catNode.getNodeValue();
try {
category = new URI(categoryStr);
} catch (URISyntaxException e) {
throw new ParsingException("Error while parsing "
+ "category: " + categoryStr, e);
}
} else if (metaData.getXACMLVersion()
< Constants.XACML_VERSION_3_0) {
if (root.getLocalName().equals("Subject")) {
NamedNodeMap xmlAttrs = root.getAttributes();
if (xmlAttrs == null) {
category = Constants.SUBJECT_CAT;
} else {
Node catNode = xmlAttrs.getNamedItem("SubjectCategory");
if (catNode == null) {
category = Constants.SUBJECT_CAT;
} else {
withSubjectCategory = true;
String categoryStr = catNode.getNodeValue();
try {
category = new URI(categoryStr);
} catch (URISyntaxException e) {
throw new ParsingException("Error while parsing "
+ "category: " + categoryStr, e);
}
}
}
} else if (root.getLocalName().equals("Resource")) {
category = Constants.RESOURCE_CAT;
} else if (root.getLocalName().equals("Action")) {
category = Constants.ACTION_CAT;
} else if (metaData.getXACMLVersion()
== Constants.XACML_VERSION_2_0
&& root.getLocalName().equals("Environment")) {
category = Constants.ENVIRONMENT_CAT;
} else {
throw new ParsingException("Invalid node: "
+ root.getLocalName() + " in XACML "
+ metaData.getXACMLVersion() + " request");
}
} else {
throw new ParsingException("Invalid/Unsupported XACML version");
}
// Find the content element if there is one
Node content = null;
NodeList children = root.getChildNodes();
for(int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
if (node.getLocalName().equals("Content")) {
if (metaData.getXACMLVersion()
< Constants.XACML_VERSION_3_0) {
throw new ParsingException("Content element not"+
" supported for XACML version "
+ metaData.getXACMLVersion());
}
content = node.cloneNode(true);
break;
} else if (node.getLocalName().equals("ResourceContent")) {
if (metaData.getXACMLVersion()
> Constants.XACML_VERSION_2_0) {
throw new ParsingException("ResourceContent element not"
+ " supported for XACML version "
+ metaData.getXACMLVersion());
}
content = node.cloneNode(true);
break;
}
}
}
Set<Attribute> attributes = parseAttributes(root);
return new RequestElement(category, attributes, content,
metaData.getXACMLVersion(), withSubjectCategory);
}
/**
* Helper method that parses a set of Attribute types.
* @param root the root <code>Node</code> containing the set of
* Attributes and Contents
* @return A <code>Set</code> <code>Attribute</code>s.
*/
private static Set<Attribute> parseAttributes(Node root) throws ParsingException {
Set<Attribute> set = new HashSet<Attribute>();
NodeList nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE
&& node.getLocalName().equals("Attribute")) {
List<Attribute> attrs = Attribute.getInstances(node);
set.addAll(attrs);
}
}
return set;
}
/**
* Get the category of this request element.
*
* @return the category of the request element
*/
public URI getCategory() {
return this.category;
}
/**
* Get the attributes of the request element as a <code>Map</code>
* The keys of the Map are the attribute ids.
*
* @return the <code>Map</code> of <code>Set</code>s of
* <code>Attribute</code>s of the request element.
*/
public Map<URI, Set<Attribute>> getAttributes() {
return Collections.unmodifiableMap(this.attributes);
}
/**
* Get the content node of the request element if there is one.
*
* @return a <code>Node</code> or null.
*
*/
public Node getContent() {
if (this.content == null) {
return null;
}
return this.content.cloneNode(true);
}
/**
* @return The <code>Set</code> of attributes with the includeInResult
* flag.
*/
public Set<Attribute> getIncludeInResultSet() {
return Collections.unmodifiableSet(this.includeAttributes);
}
/**
* Encodes this request element into its XML representation and writes
* this encoding to the given <code>OutputStream</code> without
* indentation.
*
* @param output a stream into which the XML-encoded data is written
* @param charsetName the character set to use in encoding of strings.
* This may be null in which case the platform
* default character set will be used.
*
* @throws UnsupportedEncodingException
*/
public void encode(OutputStream output, String charsetName)
throws UnsupportedEncodingException {
encode(output, charsetName, new Indenter(0));
}
/**
* Encodes this request element into its XML representation and writes
* this encoding to the given <code>OutputStream</code> with
* indentation.
*
* @param output a stream into which the XML-encoded data is written
* @param charsetName the character set to use in encoding of strings.
* This may be null in which case the platform
* default character set will be used.
* @param indenter an object that creates indentation strings
*
* @throws UnsupportedEncodingException
*/
public void encode(OutputStream output, String charsetName,
Indenter indenter)
throws UnsupportedEncodingException {
// setup the formatting & outstream stuff
String indent = indenter.makeString();
PrintStream out;
if(charsetName == null) {
out = new PrintStream(output);
} else {
out = new PrintStream(output, false, charsetName);
}
// write out the encoded form
String endTag = null;
if (this.xacmlVersion > Constants.XACML_VERSION_2_0) {
out.println(indent + "<Attributes Category=\""
+ this.category.toString() + "\">");
endTag = indent + "</Attributes>";
} else {
if (this.category.equals(Constants.RESOURCE_CAT)) {
out.println(indent + "<Resource>");
endTag = indent + "</Resource>";
} else if (this.category.equals(Constants.ACTION_CAT)) {
out.println(indent + "<Action>");
endTag = indent + "</Action>";
} else if (this.xacmlVersion == Constants.XACML_VERSION_2_0
&& this.category.equals(Constants.ENVIRONMENT_CAT)) {
out.println(indent + "<Environment>");
endTag = indent + "</Environment>";
} else {
if (this.withSubjectCategory) {
out.println(indent + "<Subject SubjectCategory=\""
+ this.category.toString() + "\">");
endTag = indent + "</Subject>";
} else {
out.println(indent + "<Subject>");
endTag = indent + "</Subject>";
}
}
}
// go in one more for next-level elements...
indenter.in();
if (this.content != null) {
out.println(indenter.makeString() + "<Content>");
encodeContent(this.content, output, charsetName, indenter);
out.println(indenter.makeString() + "</Content>");
}
encodeAttributes(this.attributes, output, charsetName, indenter);
indenter.out();
//Notice: the indent was already included previously
out.println(endTag);
}
/**
* Private helper function to encode the content node.
* @throws UnsupportedEncodingException
*/
private void encodeContent(Node content, OutputStream output,
String charsetName, Indenter indenter)
throws UnsupportedEncodingException {
indenter.in();
String indent = indenter.makeString();
PrintStream out;
if(charsetName == null) {
out = new PrintStream(output);
} else {
out = new PrintStream(output, false, charsetName);
}
StringWriter sw = new StringWriter();
try {
Transformer serializer
= TransformerFactory.newInstance().newTransformer();
serializer.setOutputProperty(
OutputKeys.OMIT_XML_DECLARATION, "yes");
serializer.transform(new DOMSource(content), new StreamResult(sw));
} catch (TransformerException e) {
throw new RuntimeException(
"'This never happens' error happened");
}
out.println(indent + sw.toString());
indenter.out();
}
/**
* Private helper function to encode the attribute sets
* @throws UnsupportedEncodingException
*/
private void encodeAttributes(Map<URI, Set<Attribute>> attributes, OutputStream output,
String charsetName, Indenter indenter)
throws UnsupportedEncodingException {
Iterator<Map.Entry<URI, Set<Attribute>>> it = attributes.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<URI, Set<Attribute>> entry = it.next();
Set<Attribute> set = entry.getValue();
Iterator<Attribute> it2 = set.iterator();
while (it2.hasNext()) {
Attribute attr = (Attribute) it2.next();
attr.encode(output, charsetName, indenter);
}
}
}
}