Fixed a thread-safety bug. See bugzilla #464.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / attrresolv / provider / CompositeAttributeDefinition.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /*
18  * Contributed by SunGard SCT.
19  */
20
21 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
22
23 import java.security.Principal;
24 import java.text.MessageFormat;
25 import java.util.ArrayList;
26 import java.util.HashSet;
27 import java.util.Iterator;
28 import java.util.Set;
29 import java.util.StringTokenizer;
30
31 import javax.naming.directory.Attribute;
32 import javax.naming.directory.Attributes;
33 import javax.naming.directory.BasicAttribute;
34 import javax.naming.directory.BasicAttributes;
35
36 import org.apache.log4j.Logger;
37 import org.w3c.dom.Element;
38
39 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn;
40 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
41 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
42 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute;
43
44 /**
45  * The CompositeAttributeDefinition allows composing a single attribute from multiple attributes. It is particularly
46  * useful when values from several columns of a DataBase must be 'concatenated' to form a single composite attribute. To
47  * ensure that the results are same for a given set of source values, multi-valued source attributes must be ordered and
48  * each must have the same number of values. This is true for the attribute values read using the JDBCDataConnector, but
49  * not true with attributes read using the JNDIDirectoryDataConnector or the LDAPDirectoryDataConnector. Hence,
50  * CompositeAttributeDefinition is only currently meaningful for attributes read from an RDB using the
51  * JDBCDataConnector. The specification of this attribute definition is simple. You specify which source attributes to
52  * compose and the format for composing them using the notation of java.text.MessageFormat. The format defaults to a
53  * space separated concatenation of all source attributes. One use case is in the construction of a labeledURI attribute
54  * from two attributes, the 'URL' and the 'URL_Title' that may appear as two columns in a DataBase. As per the
55  * definition of labeledURI, it is essentially a 'space' separated concatenation of URL followed by the title. Since URL
56  * itself should not contain any spaces (assuming it is properly encoded, converting spaces to +), where the URL ends
57  * and the title begins is unambiguous. The specification fpr such a composite attribute is simply: format="{0} {1}"
58  * orderedSourceNames="URL, URL_Title" The format definition in the above example is optional, since that is infact the
59  * default if not specified. Another example of usage of this attribute is as follows: format="{0} ({1})"
60  * orderedSourceNames="Group_Name, Group_Title" Notice that in this example, we are composing the name of a Group and
61  * the descriptive title of the group in parenthesis using the format to create a single attribute from two attributes.
62  * 
63  * @author <a href="mailto:vgoenka@sungardsct.com">Vishal Goenka </a>
64  */
65
66 /**
67  * @author Walter Hoehn
68  */
69 public class CompositeAttributeDefinition extends BaseAttributeDefinition implements AttributeDefinitionPlugIn {
70
71         private static Logger log = Logger.getLogger(CompositeAttributeDefinition.class.getName());
72
73         // The formatter used to compose from all Source Attributes
74         private MessageFormat sourceFormat;
75
76         // Names of source attributes to compose the target from, in ordered list
77         private String[] sourceNames;
78
79         // Names of source attributes in a Set for convenience of checking membership
80         private Set<String> sourceNamesSet;
81
82         public CompositeAttributeDefinition(Element e) throws ResolutionPlugInException {
83
84                 super(e);
85
86                 try {
87                         // Since there are more than one source objects in an ordered list, one sourceName doesn't make sense. In
88                         // this
89                         // respect, it differs from other attribute definition
90                         if (e.hasAttribute("sourceName"))
91                                 throw new ResolutionPlugInException(
92                                                 "sourceName is not an allowed attribute for CompositeAttributeDefinition (" + getId() + ")");
93
94                         String orderedSourceNames = e.getAttribute("orderedSourceNames");
95                         if ((orderedSourceNames == null) || ("".equals(orderedSourceNames)))
96                                 throw new ResolutionPlugInException(
97                                                 "orderedSourceNames is a required attribute for CompositeAttributeDefinition (" + getId() + ")");
98
99                         // We assume space or comma as separators
100                         StringTokenizer st = new StringTokenizer(orderedSourceNames, " ,");
101                         ArrayList<String> sourceNamesList = new ArrayList<String>();
102                         while (st.hasMoreTokens()) {
103                                 String token = st.nextToken().trim();
104                                 if (token.length() > 0) sourceNamesList.add(token);
105                         }
106                         sourceNamesSet = new HashSet<String>();
107                         sourceNamesSet.addAll(sourceNamesList);
108                         sourceNames = (String[]) sourceNamesList.toArray(new String[0]);
109
110                         String format = e.getAttribute("format");
111                         // default format is essentially all ordered attribute values separated by a space
112                         if ((format == null) || ("".equals(format))) {
113                                 StringBuffer defaultFormat = new StringBuffer();
114                                 for (int i = 0; i < sourceNames.length; i++) {
115                                         defaultFormat.append("{").append(i).append("}");
116                                         if (i < sourceNames.length - 1) defaultFormat.append(" ");
117                                 }
118                                 format = defaultFormat.toString();
119                         }
120                         sourceFormat = new MessageFormat(format);
121                 } catch (ResolutionPlugInException ex) {
122                         // To ensure that exceptions thrown in the constructor are logged!
123                         log.error(ex.getMessage());
124                         throw ex;
125                 } catch (RuntimeException ex) {
126                         // To ensure that exceptions thrown in the constructor are logged!
127                         log.error(ex.getMessage());
128                         throw ex;
129                 }
130         }
131
132         /**
133          * Get ordered attribute values for all source attributes from the dependent data connectors. The values of all
134          * multi-valued attribute MUST be ordered and MUST be of same size or else the results can be unpredictable.
135          */
136         private void addAttributesFromConnectors(Dependencies depends, Attributes sourceAttrs, int valueCount)
137                         throws ResolutionPlugInException {
138
139                 Iterator connectorDependIt = connectorDependencyIds.iterator();
140                 while (connectorDependIt.hasNext()) {
141                         Attributes attrs = depends.getConnectorResolution((String) connectorDependIt.next());
142                         if (attrs != null) {
143                                 for (int i = 0; i < sourceNames.length; i++) {
144                                         Attribute attr = attrs.get(sourceNames[i]);
145                                         if (attr != null) {
146                                                 int size = attr.size();
147                                                 if (!attr.isOrdered() && (size > 1)) { throw new ResolutionPlugInException(
148                                                                 "Multi-valued attribute (" + attr.getID()
149                                                                                 + ") MUST be ordered for CompositeAttributeDefinition (" + getId() + ")"); }
150                                                 if (valueCount == -1) {
151                                                         valueCount = size; // initialize valueCount
152                                                 } else if (valueCount != size) { throw new ResolutionPlugInException("Multi-valued attribute ("
153                                                                 + attr.getID() + ") has different number of values (" + size
154                                                                 + ") than other attribute(s) that have (" + valueCount + ") values. "
155                                                                 + "All attributes must have same number of values for CompositeAttributeDefinition ("
156                                                                 + getId() + ")"); }
157                                                 if (sourceAttrs.put(attr) != null) { throw new ResolutionPlugInException("Attribute ("
158                                                                 + attr.getID() + ") occured more than once in the dependency chain for (" + getId()
159                                                                 + ") and I don't know which one to pick"); }
160                                         }
161                                 }
162                         }
163                 }
164         }
165
166         /**
167          * Get ordered attribute values for all source attributes from the dependent attributes. The values of all
168          * multi-valued attribute MUST be ordered and MUST be of same size or else the results can be unpredictable.
169          */
170         private void addAttributesFromAttributeDependencies(Dependencies depends, Attributes sourceAttrs, int valueCount)
171                         throws ResolutionPlugInException {
172
173                 Iterator attrDependIt = attributeDependencyIds.iterator();
174                 while (attrDependIt.hasNext()) {
175                         ResolverAttribute attribute = depends.getAttributeResolution((String) attrDependIt.next());
176                         if (attribute != null) {
177                                 if (sourceNamesSet.contains(attribute.getName())) {
178                                         BasicAttribute attr = new BasicAttribute(attribute.getName(), true);
179                                         int size = 0;
180                                         for (Iterator iterator = attribute.getValues(); iterator.hasNext();) {
181                                                 attr.add(size++, iterator.next());
182                                         }
183                                         if (valueCount == -1) {
184                                                 valueCount = size; // initialize valueCount
185                                         } else if (valueCount != size) { throw new ResolutionPlugInException("Multi-valued attribute ("
186                                                         + attr.getID() + ") has different number of values (" + size
187                                                         + ") than other attribute(s) that have (" + valueCount + ") values. "
188                                                         + "All attributes must have same number of values for CompositeAttributeDefinition ("
189                                                         + getId() + ")"); }
190                                         if (sourceAttrs.put(attr) != null) { throw new ResolutionPlugInException("Attribute ("
191                                                         + attr.getID() + ") occured more than once in the dependency chain for (" + getId()
192                                                         + ") and I don't know which one to pick"); }
193                                 } else {
194                                         log
195                                                         .warn("Attribute Dependency ("
196                                                                         + attribute.getName()
197                                                                         + ") is not listed in the orderedSourceNames attribute for the CustomAttributeDefinition for ("
198                                                                         + getId() + ")");
199                                 }
200                         }
201                 }
202         }
203
204         /**
205          * @see edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn#resolve(edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute,
206          *      java.security.Principal, java.lang.String, java.lang.String,
207          *      edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies)
208          */
209         public void resolve(ResolverAttribute attribute, Principal principal, String requester, String responder,
210                         Dependencies depends) throws ResolutionPlugInException {
211
212                 super.resolve(attribute, principal, requester, responder, depends);
213
214                 // Number of values that each source attribute has (must be same for all attributes)
215                 int valueCount = -1;
216
217                 // Collect attribute values from dependencies
218                 BasicAttributes attributes = new BasicAttributes();
219                 addAttributesFromConnectors(depends, attributes, valueCount);
220                 addAttributesFromAttributeDependencies(depends, attributes, valueCount);
221
222                 // If we got this far, all attributes are ordered and have 'valueCount' number of values
223                 for (int i = 0; i < valueCount; i++) {
224                         // put values in an array so we can use the formatter for creating the composite value
225                         Object[] values = new Object[sourceNames.length];
226                         try {
227                                 for (int j = 0; j < sourceNames.length; j++) {
228                                         Attribute attr = attributes.get(sourceNames[j]);
229                                         if (attr == null)
230                                                 throw new ResolutionPlugInException("No value found for attribute (" + sourceNames[j]
231                                                                 + ") during resolution of (" + getId() + ")");
232                                         // get the ordered (i'th) value of the attribute
233                                         values[j] = attr.get(i);
234                                 }
235                                 attribute.addValue(sourceFormat.format(values));
236                         } catch (ResolutionPlugInException e) {
237                                 // Simply rethrow ...
238                                 throw e;
239                         } catch (Exception e) {
240                                 StringBuffer err = new StringBuffer();
241                                 err.append("Error creating composite attribute [");
242                                 if (values != null) {
243                                         for (int ii = 0; ii < values.length; ii++) {
244                                                 if (values[ii] != null) err.append(values[ii].toString()).append(", ");
245                                                 else err.append("null ");
246                                         }
247                                 } else err.append(" null ");
248                                 err.append("] using format ").append(sourceFormat.toPattern()).append(" for (").append(getId()).append(
249                                                 "): ").append(e.getMessage());
250                                 log.error(err.toString());
251                                 throw new ResolutionPlugInException(err.toString());
252                         }
253                 }
254
255                 attribute.setResolved();
256         }
257
258 }