[java-opensaml] 02/02: OSJ-181: Enhance dynamic metadata resolvers to support non-entityID criteria

Brent Putman putmanb at georgetown.edu
Mon Oct 1 18:09:29 EDT 2018


This is an automated email from the git hooks/post-receive script.

putmanb pushed a commit to branch master
in repository java-opensaml.

View the commit online:
http://git.shibboleth.net/view/?p=java-opensaml.git;a=commit;h=23e8004daf7e5734104b4a34673d5e277a466c51

commit 23e8004daf7e5734104b4a34673d5e277a466c51
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Sun Sep 23 18:58:04 2018 -0400

    OSJ-181: Enhance dynamic metadata resolvers to support non-entityID criteria
---
 .../impl/AbstractDynamicMetadataResolver.java      | 420 ++++++++++++++++++---
 .../DefaultLocalDynamicSourceKeyGenerator.java     |  90 +++++
 .../resolver/impl/EntityIDDigestGenerator.java     |  31 +-
 .../FunctionDrivenDynamicHTTPMetadataResolver.java |  25 +-
 .../impl/HTTPEntityIDRequestURLBuilder.java        |  17 +-
 .../impl/LocalDynamicMetadataResolver.java         |   7 +-
 .../MetadataQueryProtocolRequestURLBuilder.java    | 144 ++++++-
 .../impl/MetadataQueryProtocolSHA1Transformer.java |  61 ---
 .../resolver/impl/RegexRequestURLBuilder.java      |  17 +-
 .../resolver/impl/SAMLArtifactURLBuilder.java      |  67 ++++
 .../resolver/impl/TemplateRequestURLBuilder.java   |  15 +-
 .../index/impl/LockableMetadataIndexManager.java   |  75 ++++
 .../AbstractDynamicHTTPMetadataResolverTest.java   |   4 +-
 .../impl/AbstractDynamicMetadataResolverTest.java  | 214 +++++++++--
 .../DefaultLocalDynamicSourceKeyGeneratorTest.java | 126 +++++++
 ...ctionDrivenDynamicHTTPMetadataResolverTest.java |  69 +++-
 .../impl/HTTPEntityIDRequestURLBuilderTest.java    |  18 +-
 ...MetadataQueryProtocolRequestURLBuilderTest.java |  63 +++-
 .../MetadataQueryProtocolSHA1TransformerTest.java  |  31 --
 .../resolver/impl/RegexRequestURLBuilderTest.java  |  15 +-
 .../resolver/impl/SAMLArtifactURLBuilderTest.java  |  85 +++++
 .../impl/TemplateRequestURLBuilderTest.java        |  18 +-
 22 files changed, 1332 insertions(+), 280 deletions(-)

diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicMetadataResolver.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicMetadataResolver.java
index daae4fb..5814ac0 100644
--- a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicMetadataResolver.java
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicMetadataResolver.java
@@ -19,10 +19,13 @@ package org.opensaml.saml.metadata.resolver.impl;
 
 import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.Timer;
 import java.util.TimerTask;
 import java.util.concurrent.ConcurrentHashMap;
@@ -45,9 +48,11 @@ import org.opensaml.core.xml.util.XMLObjectSupport;
 import org.opensaml.core.xml.util.XMLObjectSupport.CloneOutputOption;
 import org.opensaml.saml.metadata.resolver.ClearableMetadataResolver;
 import org.opensaml.saml.metadata.resolver.DynamicMetadataResolver;
-import org.opensaml.saml.metadata.resolver.MetadataResolver;
 import org.opensaml.saml.metadata.resolver.filter.FilterException;
+import org.opensaml.saml.metadata.resolver.index.MetadataIndex;
+import org.opensaml.saml.metadata.resolver.index.impl.LockableMetadataIndexManager;
 import org.opensaml.saml.saml2.common.SAML2Support;
+import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
 import org.opensaml.saml.saml2.metadata.EntityDescriptor;
 import org.opensaml.security.crypto.JCAConstants;
 import org.slf4j.Logger;
@@ -59,20 +64,26 @@ import com.codahale.metrics.RatioGauge;
 import com.codahale.metrics.Timer.Context;
 import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
-import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableSet;
 
 import net.shibboleth.utilities.java.support.annotation.Duration;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
 import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 import net.shibboleth.utilities.java.support.codec.StringDigester;
 import net.shibboleth.utilities.java.support.codec.StringDigester.OutputFormat;
 import net.shibboleth.utilities.java.support.collection.Pair;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
+import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 import net.shibboleth.utilities.java.support.primitive.TimerSupport;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
@@ -170,6 +181,9 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
     /** Object tracking metrics related to the persistent cache initialization. */
     @NonnullAfterInit private PersistentCacheInitializationMetrics persistentCacheInitMetrics;
     
+    /** The set of indexes configured. */
+    private Set<MetadataIndex> indexes;
+    
     /** Flag used to track state of whether currently initializing or not. */
     private boolean initializing;
     
@@ -181,6 +195,8 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
     public AbstractDynamicMetadataResolver(@Nullable final Timer backgroundTaskTimer) {
         super();
         
+        indexes = Collections.emptySet();
+        
         if (backgroundTaskTimer == null) {
             taskTimer = new Timer(TimerSupport.getTimerName(this), true);
             createdOwnTaskTimer = true;
@@ -529,6 +545,34 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
         metricsBaseName = StringSupport.trimOrNull(baseName);
     }
     
+    /**
+     * Get the configured indexes.
+     * 
+     * @return the set of configured indexes
+     */
+    @Nonnull @NonnullElements @Unmodifiable @NotLive public Set<MetadataIndex> getIndexes() {
+        return ImmutableSet.copyOf(indexes);
+    }
+
+    /**
+     * Set the configured indexes.
+     * 
+     * @param newIndexes the new indexes to set
+     */
+    public void setIndexes(@Nullable final Set<MetadataIndex> newIndexes) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        if (newIndexes == null) {
+            indexes = Collections.emptySet();
+        } else {
+            indexes = new HashSet<>();
+            indexes.addAll(Collections2.filter(newIndexes, Predicates.notNull()));
+        }
+    }
+    
+    protected boolean indexesEnabled() {
+        return ! getBackingStore().getSecondaryIndexManager().getIndexes().isEmpty();
+    }
+    
     /** {@inheritDoc} */
     public void clear() throws ResolverException {
         final DynamicEntityBackingStore backingStore = getBackingStore();
@@ -573,48 +617,48 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
         
         final Context contextResolve = MetricsSupport.startTimer(timerResolve);
         try {
-            final EntityIdCriterion entityIdCriterion = criteria.get(EntityIdCriterion.class);
-            if (entityIdCriterion == null || Strings.isNullOrEmpty(entityIdCriterion.getEntityId())) {
-                log.info("{} Entity Id was not supplied in criteria set, skipping resolution", getLogPrefix());
-                return Collections.emptySet();
-            }
-
-            final String entityID = StringSupport.trimOrNull(criteria.get(EntityIdCriterion.class).getEntityId());
-            log.debug("{} Attempting to resolve metadata for entityID: {}", getLogPrefix(), entityID);
-
-            final EntityManagementData mgmtData = getBackingStore().getManagementData(entityID);
-            final Lock readLock = mgmtData.getReadWriteLock().readLock();
             Iterable<EntityDescriptor> candidates = null;
-            try {
-                readLock.lock();
+            
+            final String entityID = resolveEntityID(criteria);
+            if (entityID != null) {
+                log.debug("{} Resolved criteria to entityID: {}", getLogPrefix(), entityID);
 
-                final List<EntityDescriptor> descriptors = lookupEntityID(entityID);
-                if (descriptors.isEmpty()) {
-                    if (mgmtData.isNegativeLookupCacheActive()) {
-                        log.debug("{} Did not find requested metadata in backing store, " 
-                                + "and negative lookup cache is active, returning empty result", 
-                                getLogPrefix());
-                        return Collections.emptyList();
-                    } else {
-                        log.debug("{} Did not find requested metadata in backing store, " 
-                                + "attempting to resolve dynamically", 
-                                getLogPrefix());
-                    }
-                } else {
-                    if (shouldAttemptRefresh(mgmtData)) {
-                        log.debug("{} Metadata was indicated to be refreshed based on refresh trigger time", 
-                                getLogPrefix());
+                final EntityManagementData mgmtData = getBackingStore().getManagementData(entityID);
+                final Lock readLock = mgmtData.getReadWriteLock().readLock();
+                try {
+                    readLock.lock();
+
+                    final List<EntityDescriptor> descriptors = lookupEntityID(entityID);
+                    if (descriptors.isEmpty()) {
+                        if (mgmtData.isNegativeLookupCacheActive()) {
+                            log.debug("{} Did not find requested metadata in backing store, " 
+                                    + "and negative lookup cache is active, returning empty result", 
+                                    getLogPrefix());
+                            return Collections.emptyList();
+                        } else {
+                            log.debug("{} Did not find requested metadata in backing store, " 
+                                    + "attempting to resolve dynamically", 
+                                    getLogPrefix());
+                        }
                     } else {
-                        log.debug("{} Found requested metadata in backing store", getLogPrefix());
-                        candidates = descriptors;
+                        if (shouldAttemptRefresh(mgmtData)) {
+                            log.debug("{} Metadata was indicated to be refreshed based on refresh trigger time", 
+                                    getLogPrefix());
+                        } else {
+                            log.debug("{} Found requested metadata in backing store", getLogPrefix());
+                            candidates = descriptors;
+                        }
                     }
+                } finally {
+                    readLock.unlock();
                 }
-            } finally {
-                readLock.unlock();
+            } else {
+                log.debug("{} Single entityID unresolveable from criteria, will resolve from origin by criteria only",
+                        getLogPrefix());
             }
 
             if (candidates == null) {
-                candidates = resolveFromOriginSource(criteria);
+                candidates = resolveFromOriginSource(criteria, entityID);
             }
 
             return predicateFilterCandidates(candidates, criteria, false);
@@ -624,17 +668,137 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
     }
     
     /**
+    * Attempt to resolve the single entityID for the operation from the criteria set.
+    * 
+    * <p>
+    * If an {@link EntityIdCriterion} is present, that will be used. If not present, then a single
+    * entityID will be resolved via the secondary index manager of the backing store.
+    * </p>
+    * 
+    * @param criteria the criteria set on which to operate
+    * @return the resolve entityID, or null if a single entityID could not be resolved
+    */
+    @Nullable protected String resolveEntityID(@Nonnull final CriteriaSet criteria) {
+        final Set<String> entityIDs = resolveEntityIDs(criteria);
+        if (entityIDs.size() == 1) {
+            return entityIDs.iterator().next();
+        } else {
+            return null;
+        }
+    }
+    
+    /**
+    * 
+    * Attempt to resolve all the entityIDs represented by the criteria set.
+    * 
+    * <p>
+    * If an {@link EntityIdCriterion} is present, that will be used. If not present, then 
+    * entityIDs will be resolved via the secondary index manager of the backing store.
+    * </p>
+    * 
+    * @param criteria the criteria set on which to operate
+    * @return the resolved entityIDs, may be empty
+    */
+    @Nonnull protected Set<String> resolveEntityIDs(@Nonnull final CriteriaSet criteria) {
+        final EntityIdCriterion entityIdCriterion = criteria.get(EntityIdCriterion.class);
+        if (entityIdCriterion != null) {
+            log.debug("{} Found entityID in criteria: {}", getLogPrefix(), entityIdCriterion.getEntityId());
+            return Collections.singleton(entityIdCriterion.getEntityId());
+        } else {
+            log.debug("{} EntityID was not supplied in criteria, processing criteria with secondary indexes",
+                    getLogPrefix());
+        }
+        
+        if (!indexesEnabled()) {
+            log.trace("Indexes not enabled, skipping secondary index processing");
+            return Collections.emptySet();
+        }
+
+        Optional<Set<String>> indexedResult = null;
+        final Lock readLock = getBackingStore().getSecondaryIndexManager().getReadWriteLock().readLock();
+        try {
+            readLock.lock();
+            indexedResult = getBackingStore().getSecondaryIndexManager().lookupIndexedItems(criteria);
+        } finally {
+            readLock.unlock();
+        }
+
+        if (indexedResult.isPresent()) {
+            final Set<String> entityIDs = indexedResult.get();
+            if (entityIDs.isEmpty()) {
+                log.debug("{} No entityIDs resolved from secondary indexes (Optional 'present' with empty set)",
+                        getLogPrefix());
+                return Collections.emptySet();
+            } else if (entityIDs.size() > 1) {
+                log.debug("{} Multiple entityIDs resolved from secondary indexes: {}", 
+                        getLogPrefix(), entityIDs);
+                return new HashSet<>(entityIDs);
+            } else {
+                final String entityID = entityIDs.iterator().next();
+                log.debug("{} Resolved 1 entityID from secondary indexes: {}", getLogPrefix(), entityID);
+                return Collections.singleton(entityID);
+            }
+        } else {
+            log.debug("{} No entityIDs resolved from secondary indexes (Optional 'absent').", getLogPrefix());
+            return null;
+        }
+    }
+    
+    /**
      * Fetch metadata from an origin source based on the input criteria, store it in the backing store 
      * and then return it.
      * 
      * @param criteria the input criteria set
      * @return the resolved metadata
      * @throws ResolverException  if there is a fatal error attempting to resolve the metadata
+     * 
+     * @deprecated instead use {@link #resolveFromOriginSource(CriteriaSet, String)}
      */
-    @Nonnull @NonnullElements protected Iterable<EntityDescriptor> resolveFromOriginSource(
+    @Deprecated
+    @Nonnull @NonnullElements 
+    protected Iterable<EntityDescriptor> resolveFromOriginSource(
             @Nonnull final CriteriaSet criteria) throws ResolverException {
         
-        final String entityID = StringSupport.trimOrNull(criteria.get(EntityIdCriterion.class).getEntityId());
+        DeprecationSupport.warnOnce(ObjectType.METHOD, "resolveFromOriginSource", null, "2-arg same-named method");
+        
+        return resolveFromOriginSource(criteria, resolveEntityID(criteria));
+    }
+    
+    /**
+     * Fetch metadata from an origin source based on the input criteria, store it in the backing store 
+     * and then return it.
+     * 
+     * @param criteria the input criteria set
+     * @param entityID the previously resolved single entityID
+     * @return the resolved metadata
+     * @throws ResolverException  if there is a fatal error attempting to resolve the metadata
+     */
+    @Nonnull @NonnullElements protected Iterable<EntityDescriptor> resolveFromOriginSource(
+            @Nonnull final CriteriaSet criteria, @Nullable final String entityID) throws ResolverException {
+        
+        if (entityID != null) {
+            log.debug("{} Resolving from origin source based on entityID: {}", getLogPrefix(), entityID);
+            return resolveFromOriginSourceWithEntityID(criteria, entityID);
+        } else {
+            log.debug("{} Resolving from origin source based on non-entityID criteria", getLogPrefix());
+            return resolveFromOriginSourceWithoutEntityID(criteria);
+        }
+        
+    }
+ 
+    /**
+     * Fetch metadata from an origin source based on the input criteria when the entityID is known,
+     * store it in the backing store and then return it.
+     * 
+     * @param criteria the input criteria set
+     * @param entityID the entityID known to be represented by the criteria set
+     * @return the resolved metadata
+     * @throws ResolverException  if there is a fatal error attempting to resolve the metadata
+     */
+    @Nonnull @NonnullElements
+    protected Iterable<EntityDescriptor> resolveFromOriginSourceWithEntityID(
+            @Nonnull final CriteriaSet criteria, @Nonnull final String entityID) throws ResolverException {
+        
         final EntityManagementData mgmtData = getBackingStore().getManagementData(entityID);
         final Lock writeLock = mgmtData.getReadWriteLock().writeLock(); 
         
@@ -690,6 +854,120 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
         }
         
     }
+    
+    /**
+     * Fetch metadata from an origin source based on the input criteria when the entityID is not known,
+     * store it in the backing store and then return it.
+     * 
+     * @param criteria the input criteria set
+     * @return the resolved metadata
+     * @throws ResolverException if there is a fatal error attempting to resolve the metadata
+     */
+    @Nonnull @NonnullElements 
+    protected Iterable<EntityDescriptor> resolveFromOriginSourceWithoutEntityID(@Nonnull final CriteriaSet criteria) 
+            throws ResolverException {
+        
+        XMLObject root = null;
+        final Context contextFetchFromOriginSource = MetricsSupport.startTimer(timerFetchFromOriginSource);
+        try {
+            root = fetchFromOriginSource(criteria);
+        } catch (final IOException e) {
+            log.error("{} Error fetching metadata from origin source", getLogPrefix(), e);
+            return lookupCriteria(criteria);
+        } finally {
+            MetricsSupport.stopTimer(contextFetchFromOriginSource);
+        }
+        
+        if (root == null) {
+            log.debug("{} No metadata was fetched from the origin source", getLogPrefix());
+            return lookupCriteria(criteria);
+        } else if (root instanceof EntityDescriptor){
+            log.debug("{} Fetched EntityDescriptor from the origin source", getLogPrefix());
+            return processNonEntityIDFetchedEntityDescriptor((EntityDescriptor) root);
+        } else if (root instanceof EntitiesDescriptor) {
+            log.debug("{} Fetched EntitiesDescriptor from the origin source", getLogPrefix());
+            return processNonEntityIDFetchedEntittiesDescriptor((EntitiesDescriptor) root);
+        } else {
+            log.warn("{} Fetched metadata was of an unsupported type: {}", getLogPrefix(), root.getClass().getName());
+            return lookupCriteria(criteria);
+        }
+    }
+    
+    /**
+     * Lookup and return all EntityDescriptors currently available in the resolver cache 
+     * which match either entityID or secondary-indexed criteria.
+     * 
+     * @param criteria the input criteria set
+     * @return the resolved metadata
+     * @throws ResolverException if there is a fatal error attempting to resolve the metadata
+     */
+    @Nonnull @NonnullElements 
+    protected Iterable<EntityDescriptor> lookupCriteria(@Nonnull final CriteriaSet criteria) throws ResolverException {
+        final List<EntityDescriptor> entities = new ArrayList<>();
+        final Set<String> entityIDs = resolveEntityIDs(criteria);
+        for (final String entityID : entityIDs) {
+            final EntityManagementData mgmtData = getBackingStore().getManagementData(entityID);
+            final Lock readLock = mgmtData.getReadWriteLock().readLock();
+            try {
+                readLock.lock();
+                
+                entities.addAll(lookupEntityID(entityID));
+            } finally {
+               readLock.unlock(); 
+            }
+        }
+        return entities;
+    }
+    
+    /**
+     * Process an EntitiesDescriptor received from a non-entityID-based fetch.
+     * 
+     * @param entities the metadata to process
+     * @return the resolved descriptor(s)
+     * @throws ResolverException if there is a fatal error attempting to resolve the metadata
+     */
+    @Nullable protected List<EntityDescriptor> processNonEntityIDFetchedEntittiesDescriptor(
+            @Nonnull final EntitiesDescriptor entities) throws ResolverException {
+        
+        final List<EntityDescriptor> returnedEntities = new ArrayList<>();
+        
+        for (final EntitiesDescriptor childEntities : entities.getEntitiesDescriptors()) {
+            returnedEntities.addAll(processNonEntityIDFetchedEntittiesDescriptor(childEntities));
+        }
+        
+        for (final EntityDescriptor entity : entities.getEntityDescriptors()) {
+            returnedEntities.addAll(processNonEntityIDFetchedEntityDescriptor(entity));
+        }
+        
+        return returnedEntities;
+    }
+    
+    /**
+     * Process an EntityDescriptor received from a non-entityID-based fetch.
+     * 
+     * @param entity the metadata to process
+     * @return the resolved descriptor(s)
+     * @throws ResolverException if there is a fatal error attempting to resolve the metadata
+     */
+    @Nullable protected List<EntityDescriptor> processNonEntityIDFetchedEntityDescriptor(
+            @Nonnull final EntityDescriptor entity) throws ResolverException {
+        
+        final String entityID = entity.getEntityID();
+        final EntityManagementData mgmtData = getBackingStore().getManagementData(entityID);
+        final Lock writeLock = mgmtData.getReadWriteLock().writeLock(); 
+        try {
+            writeLock.lock();            
+            mgmtData.clearNegativeLookupCache();
+            processNewMetadata(entity, entityID);
+            return lookupEntityID(entityID);
+        } catch (final FilterException e) {
+            log.error("{} Metadata filtering problem processing non-entityID fetched EntityDescriptor", 
+                    getLogPrefix(), e);
+            return lookupEntityID(entityID);
+        } finally {
+            writeLock.unlock();
+        }
+    }
 
     /**
      * Fetch the metadata from the origin source.
@@ -928,7 +1206,7 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
     /** {@inheritDoc} */
     @Override
     @Nonnull protected DynamicEntityBackingStore createNewBackingStore() {
-        return new DynamicEntityBackingStore();
+        return new DynamicEntityBackingStore(getIndexes());
     }
     
     /** {@inheritDoc} */
@@ -1137,10 +1415,23 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
     /** {@inheritDoc} */
     @Override
     protected void removeByEntityID(final String entityID, final EntityBackingStore backingStore) {
-        if (isPersistentCachingEnabled()) {
-            final List<EntityDescriptor> descriptors = backingStore.getIndexedDescriptors().get(entityID);
-            if (descriptors != null) {
-                for (final EntityDescriptor descriptor : descriptors) {
+        final List<EntityDescriptor> descriptors = backingStore.getIndexedDescriptors().get(entityID);
+        if (descriptors != null) {
+            for (final EntityDescriptor descriptor : descriptors) {
+
+                if (indexesEnabled()) {
+                    final DynamicEntityBackingStore dynamicStore = (DynamicEntityBackingStore) backingStore;
+
+                    final Lock writeLock = dynamicStore.getSecondaryIndexManager().getReadWriteLock().writeLock();
+                    try {
+                        writeLock.lock();
+                        dynamicStore.getSecondaryIndexManager().deindexEntityDescriptor(descriptor);
+                    } finally {
+                        writeLock.unlock();
+                    }
+                }
+                
+                if (isPersistentCachingEnabled()) {
                     final String key = getPersistentCacheKeyGenerator().apply(descriptor);
                     try {
                         getPersistentCacheManager().remove(key);
@@ -1149,6 +1440,7 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
                                 getLogPrefix(), descriptor.getEntityID(), key);
                     }
                 }
+                    
             }
         }
         
@@ -1188,6 +1480,24 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
         super.doDestroy();
     }
     
+    /** {@inheritDoc} */
+    @Override protected void indexEntityDescriptor(@Nonnull final EntityDescriptor entityDescriptor, 
+            @Nonnull final EntityBackingStore backingStore) {
+        super.indexEntityDescriptor(entityDescriptor, backingStore);
+        
+        if (indexesEnabled()) {
+            final DynamicEntityBackingStore dynamicStore = (DynamicEntityBackingStore) backingStore;
+
+            final Lock writeLock = dynamicStore.getSecondaryIndexManager().getReadWriteLock().writeLock();
+            try {
+                writeLock.lock();
+                dynamicStore.getSecondaryIndexManager().indexEntityDescriptor(entityDescriptor);
+            } finally {
+                writeLock.unlock();
+            }
+        }
+    }
+
     /**
      * Specialized entity backing store implementation for dynamic metadata resolvers.
      */
@@ -1196,10 +1506,30 @@ public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataRe
         /** Map holding management data for each entityID. */
         private Map<String, EntityManagementData> mgmtDataMap;
         
-        /** Constructor. */
-        protected DynamicEntityBackingStore() {
+        /** Manager for secondary indexes. */
+        private LockableMetadataIndexManager<String> secondaryIndexManager;
+        
+        /** 
+         * Constructor.
+         * 
+         *  @param initIndexes secondary indexes for which to initialize storage
+         */
+        protected DynamicEntityBackingStore(
+                @Nullable @NonnullElements @Unmodifiable @NotLive final Set<MetadataIndex> initIndexes) {
             super();
             mgmtDataMap = new ConcurrentHashMap<>();
+            secondaryIndexManager = new LockableMetadataIndexManager(initIndexes, 
+                    new LockableMetadataIndexManager.EntityIDExtractionFunction()); 
+
+        }
+        
+        /**
+         * Get the secondary index manager.
+         * 
+         * @return the manager for secondary indexes
+         */
+        public LockableMetadataIndexManager<String> getSecondaryIndexManager() {
+            return secondaryIndexManager;
         }
         
         /**
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/DefaultLocalDynamicSourceKeyGenerator.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/DefaultLocalDynamicSourceKeyGenerator.java
new file mode 100644
index 0000000..05044e0
--- /dev/null
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/DefaultLocalDynamicSourceKeyGenerator.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 org.opensaml.saml.metadata.resolver.impl;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.saml.common.binding.artifact.SAMLArtifact;
+import org.opensaml.saml.common.binding.artifact.SAMLSourceIDArtifact;
+import org.opensaml.saml.criterion.ArtifactCriterion;
+
+import com.google.common.base.Function;
+import com.google.common.io.BaseEncoding;
+
+import net.shibboleth.utilities.java.support.codec.StringDigester;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * A default implementation of {@link Function} for use as a source key generator 
+ * with {@link LocalDynamicMetadataResolver}.
+ * 
+ * <p>
+ * This is a simple subclass of {@link EntityIDDigestGenerator} which defaults in the {@link StringDigester}
+ * to use lower-case hex encoding of the SHA-1 digest of the entity ID from {@link EntityIdCriterion}.
+ * Since this is the same representation typically used for the SAML SourceID used in artifacts,
+ * this implementation adds in support for understanding {@link ArtifactCriterion} carrying a 
+ * {@link SAMLSourceIDArtifact}.
+ * </p>
+ */
+public class DefaultLocalDynamicSourceKeyGenerator extends EntityIDDigestGenerator {
+    
+    /** Hex encoder. */
+    private static final BaseEncoding HEX = BaseEncoding.base16().lowerCase();
+
+    /**
+     * Constructor.
+     *
+     */
+    public DefaultLocalDynamicSourceKeyGenerator() {
+        super();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param keyPrefix optional prefix for the digested value
+     * @param keySuffix optional suffix for the digested value
+     * @param valueSeparator optional separator between the prefix, digest and suffix values
+     */
+    public DefaultLocalDynamicSourceKeyGenerator(@Nullable final String keyPrefix, @Nullable final String keySuffix, 
+            @Nullable final String valueSeparator) {
+        super(null, keyPrefix, keySuffix, valueSeparator);
+    }
+
+    /** {@inheritDoc} */
+    public String apply(@Nullable final CriteriaSet criteria) {
+        if (criteria == null) {
+            return null;
+        }
+        
+        if (criteria.contains(EntityIdCriterion.class)) {
+            return super.apply(criteria);
+        }
+        
+        if (criteria.contains(ArtifactCriterion.class)) {
+            final SAMLArtifact artifact = criteria.get(ArtifactCriterion.class).getArtifact();
+            if (artifact instanceof SAMLSourceIDArtifact) {
+                return buildKey(HEX.encode(((SAMLSourceIDArtifact)artifact).getSourceID()));
+            }
+        }
+        
+        return null;
+    }
+
+}
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/EntityIDDigestGenerator.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/EntityIDDigestGenerator.java
index e273a9e..2d25762 100644
--- a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/EntityIDDigestGenerator.java
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/EntityIDDigestGenerator.java
@@ -87,25 +87,26 @@ public class EntityIDDigestGenerator implements Function<CriteriaSet, String> {
 
     /** {@inheritDoc} */
     @Override
-    public String apply(final CriteriaSet input) {
-        if (input == null) {
+    public String apply(final CriteriaSet criteria) {
+        if (criteria == null || !criteria.contains(EntityIdCriterion.class)) {
             return null;
         }
         
-        final EntityIdCriterion entityIDCrit = input.get(EntityIdCriterion.class);
-        if (entityIDCrit == null) { 
-            return null;
-        }
-        
-        final String entityID = StringSupport.trimOrNull(entityIDCrit.getEntityId());
-        if (entityID == null) {
-            return null;
-        }
-        
-        final String digested = digester.apply(entityID);
+        final String digested = digester.apply(criteria.get(EntityIdCriterion.class).getEntityId());
         
+        return buildKey(digested);
+    }
+    
+    /**
+     * Build the key by applying the configured prefix and/or suffix, if present.
+     * 
+     * @param keyValue the primary key value data being represented
+     * 
+     * @return the key value with prefix and suffix applied
+     */
+    protected String buildKey(@Nonnull final String keyValue) {
         if (prefix == null && suffix == null) {
-            return digested;
+            return keyValue;
         } else {
             final StringBuffer buffer = new StringBuffer();
             if (prefix != null) {
@@ -114,7 +115,7 @@ public class EntityIDDigestGenerator implements Function<CriteriaSet, String> {
                     buffer.append(separator);
                 }
             }
-            buffer.append(digested);
+            buffer.append(keyValue);
             if (suffix != null) {
                 if (separator != null) {
                     buffer.append(separator);
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/FunctionDrivenDynamicHTTPMetadataResolver.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/FunctionDrivenDynamicHTTPMetadataResolver.java
index c00d1f9..d3f6182 100644
--- a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/FunctionDrivenDynamicHTTPMetadataResolver.java
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/FunctionDrivenDynamicHTTPMetadataResolver.java
@@ -22,18 +22,16 @@ import java.util.Timer;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.primitive.StringSupport;
-import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
-
 import org.apache.http.client.HttpClient;
-import org.opensaml.core.criterion.EntityIdCriterion;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Function;
 
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
 /**
  * Simple implementation of an HTTP-based dynamic metadata resolver which builds the request URL
  * to process based on a {@link Function} instance.
@@ -50,7 +48,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolver extends AbstractDynamicHT
     private final Logger log = LoggerFactory.getLogger(FunctionDrivenDynamicHTTPMetadataResolver.class);
     
     /** Function for building the request URL. */
-    private Function<String, String> requestURLBuilder;
+    private Function<CriteriaSet, String> requestURLBuilder;
 
     /**
      * Constructor.
@@ -80,7 +78,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolver extends AbstractDynamicHT
      * 
      * @return the request URL builder function instance
      */
-    @Nonnull public Function<String, String> getRequestURLBuilder() {
+    @Nonnull public Function<CriteriaSet, String> getRequestURLBuilder() {
         return requestURLBuilder;
     }
 
@@ -90,9 +88,9 @@ public class FunctionDrivenDynamicHTTPMetadataResolver extends AbstractDynamicHT
      * 
      * <p>Defaults to an instance of {@link HTTPEntityIDRequestURLBuilder}.</p>
      * 
-     * @param builder the reqeust URL builder function instance
+     * @param builder the request URL builder function instance
      */
-    public void setRequestURLBuilder(@Nonnull final Function<String, String> builder) {
+    public void setRequestURLBuilder(@Nonnull final Function<CriteriaSet, String> builder) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
         requestURLBuilder = Constraint.isNotNull(builder, "Request URL builder function was null");
@@ -101,12 +99,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolver extends AbstractDynamicHT
     /** {@inheritDoc} */
     @Override
     @Nullable protected String buildRequestURL(@Nonnull final CriteriaSet criteria) {
-        final String entityID = StringSupport.trimOrNull(criteria.get(EntityIdCriterion.class).getEntityId());
-        if (entityID == null) {
-            return null;
-        }
-        
-        final String url = getRequestURLBuilder().apply(entityID);
+        final String url = getRequestURLBuilder().apply(criteria);
         
         log.debug("{} URL generated by request builder was: {}", getLogPrefix(), url);
         
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/HTTPEntityIDRequestURLBuilder.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/HTTPEntityIDRequestURLBuilder.java
index 6f0269d..0e19026 100644
--- a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/HTTPEntityIDRequestURLBuilder.java
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/HTTPEntityIDRequestURLBuilder.java
@@ -21,23 +21,32 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
+import org.opensaml.core.criterion.EntityIdCriterion;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Function;
 
 /**
- * Function which examines an entity ID and returns it as a metadata request URL if and only if the entity ID 
- * is an HTTP or HTTPS URL.
+ * Function which examines an entity ID from supplied criteria and returns it as a metadata request URL 
+ * if and only if the entity ID is an HTTP or HTTPS URL.
  */
-public class HTTPEntityIDRequestURLBuilder implements Function<String, String> {
+public class HTTPEntityIDRequestURLBuilder implements Function<CriteriaSet, String> {
     
     /** Logger. */
     private final Logger log = LoggerFactory.getLogger(HTTPEntityIDRequestURLBuilder.class);
 
     /** {@inheritDoc} */
-    @Override @Nullable public String apply(@Nonnull final String entityID) {
+    @Override @Nullable public String apply(@Nonnull final CriteriaSet criteria) {
+        Constraint.isNotNull(criteria, "Criteria was null");
+        if (!criteria.contains(EntityIdCriterion.class)) {
+            log.trace("Criteria did not contain entity ID, unable to build request URL");
+            return null;
+        }
+        final String entityID = criteria.get(EntityIdCriterion.class).getEntityId();
+        
         Constraint.isNotNull(entityID, "Entity ID was null");
         
         if (entityID.toLowerCase().startsWith("http:") || entityID.toLowerCase().startsWith("https:")) {
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/LocalDynamicMetadataResolver.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/LocalDynamicMetadataResolver.java
index fca963b..ab7ebe1 100644
--- a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/LocalDynamicMetadataResolver.java
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/LocalDynamicMetadataResolver.java
@@ -54,7 +54,7 @@ public class LocalDynamicMetadataResolver extends AbstractDynamicMetadataResolve
      * Constructor.
      * 
      * <p>
-     * Source key generator will be an internal instance of {@link EntityIDDigestGenerator},
+     * Source key generator will be an internal instance of {@link DefaultLocalDynamicSourceKeyGenerator},
      * with all default parameters.
      * </p>
      *
@@ -79,7 +79,8 @@ public class LocalDynamicMetadataResolver extends AbstractDynamicMetadataResolve
      * Constructor.
      *
      * <p>
-     * If the supplied source key generator is null, an internal instance of {@link EntityIDDigestGenerator}
+     * If the supplied source key generator is null, an internal instance of 
+     * {@link DefaultLocalDynamicSourceKeyGenerator}
      * will be used, with all default parameters.
      * </p>
      * @param backgroundTaskTimer timer for management of background tasks
@@ -96,7 +97,7 @@ public class LocalDynamicMetadataResolver extends AbstractDynamicMetadataResolve
         
         sourceKeyGenerator = keyGenerator;
         if (sourceKeyGenerator == null) {
-            sourceKeyGenerator = new EntityIDDigestGenerator();
+            sourceKeyGenerator = new DefaultLocalDynamicSourceKeyGenerator();
         }
     }
     
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolRequestURLBuilder.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolRequestURLBuilder.java
index 30feff4..a68e8a5 100644
--- a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolRequestURLBuilder.java
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolRequestURLBuilder.java
@@ -17,26 +17,44 @@
 
 package org.opensaml.saml.metadata.resolver.impl;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.core.criterion.EntityIdCriterion;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
 import com.google.common.escape.Escaper;
 import com.google.common.net.UrlEscapers;
 
+import net.shibboleth.utilities.java.support.annotation.ParameterName;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
 //TODO reference to protocol spec pending in Javadoc.
 
 /**
- * Function which produces a URL according to the Metadata Query Protocol specification.
+ * Function which produces a URL according to the Metadata Query Protocol (MDQ) specification.
+ * 
+ * <p>
+ * Support for building request URLs per the MDQ SAML profile based on an {@link EntityIdCriterion} is built-in.
+ * </p>
+ * 
+ * <p>
+ * Support for building request URLs via other criteria may be specified via ordered instances 
+ * of {@link MetadataQueryProtocolURLBuilder}. These are evaluated in the supplied order,
+ * and the first non-null result will be returned.
+ * </p>
  */
-public class MetadataQueryProtocolRequestURLBuilder implements Function<String, String> {
+public class MetadataQueryProtocolRequestURLBuilder implements Function<CriteriaSet, String> {
     
     /** Logger. */
     private final Logger log = LoggerFactory.getLogger(MetadataQueryProtocolRequestURLBuilder.class);
@@ -50,23 +68,57 @@ public class MetadataQueryProtocolRequestURLBuilder implements Function<String,
     /** Path escaper for escaping the input value inserted into the URL path. */
     private Escaper pathEscaper = UrlEscapers.urlPathSegmentEscaper();
     
+    /** List of secondary URL builders. */
+    private List<MetadataQueryProtocolURLBuilder> urlBuilders;
+    
     /**
      * Constructor.
      *
      * @param baseURL the base URL for the metadata responder
      */
-    public MetadataQueryProtocolRequestURLBuilder(@Nonnull @NotEmpty final String baseURL) {
-        this(baseURL, null);
+    public MetadataQueryProtocolRequestURLBuilder(
+            @ParameterName(name="baseURL") @Nonnull @NotEmpty final String baseURL) {
+        this(baseURL, null, null);
     }
     
+    
     /**
      * Constructor.
      *
      * @param baseURL the base URL for the metadata responder
      * @param transform function which transforms the entityID prior to URL construction substitution, may be null
      */
-    public MetadataQueryProtocolRequestURLBuilder(@Nonnull @NotEmpty final String baseURL, 
-            @Nullable final Function<String,String> transform) {
+    public MetadataQueryProtocolRequestURLBuilder(
+            @ParameterName(name="baseURL") @Nonnull @NotEmpty final String baseURL,
+            @ParameterName(name="transform") @Nullable final Function<String,String> transform) {
+        this(baseURL, transform, null);
+    }
+    /**
+     * Constructor.
+     *
+     * @param baseURL the base URL for the metadata responder
+     * @param secondaryURLBuilders the list of secondary URL builders, may be null
+     */
+    public MetadataQueryProtocolRequestURLBuilder(
+            @ParameterName(name="baseURL") @Nonnull @NotEmpty final String baseURL,
+            @ParameterName(name="secondaryURLBuilders") @Nullable 
+                final List<MetadataQueryProtocolURLBuilder> secondaryURLBuilders) {
+        this(baseURL, null, secondaryURLBuilders);
+    }
+    
+    /**
+     * Constructor.
+     *
+     * @param baseURL the base URL for the metadata responder
+     * @param transform function which transforms the entityID prior to URL construction substitution, may be null
+     * @param secondaryURLBuilders the list of secondary URL builders, may be null
+     */
+    public MetadataQueryProtocolRequestURLBuilder(
+            @ParameterName(name="baseURL") @Nonnull @NotEmpty final String baseURL, 
+            @ParameterName(name="transform") @Nullable final Function<String,String> transform,
+            @ParameterName(name="secondaryURLBuilders") @Nullable 
+                final List<MetadataQueryProtocolURLBuilder> secondaryURLBuilders
+            ) {
         base = Constraint.isNotNull(StringSupport.trimOrNull(baseURL), "Base URL was null or empty");
         if (!base.endsWith("/")) {
             log.debug("Base URL did not end in a trailing '/', one will be added");
@@ -75,14 +127,37 @@ public class MetadataQueryProtocolRequestURLBuilder implements Function<String,
         log.debug("Effective base URL value was: {}", base);
         
         transformer = transform;
+        
+        if (secondaryURLBuilders != null) {
+            urlBuilders = new ArrayList<>(Collections2.filter(secondaryURLBuilders, Predicates.notNull()));
+        }
     }
 
     /** {@inheritDoc} */
-    @Override @Nullable public String apply(@Nonnull final String input) {
-        String entityID = Constraint.isNotNull(input, "Entity ID was null");
-        
+    @Override @Nullable public String apply(@Nonnull final CriteriaSet criteria) {
+        Constraint.isNotNull(criteria, "Criteria was null");
+        if (criteria.contains(EntityIdCriterion.class)) {
+            log.debug("Criteria contained entity ID, building on that basis");
+            return buildFromEntityID(criteria.get(EntityIdCriterion.class).getEntityId());
+        } else if (urlBuilders != null) {
+            log.debug("Criteria did not contain entity ID, attempting to build using secondary URL builders");
+            return buildFromSecondaryLookups(criteria);
+        } else {
+            log.debug("Criteria did not contain entity ID and no secondary URL builders were configured");
+            return null;
+        }
+    }
+    
+    /**
+     * Build request URL from entityID.
+     * 
+     * @param inputEntityID the entityID
+     * @return the request URL, or null
+     */
+    private String buildFromEntityID(@Nonnull final String inputEntityID) {
+        String entityID = inputEntityID;
         if (transformer != null) {
-            entityID = transformer.apply(entityID);
+            entityID = transformer.apply(inputEntityID);
             log.debug("Transformed entityID is '{}'", entityID);
             if (entityID == null) {
                 log.debug("Transformed entityID was null");
@@ -90,15 +165,48 @@ public class MetadataQueryProtocolRequestURLBuilder implements Function<String,
             }
         }
         
-        try {
-            final String result = base +  "entities/" + pathEscaper.escape(entityID);
-            log.debug("From entityID '{}' and base URL '{}', built request URL: {}", 
-                    entityID, base, result);
-            return result;
-        } catch (final Throwable t) {
-            log.error("Encountered fatal error attempting to build request URL", t);
-            return null;
+        final String result = base +  "entities/" + pathEscaper.escape(entityID);
+        log.debug("From entityID '{}' and base URL '{}', built request URL: {}", 
+                entityID, base, result);
+        return result;
+    }
+
+    /**
+     * Build request URL from secondary lookup criteria.
+     * 
+     * @param criteria the criteria
+     * @return the request URL, or null
+     */
+    private String buildFromSecondaryLookups(@Nonnull final CriteriaSet criteria) {
+        if (urlBuilders != null) {
+            for (final MetadataQueryProtocolURLBuilder builder : urlBuilders) {
+                final String url = builder.buildURL(base, criteria);
+                log.debug("Secondary URL builder '{}' produced URL: {}", builder.getClass().getName(), url);
+                if (url != null) {
+                    return url;
+                }
+            }
         }
+        log.debug("No configured secondary URL builders produced a non-null request URL");
+        return null;
+    }
+    
+    /**
+     * Interface for a component which builds a Metadata Query Protocol request URL from a base URL
+     * and criteria.
+     */
+    public interface MetadataQueryProtocolURLBuilder {
+        
+        /**
+         * Build a request URL using the supplied base service URL and criteria.
+         * 
+         * @param baseURL the service base URL
+         * @param criteria the criteria
+         * @return a URL based on the supplied inputs, or null if the implementation did not support
+         *     or understand any of the supplied criteria 
+         */
+        @Nullable public String buildURL(@Nonnull final String baseURL, @Nonnull final CriteriaSet criteria);
+        
     }
 
 }
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolSHA1Transformer.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolSHA1Transformer.java
deleted file mode 100644
index c9ef1c3..0000000
--- a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolSHA1Transformer.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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 org.opensaml.saml.metadata.resolver.impl;
-
-import java.security.NoSuchAlgorithmException;
-
-import javax.annotation.Nullable;
-
-import net.shibboleth.utilities.java.support.codec.StringDigester;
-import net.shibboleth.utilities.java.support.codec.StringDigester.OutputFormat;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.primitive.StringSupport;
-
-import com.google.common.base.Function;
-
-//TODO reference to protocol spec pending in Javadoc.
-
-/**
- * Function which digests an entity ID using SHA-1 and returns it in the format specified by
- * the Metadata Query Protocol specification.
- */
-public class MetadataQueryProtocolSHA1Transformer implements Function<String, String> {
-    
-    /** String digester instance. */
-    private StringDigester digester;
-    
-    /** Constructor. */
-    public MetadataQueryProtocolSHA1Transformer() {
-        try {
-            digester = new StringDigester("SHA-1", OutputFormat.HEX_LOWER);
-        } catch (final NoSuchAlgorithmException e) {
-            // this can't really happen b/c SHA-1 is required to be supported on all JREs.
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override @Nullable public String apply(@Nullable final String input) {
-        final String entityID = Constraint.isNotNull(StringSupport.trimOrNull(input), "Entity ID was null or empty");
-        final String digested = digester.apply(entityID);
-        if (digested == null) {
-            return null;
-        }
-        return "{sha1}" + digested;
-    }
-
-}
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/RegexRequestURLBuilder.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/RegexRequestURLBuilder.java
index 050fb99..34b4b5b 100644
--- a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/RegexRequestURLBuilder.java
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/RegexRequestURLBuilder.java
@@ -26,15 +26,17 @@ import javax.annotation.Nullable;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
+import org.opensaml.core.criterion.EntityIdCriterion;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Function;
 
 /**
- * Function which produces a URL by evaluating a supplied regular expression against the entity ID, and applying the
- * result to a supplied replacement string.
+ * Function which produces a URL by evaluating a supplied regular expression against the criteria entity ID, 
+ * and applying the result to a supplied replacement string.
  * 
  * <p>
  * The function uses standard Java regular expression components from the <code>java.util.regex</code> package.
@@ -70,7 +72,7 @@ import com.google.common.base.Function;
  * </p>
  * 
  */
-public class RegexRequestURLBuilder implements Function<String, String> {
+public class RegexRequestURLBuilder implements Function<CriteriaSet, String> {
     
     /** Logger. */
     private final Logger log = LoggerFactory.getLogger(RegexRequestURLBuilder.class);
@@ -106,8 +108,13 @@ public class RegexRequestURLBuilder implements Function<String, String> {
 
     /** {@inheritDoc} */
     @Override
-    @Nullable public String apply(@Nonnull final String entityID) {
-        Constraint.isNotNull(entityID, "Entity ID was null");
+    @Nullable public String apply(@Nonnull final CriteriaSet criteria) {
+        Constraint.isNotNull(criteria, "Criteria was null");
+        if (!criteria.contains(EntityIdCriterion.class)) {
+            log.trace("Criteria did not contain entity ID, unable to build request URL");
+            return null;
+        }
+        final String entityID = criteria.get(EntityIdCriterion.class).getEntityId();
         
         try {
             final Matcher matcher = pattern.matcher(entityID);
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/SAMLArtifactURLBuilder.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/SAMLArtifactURLBuilder.java
new file mode 100644
index 0000000..b11de63
--- /dev/null
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/SAMLArtifactURLBuilder.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 org.opensaml.saml.metadata.resolver.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.saml.common.binding.artifact.SAMLSourceIDArtifact;
+import org.opensaml.saml.criterion.ArtifactCriterion;
+import org.opensaml.saml.metadata.resolver.impl.MetadataQueryProtocolRequestURLBuilder.MetadataQueryProtocolURLBuilder;
+
+import com.google.common.io.BaseEncoding;
+
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * Implementation of {@link MetadataQueryProtocolURLBuilder} which understands {@link ArtifactCriterion}.
+ */
+public class SAMLArtifactURLBuilder implements MetadataQueryProtocolURLBuilder {
+    
+    /** Hex encoder. */
+    private static final BaseEncoding HEX = BaseEncoding.base16().lowerCase();
+
+    /** {@inheritDoc} */
+    public String buildURL(@Nonnull final String baseURL, @Nonnull final CriteriaSet criteria) {
+        if (!criteria.contains(ArtifactCriterion.class)) {
+            return null;
+        }
+        final ArtifactCriterion artifactCriterion = criteria.get(ArtifactCriterion.class);
+        
+        if (artifactCriterion.getArtifact() instanceof SAMLSourceIDArtifact) {
+            return buildFromSourceID(baseURL, (SAMLSourceIDArtifact)artifactCriterion.getArtifact());
+        }
+        
+        return null;
+    }
+
+    /**
+     * Builder URL from SAML artifact source ID.
+     * 
+     * @param baseURL  the base URL
+     * @param sourceIDArtifact the source ID artifact 
+     * @return the request URL
+     * 
+     */
+    private String buildFromSourceID(@Nonnull final String baseURL, 
+            @Nonnull final SAMLSourceIDArtifact sourceIDArtifact) {
+        
+        // We just statically escape "{sha1}" here
+        return baseURL + "entities/" + "%7Bsha1%7D" + HEX.encode(sourceIDArtifact.getSourceID());
+    }
+
+}
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/TemplateRequestURLBuilder.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/TemplateRequestURLBuilder.java
index f4514ae..741a974 100644
--- a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/TemplateRequestURLBuilder.java
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/impl/TemplateRequestURLBuilder.java
@@ -25,6 +25,7 @@ import javax.annotation.Nullable;
 
 import org.apache.velocity.VelocityContext;
 import org.apache.velocity.app.VelocityEngine;
+import org.opensaml.core.criterion.EntityIdCriterion;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -35,11 +36,12 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
 import net.shibboleth.utilities.java.support.velocity.Template;
 
 /**
- * Function which produces a URL by substituting an entity ID value into a Velocity template string.
+ * Function which produces a URL by substituting an entity ID value from criteria into a Velocity template string.
  * 
  * <p>
  * The entity ID will be replaced in the template string according to the template variable <code>entityID</code>, 
@@ -54,7 +56,7 @@ import net.shibboleth.utilities.java.support.velocity.Template;
  * </p>
  * 
  */
-public class TemplateRequestURLBuilder implements Function<String, String> {
+public class TemplateRequestURLBuilder implements Function<CriteriaSet, String> {
     
     /** EntityID Encoding style. */
     public enum EncodingStyle {
@@ -213,8 +215,13 @@ public class TemplateRequestURLBuilder implements Function<String, String> {
 
     /** {@inheritDoc} */
     @Override
-    @Nullable public String apply(@Nonnull final String input) {
-        String entityID = Constraint.isNotNull(input, "Entity ID was null");
+    @Nullable public String apply(@Nonnull final CriteriaSet criteria) {
+        Constraint.isNotNull(criteria, "Criteria was null");
+        if (!criteria.contains(EntityIdCriterion.class)) {
+            log.trace("Criteria did not contain entity ID, unable to build request URL");
+            return null;
+        }
+        String entityID = criteria.get(EntityIdCriterion.class).getEntityId();
         
         log.debug("Saw input entityID '{}'", entityID);
         
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/index/impl/LockableMetadataIndexManager.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/index/impl/LockableMetadataIndexManager.java
new file mode 100644
index 0000000..891ee6e
--- /dev/null
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/metadata/resolver/index/impl/LockableMetadataIndexManager.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 org.opensaml.saml.metadata.resolver.index.impl;
+
+import java.util.Set;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.metadata.resolver.index.MetadataIndex;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+
+import com.google.common.base.Function;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+
+/**
+ * A specialization of {@link MetadataIndexManager} capable of read/write locking.
+ * 
+ * @param <T> the type of data being indexed
+ */
+public class LockableMetadataIndexManager<T> extends MetadataIndexManager<T> {
+    
+    /** The manager's read write lock. */
+    @Nonnull private final ReadWriteLock readWriteLock;
+
+    /**
+     * Constructor.
+     *
+     * @param initIndexes indexes for which to initialize storage
+     * @param extractionFunction function to extract the indexed data item from an EntityDescriptor
+     */
+    public LockableMetadataIndexManager(
+            @Nullable @NonnullElements @Unmodifiable @NotLive final Set<MetadataIndex> initIndexes,
+            @Nonnull final Function<EntityDescriptor, T> extractionFunction
+            ) {
+        super(initIndexes, extractionFunction);
+        readWriteLock = new ReentrantReadWriteLock(true);
+    }
+    
+    /**
+     * Get the manager's instance of the {@link ReadWriteLock}.
+     * 
+     * <p>
+     * Callers of the manager are responsible for explicitly locking (and unlocking)
+     * for reading and/or writing, based on application use cases.
+     * </p>
+     * 
+     * @return Returns the rwlock.
+     */
+    @Nonnull public ReadWriteLock getReadWriteLock() {
+        return readWriteLock;
+    }
+
+
+}
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicHTTPMetadataResolverTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicHTTPMetadataResolverTest.java
index 7fa79a5..62c0c45 100644
--- a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicHTTPMetadataResolverTest.java
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicHTTPMetadataResolverTest.java
@@ -85,9 +85,9 @@ public class AbstractDynamicHTTPMetadataResolverTest extends XMLObjectBaseTestCa
         // Test uses MDQ protocol
         String baseURL = "http://shibboleth.net:9000";
         String entityID = "https://foo1.example.org/idp/shibboleth";
-        String requestURL = new MetadataQueryProtocolRequestURLBuilder(baseURL).apply(entityID);
+        String requestURL = new MetadataQueryProtocolRequestURLBuilder(baseURL).apply(new CriteriaSet(new EntityIdCriterion(entityID)));
         
-        CriteriaSet criteriaSet = new CriteriaSet(new EntityIdCriterion(entityID), new RequestURLCriterion(requestURL));
+        CriteriaSet criteriaSet = new CriteriaSet(new RequestURLCriterion(requestURL));
         
         EntityDescriptor ed = resolver.resolveSingle(criteriaSet);
         Assert.assertNotNull(ed);
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicMetadataResolverTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicMetadataResolverTest.java
index a290657..f310ad8 100644
--- a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicMetadataResolverTest.java
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/AbstractDynamicMetadataResolverTest.java
@@ -23,7 +23,9 @@ import java.security.KeyPair;
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.Timer;
 
 import org.opensaml.core.criterion.EntityIdCriterion;
@@ -34,9 +36,19 @@ import org.opensaml.core.xml.io.MarshallingException;
 import org.opensaml.core.xml.persist.MapLoadSaveManager;
 import org.opensaml.core.xml.persist.XMLObjectLoadSaveManager;
 import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.saml.criterion.EntityRoleCriterion;
 import org.opensaml.saml.metadata.resolver.filter.impl.SignatureValidationFilter;
 import org.opensaml.saml.metadata.resolver.impl.AbstractDynamicMetadataResolver.DynamicEntityBackingStore;
+import org.opensaml.saml.metadata.resolver.index.MetadataIndex;
+import org.opensaml.saml.metadata.resolver.index.impl.FunctionDrivenMetadataIndex;
+import org.opensaml.saml.metadata.resolver.index.impl.RoleMetadataIndex;
+import org.opensaml.saml.metadata.resolver.index.impl.SimpleStringCriteriaFunction;
+import org.opensaml.saml.metadata.resolver.index.impl.SimpleStringCriterion;
+import org.opensaml.saml.metadata.resolver.index.impl.UppercaseEntityIdDescriptorFunction;
 import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
+import org.opensaml.saml.saml2.metadata.RoleDescriptor;
+import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
 import org.opensaml.security.SecurityException;
 import org.opensaml.security.credential.Credential;
 import org.opensaml.security.credential.CredentialSupport;
@@ -58,8 +70,11 @@ import org.testng.annotations.Test;
 
 import com.google.common.base.Function;
 import com.google.common.base.Objects;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
@@ -109,6 +124,7 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         id1 = "urn:test:entity:1";
         ed1 = buildXMLObject(EntityDescriptor.DEFAULT_ELEMENT_NAME);
         ed1.setEntityID(id1);
+        ed1.getRoleDescriptors().add((RoleDescriptor) buildXMLObject(SPSSODescriptor.DEFAULT_ELEMENT_NAME));
         SignatureSupport.signObject(ed1, signingParams);
         Assert.assertTrue(ed1.isSigned());
         baos = new ByteArrayOutputStream();
@@ -120,6 +136,7 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         id2 = "urn:test:entity:2";
         ed2 = buildXMLObject(EntityDescriptor.DEFAULT_ELEMENT_NAME);
         ed2.setEntityID(id2);
+        ed2.getRoleDescriptors().add((RoleDescriptor) buildXMLObject(SPSSODescriptor.DEFAULT_ELEMENT_NAME));
         SignatureSupport.signObject(ed2, signingParams);
         Assert.assertTrue(ed2.isSigned());
         baos = new ByteArrayOutputStream();
@@ -131,6 +148,7 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         id3 = "urn:test:entity:3";
         ed3 = buildXMLObject(EntityDescriptor.DEFAULT_ELEMENT_NAME);
         ed3.setEntityID(id3);
+        ed3.getRoleDescriptors().add((RoleDescriptor) buildXMLObject(IDPSSODescriptor.DEFAULT_ELEMENT_NAME));
         SignatureSupport.signObject(ed3, signingParams);
         Assert.assertTrue(ed3.isSigned());
         baos = new ByteArrayOutputStream();
@@ -178,9 +196,9 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         
         resolver.initialize();
         
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))));
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))), ed2);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))), ed3);
         
         DynamicEntityBackingStore backingStore = resolver.getBackingStore();
         
@@ -202,9 +220,9 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         
         resolver.initialize();
         
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))));
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))), ed2);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))), ed3);
         
         DynamicEntityBackingStore backingStore = resolver.getBackingStore();
         
@@ -214,9 +232,9 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         Assert.assertFalse(backingStore.getIndexedDescriptors().containsKey(id2));
         Assert.assertFalse(backingStore.getIndexedDescriptors().containsKey(id3));
         
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))));
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))), ed2);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))), ed3);
     }
     
     @Test
@@ -227,9 +245,9 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         
         resolver.initialize();
         
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))));
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))), ed2);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))), ed3);
         
         DynamicEntityBackingStore backingStore = resolver.getBackingStore();
         
@@ -240,9 +258,9 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         Assert.assertFalse(backingStore.getIndexedDescriptors().containsKey(id2));
         Assert.assertTrue(backingStore.getIndexedDescriptors().containsKey(id3));
         
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))));
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))), ed2);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))), ed3);
     }
     
     @Test
@@ -269,7 +287,7 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         
         Assert.assertEquals(persistentCacheMap.size(), 0);
         
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
         
         Assert.assertEquals(persistentCacheMap.size(), 1);
         
@@ -277,8 +295,44 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         Assert.assertTrue(persistentCacheMap.containsKey(cacheKey));
         Assert.assertSame(persistentCacheMap.get(cacheKey), ed1);
         
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))));
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))), ed2);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))), ed2);
+        
+        Assert.assertEquals(persistentCacheMap.size(), 3);
+        
+        cacheKey = resolver.getPersistentCacheKeyGenerator().apply(ed2);
+        Assert.assertTrue(persistentCacheMap.containsKey(cacheKey));
+        Assert.assertSame(persistentCacheMap.get(cacheKey), ed2);
+        
+        cacheKey = resolver.getPersistentCacheKeyGenerator().apply(ed3);
+        Assert.assertTrue(persistentCacheMap.containsKey(cacheKey));
+        Assert.assertSame(persistentCacheMap.get(cacheKey), ed3);
+        
+    }
+    
+    @Test
+    public void testWithPersistentCache() throws ComponentInitializationException, ResolverException {
+        sourceMap.put(id1, ed1);
+        sourceMap.put(id2, ed2);
+        sourceMap.put(id3, ed3);
+        
+        resolver.setPersistentCacheManager(persistentCacheManager);
+        resolver.initialize();
+        
+        Assert.assertTrue(resolver.isPersistentCachingEnabled());
+        
+        Assert.assertEquals(persistentCacheMap.size(), 0);
+        
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
+        
+        Assert.assertEquals(persistentCacheMap.size(), 1);
+        
+        String cacheKey = resolver.getPersistentCacheKeyGenerator().apply(ed1);
+        Assert.assertTrue(persistentCacheMap.containsKey(cacheKey));
+        Assert.assertSame(persistentCacheMap.get(cacheKey), ed1);
+        
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))), ed2);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))), ed3);
         
         Assert.assertEquals(persistentCacheMap.size(), 3);
         
@@ -307,6 +361,7 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         Assert.assertEquals(persistentCacheMap.size(), 0);
         
         Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
+        Assert.assertEquals(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))).getEntityID(), id1);
         
         Assert.assertEquals(persistentCacheMap.size(), 1);
         
@@ -315,7 +370,9 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         Assert.assertSame(persistentCacheMap.get(cacheKey), ed1);
         
         Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))));
+        Assert.assertEquals(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))).getEntityID(), id2);
         Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))));
+        Assert.assertEquals(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))).getEntityID(), id3);
         
         Assert.assertEquals(persistentCacheMap.size(), 3);
         
@@ -359,6 +416,7 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         for (String entityID : Lists.newArrayList(id1, id2, id3)) {
             EntityDescriptor ed = resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(entityID)));
             Assert.assertNotNull(ed);
+            Assert.assertEquals(ed.getEntityID(), entityID);
             Assert.assertNull(ed.getDOM());
         }
     }
@@ -399,7 +457,7 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         
         
         Assert.assertTrue(sourceMap.isEmpty());
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
         Assert.assertNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))));
         Assert.assertNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))));
     }
@@ -440,9 +498,106 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
         Assert.assertEquals(backingStore.getIndexedDescriptors().get(id3).size(), 1);
         
         Assert.assertTrue(sourceMap.isEmpty());
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))));
-        Assert.assertNotNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))));
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))), ed2);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))), ed3);
+    }
+    
+    @Test
+    public void testSecondaryIndexAfterEntityIDResolution() throws ComponentInitializationException, ResolverException {
+        sourceMap.put(id1, ed1);
+        
+        HashSet<MetadataIndex> indexes = new HashSet<>();
+        indexes.add(new FunctionDrivenMetadataIndex(new UppercaseEntityIdDescriptorFunction(), new SimpleStringCriteriaFunction()));
+        resolver.setIndexes(indexes);
+        
+        resolver.initialize();
+        
+        DynamicEntityBackingStore backingStore = resolver.getBackingStore();
+        Optional<Set<String>> indexedData = null;
+        
+        Assert.assertNull(resolver.resolveSingle(new CriteriaSet(new SimpleStringCriterion(id1.toUpperCase()))));
+        
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
+        
+        indexedData = backingStore.getSecondaryIndexManager().lookupIndexedItems(new CriteriaSet(new SimpleStringCriterion(id1.toUpperCase())));
+        Assert.assertTrue(indexedData.isPresent());
+        Assert.assertEquals(indexedData.get(), Sets.newHashSet(id1));
+        
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new SimpleStringCriterion(id1.toUpperCase()))), ed1);
+    }
+        
+    @Test
+    public void testSecondaryIndexAfterEntityIDResolutionMultipleResults() throws ComponentInitializationException, ResolverException {
+        sourceMap.put(id1, ed1);
+        sourceMap.put(id2, ed2);
+        sourceMap.put(id3, ed3);
+        
+        HashSet<MetadataIndex> indexes = new HashSet<>();
+        indexes.add(new RoleMetadataIndex());
+        resolver.setIndexes(indexes);
+        
+        resolver.initialize();
+        
+        DynamicEntityBackingStore backingStore = resolver.getBackingStore();
+        Optional<Set<String>> indexedData = null;
+        
+        Set<EntityDescriptor> results = new HashSet<>();
+        
+        results.clear();
+        Iterables.addAll(results, resolver.resolve(new CriteriaSet(new EntityRoleCriterion(SPSSODescriptor.DEFAULT_ELEMENT_NAME))));
+        Assert.assertEquals(results.size(), 0);
+        
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
+        
+        indexedData = backingStore.getSecondaryIndexManager().lookupIndexedItems(new CriteriaSet(new EntityRoleCriterion(SPSSODescriptor.DEFAULT_ELEMENT_NAME)));
+        Assert.assertTrue(indexedData.isPresent());
+        Assert.assertEquals(indexedData.get(), Sets.newHashSet(id1));
+        
+        results.clear();
+        Iterables.addAll(results, resolver.resolve(new CriteriaSet(new EntityRoleCriterion(SPSSODescriptor.DEFAULT_ELEMENT_NAME))));
+        Assert.assertEquals(results.size(), 1);
+        Assert.assertEquals(results, Sets.newHashSet(ed1));
+        
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id2))), ed2);
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id3))), ed3);
+        
+        indexedData = backingStore.getSecondaryIndexManager().lookupIndexedItems(new CriteriaSet(new EntityRoleCriterion(SPSSODescriptor.DEFAULT_ELEMENT_NAME)));
+        Assert.assertTrue(indexedData.isPresent());
+        Assert.assertEquals(indexedData.get(), Sets.newHashSet(id1, id2));
+        
+        results.clear();
+        Iterables.addAll(results, resolver.resolve(new CriteriaSet(new EntityRoleCriterion(SPSSODescriptor.DEFAULT_ELEMENT_NAME))));
+        Assert.assertEquals(results.size(), 2);
+        Assert.assertEquals(results, Sets.newHashSet(ed1, ed2));
+    }
+    
+    @Test
+    public void testSecondaryLookupThenEntityID() throws ComponentInitializationException, ResolverException {
+        sourceMap.put(id1.toUpperCase(), ed1);
+        
+        resolver.setSecondaryLookup(true);
+        
+        HashSet<MetadataIndex> indexes = new HashSet<>();
+        indexes.add(new FunctionDrivenMetadataIndex(new UppercaseEntityIdDescriptorFunction(), new SimpleStringCriteriaFunction()));
+        resolver.setIndexes(indexes);
+        
+        resolver.initialize();
+        
+        DynamicEntityBackingStore backingStore = resolver.getBackingStore();
+        Optional<Set<String>> indexedData = null;
+        
+        Assert.assertNull(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))));
+        
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new SimpleStringCriterion(id1.toUpperCase()))), ed1);
+        
+        backingStore.getIndexedDescriptors().containsKey(id1);
+        
+        indexedData = backingStore.getSecondaryIndexManager().lookupIndexedItems(new CriteriaSet(new SimpleStringCriterion(id1.toUpperCase())));
+        Assert.assertTrue(indexedData.isPresent());
+        Assert.assertEquals(indexedData.get(), Sets.newHashSet(id1));
+        
+        Assert.assertSame(resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(id1))), ed1);
     }
     
     // Helper classes
@@ -450,25 +605,32 @@ public class AbstractDynamicMetadataResolverTest extends XMLObjectBaseTestCase {
     private static class MockDynamicResolver extends AbstractDynamicMetadataResolver {
         
         private Map<String,EntityDescriptor> originSourceMap;
+        
+        private boolean secondaryLookup;
 
         public MockDynamicResolver(Map<String, EntityDescriptor> map) {
             this(map, null);
         }
         
+        public void setSecondaryLookup(boolean flag) {
+            secondaryLookup = flag;
+        }
+        
         public MockDynamicResolver(Map<String, EntityDescriptor> map, Timer backgroundTaskTimer) {
             super(backgroundTaskTimer);
             originSourceMap = map;
         }
 
         protected XMLObject fetchFromOriginSource(CriteriaSet criteria) throws IOException {
-            EntityIdCriterion entityIdCriterion = criteria.get(EntityIdCriterion.class);
-            if (entityIdCriterion != null) {
-                return originSourceMap.get(entityIdCriterion.getEntityId());
+            if (criteria.contains(EntityIdCriterion.class)) {
+                return originSourceMap.get(criteria.get(EntityIdCriterion.class).getEntityId());
+            } else if (secondaryLookup && criteria.contains(SimpleStringCriterion.class)) {
+                return originSourceMap.get(criteria.get(SimpleStringCriterion.class).getValue());
             } else {
                 return null;
             }
         }
-        
+
     }
     
 }
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/DefaultLocalDynamicSourceKeyGeneratorTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/DefaultLocalDynamicSourceKeyGeneratorTest.java
new file mode 100644
index 0000000..6b98c42
--- /dev/null
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/DefaultLocalDynamicSourceKeyGeneratorTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 org.opensaml.saml.metadata.resolver.impl;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.saml.common.binding.artifact.SAMLSourceIDArtifact;
+import org.opensaml.saml.common.binding.artifact.SAMLSourceLocationArtifact;
+import org.opensaml.saml.criterion.ArtifactCriterion;
+import org.opensaml.saml.metadata.resolver.index.impl.SimpleStringCriterion;
+import org.opensaml.saml.saml1.binding.artifact.SAML1ArtifactType0002;
+import org.opensaml.saml.saml2.binding.artifact.SAML2ArtifactType0004;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.io.BaseEncoding;
+
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ *
+ */
+public class DefaultLocalDynamicSourceKeyGeneratorTest {
+    
+    private String controlValue;
+    
+    private String controlValueSHA1Hex;
+    private byte[] controlValueSHA1Bytes;
+    
+    private SAMLSourceIDArtifact sourceIDArtifact;
+    private SAMLSourceLocationArtifact sourceLocationArtifact;
+    
+    private CriteriaSet criteria;
+    
+    private DefaultLocalDynamicSourceKeyGenerator generator;
+    
+    @BeforeMethod
+    public void setUp() throws NoSuchAlgorithmException {
+        controlValue = "urn:test:foobar";
+        controlValueSHA1Hex = "d278c9975472a6b4827b1a8723192b4e99aa969c";
+        controlValueSHA1Bytes = BaseEncoding.base16().lowerCase().decode(controlValueSHA1Hex);
+        criteria = new CriteriaSet();
+        
+        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
+        byte[] messageHandle = new byte[20];
+        secureRandom.nextBytes(messageHandle);
+        sourceIDArtifact = new SAML2ArtifactType0004(new byte[] {0, 0} , controlValueSHA1Bytes, messageHandle);
+        sourceLocationArtifact = new SAML1ArtifactType0002(messageHandle, "https://test.foobar.com/artifactk");
+    }
+    
+    @Test
+    public void testEntityIdCriterion() throws NoSuchAlgorithmException {
+        generator = new DefaultLocalDynamicSourceKeyGenerator();
+        
+        Assert.assertNull(generator.apply(null));
+        
+        criteria.clear();
+        Assert.assertNull(generator.apply(criteria));
+        
+        criteria.add(new EntityIdCriterion(controlValue));
+        
+        Assert.assertEquals(controlValueSHA1Hex, generator.apply(criteria));
+        
+        generator = new DefaultLocalDynamicSourceKeyGenerator("metadata-", ".xml", null);
+        
+        Assert.assertEquals("metadata-" + controlValueSHA1Hex + ".xml", generator.apply(criteria));
+        
+        generator = new DefaultLocalDynamicSourceKeyGenerator("metadata", "xml", ".");
+        
+        Assert.assertEquals("metadata." + controlValueSHA1Hex + ".xml", generator.apply(criteria));
+    }
+    
+    @Test
+    public void testArtifactCriterion() throws NoSuchAlgorithmException {
+        generator = new DefaultLocalDynamicSourceKeyGenerator();
+        
+        Assert.assertNull(generator.apply(null));
+        
+        criteria.clear();
+        Assert.assertNull(generator.apply(criteria));
+        
+        criteria.add(new ArtifactCriterion(sourceIDArtifact));
+        
+        Assert.assertEquals(controlValueSHA1Hex, generator.apply(criteria));
+        
+        generator = new DefaultLocalDynamicSourceKeyGenerator("metadata-", ".xml", null);
+        
+        Assert.assertEquals("metadata-" + controlValueSHA1Hex + ".xml", generator.apply(criteria));
+        
+        generator = new DefaultLocalDynamicSourceKeyGenerator("metadata", "xml", ".");
+        
+        Assert.assertEquals("metadata." + controlValueSHA1Hex + ".xml", generator.apply(criteria));
+        
+        // Source location artifact is not supported
+        criteria.clear();
+        criteria.add(new ArtifactCriterion(sourceLocationArtifact));
+        Assert.assertNull(generator.apply(criteria));
+    }
+   
+    @Test
+    public void testUnsupportedCriterion() {
+        generator = new DefaultLocalDynamicSourceKeyGenerator();
+        
+        criteria.add(new SimpleStringCriterion("foobar"));
+        Assert.assertNull(generator.apply(criteria));
+    }
+
+}
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/FunctionDrivenDynamicHTTPMetadataResolverTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/FunctionDrivenDynamicHTTPMetadataResolverTest.java
index db3accb..5660753 100644
--- a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/FunctionDrivenDynamicHTTPMetadataResolverTest.java
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/FunctionDrivenDynamicHTTPMetadataResolverTest.java
@@ -19,6 +19,8 @@ package org.opensaml.saml.metadata.resolver.impl;
 
 import java.io.File;
 import java.net.URISyntaxException;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.Arrays;
@@ -37,8 +39,14 @@ import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 import org.opensaml.core.criterion.EntityIdCriterion;
 import org.opensaml.core.xml.XMLObjectBaseTestCase;
+import org.opensaml.saml.common.binding.artifact.SAMLSourceIDArtifact;
+import org.opensaml.saml.criterion.ArtifactCriterion;
+import org.opensaml.saml.metadata.resolver.impl.MetadataQueryProtocolRequestURLBuilder.MetadataQueryProtocolURLBuilder;
+import org.opensaml.saml.metadata.resolver.impl.TemplateRequestURLBuilder.EncodingStyle;
+import org.opensaml.saml.saml2.binding.artifact.SAML2ArtifactType0004;
 import org.opensaml.saml.saml2.metadata.EntityDescriptor;
 import org.opensaml.security.credential.impl.StaticCredentialResolver;
+import org.opensaml.security.crypto.JCAConstants;
 import org.opensaml.security.httpclient.impl.SecurityEnhancedTLSSocketFactory;
 import org.opensaml.security.trust.TrustEngine;
 import org.opensaml.security.trust.impl.ExplicitKeyTrustEngine;
@@ -56,6 +64,8 @@ import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.Lists;
+
 public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBaseTestCase {
     
     private static final String DATA_PATH = "/org/opensaml/saml/metadata/resolver/impl/";
@@ -86,7 +96,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         resolver = new FunctionDrivenDynamicHTTPMetadataResolver(httpClientBuilder.buildClient());
@@ -114,7 +124,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         resolver = new FunctionDrivenDynamicHTTPMetadataResolver(httpClientBuilder.buildClient());
@@ -143,7 +153,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         resolver = new FunctionDrivenDynamicHTTPMetadataResolver(httpClientBuilder.buildClient());
@@ -167,7 +177,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true);
+                EncodingStyle.form);
         
         resolver = new FunctionDrivenDynamicHTTPMetadataResolver(httpClientBuilder.buildClient());
         resolver.setId("myDynamicResolver");
@@ -190,7 +200,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true);
+                EncodingStyle.form);
         
         resolver = new FunctionDrivenDynamicHTTPMetadataResolver(httpClientBuilder.buildClient());
         resolver.setId("myDynamicResolver");
@@ -247,6 +257,35 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
     }
     
     @Test
+    public void testMDQViaArtifact() throws Exception {
+        String baseURL = "http://shibboleth.net:9000";
+        String entityID = "https://foo1.example.org/idp/shibboleth";
+        
+        MetadataQueryProtocolRequestURLBuilder requestURLBuilder = new MetadataQueryProtocolRequestURLBuilder(baseURL,
+                Lists.<MetadataQueryProtocolURLBuilder>newArrayList(new SAMLArtifactURLBuilder()));
+        
+        resolver = new FunctionDrivenDynamicHTTPMetadataResolver(httpClientBuilder.buildClient());
+        resolver.setId("myDynamicResolver");
+        resolver.setParserPool(parserPool);
+        resolver.setRequestURLBuilder(requestURLBuilder);
+        resolver.initialize();
+        
+        MessageDigest sha1Digester = MessageDigest.getInstance(JCAConstants.DIGEST_SHA1);
+        byte[] entityIDSourceID = sha1Digester.digest(entityID.getBytes("UTF-8"));
+        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
+        byte[] messageHandle = new byte[20];
+        secureRandom.nextBytes(messageHandle);
+        SAMLSourceIDArtifact sourceIDArtifact = new SAML2ArtifactType0004(new byte[] {0, 0} , entityIDSourceID, messageHandle);
+        
+        final CriteriaSet criteriaSet = new CriteriaSet( new ArtifactCriterion(sourceIDArtifact));
+        
+        EntityDescriptor ed = resolver.resolveSingle(criteriaSet);
+        Assert.assertNotNull(ed);
+        Assert.assertEquals(ed.getEntityID(), entityID);
+        Assert.assertNull(ed.getDOM());
+    }
+    
+    @Test
     public void testTrustEngineSocketFactoryNoHTTPSNoTrustEngine() throws Exception {
         String template = RepositorySupport.buildHTTPSResourceURL("java-opensaml", "opensaml-saml-impl/src/test/resources/org/opensaml/saml/metadata/resolver/impl/${entityID}.xml");
         String entityID = "https://www.example.org/sp";
@@ -255,7 +294,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         httpClientBuilder.setTLSSocketFactory(buildTrustEngineSocketFactory(false));
@@ -283,7 +322,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         httpClientBuilder.setTLSSocketFactory(buildTrustEngineSocketFactory());
@@ -312,7 +351,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         httpClientBuilder.setTLSSocketFactory(buildTrustEngineSocketFactory(false));
@@ -340,7 +379,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         httpClientBuilder.setTLSSocketFactory(buildTrustEngineSocketFactory());
@@ -369,7 +408,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         httpClientBuilder.setTLSSocketFactory(buildTrustEngineSocketFactory());
@@ -396,7 +435,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         httpClientBuilder.setTLSSocketFactory(buildTrustEngineSocketFactory());
@@ -425,7 +464,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         httpClientBuilder.setTLSSocketFactory(buildTrustEngineSocketFactory());
@@ -454,7 +493,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         httpClientBuilder.setTLSSocketFactory(buildTrustEngineSocketFactory());
@@ -481,7 +520,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         httpClientBuilder.setTLSSocketFactory(buildTrustEngineSocketFactory());
@@ -508,7 +547,7 @@ public class FunctionDrivenDynamicHTTPMetadataResolverTest extends XMLObjectBase
         TemplateRequestURLBuilder requestURLBuilder = new TemplateRequestURLBuilder(
                 VelocityEngine.newVelocityEngine(), 
                 template, 
-                true, 
+                EncodingStyle.path, 
                 new StringDigester("SHA-1", OutputFormat.HEX_LOWER));
         
         // Trust engine set, but appropriate socket factory not set
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/HTTPEntityIDRequestURLBuilderTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/HTTPEntityIDRequestURLBuilderTest.java
index 3f9eee4..73e2855 100644
--- a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/HTTPEntityIDRequestURLBuilderTest.java
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/HTTPEntityIDRequestURLBuilderTest.java
@@ -18,7 +18,9 @@
 package org.opensaml.saml.metadata.resolver.impl;
 
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
+import org.opensaml.core.criterion.EntityIdCriterion;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -28,20 +30,20 @@ public class HTTPEntityIDRequestURLBuilderTest {
     
     @Test
     public void testHTTP() {
-        Assert.assertEquals(function.apply("http://www.example.com/sp"), "http://www.example.com/sp");
-        Assert.assertEquals(function.apply("https://www.example.com/sp"), "https://www.example.com/sp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://www.example.com/sp"))), "http://www.example.com/sp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("https://www.example.com/sp"))), "https://www.example.com/sp");
         
-        Assert.assertEquals(function.apply("HTTP://www.example.com/sp"), "HTTP://www.example.com/sp");
-        Assert.assertEquals(function.apply("HTTPS://www.example.com/sp"), "HTTPS://www.example.com/sp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("HTTP://www.example.com/sp"))), "HTTP://www.example.com/sp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("HTTPS://www.example.com/sp"))), "HTTPS://www.example.com/sp");
         
     }
     
     @Test
     public void testNonHTTP() {
-        Assert.assertNull(function.apply("urn:test:sp"));
-        Assert.assertNull(function.apply("foo"));
-        Assert.assertNull(function.apply("httpblah://not.a.url.com"));
-        Assert.assertNull(function.apply(""));
+        Assert.assertNull(function.apply(new CriteriaSet(new EntityIdCriterion("urn:test:sp"))));
+        Assert.assertNull(function.apply(new CriteriaSet(new EntityIdCriterion("foo"))));
+        Assert.assertNull(function.apply(new CriteriaSet(new EntityIdCriterion("httpblah://not.a.url.com"))));
+        Assert.assertNull(function.apply(new CriteriaSet()));
     }
     
     @Test(expectedExceptions=ConstraintViolationException.class)
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolRequestURLBuilderTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolRequestURLBuilderTest.java
index f4e0b2a..b099598 100644
--- a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolRequestURLBuilderTest.java
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolRequestURLBuilderTest.java
@@ -17,11 +17,17 @@
 
 package org.opensaml.saml.metadata.resolver.impl;
 
-import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
-
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.saml.metadata.resolver.impl.MetadataQueryProtocolRequestURLBuilder.MetadataQueryProtocolURLBuilder;
+import org.opensaml.saml.metadata.resolver.index.impl.SimpleStringCriterion;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.Lists;
+
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
 public class MetadataQueryProtocolRequestURLBuilderTest {
     
     private MetadataQueryProtocolRequestURLBuilder function;
@@ -30,29 +36,60 @@ public class MetadataQueryProtocolRequestURLBuilderTest {
     public void testWithoutTrailingSlash() {
         function = new MetadataQueryProtocolRequestURLBuilder("http://metadata.example.org/service");
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://metadata.example.org/service/entities/http:%2F%2Fexample.org%2Fidp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://metadata.example.org/service/entities/http:%2F%2Fexample.org%2Fidp");
     }
 
     @Test
     public void testWithTrailingSlash() {
         function = new MetadataQueryProtocolRequestURLBuilder("http://metadata.example.org/service/");
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://metadata.example.org/service/entities/http:%2F%2Fexample.org%2Fidp");
-    }
-    
-    @Test
-    public void testWithSHA1Transformer() {
-        function = new MetadataQueryProtocolRequestURLBuilder("http://metadata.example.org/service/", 
-                new MetadataQueryProtocolSHA1Transformer());
-        
-        Assert.assertEquals(function.apply("http://example.org/service"), "http://metadata.example.org/service/entities/%7Bsha1%7D11d72e8cf351eb6c75c721e838f469677ab41bdb");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://metadata.example.org/service/entities/http:%2F%2Fexample.org%2Fidp");
     }
     
     @Test(expectedExceptions=ConstraintViolationException.class)
-    public void testNullEntityID() {
+    public void testNullCriteria() {
         function = new MetadataQueryProtocolRequestURLBuilder("http://metadata.example.org/service/");
         function.apply(null);
     }
     
+    @Test
+    public void testSecondaryURLBuilders() {
+        MetadataQueryProtocolURLBuilder foo = new MockURLBuilder("tags/foo");
+        MetadataQueryProtocolURLBuilder bar = new MockURLBuilder("tags/bar");
+        MetadataQueryProtocolURLBuilder noValue = new MockURLBuilder(null);
+        
+        function = new MetadataQueryProtocolRequestURLBuilder("http://metadata.example.org/service", Lists.newArrayList(foo,bar));
+        Assert.assertEquals(function.apply(new CriteriaSet(new SimpleStringCriterion("will-be-ignored"))), "http://metadata.example.org/service/tags/foo");
+        
+        function = new MetadataQueryProtocolRequestURLBuilder("http://metadata.example.org/service", Lists.newArrayList(bar,foo));
+        Assert.assertEquals(function.apply(new CriteriaSet(new SimpleStringCriterion("will-be-ignored"))), "http://metadata.example.org/service/tags/bar");
+        
+        function = new MetadataQueryProtocolRequestURLBuilder("http://metadata.example.org/service", Lists.newArrayList(null, null, foo, bar));
+        Assert.assertEquals(function.apply(new CriteriaSet(new SimpleStringCriterion("will-be-ignored"))), "http://metadata.example.org/service/tags/foo");
+        
+        function = new MetadataQueryProtocolRequestURLBuilder("http://metadata.example.org/service", Lists.newArrayList(noValue, foo, bar));
+        Assert.assertEquals(function.apply(new CriteriaSet(new SimpleStringCriterion("will-be-ignored"))), "http://metadata.example.org/service/tags/foo");
+    }
+    
+    // Test helpers
+    
+    public static class MockURLBuilder implements MetadataQueryProtocolURLBuilder {
+        
+        private String suffix;
+        
+        public MockURLBuilder(String suffix) {
+            this.suffix = suffix;
+        }
+
+        /** {@inheritDoc} */
+        public String buildURL(String baseURL, CriteriaSet criteria) {
+            if (suffix == null) {
+                return null;
+            } else {
+                return baseURL + suffix;
+            }
+        }
+        
+    }
 
 }
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolSHA1TransformerTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolSHA1TransformerTest.java
deleted file mode 100644
index e4992f3..0000000
--- a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/MetadataQueryProtocolSHA1TransformerTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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 org.opensaml.saml.metadata.resolver.impl;
-
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
-public class MetadataQueryProtocolSHA1TransformerTest {
-    
-    @Test
-    public void testBasic() {
-       MetadataQueryProtocolSHA1Transformer transformer = new MetadataQueryProtocolSHA1Transformer(); 
-       Assert.assertEquals(transformer.apply("foobarbaz"), "{sha1}5f5513f8822fdbe5145af33b64d8d970dcf95c6e");
-    }
-
-}
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/RegexRequestURLBuilderTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/RegexRequestURLBuilderTest.java
index b26262c..f4e88d8 100644
--- a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/RegexRequestURLBuilderTest.java
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/RegexRequestURLBuilderTest.java
@@ -17,9 +17,12 @@
 
 package org.opensaml.saml.metadata.resolver.impl;
 
+import org.opensaml.core.criterion.EntityIdCriterion;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
 public class RegexRequestURLBuilderTest {
     
     private RegexRequestURLBuilder function;
@@ -29,14 +32,14 @@ public class RegexRequestURLBuilderTest {
         // Attempt to pluck out the domain name into match group $1.
         function = new RegexRequestURLBuilder("^https?://([a-zA-Z0-9\\.]+).*$", "http://metadata.example.org/query?domain=$1");
         
-        Assert.assertEquals(function.apply("http://example.org"), "http://metadata.example.org/query?domain=example.org");
-        Assert.assertEquals(function.apply("http://example.org/"), "http://metadata.example.org/query?domain=example.org");
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://metadata.example.org/query?domain=example.org");
-        Assert.assertEquals(function.apply("http://example.org:443/idp"), "http://metadata.example.org/query?domain=example.org");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org"))), "http://metadata.example.org/query?domain=example.org");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/"))), "http://metadata.example.org/query?domain=example.org");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://metadata.example.org/query?domain=example.org");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org:443/idp"))), "http://metadata.example.org/query?domain=example.org");
         
         // These shouldn't match, so should return null.
-        Assert.assertNull(function.apply("urn:test:foo"));
-        Assert.assertNull(function.apply("ftp://example.org"));
+        Assert.assertNull(function.apply(new CriteriaSet(new EntityIdCriterion("urn:test:foo"))));
+        Assert.assertNull(function.apply(new CriteriaSet(new EntityIdCriterion("ftp://example.org"))));
     }
 
 }
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/SAMLArtifactURLBuilderTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/SAMLArtifactURLBuilderTest.java
new file mode 100644
index 0000000..3519fa8
--- /dev/null
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/SAMLArtifactURLBuilderTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 org.opensaml.saml.metadata.resolver.impl;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import org.opensaml.saml.common.binding.artifact.SAMLSourceIDArtifact;
+import org.opensaml.saml.common.binding.artifact.SAMLSourceLocationArtifact;
+import org.opensaml.saml.criterion.ArtifactCriterion;
+import org.opensaml.saml.metadata.resolver.index.impl.SimpleStringCriterion;
+import org.opensaml.saml.saml1.binding.artifact.SAML1ArtifactType0002;
+import org.opensaml.saml.saml2.binding.artifact.SAML2ArtifactType0004;
+import org.opensaml.security.crypto.JCAConstants;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.io.BaseEncoding;
+
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ *
+ */
+public class SAMLArtifactURLBuilderTest {
+    
+    private SAMLArtifactURLBuilder builder = new SAMLArtifactURLBuilder();
+    
+    private String baseURL = "http://metadata.example.org/service/";
+    
+    private BaseEncoding HEX = BaseEncoding.base16().lowerCase();
+    
+    @Test
+    public void testSourceIDArtifact() throws NoSuchAlgorithmException, UnsupportedEncodingException {
+        String entityID = "https://www.example.com/saml";
+        MessageDigest sha1Digester = MessageDigest.getInstance(JCAConstants.DIGEST_SHA1);
+        byte[] entityIDSourceID = sha1Digester.digest(entityID.getBytes("UTF-8"));
+        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
+        byte[] messageHandle = new byte[20];
+        secureRandom.nextBytes(messageHandle);
+        
+        SAMLSourceIDArtifact sourceIDArtifact = new SAML2ArtifactType0004(new byte[] {0, 0} , entityIDSourceID, messageHandle);
+        
+        Assert.assertEquals(builder.buildURL(baseURL, new CriteriaSet(new ArtifactCriterion(sourceIDArtifact))),
+                "http://metadata.example.org/service/entities/%7Bsha1%7D" + HEX.encode(entityIDSourceID));
+        
+    }
+    
+    @Test
+    public void testSourceLocationArtifact() throws NoSuchAlgorithmException {
+        // Note: Not currently supported by MDQ protocol.  Testing for code branch correctness.
+        String sourceLocation = "https://www.example.com/saml/artifactResolve1";
+        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
+        byte[] messageHandle = new byte[20];
+        secureRandom.nextBytes(messageHandle);
+        
+        SAMLSourceLocationArtifact sourceLocationArtifact = new SAML1ArtifactType0002(messageHandle, sourceLocation);
+        
+        Assert.assertNull(builder.buildURL(baseURL, new CriteriaSet(new ArtifactCriterion(sourceLocationArtifact))));
+    }
+    
+    @Test
+    public void testNonArtifact() {
+        Assert.assertNull(builder.buildURL(baseURL, new CriteriaSet(new SimpleStringCriterion("blah"))));
+    }
+    
+
+}
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/TemplateRequestURLBuilderTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/TemplateRequestURLBuilderTest.java
index fc6e4aa..ec69730 100644
--- a/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/TemplateRequestURLBuilderTest.java
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/metadata/resolver/impl/TemplateRequestURLBuilderTest.java
@@ -20,8 +20,10 @@ package org.opensaml.saml.metadata.resolver.impl;
 import javax.annotation.Nullable;
 
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
 import org.apache.velocity.app.VelocityEngine;
+import org.opensaml.core.criterion.EntityIdCriterion;
 import org.opensaml.saml.metadata.resolver.impl.TemplateRequestURLBuilder.EncodingStyle;
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
@@ -46,7 +48,7 @@ public class TemplateRequestURLBuilderTest {
     public void testEncodedQueryParamLegacy() {
         function = new TemplateRequestURLBuilder(engine, "http://metadata.example.org/?entity=${entityID}", true);
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://metadata.example.org/?entity=http%3A%2F%2Fexample.org%2Fidp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://metadata.example.org/?entity=http%3A%2F%2Fexample.org%2Fidp");
     }
     
     @Test
@@ -54,7 +56,7 @@ public class TemplateRequestURLBuilderTest {
     public void testMDQStyleLegacy() {
         function = new TemplateRequestURLBuilder(engine, "http://metadata.example.org/entities/${entityID}", true);
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://metadata.example.org/entities/http%3A%2F%2Fexample.org%2Fidp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://metadata.example.org/entities/http%3A%2F%2Fexample.org%2Fidp");
     }
 
     @Test
@@ -62,35 +64,35 @@ public class TemplateRequestURLBuilderTest {
     public void testWellKnownLocationStyleLegacy() {
         function = new TemplateRequestURLBuilder(engine, "${entityID}", false);
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://example.org/idp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://example.org/idp");
     }
     
     @Test
     public void testEncodedQueryParam() {
         function = new TemplateRequestURLBuilder(engine, "http://metadata.example.org/?entity=${entityID}", EncodingStyle.form);
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://metadata.example.org/?entity=http%3A%2F%2Fexample.org%2Fidp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://metadata.example.org/?entity=http%3A%2F%2Fexample.org%2Fidp");
     }
     
     @Test
     public void testEncodedPath() {
         function = new TemplateRequestURLBuilder(engine, "http://metadata.example.org/entities/${entityID}", EncodingStyle.path);
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://metadata.example.org/entities/http:%2F%2Fexample.org%2Fidp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://metadata.example.org/entities/http:%2F%2Fexample.org%2Fidp");
     }
 
     @Test
     public void testEncodedFragment() {
         function = new TemplateRequestURLBuilder(engine, "http://metadata.example.org/entities#${entityID}", EncodingStyle.fragment);
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://metadata.example.org/entities#http://example.org/idp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://metadata.example.org/entities#http://example.org/idp");
     }
 
     @Test
     public void testWellKnownLocationStyle() {
         function = new TemplateRequestURLBuilder(engine, "${entityID}", EncodingStyle.none);
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "http://example.org/idp");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "http://example.org/idp");
     }
     
     @Test
@@ -103,7 +105,7 @@ public class TemplateRequestURLBuilderTest {
         
         function = new TemplateRequestURLBuilder(engine, "${entityID}", EncodingStyle.none, transformer);
         
-        Assert.assertEquals(function.apply("http://example.org/idp"), "HTTP://EXAMPLE.ORG/IDP");
+        Assert.assertEquals(function.apply(new CriteriaSet(new EntityIdCriterion("http://example.org/idp"))), "HTTP://EXAMPLE.ORG/IDP");
     }
     
     @Test(expectedExceptions=ConstraintViolationException.class)

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list