Push SAML Attribute namespace configuration into the resolver. (Needed for proper...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / attrresolv / AttributeResolver.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4  * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5  * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
6  * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7  * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8  * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2 Project.
9  * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10  * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
11  * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12  * products derived from this software without specific prior written permission. For written permission, please contact
13  * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14  * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15  * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16  * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18  * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19  * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 package edu.internet2.middleware.shibboleth.aa.attrresolv;
27
28 import java.io.IOException;
29 import java.security.Principal;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38
39 import javax.naming.directory.Attributes;
40 import javax.naming.directory.BasicAttributes;
41
42 import org.apache.log4j.Logger;
43 import org.opensaml.SAMLException;
44 import org.w3c.dom.Document;
45 import org.w3c.dom.Element;
46 import org.w3c.dom.Node;
47 import org.w3c.dom.NodeList;
48 import org.xml.sax.InputSource;
49 import org.xml.sax.SAXException;
50
51 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttributeSet.ResolverAttributeIterator;
52 import edu.internet2.middleware.shibboleth.aa.attrresolv.provider.ValueHandler;
53 import edu.internet2.middleware.shibboleth.common.ShibResource;
54 import edu.internet2.middleware.shibboleth.common.ShibResource.ResourceNotAvailableException;
55 import edu.internet2.middleware.shibboleth.idp.IdPConfig;
56 import edu.internet2.middleware.shibboleth.xml.Parser;
57
58 /**
59  * An engine for obtaining attribute values for specified principals. Attributes values are resolved using a directed
60  * graph of pluggable attribute definitions and data connectors.
61  * 
62  * @author Walter Hoehn (wassa@columbia.edu)
63  */
64
65 public class AttributeResolver {
66
67         private static Logger log = Logger.getLogger(AttributeResolver.class.getName());
68         private HashMap plugIns = new HashMap();
69         private ResolverCache resolverCache = new ResolverCache();
70         public static final String resolverNamespace = "urn:mace:shibboleth:resolver:1.0";
71
72         public AttributeResolver(IdPConfig configuration) throws AttributeResolverException {
73
74                 if (configuration == null || configuration.getResolverConfigLocation() == null) {
75                         log.error("No Attribute Resolver configuration file specified.");
76                         throw new AttributeResolverException("No Attribute Resolver configuration file specified.");
77                 }
78
79                 loadConfig(configuration.getResolverConfigLocation());
80         }
81
82         public AttributeResolver(String configFileLocation) throws AttributeResolverException {
83
84                 loadConfig(configFileLocation);
85         }
86
87         private void loadConfig(String configFile) throws AttributeResolverException {
88
89                 try {
90                         ShibResource config = new ShibResource(configFile, this.getClass());
91                         Parser.DOMParser parser = new Parser.DOMParser(true);
92                         parser.parse(new InputSource(config.getInputStream()));
93                         loadConfig(parser.getDocument());
94
95                 } catch (ResourceNotAvailableException e) {
96                         log.error("No Attribute Resolver configuration could be loaded from (" + configFile + "): " + e);
97                         throw new AttributeResolverException("No Attribute Resolver configuration found.");
98                 } catch (SAXException e) {
99                         log.error("Error parsing Attribute Resolver Configuration file: " + e);
100                         throw new AttributeResolverException("Error parsing Attribute Resolver Configuration file.");
101                 } catch (IOException e) {
102                         log.error("Error reading Attribute Resolver Configuration file: " + e);
103                         throw new AttributeResolverException("Error reading Attribute Resolver Configuration file.");
104                 } catch (SAMLException e) {
105                         log.error("Error parsing Attribute Resolver Configuration file: " + e);
106                         throw new AttributeResolverException("Error parsing Attribute Resolver Configuration file.");
107                 }
108         }
109
110         private void loadConfig(Document document) throws AttributeResolverException {
111
112                 log.info("Configuring Attribute Resolver.");
113                 if (!document.getDocumentElement().getTagName().equals("AttributeResolver")) {
114                         log.error("Configuration must include <AttributeResolver> as the root node.");
115                         throw new AttributeResolverException("Cannot load Attribute Resolver.");
116                 }
117
118                 NodeList plugInNodes = document.getElementsByTagNameNS(resolverNamespace, "AttributeResolver").item(0)
119                                 .getChildNodes();
120                 if (plugInNodes.getLength() <= 0) {
121                         log.error("Configuration inclues no PlugIn definitions.");
122                         throw new AttributeResolverException("Cannot load Attribute Resolver.");
123                 }
124                 for (int i = 0; plugInNodes.getLength() > i; i++) {
125                         if (plugInNodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
126                                 try {
127                                         log.info("Found a PlugIn. Loading...");
128                                         ResolutionPlugIn plugIn = ResolutionPlugInFactory.createPlugIn((Element) plugInNodes.item(i));
129                                         registerPlugIn(plugIn, plugIn.getId());
130                                 } catch (DuplicatePlugInException dpe) {
131                                         log.warn("Skipping PlugIn: " + dpe.getMessage());
132                                 } catch (ClassCastException cce) {
133                                         log.error("Problem realizing PlugIn configuration" + cce.getMessage());
134                                 } catch (AttributeResolverException are) {
135                                         log.warn("Skipping PlugIn: " + ((Element) plugInNodes.item(i)).getAttribute("id"));
136                                 }
137                         }
138                 }
139
140                 verifyPlugIns();
141                 log.info("Configuration complete.");
142         }
143
144         private void verifyPlugIns() throws AttributeResolverException {
145
146                 log.info("Verifying PlugIn graph consitency.");
147                 Set inconsistent = new HashSet();
148                 Iterator registered = plugIns.keySet().iterator();
149
150                 while (registered.hasNext()) {
151                         ResolutionPlugIn plugIn = lookupPlugIn((String) registered.next());
152                         log.debug("Checking PlugIn (" + plugIn.getId() + ") for consistency.");
153                         verifyPlugIn(plugIn, new HashSet(), inconsistent);
154                 }
155
156                 if (!inconsistent.isEmpty()) {
157                         log.info("Unloading inconsistent PlugIns.");
158                         Iterator inconsistentIt = inconsistent.iterator();
159                         while (inconsistentIt.hasNext()) {
160                                 plugIns.remove(inconsistentIt.next());
161                         }
162                 }
163
164                 if (plugIns.size() < 1) {
165                         log.error("Failed to load any PlugIn definitions.");
166                         throw new AttributeResolverException("Cannot load Attribute Resolver.");
167                 }
168
169         }
170
171         private void verifyPlugIn(ResolutionPlugIn plugIn, Set verifyChain, Set inconsistent) {
172
173                 // Short-circuit if we have already found this PlugIn to be inconsistent
174                 if (inconsistent.contains(plugIn.getId())) { return; }
175
176                 // Make sure that we don't have a circular dependency
177                 if (verifyChain.contains(plugIn.getId())) {
178                         log.error("The PlugIn (" + plugIn.getId()
179                                         + ") is inconsistent.  It is involved in a circular dependency chain.");
180                         inconsistent.add(plugIn.getId());
181                         return;
182                 }
183
184                 // Recursively go through all DataConnector dependencies and make sure all are registered and consistent.
185                 List depends = new ArrayList();
186                 depends.addAll(Arrays.asList(plugIn.getDataConnectorDependencyIds()));
187                 Iterator dependsIt = depends.iterator();
188                 while (dependsIt.hasNext()) {
189                         String key = (String) dependsIt.next();
190                         if (!plugIns.containsKey(key)) {
191                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
192                                                 + ") that is not registered.");
193                                 inconsistent.add(plugIn.getId());
194                                 return;
195                         }
196
197                         ResolutionPlugIn dependent = lookupPlugIn(key);
198                         if (!(dependent instanceof DataConnectorPlugIn)) {
199                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
200                                                 + ") that is mislabeled as an DataConnectorPlugIn.");
201                                 inconsistent.add(plugIn.getId());
202                                 return;
203                         }
204
205                         verifyChain.add(plugIn.getId());
206                         verifyPlugIn(dependent, verifyChain, inconsistent);
207
208                         if (inconsistent.contains(key)) {
209                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
210                                                 + ") that is inconsistent.");
211                                 inconsistent.add(plugIn.getId());
212                                 return;
213                         }
214                 }
215                 verifyChain.remove(plugIn.getId());
216
217                 // Recursively go through all AttributeDefinition dependencies and make sure all are registered and consistent.
218                 depends.clear();
219                 depends.addAll(Arrays.asList(plugIn.getAttributeDefinitionDependencyIds()));
220                 dependsIt = depends.iterator();
221                 while (dependsIt.hasNext()) {
222                         String key = (String) dependsIt.next();
223
224                         if (!plugIns.containsKey(key)) {
225                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
226                                                 + ") that is not registered.");
227                                 inconsistent.add(plugIn.getId());
228                                 return;
229                         }
230
231                         ResolutionPlugIn dependent = lookupPlugIn(key);
232                         if (!(dependent instanceof AttributeDefinitionPlugIn)) {
233                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
234                                                 + ") that is mislabeled as an AttributeDefinitionPlugIn.");
235                                 inconsistent.add(plugIn.getId());
236                                 return;
237                         }
238
239                         verifyChain.add(plugIn.getId());
240                         verifyPlugIn(dependent, verifyChain, inconsistent);
241
242                         if (inconsistent.contains(key)) {
243                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
244                                                 + ") that is inconsistent.");
245                                 inconsistent.add(plugIn.getId());
246                                 return;
247                         }
248                 }
249                 verifyChain.remove(plugIn.getId());
250
251                 // Check the failover dependency, if there is one.
252                 if (plugIn instanceof DataConnectorPlugIn) {
253                         String key = ((DataConnectorPlugIn) plugIn).getFailoverDependencyId();
254                         if (key != null) {
255                                 if (!plugIns.containsKey(key)) {
256                                         log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
257                                                         + ") that is not registered.");
258                                         inconsistent.add(plugIn.getId());
259                                         return;
260                                 }
261
262                                 ResolutionPlugIn dependent = lookupPlugIn(key);
263                                 if (!(dependent instanceof DataConnectorPlugIn)) {
264                                         log.error("The PlugIn (" + plugIn.getId()
265                                                         + ") is inconsistent.  It depends on a fail-over PlugIn (" + key
266                                                         + ") that is not a DataConnectorPlugIn.");
267                                         inconsistent.add(plugIn.getId());
268                                         return;
269                                 }
270
271                                 verifyChain.add(plugIn.getId());
272                                 verifyPlugIn(dependent, verifyChain, inconsistent);
273
274                                 if (inconsistent.contains(key)) {
275                                         log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
276                                                         + ") that is inconsistent.");
277                                         inconsistent.add(plugIn.getId());
278                                         return;
279                                 }
280                         }
281                 }
282                 verifyChain.remove(plugIn.getId());
283         }
284
285         private void registerPlugIn(ResolutionPlugIn connector, String id) throws DuplicatePlugInException {
286
287                 if (plugIns.containsKey(id)) {
288                         log.error("A PlugIn is already registered with the Id (" + id + ").");
289                         throw new DuplicatePlugInException("Found a duplicate PlugIn Id.");
290                 }
291                 plugIns.put(id, connector);
292                 log.info("Registered PlugIn: (" + id + ")");
293
294         }
295
296         private ResolutionPlugIn lookupPlugIn(String id) {
297
298                 return (ResolutionPlugIn) plugIns.get(id);
299         }
300
301         /**
302          * Resolve a set of attributes for a particular principal and requester.
303          * 
304          * @param principal
305          *            the <code>Principal</code> for which the attributes should be resolved
306          * @param requester
307          *            the name of the requesting entity
308          * @param responding
309          *            the name of the entity responding to the request
310          * @param attributes
311          *            the set of attributes to be resolved
312          */
313         public void resolveAttributes(Principal principal, String requester, String responder,
314                         ResolverAttributeSet attributes) {
315
316                 HashMap requestCache = new HashMap();
317                 ResolverAttributeIterator iterator = attributes.resolverAttributeIterator();
318
319                 while (iterator.hasNext()) {
320                         ResolverAttribute attribute = iterator.nextResolverAttribute();
321                         try {
322                                 if (lookupPlugIn(attribute.getName()) == null) {
323                                         log.warn("No PlugIn registered for attribute: (" + attribute.getName() + ")");
324                                         iterator.remove();
325                                 } else {
326                                         log.info("Resolving attribute: (" + attribute.getName() + ")");
327                                         if (attribute.resolved()) {
328                                                 log.debug("Attribute (" + attribute.getName()
329                                                                 + ") already resolved for this request.  No need for further resolution.");
330
331                                         } else {
332                                                 resolveAttribute(attribute, principal, requester, responder, requestCache, attributes);
333                                         }
334
335                                         if (!attribute.hasValues()) {
336                                                 iterator.remove();
337                                         }
338                                 }
339                         } catch (ResolutionPlugInException rpe) {
340                                 log.error("Problem encountered while resolving attribute: (" + attribute.getName() + "): " + rpe);
341                                 iterator.remove();
342                         }
343                 }
344         }
345
346         public String[] listRegisteredAttributeDefinitionPlugIns() {
347
348                 log.debug("Listing available Attribute Definition PlugIns.");
349                 Set found = new HashSet();
350                 Iterator registered = plugIns.keySet().iterator();
351
352                 while (registered.hasNext()) {
353                         ResolutionPlugIn plugIn = lookupPlugIn((String) registered.next());
354                         if (plugIn instanceof AttributeDefinitionPlugIn) {
355                                 found.add(((AttributeDefinitionPlugIn) plugIn).getId());
356                         }
357                 }
358
359                 if (log.isDebugEnabled()) {
360                         for (Iterator iterator = found.iterator(); iterator.hasNext();) {
361                                 log.debug("Found registered Attribute Definition: " + (String) iterator.next());
362                         }
363                 }
364                 return (String[]) found.toArray(new String[0]);
365         }
366
367         private Attributes resolveConnector(String connector, Principal principal, String requester, String responder,
368                         Map requestCache, ResolverAttributeSet requestedAttributes) throws ResolutionPlugInException {
369
370                 DataConnectorPlugIn currentDefinition = (DataConnectorPlugIn) lookupPlugIn(connector);
371
372                 // Check to see if we have already resolved the connector during this request
373                 if (requestCache.containsKey(currentDefinition.getId())) {
374                         log.debug("Connector (" + currentDefinition.getId()
375                                         + ") already resolved for this request, using cached version");
376                         return (Attributes) requestCache.get(currentDefinition.getId());
377                 }
378
379                 // Check to see if we have a cached resolution for this connector
380                 if (currentDefinition.getTTL() > 0) {
381                         Attributes cachedAttributes = resolverCache.getResolvedConnector(principal, currentDefinition.getId());
382                         if (cachedAttributes != null) {
383                                 log.debug("Connector (" + currentDefinition.getId()
384                                                 + ") resolution cached from a previous request, using cached version");
385                                 return cachedAttributes;
386                         }
387                 }
388
389                 // Resolve all attribute dependencies
390                 String[] attributeDependencies = currentDefinition.getAttributeDefinitionDependencyIds();
391                 Dependencies depends = new Dependencies();
392
393                 for (int i = 0; attributeDependencies.length > i; i++) {
394                         log.debug("Connector (" + currentDefinition.getId() + ") depends on attribute (" + attributeDependencies[i]
395                                         + ").");
396                         ResolverAttribute dependant = requestedAttributes.getByName(attributeDependencies[i]);
397                         if (dependant == null) {
398                                 dependant = new DependentOnlyResolutionAttribute(attributeDependencies[i]);
399                         }
400                         resolveAttribute(dependant, principal, requester, responder, requestCache, requestedAttributes);
401                         depends.addAttributeResolution(attributeDependencies[i], dependant);
402
403                 }
404
405                 // Resolve all connector dependencies
406                 String[] connectorDependencies = currentDefinition.getDataConnectorDependencyIds();
407                 for (int i = 0; connectorDependencies.length > i; i++) {
408                         log.debug("Connector (" + currentDefinition.getId() + ") depends on connector (" + connectorDependencies[i]
409                                         + ").");
410                         depends.addConnectorResolution(connectorDependencies[i], resolveConnector(connectorDependencies[i],
411                                         principal, requester, responder, requestCache, requestedAttributes));
412                 }
413
414                 // Resolve the connector
415                 Attributes resolvedAttributes = null;
416                 try {
417                         resolvedAttributes = currentDefinition.resolve(principal, requester, responder, depends);
418
419                         // Add attribute resolution to cache
420                         if (currentDefinition.getTTL() > 0) {
421                                 resolverCache.cacheConnectorResolution(principal, currentDefinition.getId(),
422                                                 currentDefinition.getTTL(), resolvedAttributes);
423                         }
424                 } catch (ResolutionPlugInException e) {
425                         // Something went wrong, so check for a fail-over...
426                         if (currentDefinition.getFailoverDependencyId() != null) {
427                                 log.warn("Connector (" + currentDefinition.getId() + ") failed, invoking failover dependency");
428                                 resolvedAttributes = resolveConnector(currentDefinition.getFailoverDependencyId(), principal,
429                                                 requester, responder, requestCache, requestedAttributes);
430                         } else if (currentDefinition.getPropagateErrors()) {
431                                 throw e;
432                         } else {
433                                 log.warn("Connector (" + currentDefinition.getId()
434                                                 + ") returning empty attribute set instead of propagating error: " + e);
435                                 resolvedAttributes = new BasicAttributes();
436                         }
437                 }
438
439                 // Cache for this request
440                 requestCache.put(currentDefinition.getId(), resolvedAttributes);
441                 return resolvedAttributes;
442         }
443
444         private void resolveAttribute(ResolverAttribute attribute, Principal principal, String requester, String responder,
445                         Map requestCache, ResolverAttributeSet requestedAttributes) throws ResolutionPlugInException {
446
447                 AttributeDefinitionPlugIn currentDefinition = (AttributeDefinitionPlugIn) lookupPlugIn(attribute.getName());
448
449                 // Check to see if we have already resolved the attribute during this request
450                 // (this checks dependency-only attributes and attributes resolved with no values
451                 if (requestCache.containsKey(currentDefinition.getId())) {
452                         log.debug("Attribute (" + currentDefinition.getId()
453                                         + ") already resolved for this request, using cached version");
454                         attribute.resolveFromCached((ResolverAttribute) requestCache.get(currentDefinition.getId()));
455                         return;
456                 }
457
458                 // Check to see if we have already resolved the attribute during this request
459                 // (this checks attributes that were submitted to the AR for resolution)
460                 ResolverAttribute requestedAttribute = requestedAttributes.getByName(currentDefinition.getId());
461                 if (requestedAttribute != null) {
462                         if (requestedAttribute.resolved()) {
463                                 attribute.resolveFromCached(requestedAttribute);
464                         }
465                 }
466
467                 // Check to see if we have a cached resolution for this attribute
468                 if (currentDefinition.getTTL() > 0) {
469                         ResolverAttribute cachedAttribute = resolverCache
470                                         .getResolvedAttribute(principal, currentDefinition.getId());
471                         if (cachedAttribute != null) {
472                                 log.debug("Attribute (" + currentDefinition.getId()
473                                                 + ") resolution cached from a previous request, using cached version");
474                                 attribute.resolveFromCached(cachedAttribute);
475                                 return;
476                         }
477                 }
478
479                 // Resolve all attribute dependencies
480                 Dependencies depends = new Dependencies();
481                 String[] attributeDependencies = currentDefinition.getAttributeDefinitionDependencyIds();
482
483                 boolean dependancyOnly = false;
484                 for (int i = 0; attributeDependencies.length > i; i++) {
485                         log.debug("Attribute (" + attribute.getName() + ") depends on attribute (" + attributeDependencies[i]
486                                         + ").");
487                         ResolverAttribute dependant = requestedAttributes.getByName(attributeDependencies[i]);
488                         if (dependant == null) {
489                                 dependancyOnly = true;
490                                 dependant = new DependentOnlyResolutionAttribute(attributeDependencies[i]);
491                         }
492                         resolveAttribute(dependant, principal, requester, responder, requestCache, requestedAttributes);
493                         depends.addAttributeResolution(attributeDependencies[i], dependant);
494
495                 }
496
497                 // Resolve all connector dependencies
498                 String[] connectorDependencies = currentDefinition.getDataConnectorDependencyIds();
499                 for (int i = 0; connectorDependencies.length > i; i++) {
500                         log.debug("Attribute (" + attribute.getName() + ") depends on connector (" + connectorDependencies[i]
501                                         + ").");
502                         depends.addConnectorResolution(connectorDependencies[i], resolveConnector(connectorDependencies[i],
503                                         principal, requester, responder, requestCache, requestedAttributes));
504                 }
505
506                 // Resolve the attribute
507                 try {
508                         currentDefinition.resolve(attribute, principal, requester, responder, depends);
509
510                         // Add attribute resolution to cache
511                         if (currentDefinition.getTTL() > 0) {
512                                 resolverCache.cacheAttributeResolution(principal, attribute.getName(), currentDefinition.getTTL(),
513                                                 attribute);
514                         }
515                 } catch (ResolutionPlugInException e) {
516                         if (currentDefinition.getPropagateErrors()) {
517                                 throw e;
518                         } else {
519                                 log.warn("Attribute (" + currentDefinition.getId()
520                                                 + ") returning no values instead of propagating error: " + e);
521                         }
522                 }
523
524                 // If necessary, cache for this request
525                 if (dependancyOnly || !attribute.hasValues()) {
526                         requestCache.put(currentDefinition.getId(), attribute);
527                 }
528         }
529
530         private class DuplicatePlugInException extends Exception {
531
532                 public DuplicatePlugInException(String message) {
533
534                         super(message);
535                 }
536         }
537
538         class DependentOnlyResolutionAttribute implements ResolverAttribute {
539
540                 String name;
541                 ArrayList values = new ArrayList();
542                 boolean resolved = false;
543
544                 DependentOnlyResolutionAttribute(String name) {
545
546                         this.name = name;
547                 }
548
549                 public String getName() {
550
551                         return name;
552                 }
553
554                 public boolean resolved() {
555
556                         return resolved;
557                 }
558
559                 public void setResolved() {
560
561                         resolved = true;
562                 }
563
564                 public void resolveFromCached(ResolverAttribute attribute) {
565
566                 }
567
568                 public void setLifetime(long lifetime) {
569
570                 }
571                 
572                 public void setNamespace(String namespace) {
573                         
574                 }
575
576                 public long getLifetime() {
577
578                         return 0;
579                 }
580
581                 public void addValue(Object value) {
582
583                         values.add(value);
584                 }
585
586                 public Iterator getValues() {
587
588                         return values.iterator();
589                 }
590
591                 public boolean hasValues() {
592
593                         if (values.isEmpty()) { return false; }
594                         return true;
595                 }
596
597                 public void registerValueHandler(ValueHandler handler) {
598
599                 }
600
601                 public ValueHandler getRegisteredValueHandler() {
602
603                         return null;
604                 }
605         }
606
607         /**
608          * Cleanup resources that won't be released when this object is garbage-collected
609          */
610         public void destroy() {
611
612                 resolverCache.destroy();
613         }
614 }