[java-metadata-aggregator] branch master updated: MDA-242 - Review for thread safety

Ian Young ian at iay.org.uk
Tue Jul 14 10:57:02 UTC 2020


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

iay pushed a commit to branch master
in repository java-metadata-aggregator.

View the commit online:
http://git.shibboleth.net/view/?p=java-metadata-aggregator.git;a=commit;h=288b13145b98fb958dbbdd4499137b9097b91944

The following commit(s) were added to refs/heads/master by this push:
       new  288b131   MDA-242 - Review for thread safety
288b131 is described below

commit 288b13145b98fb958dbbdd4499137b9097b91944
Author: Ian Young <ian at iay.org.uk>
AuthorDate: Tue Jul 14 11:56:56 2020 +0100

    MDA-242 - Review for thread safety
    
    https://issues.shibboleth.net/jira/browse/MDA-242
    
    First tranche: mainly root, dom, dom.ds, dom.impl, dom.saml.
---
 ...bstractCompositeItemIdentificationStrategy.java |   6 +-
 .../metadata/CollectionMergeStrategy.java          |  14 +-
 .../metadata/DeduplicatingItemIdMergeStrategy.java |  17 +-
 .../FirstItemIdItemIdentificationStrategy.java     |   5 +-
 .../metadata/ItemCollectionSerializer.java         |   6 +
 .../metadata/ItemIdentificationStrategy.java       |  14 +-
 .../net/shibboleth/metadata/ItemSerializer.java    |   6 +
 .../metadata/SimpleCollectionMergeStrategy.java    |   8 +-
 .../metadata/SimpleItemCollectionFactory.java      |   2 +
 .../metadata/SimpleItemCollectionSerializer.java   |   2 +
 .../metadata/dom/AbstractDOMTraversalStage.java    |   6 +-
 .../metadata/dom/AbstractDOMValidationStage.java   |  21 +-
 .../metadata/dom/AbstractElementVisitingStage.java |  28 +-
 .../dom/AbstractNamespacesStrippingStage.java      |   5 +-
 .../metadata/dom/AbstractXSLProcessingStage.java   |  88 +++-
 .../net/shibboleth/metadata/dom/AttrVisitor.java   |  10 +-
 .../shibboleth/metadata/dom/CRDetectionStage.java  |   4 +-
 .../net/shibboleth/metadata/dom/Container.java     |   2 +-
 .../metadata/dom/DOMElementSerializer.java         |   3 -
 .../metadata/dom/DOMFilesystemSourceStage.java     |  74 +--
 .../metadata/dom/DOMResourceSourceStage.java       |  56 ++-
 .../metadata/dom/DOMTraversalContext.java          |   8 +
 .../net/shibboleth/metadata/dom/ElementMaker.java  |   8 +-
 .../shibboleth/metadata/dom/ElementMatcher.java    |   8 +-
 .../metadata/dom/ElementStrippingStage.java        |  22 +-
 .../shibboleth/metadata/dom/ElementVisitor.java    |   7 +
 .../dom/ElementWhitespaceTrimmingStage.java        |   2 +
 .../metadata/dom/ElementsStrippingStage.java       |  71 ++-
 .../metadata/dom/EmptyContainerStrippingStage.java |  22 +-
 .../metadata/dom/NamespaceStrippingStage.java      |   8 +-
 .../metadata/dom/NamespacesStrippingStage.java     |  19 +-
 .../net/shibboleth/metadata/dom/NodeVisitor.java   |  12 +-
 .../metadata/dom/SimpleDOMTraversalContext.java    |   2 +-
 .../metadata/dom/WhitespaceTrimmingVisitor.java    |   2 +
 .../metadata/dom/XMLSchemaValidationStage.java     |  33 +-
 .../metadata/dom/XMLSignatureSigningStage.java     | 507 +++----------------
 .../metadata/dom/XMLSignatureValidationStage.java  | 110 +++--
 .../metadata/dom/XPathFilteringStage.java          |  12 +-
 .../metadata/dom/XPathItemSelectionStrategy.java   |   6 +-
 .../metadata/dom/XSLValidationStage.java           |   6 +-
 .../metadata/dom/ds/X509ValidationStage.java       |  15 +-
 .../metadata/dom/impl/XMLSignatureSigner.java      | 545 +++++++++++++++++++++
 .../dom/{ => impl}/XMLSignatureValidator.java      |  11 +-
 .../impl/package-info.java}                        |  21 +-
 .../dom/saml/AbstractSAMLTraversalStage.java       |   9 +-
 .../metadata/dom/saml/AttributeElementMaker.java   |   4 +-
 .../metadata/dom/saml/AttributeElementMatcher.java |   2 +
 .../dom/saml/AttributeValueElementMaker.java       |   4 +-
 .../dom/saml/AttributeValueElementMatcher.java     |   2 +
 .../dom/saml/ContactPersonFilterStage.java         |  27 +-
 .../dom/saml/DiscoFeedCollectionSerializer.java    |  51 +-
 .../dom/saml/EntitiesDescriptorAssemblerStage.java |  40 +-
 .../saml/EntitiesDescriptorDisassemblerStage.java  |   4 +-
 .../metadata/dom/saml/EntityFilterStage.java       |  37 +-
 .../metadata/dom/saml/EntityRoleFilterStage.java   |  69 ++-
 .../metadata/dom/saml/GenerateIdStage.java         |   6 +-
 .../dom/saml/PullUpCacheDurationStage.java         |  18 +-
 .../metadata/dom/saml/PullUpValidUntilStage.java   |  14 +-
 .../metadata/dom/saml/SAMLMetadataSupport.java     |   2 +-
 .../dom/saml/SAMLStringElementCheckingStage.java   |   2 +
 .../shibboleth/metadata/dom/saml/SAMLSupport.java  |   3 +-
 .../metadata/dom/saml/SetCacheDurationStage.java   |   8 +-
 .../metadata/dom/saml/SetValidUntilStage.java      |   8 +-
 .../metadata/dom/saml/ValidateValidUntilStage.java |  16 +-
 .../dom/saml/mdui/IPHintValidationStage.java       |   2 +-
 .../metadata/pipeline/SimplePipeline.java          |   5 +-
 .../metadata/validate/AcceptAllValidator.java      |   2 +
 .../metadata/validate/BaseValidator.java           |   3 +-
 .../metadata/validate/RejectAllValidator.java      |   2 +
 .../shibboleth/metadata/validate/Validator.java    |  10 +-
 .../metadata/validate/ValidatorSequence.java       |  14 +-
 .../metadata/validate/x509/X509DSADetector.java    |   3 +-
 .../dom/AbstractDOMValidationStageTest.java        |   2 +-
 .../metadata/dom/XMLSignatureSigningStageTest.java |  16 +-
 .../saml/SAMLStringElementCheckingStageTest.java   |   6 +-
 75 files changed, 1321 insertions(+), 904 deletions(-)

diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/AbstractCompositeItemIdentificationStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/AbstractCompositeItemIdentificationStrategy.java
index 98639b2..6d9de2a 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/AbstractCompositeItemIdentificationStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/AbstractCompositeItemIdentificationStrategy.java
@@ -19,6 +19,7 @@ package net.shibboleth.metadata;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
@@ -42,6 +43,7 @@ import net.shibboleth.utilities.java.support.primitive.StringSupport;
 public abstract class AbstractCompositeItemIdentificationStrategy implements ItemIdentificationStrategy {
 
     /** Identifier to use if an {@link Item} does not have an {@link ItemId}. Default value: "unidentified" */
+    @Nonnull @NotEmpty @GuardedBy("this")
     private String noItemIdIdentifier = "unidentified";
 
     /**
@@ -49,7 +51,7 @@ public abstract class AbstractCompositeItemIdentificationStrategy implements Ite
      * 
      * @return identifier to use if an {@link Item} does not have an {@link ItemId}
      */
-    public String getNoItemIdIdentifier() {
+    @Nonnull @NotEmpty public final synchronized String getNoItemIdIdentifier() {
         return noItemIdIdentifier;
     }
 
@@ -58,7 +60,7 @@ public abstract class AbstractCompositeItemIdentificationStrategy implements Ite
      * 
      * @param identifier identifier to use if an {@link Item} does not have an {@link ItemId}
      */
-    public void setNoItemIdIdentifier(@Nonnull @NotEmpty final String identifier) {
+    public synchronized void setNoItemIdIdentifier(@Nonnull @NotEmpty final String identifier) {
         noItemIdIdentifier =
                 Constraint.isNotNull(StringSupport.trimOrNull(identifier), "Identifier can not be null or empty");
     }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/CollectionMergeStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/CollectionMergeStrategy.java
index d7a1163..e3988ff 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/CollectionMergeStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/CollectionMergeStrategy.java
@@ -21,10 +21,18 @@ import java.util.Collection;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 
-/** Strategy used to merge the results of each child pipeline in to the collection of Items given to this stage. */
+/**
+ * Strategy used to merge the results of each child pipeline in to the collection of Items given to this stage.
+ * 
+ * <p>
+ * All implementations of this interface <strong>must</strong> be thread-safe.
+ * </p>
+ */
+ at ThreadSafe
 public interface CollectionMergeStrategy {
 
     /**
@@ -34,6 +42,6 @@ public interface CollectionMergeStrategy {
      * @param sources collections of Items to be merged in to the target, never null not containing any null elements
      * @param <T> type of data contained in the items
      */
-    <T> void mergeCollection(@Nonnull @NonnullElements final Collection<Item<T>> target,
-            @Nonnull @NonnullElements final List<Collection<Item<T>>> sources);
+    <T> void mergeCollection(@Nonnull @NonnullElements Collection<Item<T>> target,
+            @Nonnull @NonnullElements List<Collection<Item<T>>> sources);
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/DeduplicatingItemIdMergeStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/DeduplicatingItemIdMergeStrategy.java
index bdf817d..69cdfed 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/DeduplicatingItemIdMergeStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/DeduplicatingItemIdMergeStrategy.java
@@ -22,6 +22,7 @@ import java.util.HashSet;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -31,19 +32,19 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  * the same {@link ItemId} as source item. If the source item does not contain a {@link ItemId} it is always added to
  * the target collection.
  */
+ at ThreadSafe
 public class DeduplicatingItemIdMergeStrategy implements CollectionMergeStrategy {
 
-    /** {@inheritDoc} */
-    @Override public <T> void mergeCollection(@Nonnull @NonnullElements final Collection<Item<T>> target,
+    @Override
+    public <T> void mergeCollection(@Nonnull @NonnullElements final Collection<Item<T>> target,
             @Nonnull @NonnullElements final List<Collection<Item<T>>> sources) {
         Constraint.isNotNull(target, "Target collection can not be null");
         Constraint.isNotNull(sources, "Source collections can not be null or empty");
         
-        List<ItemId> itemIds;
         final HashSet<ItemId> presentItemIds = new HashSet<>();
 
         for (final Item<T> item : target) {
-            itemIds = item.getItemMetadata().get(ItemId.class);
+            final var itemIds = item.getItemMetadata().get(ItemId.class);
             if (itemIds != null) {
                 presentItemIds.addAll(itemIds);
             }
@@ -67,16 +68,14 @@ public class DeduplicatingItemIdMergeStrategy implements CollectionMergeStrategy
     private <T> void merge(@Nonnull @NonnullElements final HashSet<ItemId> presentItemIds,
             @Nonnull @NonnullElements final Collection<Item<T>> target,
             @Nonnull @NonnullElements final Collection<Item<T>> sourceItems) {
-        boolean itemAlreadyPresent;
-        List<ItemId> itemIds;
         for (final Item<T> sourceItem : sourceItems) {
-            itemIds = sourceItem.getItemMetadata().get(ItemId.class);
+            final var itemIds = sourceItem.getItemMetadata().get(ItemId.class);
             if (itemIds == null || itemIds.isEmpty()) {
                 target.add(sourceItem);
                 continue;
             }
 
-            itemAlreadyPresent = false;
+            var itemAlreadyPresent = false;
             for (final ItemId itemId : itemIds) {
                 if (presentItemIds.contains(itemId)) {
                     itemAlreadyPresent = true;
@@ -90,4 +89,4 @@ public class DeduplicatingItemIdMergeStrategy implements CollectionMergeStrategy
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/FirstItemIdItemIdentificationStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/FirstItemIdItemIdentificationStrategy.java
index 17ab773..b8041bd 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/FirstItemIdItemIdentificationStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/FirstItemIdItemIdentificationStrategy.java
@@ -21,11 +21,13 @@ import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
 
 /**
  * Strategy that returns the first {@link ItemId} associated with an {@link Item} or, if no {@link ItemId} is
  * associated with the item, a generic identifier is returned.
  */
+ at Immutable
 public class FirstItemIdItemIdentificationStrategy extends AbstractCompositeItemIdentificationStrategy {
 
     @Override
@@ -33,9 +35,8 @@ public class FirstItemIdItemIdentificationStrategy extends AbstractCompositeItem
         final List<ItemId> itemIds = item.getItemMetadata().get(ItemId.class);
         if (!itemIds.isEmpty()) {
             return itemIds.get(0).getId();
-        } else {
-            return null;
         }
+        return null;
     }
 
     @Override
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemCollectionSerializer.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemCollectionSerializer.java
index 648ba39..5d05467 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemCollectionSerializer.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemCollectionSerializer.java
@@ -28,8 +28,14 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElemen
 /**
  * Item collection serializers convert a {@link Collection} of {@link Item}s into an octet stream.
  * 
+ * <p>
  * The caller is responsible for managing (opening, closing, etc.) the output stream.
+ * </p>
  * 
+ * <p>
+ * All implementations of this interface <strong>must</strong> be thread-safe.
+ * </p>
+ *
  * @param <T> type of data contained in each item
  *
  * @since 0.9.0
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemIdentificationStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemIdentificationStrategy.java
index 1a3de26..aab7999 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemIdentificationStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemIdentificationStrategy.java
@@ -18,8 +18,16 @@
 package net.shibboleth.metadata;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
-/** Strategy for determining a displayable identifier for a given {@link Item}. */
+/**
+ * Strategy for determining a displayable identifier for a given {@link Item}.
+ * 
+ * <p>
+ * All implementations of this interface <strong>must</strong> be thread-safe.
+ * </p>
+ */
+ at ThreadSafe
 public interface ItemIdentificationStrategy {
 
     /**
@@ -27,8 +35,8 @@ public interface ItemIdentificationStrategy {
      * 
      * @param item the item
      * 
-     * @return the identifier, never null
+     * @return the identifier, never <code>null</code>
      */
-    String getItemIdentifier(@Nonnull final Item<?> item);
+    @Nonnull String getItemIdentifier(@Nonnull Item<?> item);
 
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemSerializer.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemSerializer.java
index aff33e5..9ab6a6a 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemSerializer.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemSerializer.java
@@ -25,9 +25,15 @@ import javax.annotation.Nonnull;
 /**
  * Item serializers convert an {@link Item} into an octet stream.
  * 
+ * <p>
  * The caller is responsible for managing (opening, closing, etc.) the output stream
  * and orchestrating the serialization of collections of {@link Item}s.
+ * </p>
  * 
+ * <p>
+ * All implementations of this interface <strong>must</strong> be thread-safe.
+ * </p>
+ *
  * @param <T> type of data contained in the item
  */
 public interface ItemSerializer<T> {
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleCollectionMergeStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleCollectionMergeStrategy.java
index a7a815f..059174b 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleCollectionMergeStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleCollectionMergeStrategy.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -29,10 +30,11 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  * A {@link CollectionMergeStrategy} that adds the Item from each source, in order, by means of the
  * {@link Collection#addAll(Collection)} method on the target.
  */
+ at Immutable
 public class SimpleCollectionMergeStrategy implements CollectionMergeStrategy {
 
-    /** {@inheritDoc} */
-    @Override public <T> void mergeCollection(@Nonnull @NonnullElements final Collection<Item<T>> target,
+    @Override
+    public <T> void mergeCollection(@Nonnull @NonnullElements final Collection<Item<T>> target,
             @Nonnull @NonnullElements final List<Collection<Item<T>>> sources) {
         Constraint.isNotNull(target, "Target collection can not be null");
         Constraint.isNotNull(sources, "Source collections can not be null or empty");
@@ -41,4 +43,4 @@ public class SimpleCollectionMergeStrategy implements CollectionMergeStrategy {
             target.addAll(source);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleItemCollectionFactory.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleItemCollectionFactory.java
index ae6e161..86fd350 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleItemCollectionFactory.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleItemCollectionFactory.java
@@ -22,12 +22,14 @@ import java.util.Collection;
 import java.util.function.Supplier;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 /**
  * Implementation {@link Supplier} that produces {@link ArrayList} instances.
  * 
  * @param <T> the type of items the produced collection will contain
  */
+ at Immutable
 public class SimpleItemCollectionFactory<T> implements Supplier<Collection<Item<T>>> {
 
     @Override
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleItemCollectionSerializer.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleItemCollectionSerializer.java
index 75b82b6..920b419 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleItemCollectionSerializer.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/SimpleItemCollectionSerializer.java
@@ -22,6 +22,7 @@ import java.io.OutputStream;
 import java.util.Collection;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 
@@ -31,6 +32,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElemen
  * 
  * @param <T> type of data contained in each item
  */
+ at Immutable
 public class SimpleItemCollectionSerializer<T> implements ItemCollectionSerializer<T> {
 
     /** The {@link ItemSerializer} to use on each {@link Item} in turn. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractDOMTraversalStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractDOMTraversalStage.java
index 4aff930..a4851dd 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractDOMTraversalStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractDOMTraversalStage.java
@@ -65,10 +65,12 @@ public abstract class AbstractDOMTraversalStage<C extends DOMTraversalContext>
      * Indicates whether the visitor should be applied to a particular {@link Element}.
      * 
      * @param element {@link Element} to which we may wish to apply the visitor
+     * @param context {@link DOMTraversalContext} implementation being used to manage the traversal
      * 
      * @return <code>true</code> if the visitor should be applied to this {@link Element}.
      */
-    protected abstract boolean applicable(@Nonnull final Element element);
+    protected abstract boolean applicable(@Nonnull final Element element,
+            @Nonnull final C context);
 
     /**
      * Visit a particular {@link Element}.
@@ -96,7 +98,7 @@ public abstract class AbstractDOMTraversalStage<C extends DOMTraversalContext>
         for (final Element child : children) {
             traverse(child, context);
         }
-        if (applicable(element)) {
+        if (applicable(element, context)) {
             visit(element, context);
         }
     }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractDOMValidationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractDOMValidationStage.java
index f616f2e..8fafed1 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractDOMValidationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractDOMValidationStage.java
@@ -20,6 +20,7 @@ package net.shibboleth.metadata.dom;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.pipeline.StageProcessingException;
 import net.shibboleth.metadata.validate.Validator;
@@ -36,12 +37,21 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
  *
  * @since 0.9.0
  */
+ at ThreadSafe
 public abstract class AbstractDOMValidationStage<V, C extends DOMTraversalContext>
     extends AbstractDOMTraversalStage<C> {
 
-    /** The validator sequence to apply. */
+    /**
+     * The validator sequence to apply.
+     *
+     * <p>
+     * Thread safety: as a <code>final</code> field, access to <code>validators</code>
+     * does not need to be synchronised for thread safety. The referenced object
+     * is itself thread-safe.
+     * </p>
+     */
     @Nonnull @NonnullElements @Unmodifiable
-    private ValidatorSequence<V> validators = new ValidatorSequence<>();
+    private final ValidatorSequence<V> validators = new ValidatorSequence<>();
 
     /**
      * Set the list of validators to apply to each item.
@@ -58,7 +68,7 @@ public abstract class AbstractDOMValidationStage<V, C extends DOMTraversalContex
      * @return list of validators
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<Validator<V>> getValidators() {
+    public final List<Validator<V>> getValidators() {
         return validators.getValidators();
     }
 
@@ -76,8 +86,11 @@ public abstract class AbstractDOMValidationStage<V, C extends DOMTraversalContex
     
     @Override
     protected void doDestroy() {
+        /*
+         * We can not discard the reference to the validators object,
+         * but we can destroy it.
+         */
         validators.destroy();
-        validators = null;
         super.doDestroy();
     }
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractElementVisitingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractElementVisitingStage.java
index 4d29547..9d2c83c 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractElementVisitingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractElementVisitingStage.java
@@ -21,6 +21,8 @@ import java.util.Collection;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.namespace.QName;
 
 import org.w3c.dom.Element;
@@ -36,26 +38,20 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  *
  * @since 0.9.0
  */
+ at ThreadSafe
 public abstract class AbstractElementVisitingStage extends AbstractDOMTraversalStage<DOMTraversalContext> {
 
     /** Collection of element names for those elements we will be visiting. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Set<QName> elementNames = Set.of();
 
-    @Override
-    protected void doDestroy() {
-        elementNames = null;
-
-        super.doDestroy();
-    }
-
     /**
      * Gets the collection of element names to visit.
      * 
      * @return collection of element names to visit.
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<QName> getElementNames() {
+    public final synchronized Collection<QName> getElementNames() {
         return elementNames;
     }
 
@@ -64,7 +60,7 @@ public abstract class AbstractElementVisitingStage extends AbstractDOMTraversalS
      * 
      * @param names collection of element names to visit.
      */
-    public void setElementNames(@Nonnull @NonnullElements @Unmodifiable final Collection<QName> names) {
+    public synchronized void setElementNames(@Nonnull @NonnullElements @Unmodifiable final Collection<QName> names) {
         throwSetterPreconditionExceptions();
         Constraint.isNotNull(names, "elementNames may not be null");
         elementNames = Set.copyOf(names);
@@ -77,16 +73,16 @@ public abstract class AbstractElementVisitingStage extends AbstractDOMTraversalS
      * 
      * @param name {@link QName} for the element to be visited.
      */
-    public void setElementName(@Nonnull final QName name) {
+    public synchronized void setElementName(@Nonnull final QName name) {
         throwSetterPreconditionExceptions();
         Constraint.isNotNull(name, "elementName may not be null");
         elementNames = Set.of(name);
     }
     
     @Override
-    protected boolean applicable(@Nonnull final Element e) {
+    protected boolean applicable(@Nonnull final Element e, @Nonnull final DOMTraversalContext context) {
         final QName q = new QName(e.getNamespaceURI(), e.getLocalName());
-        return elementNames.contains(q);
+        return getElementNames().contains(q);
     }
 
     @Override
@@ -94,4 +90,10 @@ public abstract class AbstractElementVisitingStage extends AbstractDOMTraversalS
         return new SimpleDOMTraversalContext(item);
     }
 
+    @Override
+    protected void doDestroy() {
+        elementNames = null;
+
+        super.doDestroy();
+    }
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractNamespacesStrippingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractNamespacesStrippingStage.java
index 3dd784a..74787dd 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractNamespacesStrippingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractNamespacesStrippingStage.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.XMLConstants;
 
@@ -61,11 +62,11 @@ public abstract class AbstractNamespacesStrippingStage extends AbstractIterating
      * @param namespace potentially stripped namespace
      * @return <code>true</code> if this namespace should be stripped
      */
-    protected abstract boolean removingNamespace(final String namespace);
+    protected abstract boolean removingNamespace(@Nullable final String namespace);
 
     @Override
     protected void doExecute(@Nonnull final Item<Element> item) {
-        final Element element = Constraint.isNotNull(item, "Item can not be null").unwrap();
+        final Element element = item.unwrap();
     
         /*
          * We can't, by definition, remove the document element from a {@link DOMElementItem},
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractXSLProcessingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractXSLProcessingStage.java
index 5f510f6..e8c8f97 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractXSLProcessingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AbstractXSLProcessingStage.java
@@ -24,6 +24,8 @@ import java.util.Map.Entry;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.Immutable;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.transform.ErrorListener;
 import javax.xml.transform.Templates;
@@ -45,6 +47,7 @@ import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.WarningStatus;
 import net.shibboleth.metadata.pipeline.AbstractStage;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
+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.Unmodifiable;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -67,17 +70,28 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
     private final Logger log = LoggerFactory.getLogger(AbstractXSLProcessingStage.class);
 
     /** Resource that provides the XSL document. */
+    @NonnullAfterInit @GuardedBy("this")
     private Resource xslResource;
 
-    /** XSL template used to transform Elements. */
+    /**
+     * XSL template used to transform <code>Element</code>s.
+     * 
+     * <p>
+     * A single shared <code>Templates</code> object is constructed from the
+     * <code>xslResource</code>, <code>transformAttributes</code>,
+     * <code>transformFeatures</code> and any <code>uriResolver</code>
+     * during initialisation of the stage.
+     * </p>
+     */
+    @NonnullAfterInit @GuardedBy("this")
     private Templates xslTemplate;
 
     /** Attributes set on the {@link Transformer} used by this stage. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Map<String, Object> transformAttributes = Map.of();
 
     /** Features set on the {@link Transformer} used by this stage. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Map<String, Boolean> transformFeatures = Map.of();
 
     /**
@@ -85,18 +99,19 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
      * 
      * If not set, an empty collection.
      */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Map<String, Object> transformParameters = Map.of();
 
     /** {@link URIResolver} to use in the transformer. Default value: <code>null</code>. */
-    @Nullable private URIResolver uriResolver;
+    @Nullable @GuardedBy("this")
+    private URIResolver uriResolver;
 
     /**
      * Gets the resource that provides the XSL document.
      * 
      * @return resource that provides the XSL document
      */
-    @Nullable public Resource getXSLResource() {
+    @Nullable public final synchronized Resource getXSLResource() {
         return xslResource;
     }
 
@@ -110,13 +125,22 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
         xslResource = Constraint.isNotNull(resource, "XSL resource can not be null");
     }
 
+    /**
+     * Get the shared templates object.
+     *
+     * @return the shared templates object
+     */
+    @NonnullAfterInit private synchronized Templates getXSLTemplate() {
+        return xslTemplate;
+    }
+
     /**
      * Gets the unmodifiable collection of attributes used by the XSLT transformer.
      * 
      * @return unmodifiable collection of attributes used by the XSLT transformer, never null nor containing null keys
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Map<String, Object> getTransformAttributes() {
+    public final synchronized Map<String, Object> getTransformAttributes() {
         return transformAttributes;
     }
 
@@ -137,7 +161,7 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
      * @return unmodifiable collection of features used by the XSLT transformer, never null nor containing null keys
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Map<String, Boolean> getTransformFeatures() {
+    public final synchronized Map<String, Boolean> getTransformFeatures() {
         return transformFeatures;
     }
 
@@ -158,7 +182,7 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
      * @return parameters used by the XSLT transformer, never null nor containing null keys
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Map<String, Object> getTransformParameters() {
+    public final synchronized Map<String, Object> getTransformParameters() {
         return transformParameters;
     }
 
@@ -178,7 +202,7 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
      * 
      * @return the {@link URIResolver}, or <code>null</code>
      */
-    @Nullable public URIResolver getURIResolver() {
+    @Nullable public final synchronized URIResolver getURIResolver() {
         return uriResolver;
     }
 
@@ -192,12 +216,24 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
         uriResolver = resolver;
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection)
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection)
             throws StageProcessingException {
         try {
-            final Transformer transformer = xslTemplate.newTransformer();
-            for (final Map.Entry<String, Object> entry : transformParameters.entrySet()) {
+            /*
+             * Construct a per-execution Transformer from the shared template.
+             * Although a single Transformer (with parameters) could in principle be
+             * used sequentially by multiple executions, Transformers are not thread-safe
+             * and sharing one across threads would require locking against that instance
+             * for the whole duration of doExecute, or pooling.
+             * 
+             * What we're doing here allows executions to overlap, at the cost of building
+             * a Transformer for each execution.
+             */
+            final Transformer transformer = getXSLTemplate().newTransformer();
+
+            // Set each of the transform's parameters
+            for (final Map.Entry<String, Object> entry : getTransformParameters().entrySet()) {
                 transformer.setParameter(entry.getKey(), entry.getValue());
             }
 
@@ -232,8 +268,8 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
         super.doDestroy();
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doInitialize() throws ComponentInitializationException {
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
         if (xslResource == null) {
@@ -241,6 +277,11 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
                     + ", XslResource must not be null");
         }
 
+        /*
+         * Construct the shared (thread-safe) Templates instance. Note that because
+         * doInitialize() is called with the stage's monitor held, we can refer to
+         * fields directly and do not need to use getters.
+         */
         try {
             final TransformerFactory tfactory = TransformerFactory.newInstance();
 
@@ -290,6 +331,7 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
      *
      * <p>This listener works well in conjunction with <xsl:message>.</p>
      */
+    @Immutable
     public class StatusInfoAppendingErrorListener implements ErrorListener {
 
         /** Prefix used by messages that result in an {@link ErrorStatus}. */
@@ -302,7 +344,7 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
         public static final String INFO_PREFIX = "[INFO]";
 
         /** Item to which the status info will be appended. */
-        private Item<?> item;
+        private final Item<?> item;
 
         /**
          * Constructor.
@@ -313,18 +355,18 @@ public abstract class AbstractXSLProcessingStage extends AbstractStage<Element>
             item = receivingItem;
         }
 
-        /** {@inheritDoc} */
-        @Override public void error(@Nonnull final TransformerException e) throws TransformerException {
+        @Override
+        public void error(@Nonnull final TransformerException e) throws TransformerException {
             parseAndAppendStatusInfo(e);
         }
 
-        /** {@inheritDoc} */
-        @Override public void fatalError(@Nonnull final TransformerException e) throws TransformerException {
+        @Override
+        public void fatalError(@Nonnull final TransformerException e) throws TransformerException {
             parseAndAppendStatusInfo(e);
         }
 
-        /** {@inheritDoc} */
-        @Override public void warning(@Nonnull final TransformerException e) throws TransformerException {
+        @Override
+        public void warning(@Nonnull final TransformerException e) throws TransformerException {
             parseAndAppendStatusInfo(e);
         }
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AttrVisitor.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AttrVisitor.java
index 833e24e..43ede88 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AttrVisitor.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/AttrVisitor.java
@@ -19,6 +19,9 @@ package net.shibboleth.metadata.dom;
 
 import net.shibboleth.metadata.Item;
 
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+
 import org.w3c.dom.Attr;
 import org.w3c.dom.Element;
 
@@ -26,8 +29,13 @@ import org.w3c.dom.Element;
  * Provides a variation of the Visitor pattern for performing operations on
  * DOM attributes which are part of {@link Element} items.
  *
+ * <p>
+ * All implementations of this interface <strong>must</strong> be thread-safe.
+ * </p>
+ *
  * @since 0.9.0
  */
+ at ThreadSafe
 public interface AttrVisitor {
 
     /**
@@ -37,6 +45,6 @@ public interface AttrVisitor {
      * @param visited the {@link Attr} being visited.
      * @param item the {@link Item} which is the context for the visit.
      */
-    void visitAttr(Attr visited, Item<Element> item);
+    void visitAttr(@Nonnull Attr visited, @Nonnull Item<Element> item);
     
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/CRDetectionStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/CRDetectionStage.java
index e960f32..6e27fb8 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/CRDetectionStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/CRDetectionStage.java
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.dom;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Element;
 import org.w3c.dom.NamedNodeMap;
@@ -39,6 +40,7 @@ import net.shibboleth.metadata.pipeline.StageProcessingException;
  *
  * @see <a href="https://issues.shibboleth.net/jira/browse/SSPCPP-684">SSPCPP-684</a>
  */
+ at ThreadSafe
 public class CRDetectionStage extends AbstractSAMLTraversalStage<CRDetectionStage.Context> {
 
     /** Context class for this kind of traversal. */
@@ -78,7 +80,7 @@ public class CRDetectionStage extends AbstractSAMLTraversalStage<CRDetectionStag
     private static final char CR = '\r';
     
     @Override
-    protected boolean applicable(final Element element) {
+    protected boolean applicable(@Nonnull final Element element, @Nonnull final Context context) {
         // all Elements are applicable
         return true;
     }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/Container.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/Container.java
index a81704a..6cfd1c5 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/Container.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/Container.java
@@ -55,7 +55,7 @@ public class Container {
          * @param child child {@link Element} to add
          * @return a new child {@link Container} representing the child {@link Element}
          */
-        @Nonnull Container addChild(@Nonnull final Container parent, @Nonnull final Element child);
+        @Nonnull Container addChild(@Nonnull Container parent, @Nonnull Element child);
     }
 
     /**
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMElementSerializer.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMElementSerializer.java
index cd88797..457ba70 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMElementSerializer.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMElementSerializer.java
@@ -55,9 +55,6 @@ public class DOMElementSerializer implements ItemSerializer<Element>, ItemCollec
     @Override
     public void serialize(@Nonnull final Item<Element> item, @Nonnull final OutputStream output)
         throws IOException {
-        if (item == null) {
-            return;
-        }
 
         final Element documentRoot = item.unwrap();
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMFilesystemSourceStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMFilesystemSourceStage.java
index f4bb1d8..184ff49 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMFilesystemSourceStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMFilesystemSourceStage.java
@@ -27,6 +27,7 @@ import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
@@ -38,6 +39,7 @@ import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.AbstractStage;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
 import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -59,29 +61,35 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
     private final Logger log = LoggerFactory.getLogger(DOMFilesystemSourceStage.class);
 
     /** Pool of DOM parsers used to parse the XML file in to a DOM. */
+    @NonnullAfterInit @GuardedBy("this")
     private ParserPool parserPool;
 
     /** The file path to the DOM material provided by this source. May be a file or a directory. */
+    @NonnullAfterInit @GuardedBy("this")
     private File sourceFile;
 
     /**
      * Filter used to determine if a file should be included. This is only used if the {@link #sourceFile} is a
      * directory.
      */
+    @Nullable @GuardedBy("this")
     private FileFilter sourceFileFilter;
 
     /**
      * Whether or not directories are recursed if the given input file is a directory. Default value: <code>false</code>
      */
+    @GuardedBy("this")
     private boolean recurseDirectories;
 
     /** Whether the lack of source files is treated as an error. Default value: <code>false</code> */
+    @GuardedBy("this")
     private boolean noSourceFilesAnError;
 
     /**
      * Whether an error parsing one source file causes this entire {@link net.shibboleth.metadata.pipeline.Stage} to
      * fail, or just excludes the material from the offending source file. Default value: <code>true</code>
      */
+    @GuardedBy("this")
     private boolean errorCausesSourceFailure = true;
 
     /**
@@ -89,7 +97,7 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
      * 
      * @return pool of DOM parsers used to parse the XML file in to a DOM
      */
-    @Nullable public ParserPool getParserPool() {
+    @Nullable public final synchronized ParserPool getParserPool() {
         return parserPool;
     }
 
@@ -108,7 +116,7 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
      * 
      * @return path to the DOM material provided by this source
      */
-    @Nullable public File getSource() {
+    @Nullable public final synchronized File getSource() {
         return sourceFile;
     }
 
@@ -128,7 +136,7 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
      * 
      * @return filter used to determine if a file, in a directory, should be treated as a source file, may be null
      */
-    @Nullable public FileFilter getSourceFileFilter() {
+    @Nullable public final synchronized FileFilter getSourceFileFilter() {
         return sourceFileFilter;
     }
 
@@ -147,7 +155,7 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
      * 
      * @return whether directories will be recursively searched for XML input files
      */
-    public boolean getRecurseDirectories() {
+    public final synchronized boolean getRecurseDirectories() {
         return recurseDirectories;
     }
 
@@ -166,7 +174,7 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
      * 
      * @return whether the lack of source files is considered an error
      */
-    public boolean isNoSourceFilesAnError() {
+    public final synchronized boolean isNoSourceFilesAnError() {
         return noSourceFilesAnError;
     }
 
@@ -185,7 +193,7 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
      * 
      * @return whether an error parsing a single file causes the source to fail
      */
-    public boolean getErrorCausesSourceFailure() {
+    public final synchronized boolean getErrorCausesSourceFailure() {
         return errorCausesSourceFailure;
     }
 
@@ -199,28 +207,28 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
         errorCausesSourceFailure = causesFailure;
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection)
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection)
             throws StageProcessingException {
         final ArrayList<File> sourceFiles = new ArrayList<>();
-        if (sourceFile.isFile()) {
-            sourceFiles.add(sourceFile);
+
+        final var sFile = getSource();
+        if (sFile.isFile()) {
+            sourceFiles.add(sFile);
         } else {
-            getSourceFiles(sourceFile, sourceFiles);
+            getSourceFiles(sFile, sourceFiles);
         }
 
         if (sourceFiles.isEmpty()) {
-            if (!noSourceFilesAnError) {
-                log.warn("stage {}: no input XML files in source path {}", getId(), sourceFile.getPath());
+            if (!isNoSourceFilesAnError()) {
+                log.warn("stage {}: no input XML files in source path {}", getId(), sFile.getPath());
                 return;
-            } else {
-                throw new StageProcessingException("stage " + getId() + ": no source file was available for parsing");
             }
+            throw new StageProcessingException("stage " + getId() + ": no source file was available for parsing");
         }
 
-        DOMElementItem dme;
         for (final File source : sourceFiles) {
-            dme = processSourceFile(source);
+            final var dme = processSourceFile(source);
             if (dme != null) {
                 itemCollection.add(dme);
             }
@@ -235,10 +243,12 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
      * @param input the source input file, never null
      * @param collector the collector of XML input files
      */
-    protected void
-            getSourceFiles(@Nonnull final File input, @Nonnull @NonnullElements @Live final List<File> collector) {
+    protected void getSourceFiles(@Nonnull final File input,
+            @Nonnull @NonnullElements @Live final List<File> collector) {
+
         if (input.isFile()) {
-            if (sourceFileFilter == null || sourceFileFilter.accept(input)) {
+            final var filter = getSourceFileFilter();
+            if (filter == null || filter.accept(input)) {
                 collector.add(input);
             }
             return;
@@ -247,8 +257,9 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
         // file must be a directory
         final File[] files = input.listFiles();
         if (files != null) {
+            final var recursing = getRecurseDirectories();
             for (final File file : files) {
-                if (file.isFile() || (file.isDirectory() && recurseDirectories)) {
+                if (file.isFile() || (file.isDirectory() && recursing)) {
                     getSourceFiles(file, collector);
                 }
             }
@@ -272,17 +283,16 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
         try {
             log.debug("{} pipeline source parsing XML file {}", getId(), source.getPath());
             xmlIn = new FileInputStream(source);
-            final Document doc = parserPool.parse(xmlIn);
+            final Document doc = getParserPool().parse(xmlIn);
             return new DOMElementItem(doc);
         } catch (final Exception e) {
-            if (errorCausesSourceFailure) {
+            if (getErrorCausesSourceFailure()) {
                 throw new StageProcessingException(getId() + " pipeline source unable to parse XML input file "
                         + source.getPath(), e);
-            } else {
-                log.warn("{} pipeline source: unable to parse XML source file {}, ignoring it bad file", new Object[] {
-                        getId(), source.getPath(), e,});
-                return null;
             }
+            log.warn("{} pipeline source: unable to parse XML source file {}, ignoring it bad file", new Object[] {
+                    getId(), source.getPath(), e,});
+            return null;
         } finally {
             try {
                 xmlIn.close();
@@ -292,8 +302,8 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
         }
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doDestroy() {
+    @Override
+    protected void doDestroy() {
         parserPool = null;
         sourceFile = null;
         sourceFileFilter = null;
@@ -301,8 +311,8 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
         super.doDestroy();
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doInitialize() throws ComponentInitializationException {
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
         if (parserPool == null) {
@@ -319,4 +329,4 @@ public class DOMFilesystemSourceStage extends AbstractStage<Element> {
                     + sourceFile.getPath() + " can not be read");
         }
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMResourceSourceStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMResourceSourceStage.java
index 8967d7d..45708cc 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMResourceSourceStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMResourceSourceStage.java
@@ -23,6 +23,7 @@ import java.util.Collection;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
@@ -33,6 +34,7 @@ import org.w3c.dom.Element;
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.AbstractStage;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -56,15 +58,18 @@ public class DOMResourceSourceStage extends AbstractStage<Element> {
     private final Logger log = LoggerFactory.getLogger(DOMResourceSourceStage.class);
 
     /** Resource used to fetch remote XML document. */
+    @NonnullAfterInit @GuardedBy("this")
     private Resource domResource;
 
     /** Pool of parsers used to parse incoming DOM. */
+    @NonnullAfterInit @GuardedBy("this")
     private ParserPool parserPool;
 
     /**
      * Whether an error parsing one source file causes this entire {@link net.shibboleth.metadata.pipeline.Stage} to
      * fail, or just excludes the material from the offending source file. Default value: <code>true</code>
      */
+    @GuardedBy("this")
     private boolean errorCausesSourceFailure = true;
 
     /**
@@ -72,7 +77,7 @@ public class DOMResourceSourceStage extends AbstractStage<Element> {
      * 
      * @return resource from which the XML document will be fetched
      */
-    @Nullable public Resource getDOMResource() {
+    @Nullable public final synchronized Resource getDOMResource() {
         return domResource;
     }
 
@@ -91,7 +96,7 @@ public class DOMResourceSourceStage extends AbstractStage<Element> {
      * 
      * @return pool of DOM parsers used to parse the XML file in to a DOM
      */
-    @Nullable public ParserPool getParserPool() {
+    @Nullable public final synchronized ParserPool getParserPool() {
         return parserPool;
     }
 
@@ -110,7 +115,7 @@ public class DOMResourceSourceStage extends AbstractStage<Element> {
      * 
      * @return whether an error reading and parsing the XML file causes this stage to fail
      */
-    public boolean getErrorCausesSourceFailure() {
+    public final synchronized boolean getErrorCausesSourceFailure() {
         return errorCausesSourceFailure;
     }
 
@@ -124,22 +129,23 @@ public class DOMResourceSourceStage extends AbstractStage<Element> {
         errorCausesSourceFailure = causesFailure;
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection)
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection)
             throws StageProcessingException {
 
-        log.debug("Attempting to fetch XML document from '{}'", domResource.getDescription());
+        final var resource = getDOMResource();
 
-        try (InputStream ins = domResource.getInputStream()) {
-            populateItemCollection(itemCollection, ins);
+        log.debug("Attempting to fetch XML document from '{}'", resource.getDescription());
+
+        try (InputStream ins = resource.getInputStream()) {
+            populateItemCollection(itemCollection, ins, resource);
         } catch (final IOException e) {
-            if (errorCausesSourceFailure) {
+            if (getErrorCausesSourceFailure()) {
                 throw new StageProcessingException("Error retrieving XML document from " +
-                        domResource.getDescription(), e);
-            } else {
-                log.warn("stage {}: unable to read in XML file", getId());
-                log.debug("stage {}: HTTP resource exception", getId(), e);
+                        resource.getDescription(), e);
             }
+            log.warn("stage {}: unable to read in XML file", getId());
+            log.debug("stage {}: HTTP resource exception", getId(), e);
         }
     }
 
@@ -149,35 +155,35 @@ public class DOMResourceSourceStage extends AbstractStage<Element> {
      * 
      * @param itemCollection collection to which the read in and parsed document element is added
      * @param data XML input file
+     * @param resource the resource to read from
      * 
      * @throws StageProcessingException thrown if there is a problem reading and parsing the response
      */
     protected void populateItemCollection(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection,
-            final InputStream data) throws StageProcessingException {
+            @Nonnull final InputStream data, @Nonnull final Resource resource) throws StageProcessingException {
         try {
-            log.debug("Parsing XML document retrieved from '{}'", domResource.getDescription());
-            itemCollection.add(new DOMElementItem(parserPool.parse(data)));
+            log.debug("Parsing XML document retrieved from '{}'", resource.getDescription());
+            itemCollection.add(new DOMElementItem(getParserPool().parse(data)));
         } catch (final XMLParserException e) {
-            if (errorCausesSourceFailure) {
+            if (getErrorCausesSourceFailure()) {
                 throw new StageProcessingException(getId() + " unable to parse returned XML document " +
-                        domResource.getDescription(), e);
-            } else {
-                log.warn("stage {}: unable to parse XML document", getId());
-                log.debug("stage {}: parsing exception", getId(), e);
+                        resource.getDescription(), e);
             }
+            log.warn("stage {}: unable to parse XML document", getId());
+            log.debug("stage {}: parsing exception", getId(), e);
         }
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doDestroy() {
+    @Override
+    protected void doDestroy() {
         domResource = null;
         parserPool = null;
 
         super.doDestroy();
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doInitialize() throws ComponentInitializationException {
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
         if (parserPool == null) {
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMTraversalContext.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMTraversalContext.java
index 66c4e0c..24a668e 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMTraversalContext.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/DOMTraversalContext.java
@@ -26,9 +26,17 @@ import net.shibboleth.metadata.Item;
 /**
  * The context for a particular DOM traversal.
  *
+ * <p>
  * Implementations may add additional fields and methods to the definition
  * of a {@link DOMTraversalContext}, and may define {@link #end()} to perform
  * operations at the end of the traversal.
+ * </p>
+ * 
+ * <p>
+ * Note that classes implementing this interface will normally be used only by the
+ * thread in which they are created, so implementations are not required to
+ * be thread-safe.
+ * </p>
  */
 public interface DOMTraversalContext {
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementMaker.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementMaker.java
index d804ef8..b7989ce 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementMaker.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementMaker.java
@@ -20,19 +20,19 @@ package net.shibboleth.metadata.dom;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 import javax.xml.namespace.QName;
 
-import net.shibboleth.utilities.java.support.xml.ElementSupport;
-
 import org.w3c.dom.Element;
 
+import net.shibboleth.utilities.java.support.xml.ElementSupport;
+
 /**
  * Basic maker class for {@link Element}s for use with the {@link Container} system.
  *
  * @since 0.10.0
  */
- at ThreadSafe
+ at Immutable
 public class ElementMaker implements Function<Container, Element> {
 
     /** Qualified name for the {@link Element} to be created. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementMatcher.java
index a581a52..dc47fc4 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementMatcher.java
@@ -21,19 +21,19 @@ import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 import javax.xml.namespace.QName;
 
-import net.shibboleth.utilities.java.support.xml.ElementSupport;
-
 import org.w3c.dom.Element;
 
+import net.shibboleth.utilities.java.support.xml.ElementSupport;
+
 /**
  * Basic matcher class for {@link Element}s for use with the {@link Container} system.
  *
  * @since 0.10.0
  */
- at ThreadSafe
+ at Immutable
 public class ElementMatcher implements Predicate<Element> {
 
     /** Element {@link QName} to match. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementStrippingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementStrippingStage.java
index ef721b2..ff452ac 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementStrippingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementStrippingStage.java
@@ -22,6 +22,7 @@ import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Element;
@@ -30,6 +31,7 @@ import org.w3c.dom.NodeList;
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.AbstractIteratingStage;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -42,9 +44,11 @@ import net.shibboleth.utilities.java.support.primitive.StringSupport;
 public class ElementStrippingStage extends AbstractIteratingStage<Element> {
 
     /** Namespace of the element to strip. */
+    @NonnullAfterInit @NotEmpty @GuardedBy("this")
     private String elementNamespace;
 
     /** Name of the element to strip. */
+    @NonnullAfterInit @NotEmpty @GuardedBy("this")
     private String elementName;
 
     /**
@@ -52,7 +56,7 @@ public class ElementStrippingStage extends AbstractIteratingStage<Element> {
      * 
      * @return namespace of the element to strip
      */
-    @Nullable public String getElementNamespace() {
+    @Nullable public final synchronized String getElementNamespace() {
         return elementNamespace;
     }
 
@@ -61,7 +65,7 @@ public class ElementStrippingStage extends AbstractIteratingStage<Element> {
      * 
      * @param namespace namespace of the element to strip
      */
-    public void setElementNamespace(@Nonnull @NotEmpty final String namespace) {
+    public synchronized void setElementNamespace(@Nonnull @NotEmpty final String namespace) {
         throwSetterPreconditionExceptions();
         elementNamespace = Constraint.isNotNull(StringSupport.trimOrNull(namespace),
                 "target namespace can not be null or empty");
@@ -72,7 +76,7 @@ public class ElementStrippingStage extends AbstractIteratingStage<Element> {
      * 
      * @return the name of the element to strip
      */
-    @Nullable public String getElementName() {
+    @Nullable public final synchronized String getElementName() {
         return elementName;
     }
 
@@ -81,7 +85,7 @@ public class ElementStrippingStage extends AbstractIteratingStage<Element> {
      * 
      * @param name the name of the element to strip
      */
-    public void setElementName(@Nonnull @NotEmpty final String name) {
+    public synchronized void setElementName(@Nonnull @NotEmpty final String name) {
         throwSetterPreconditionExceptions();
         elementName = Constraint.isNotNull(StringSupport.trimOrNull(name),
                 "target element name can not be null or empty");
@@ -94,7 +98,7 @@ public class ElementStrippingStage extends AbstractIteratingStage<Element> {
 
         // List all the matching descendant elements in this document in document order
         // Note that this list will never include the document element itself
-        final NodeList nodeList = docElement.getElementsByTagNameNS(elementNamespace, elementName);
+        final NodeList nodeList = docElement.getElementsByTagNameNS(getElementNamespace(), getElementName());
 
         // Copy these into a list, because a NodeList can change length at any time
         final int nNodes = nodeList.getLength();
@@ -109,16 +113,16 @@ public class ElementStrippingStage extends AbstractIteratingStage<Element> {
         }
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doDestroy() {
+    @Override
+    protected void doDestroy() {
         elementNamespace = null;
         elementName = null;
 
         super.doDestroy();
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doInitialize() throws ComponentInitializationException {
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
         if (elementNamespace == null) {
             throw new ComponentInitializationException("target namespace can not be null or empty");
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementVisitor.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementVisitor.java
index 84cd9d8..40a61e9 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementVisitor.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementVisitor.java
@@ -19,14 +19,21 @@ package net.shibboleth.metadata.dom;
 
 import net.shibboleth.metadata.Item;
 
+import javax.annotation.concurrent.ThreadSafe;
+
 import org.w3c.dom.Element;
 
 /**
  * Provides a variation of the Visitor pattern for performing operations on
  * DOM elements which are part of {@link Element} items.
  *
+ * <p>
+ * All implementations of this interface <strong>must</strong> be thread-safe.
+ * </p>
+ *
  * @since 0.9.0
  */
+ at ThreadSafe
 public interface ElementVisitor {
 
     /**
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementWhitespaceTrimmingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementWhitespaceTrimmingStage.java
index 86d58d6..9dafae7 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementWhitespaceTrimmingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementWhitespaceTrimmingStage.java
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.dom;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Element;
 
@@ -27,6 +28,7 @@ import org.w3c.dom.Element;
  *
  * @since 0.9.0
  */
+ at ThreadSafe
 public class ElementWhitespaceTrimmingStage extends AbstractElementVisitingStage {
 
     /** Visitor to apply to each visited element. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementsStrippingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementsStrippingStage.java
index 5eef774..4189079 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementsStrippingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ElementsStrippingStage.java
@@ -23,12 +23,13 @@ import java.util.List;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Element;
 
 import net.shibboleth.metadata.Item;
+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.NotEmpty;
 import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
@@ -58,22 +59,63 @@ import net.shibboleth.utilities.java.support.primitive.StringSupport;
 @ThreadSafe
 public class ElementsStrippingStage extends AbstractDOMTraversalStage<ElementsStrippingStage.Context> {
 
-    /** Context class for this kind of traversal. */
-    protected static class Context extends SimpleDOMTraversalContext {
+    /**
+     * Context class for this kind of traversal.
+     * 
+     * <p>
+     * An instance of this class is passed around during the traversal.
+     * To reduce the number of synchronizations, it includes a copy of
+     * each of the guarded state variables from the stage class instance.
+     * </p>
+     */
+    protected static final class Context extends SimpleDOMTraversalContext {
 
         /**
          * List of {@link Element}s to be removed from the document at the
          * end of the traversal.
          */
-        private List<Element> elements = new ArrayList<>();
+        @Nonnull @NonnullElements
+        private final List<Element> elements = new ArrayList<>();
+
+        /** Namespace of the elements to strip. */
+        @Nonnull @NotEmpty
+        private final String elementNamespace;
+
+        /** Names of the elements to strip. */
+        @Nonnull @NonnullElements @Unmodifiable
+        private final Collection<String> elementNames;
+
+        /** Whether we are operating in a whitelisting mode (<code>false</code> by default). */
+        private final boolean whitelisting;
 
         /**
          * Constructor.
          *
          * @param contextItem the {@link Item} we are traversing
+         * @param namespace 
+         * @param names 
+         * @param wl 
          */
-        public Context(@Nonnull final Item<Element> contextItem) {
+        public Context(@Nonnull final Item<Element> contextItem,
+                @Nonnull @NotEmpty final String namespace,
+                @Nonnull @NonnullElements @Unmodifiable final Collection<String> names,
+                final boolean wl) {
             super(contextItem);
+            elementNamespace = namespace;
+            elementNames = names;
+            whitelisting = wl;
+        }
+
+        protected final String getElementNamespace() {
+            return elementNamespace;
+        }
+
+        protected final Collection<String> getElementNames() {
+            return elementNames;
+        }
+        
+        protected final boolean isWhitelisting() {
+            return whitelisting;
         }
 
         /**
@@ -94,10 +136,11 @@ public class ElementsStrippingStage extends AbstractDOMTraversalStage<ElementsSt
     }
 
     /** Namespace of the elements to strip. */
+    @NonnullAfterInit @NotEmpty @GuardedBy("this")
     private String elementNamespace;
 
     /** Names of the elements to strip. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Set<String> elementNames = Set.of();
 
     /** Whether we are operating in a whitelisting mode (<code>false</code> by default). */
@@ -108,7 +151,7 @@ public class ElementsStrippingStage extends AbstractDOMTraversalStage<ElementsSt
      * 
      * @return namespace of the elements to strip
      */
-    @Nullable public String getElementNamespace() {
+    @NonnullAfterInit @NotEmpty public final synchronized String getElementNamespace() {
         return elementNamespace;
     }
 
@@ -129,7 +172,7 @@ public class ElementsStrippingStage extends AbstractDOMTraversalStage<ElementsSt
      * @return the names of the elements to strip
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<String> getElementNames() {
+    public final synchronized Collection<String> getElementNames() {
         return elementNames;
     }
 
@@ -138,7 +181,7 @@ public class ElementsStrippingStage extends AbstractDOMTraversalStage<ElementsSt
      * 
      * @param names the names of the elements to strip
      */
-    public void setElementNames(
+    public synchronized void setElementNames(
             @Nonnull @NonnullElements @Unmodifiable @NotEmpty final Collection<String> names) {
         throwSetterPreconditionExceptions();
         elementNames = Set.copyOf(names);
@@ -165,14 +208,14 @@ public class ElementsStrippingStage extends AbstractDOMTraversalStage<ElementsSt
     }
 
     @Override
-    protected boolean applicable(@Nonnull final Element element) {
+    protected boolean applicable(@Nonnull final Element element, @Nonnull final Context context) {
         // ignore all elements not in the given namespace
-        if (!elementNamespace.equals(element.getNamespaceURI())) {
+        if (!context.getElementNamespace().equals(element.getNamespaceURI())) {
             return false;
         }
 
         // Whitelisting reverses the meaning of presence in the list
-        return whitelisting ^ elementNames.contains(element.getLocalName());
+        return context.isWhitelisting() ^ context.getElementNames().contains(element.getLocalName());
     }
 
     @Override
@@ -198,8 +241,8 @@ public class ElementsStrippingStage extends AbstractDOMTraversalStage<ElementsSt
     }
 
     @Override
-    protected Context buildContext(@Nonnull final Item<Element> item) {
-        return new Context(item);
+    protected synchronized Context buildContext(@Nonnull final Item<Element> item) {
+        return new Context(item, getElementNamespace(), getElementNames(), isWhitelisting());
     }
 
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/EmptyContainerStrippingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/EmptyContainerStrippingStage.java
index 50d045c..86c9cab 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/EmptyContainerStrippingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/EmptyContainerStrippingStage.java
@@ -19,6 +19,7 @@ package net.shibboleth.metadata.dom;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Element;
@@ -27,6 +28,7 @@ import org.w3c.dom.NodeList;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.AbstractIteratingStage;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -40,9 +42,11 @@ import net.shibboleth.utilities.java.support.xml.ElementSupport;
 public class EmptyContainerStrippingStage extends AbstractIteratingStage<Element> {
 
     /** Namespace of the element to strip. */
+    @NonnullAfterInit @NotEmpty @GuardedBy("this")
     private String elementNamespace;
 
     /** Name of the element to strip. */
+    @NonnullAfterInit @NotEmpty @GuardedBy("this")
     private String elementName;
 
     /**
@@ -50,7 +54,7 @@ public class EmptyContainerStrippingStage extends AbstractIteratingStage<Element
      * 
      * @return namespace of the element to strip
      */
-    @Nullable public String getElementNamespace() {
+    @Nullable public final synchronized String getElementNamespace() {
         return elementNamespace;
     }
 
@@ -59,7 +63,7 @@ public class EmptyContainerStrippingStage extends AbstractIteratingStage<Element
      * 
      * @param namespace namespace of the element to strip
      */
-    public void setElementNamespace(@Nonnull @NotEmpty final String namespace) {
+    public synchronized void setElementNamespace(@Nonnull @NotEmpty final String namespace) {
         throwSetterPreconditionExceptions();
         elementNamespace = Constraint.isNotNull(StringSupport.trimOrNull(namespace),
                 "target namespace can not be null or empty");
@@ -70,7 +74,7 @@ public class EmptyContainerStrippingStage extends AbstractIteratingStage<Element
      * 
      * @return the name of the element to strip
      */
-    @Nullable public String getElementName() {
+    @Nullable public final synchronized String getElementName() {
         return elementName;
     }
 
@@ -79,7 +83,7 @@ public class EmptyContainerStrippingStage extends AbstractIteratingStage<Element
      * 
      * @param name the name of the element to strip
      */
-    public void setElementName(@Nonnull @NotEmpty final String name) {
+    public synchronized void setElementName(@Nonnull @NotEmpty final String name) {
         throwSetterPreconditionExceptions();
         elementName = Constraint.isNotNull(StringSupport.trimOrNull(name),
                 "target element name can not be null or empty");
@@ -102,7 +106,7 @@ public class EmptyContainerStrippingStage extends AbstractIteratingStage<Element
         final Element element = item.unwrap();
 
         // List all the relevant elements in this document in document order
-        final NodeList extensionList = element.getElementsByTagNameNS(elementNamespace, elementName);
+        final NodeList extensionList = element.getElementsByTagNameNS(getElementNamespace(), getElementName());
 
         // Process in reverse order so that, for example, Extensions inside Extensions are
         // handled correctly.
@@ -114,16 +118,16 @@ public class EmptyContainerStrippingStage extends AbstractIteratingStage<Element
         }
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doDestroy() {
+    @Override
+    protected void doDestroy() {
         elementNamespace = null;
         elementName = null;
 
         super.doDestroy();
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doInitialize() throws ComponentInitializationException {
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
         if (elementNamespace == null) {
             throw new ComponentInitializationException("target namespace can not be null or empty");
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NamespaceStrippingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NamespaceStrippingStage.java
index 1174001..99fb9b8 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NamespaceStrippingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NamespaceStrippingStage.java
@@ -19,6 +19,7 @@ package net.shibboleth.metadata.dom;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
@@ -37,6 +38,7 @@ public class NamespaceStrippingStage extends AbstractNamespacesStrippingStage {
     /**
      * XML namespace to remove.
      */
+    @Nonnull @NotEmpty @GuardedBy("this")
     private String namespace;
 
     /**
@@ -44,7 +46,7 @@ public class NamespaceStrippingStage extends AbstractNamespacesStrippingStage {
      * 
      * @return namespace URI
      */
-    @Nullable public String getNamespace() {
+    @Nullable public final synchronized String getNamespace() {
         return namespace;
     }
 
@@ -53,7 +55,7 @@ public class NamespaceStrippingStage extends AbstractNamespacesStrippingStage {
      * 
      * @param ns namespace URI as a string
      */
-    public void setNamespace(@Nonnull @NotEmpty final String ns) {
+    public synchronized void setNamespace(@Nonnull @NotEmpty final String ns) {
         throwSetterPreconditionExceptions();
         namespace = Constraint.isNotNull(StringSupport.trimOrNull(ns),
                 "target namespace can not be null or empty");
@@ -61,7 +63,7 @@ public class NamespaceStrippingStage extends AbstractNamespacesStrippingStage {
 
     @Override
     protected boolean removingNamespace(final String ns) {
-        return namespace.equals(ns);
+        return getNamespace().equals(ns);
     }
 
     @Override
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NamespacesStrippingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NamespacesStrippingStage.java
index b43f442..b56695e 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NamespacesStrippingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NamespacesStrippingStage.java
@@ -21,6 +21,8 @@ import java.util.Collection;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
@@ -48,12 +50,13 @@ public class NamespacesStrippingStage extends AbstractNamespacesStrippingStage {
     /**
      * XML namespaces to whitelist or blacklist.
      */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Set<String> namespaces = Set.of();
 
     /**
      * Whether we are whitelisting or blacklisting (default: blacklisting).
      */
+    @GuardedBy("this")
     private boolean whitelisting;
 
     /**
@@ -62,7 +65,7 @@ public class NamespacesStrippingStage extends AbstractNamespacesStrippingStage {
      * @return collection of namespaces being removed
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<String> getNamespaces() {
+    public final synchronized Collection<String> getNamespaces() {
         return namespaces;
     }
     
@@ -71,7 +74,7 @@ public class NamespacesStrippingStage extends AbstractNamespacesStrippingStage {
      * 
      * @param nss collection of namespaces
      */
-    public void setNamespaces(@Nonnull @NonnullElements @Unmodifiable final Collection<String> nss) {
+    public synchronized void setNamespaces(@Nonnull @NonnullElements @Unmodifiable final Collection<String> nss) {
         throwSetterPreconditionExceptions();
         namespaces = Set.copyOf(nss);
     }
@@ -81,7 +84,7 @@ public class NamespacesStrippingStage extends AbstractNamespacesStrippingStage {
      * 
      * @return <code>true</code> for whitelisting, <code>false</code> for blacklisting (the default)
      */
-    public boolean isWhitelisting() {
+    public final synchronized boolean isWhitelisting() {
         return whitelisting;
     }
 
@@ -90,20 +93,20 @@ public class NamespacesStrippingStage extends AbstractNamespacesStrippingStage {
      * 
      * @param wl <code>true</code> for whitelisting, <code>false</code> for blacklisting
      */
-    public void setWhitelisting(final boolean wl) {
+    public synchronized void setWhitelisting(final boolean wl) {
         throwSetterPreconditionExceptions();
         whitelisting = wl;
     }
     
     @Override
-    protected boolean removingNamespace(final String namespace) {
+    protected boolean removingNamespace(@Nullable final String namespace) {
         // Handle ineligible null element, for the default namespace case
         if (namespace == null) {
-            return whitelisting;
+            return isWhitelisting();
         }
         
         // Handle normal namespaces
-        return whitelisting ^ namespaces.contains(namespace);
+        return isWhitelisting() ^ getNamespaces().contains(namespace);
     }
 
     @Override
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NodeVisitor.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NodeVisitor.java
index 10ff174..fb9c12d 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NodeVisitor.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/NodeVisitor.java
@@ -19,6 +19,9 @@ package net.shibboleth.metadata.dom;
 
 import net.shibboleth.metadata.Item;
 
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 
@@ -26,8 +29,13 @@ import org.w3c.dom.Node;
  * Provides a variation of the Visitor pattern for performing operations on
  * DOM nodes which are part of {@link Element} items.
  *
+ * <p>
+ * All implementations of this interface <strong>must</strong> be thread-safe.
+ * </p>
+ *
  * @since 0.9.0
  */
+ at ThreadSafe
 public interface NodeVisitor {
 
     /**
@@ -37,6 +45,6 @@ public interface NodeVisitor {
      * @param visited the {@link Node} being visited.
      * @param item the {@link Item} which is the context for the visit.
      */
-    void visitNode(Node visited, Item<Element> item);
-    
+    void visitNode(@Nonnull Node visited, @Nonnull Item<Element> item);
+
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/SimpleDOMTraversalContext.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/SimpleDOMTraversalContext.java
index 24b1f6a..efedfed 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/SimpleDOMTraversalContext.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/SimpleDOMTraversalContext.java
@@ -41,7 +41,7 @@ public class SimpleDOMTraversalContext implements DOMTraversalContext {
     }
 
     @Override
-    public Item<Element> getItem() {
+    public final Item<Element> getItem() {
         return item;
     }
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/WhitespaceTrimmingVisitor.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/WhitespaceTrimmingVisitor.java
index 7899080..325da6d 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/WhitespaceTrimmingVisitor.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/WhitespaceTrimmingVisitor.java
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.dom;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 import org.w3c.dom.Attr;
 import org.w3c.dom.Element;
@@ -31,6 +32,7 @@ import net.shibboleth.metadata.Item;
  *
  * @since 0.9.0
  */
+ at Immutable
 class WhitespaceTrimmingVisitor implements NodeVisitor, ElementVisitor, AttrVisitor {
 
     @Override
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSchemaValidationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSchemaValidationStage.java
index a460ba8..b8c0958 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSchemaValidationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSchemaValidationStage.java
@@ -22,6 +22,7 @@ import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamSource;
@@ -39,6 +40,7 @@ import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.WarningStatus;
 import net.shibboleth.metadata.pipeline.AbstractIteratingStage;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
+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.Unmodifiable;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -65,13 +67,15 @@ public class XMLSchemaValidationStage extends AbstractIteratingStage<Element> {
     private final Logger log = LoggerFactory.getLogger(XMLSchemaValidationStage.class);
 
     /** Collection of schema resources. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<Resource> schemaResources = List.of();
 
     /** Whether Elements are required to be schema valid. Default value: <code>true</code> */
+    @GuardedBy("this")
     private boolean elementRequiredToBeSchemaValid = true;
 
-    /** Schema used to validate Elements. */
+    /** Schema used to validate Elements. Built during initialization. */
+    @NonnullAfterInit @GuardedBy("this")
     private Schema validationSchema;
 
     /**
@@ -80,7 +84,7 @@ public class XMLSchemaValidationStage extends AbstractIteratingStage<Element> {
      * @return unmodifiable list of schema resources against which Elements are validated
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<Resource> getSchemaResources() {
+    public final synchronized List<Resource> getSchemaResources() {
         return schemaResources;
     }
 
@@ -100,7 +104,7 @@ public class XMLSchemaValidationStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether Elements are required to be schema valid
      */
-    public boolean isElementRequiredToBeSchemaValid() {
+    public final synchronized boolean isElementRequiredToBeSchemaValid() {
         return elementRequiredToBeSchemaValid;
     }
 
@@ -114,18 +118,27 @@ public class XMLSchemaValidationStage extends AbstractIteratingStage<Element> {
         elementRequiredToBeSchemaValid = isRequired;
     }
 
+    /**
+     * Returns the shared validation schema built during initialization.
+     *
+     * @return the shared validation schema
+     */
+    private synchronized Schema getValidationSchema() {
+        return validationSchema;
+    }
+
     @Override
     protected void doExecute(@Nonnull final Item<Element> item) throws StageProcessingException {
         log.debug("{} pipeline stage schema validating DOM Element collection elements", getId());
 
-        final Validator validator = validationSchema.newValidator();
+        final Validator validator = getValidationSchema().newValidator();
         try {
             validator.validate(new DOMSource(item.unwrap()));
         } catch (final Exception e) {
             if (log.isDebugEnabled()) {
                 log.debug("DOM Element was not valid:\n{}", SerializeSupport.prettyPrintXML(item.unwrap()), e);
             }
-            if (elementRequiredToBeSchemaValid) {
+            if (isElementRequiredToBeSchemaValid()) {
                 item.getItemMetadata().put(new ErrorStatus(getId(), e.getMessage()));
             } else {
                 item.getItemMetadata().put(new WarningStatus(getId(), e.getMessage()));
@@ -133,16 +146,16 @@ public class XMLSchemaValidationStage extends AbstractIteratingStage<Element> {
         }
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doDestroy() {
+    @Override
+    protected void doDestroy() {
         schemaResources = null;
         validationSchema = null;
         
         super.doDestroy();
     }
     
-    /** {@inheritDoc} */
-    @Override protected void doInitialize() throws ComponentInitializationException {
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
         if (schemaResources.isEmpty()) {
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureSigningStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureSigningStage.java
index f2a09cd..3f04b67 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureSigningStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureSigningStage.java
@@ -17,60 +17,33 @@
 
 package net.shibboleth.metadata.dom;
 
-import java.math.BigInteger;
-import java.nio.charset.StandardCharsets;
 import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.cert.X509CRL;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Collection;
 import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
-import javax.security.auth.x500.X500Principal;
-import javax.xml.crypto.XMLStructure;
-import javax.xml.crypto.dsig.CanonicalizationMethod;
-import javax.xml.crypto.dsig.DigestMethod;
-import javax.xml.crypto.dsig.Reference;
-import javax.xml.crypto.dsig.SignatureMethod;
-import javax.xml.crypto.dsig.SignedInfo;
-import javax.xml.crypto.dsig.Transform;
-import javax.xml.crypto.dsig.XMLSignContext;
-import javax.xml.crypto.dsig.XMLSignature;
-import javax.xml.crypto.dsig.XMLSignatureFactory;
-import javax.xml.crypto.dsig.dom.DOMSignContext;
-import javax.xml.crypto.dsig.keyinfo.KeyInfo;
-import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
-import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
-import javax.xml.crypto.dsig.spec.DigestMethodParameterSpec;
-import javax.xml.crypto.dsig.spec.ExcC14NParameterSpec;
-import javax.xml.crypto.dsig.spec.TransformParameterSpec;
 import javax.xml.namespace.QName;
 
 import org.apache.xml.security.Init;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.w3c.dom.Attr;
 import org.w3c.dom.Element;
-import org.w3c.dom.NamedNodeMap;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
 
 import net.shibboleth.metadata.Item;
-import net.shibboleth.metadata.dom.ds.XMLDSIGSupport;
-import net.shibboleth.metadata.pipeline.AbstractIteratingStage;
+import net.shibboleth.metadata.dom.impl.XMLSignatureSigner;
+import net.shibboleth.metadata.pipeline.AbstractStage;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
-import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+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.Unmodifiable;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.primitive.StringSupport;
-import net.shibboleth.utilities.java.support.xml.ElementSupport;
-import net.shibboleth.utilities.java.support.xml.QNameSupport;
 
 /**
  * A pipeline stage that creates, and adds, an enveloped signature for each element in the given {@link DOMElementItem}
@@ -83,80 +56,72 @@ import net.shibboleth.utilities.java.support.xml.QNameSupport;
  * </ul>
  */
 @ThreadSafe
-public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
+public class XMLSignatureSigningStage extends AbstractStage<Element> {
 
     /** The variant of SHA to use in the various signature algorithms. */
     public static enum ShaVariant {
-        SHA1, SHA256, SHA384, SHA512
+        /** 160-bit SHA-1. */
+        SHA1,
+        /** 256-bit SHA-2. */
+        SHA256,
+        /** 384-bit SHA-2. */
+        SHA384,
+        /** 512-bit SHA-2. */
+        SHA512
     };
 
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(XMLSignatureSigningStage.class);
 
-    /** Factory used to create XML signature objects. */
-    private XMLSignatureFactory xmlSigFactory;
-
-    /** Factory used to create KeyInfo objects. */
-    private KeyInfoFactory keyInfoFactory;
-
     /** SHA algorithm variant used in signature and digest algorithms. Default value: <code>ShaVariant.SHA256</code> */
+    @Nonnull @GuardedBy("this")
     private ShaVariant shaVariant = ShaVariant.SHA256;
 
     /** Private key used to sign data. */
+    @NonnullAfterInit @GuardedBy("this")
     private PrivateKey privKey;
 
     /** Public key associated with the given private key. */
+    @Nullable @GuardedBy("this")
     private PublicKey publicKey;
 
     /**
      * Certificate chain, with end entity certificate as element 0, to be included with the signature. Default value:
      * empty list
      */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<X509Certificate> certificates = List.of();
 
     /** CRLs to be included with the signature. Default value: empty list */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<X509CRL> crls = List.of();
 
-    /** Signature algorithm used. */
-    private String sigAlgo;
-
-    /** Digest algorithm used. */
-    private String digestAlgo;
-
     /** Whether to use exclusive canonicalization. Default value: <code>true</code> */
-    private boolean c14nExclusive = true;
+    @GuardedBy("this") private boolean c14nExclusive = true;
 
     /** Whether to include comments in the canonicalized data. Default value: <code>false</code> */
-    private boolean c14nWithComments;
+    @GuardedBy("this") private boolean c14nWithComments;
 
     /** Whether to remove CR characters from generated signatures. Default value: <code>true</code>. */
-    private boolean removingCRsFromSignature = true;
-
-    /**
-     * Canonicalization algorithm to use. This is determined from the {@link #c14nExclusive} and
-     * {@link #c14nWithComments} properties.
-     */
-    private String c14nAlgo;
+    @GuardedBy("this") private boolean removingCRsFromSignature = true;
 
     /** Inclusive prefix list used with exclusive canonicalization. Default value: empty list */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<String> inclusivePrefixList = List.of();
 
     /**
      * Names of attributes to treat as ID attributes for signature referencing. Default value: list containing the
      * non-namespace-qualified attributes 'ID', 'Id', 'id'
      */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<QName> idAttributeNames = List.of(new QName("ID"), new QName("id"), new QName("Id"));
 
     /** Explicit names to associate with the given signing key. Default value: empty list */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<String> keyNames = List.of();
 
     /** Whether key names should be included in the signature's KeyInfo. Default value: <code>true</code> */
-    private boolean includeKeyNames = true;
+    @GuardedBy("this") private boolean includeKeyNames = true;
 
     /**
      * Whether the public key should be included in the signature's KeyInfo.
@@ -166,44 +131,37 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * Default value: <code>false</code>
      */
-    private boolean includeKeyValue;
+    @GuardedBy("this") private boolean includeKeyValue;
 
     /**
      * Whether the end-entity certificate's subject name should be included in the signature's KeyInfo. Default value:
      * <code>false</code>
      */
-    private boolean includeX509SubjectName;
+    @GuardedBy("this") private boolean includeX509SubjectName;
 
     /**
      * Whether the certificates chain should be included in the signature's KeyInfo. Default value: <code>true</code>
      */
-    private boolean includeX509Certificates = true;
+    @GuardedBy("this") private boolean includeX509Certificates = true;
 
     /** Whether the CRLs should be included in the signature's KeyInfo. Default value: <code>false</code> */
-    private boolean includeX509Crls;
+    @GuardedBy("this") private boolean includeX509Crls;
 
     /**
      * Whether the end-entity certificate's issuer and serial number should be included in the signature's KeyInfo.
      * Default value: <code>false</code>
      */
-    private boolean includeX509IssuerSerial;
+    @GuardedBy("this") private boolean includeX509IssuerSerial;
 
     /** Whether to debug digest operations by logging the pre-digest data stream. Default value: <code>false</code> */
-    private boolean debugPreDigest;
-    
-    /**
-     * Constructor.
-     */
-    public XMLSignatureSigningStage() {
-        shaVariant = ShaVariant.SHA256;
-    }
+    @GuardedBy("this") private boolean debugPreDigest;
 
     /**
      * Gets the SHA algorithm variant used when computing the signature and digest.
      * 
      * @return SHA algorithm variant used when computing the signature and digest
      */
-    @Nonnull public ShaVariant getShaVariant() {
+    @Nonnull public final synchronized ShaVariant getShaVariant() {
         return shaVariant;
     }
 
@@ -222,7 +180,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return the privKey private key used to sign the content
      */
-    @Nullable public PrivateKey getPrivateKey() {
+    @Nullable public final synchronized PrivateKey getPrivateKey() {
         return privKey;
     }
 
@@ -241,7 +199,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return public key associated with private key used to sign the content
      */
-    @Nullable public PublicKey getPublicKey() {
+    @Nullable public final synchronized PublicKey getPublicKey() {
         return publicKey;
     }
 
@@ -262,7 +220,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * @return certificates associated with the key used to sign the content
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<X509Certificate> getCertificates() {
+    public final synchronized List<X509Certificate> getCertificates() {
         return certificates;
     }
 
@@ -284,7 +242,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * @return CRLs associated with certificates
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<X509CRL> getCrls() {
+    public final synchronized List<X509CRL> getCrls() {
         return crls;
     }
 
@@ -304,7 +262,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether exclusive canonicalization will be used
      */
-    public boolean isC14nExclusive() {
+    public final synchronized boolean isC14nExclusive() {
         return c14nExclusive;
     }
 
@@ -323,7 +281,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether comments are canonicalized
      */
-    public boolean isC14nWithComments() {
+    public final synchronized boolean isC14nWithComments() {
         return c14nWithComments;
     }
 
@@ -343,7 +301,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * @return inclusive prefix list used during exclusive canonicalization
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<String> getInclusivePrefixList() {
+    public final synchronized List<String> getInclusivePrefixList() {
         return inclusivePrefixList;
     }
 
@@ -364,7 +322,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * @return names of the attributes treated as reference IDs
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<QName> getIdAttributeNames() {
+    public final synchronized List<QName> getIdAttributeNames() {
         return idAttributeNames;
     }
 
@@ -387,7 +345,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * @return explicit key names added to the KeyInfo
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<String> getKeyNames() {
+    public final synchronized List<String> getKeyNames() {
         return keyNames;
     }
 
@@ -407,7 +365,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether key names are included in the KeyInfo
      */
-    public boolean isIncludeKeyNames() {
+    public final synchronized boolean isIncludeKeyNames() {
         return includeKeyNames;
     }
 
@@ -426,7 +384,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether key values are included in the KeyInfo
      */
-    public boolean isIncludeKeyValue() {
+    public final synchronized boolean isIncludeKeyValue() {
         return includeKeyValue;
     }
 
@@ -445,7 +403,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether end-entity certifcate's subject name is included in the KeyInfo
      */
-    public boolean isIncludeX509SubjectName() {
+    public final synchronized boolean isIncludeX509SubjectName() {
         return includeX509SubjectName;
     }
 
@@ -464,7 +422,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether X509 certificates are included in the KeyInfo
      */
-    public boolean isIncludeX509Certificates() {
+    public final synchronized boolean isIncludeX509Certificates() {
         return includeX509Certificates;
     }
 
@@ -483,7 +441,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether CRLs are included in the KeyInfo
      */
-    public boolean isIncludeX509Crls() {
+    public final synchronized boolean isIncludeX509Crls() {
         return includeX509Crls;
     }
 
@@ -502,7 +460,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether the end-entity certificate's issuer and serial number are included in the KeyInfo
      */
-    public boolean isIncludeX509IssuerSerial() {
+    public final synchronized boolean isIncludeX509IssuerSerial() {
         return includeX509IssuerSerial;
     }
 
@@ -521,7 +479,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
      * 
      * @return whether logging of the pre-digest data stream is enabled
      */
-    public boolean isDebugPreDigest() {
+    public final synchronized boolean isDebugPreDigest() {
         return debugPreDigest;
     }
     
@@ -535,30 +493,12 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
         debugPreDigest = debug;
     }
     
-    /**
-     * Gets the signature algorithm used when signing.
-     * 
-     * @return signature algorithm used when signing
-     */
-    public String getSigAlgo() {
-        return sigAlgo;
-    }
-
-    /**
-     * Gets the digest algorithm used when signing.
-     * 
-     * @return digest algorithm used when signing
-     */
-    public String getDigestAlgo() {
-        return digestAlgo;
-    }
-
     /**
      * Gets whether CR characters will be removed from generated signatures.
      *
      * @return <code>true</code> if CR characters will be removed from generated signatures.
      */
-    public boolean isRemovingCRsFromSignature() {
+    public final synchronized boolean isRemovingCRsFromSignature() {
         return removingCRsFromSignature;
     }
 
@@ -572,314 +512,21 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
         removingCRsFromSignature = newValue;
     }
 
-    /**
-     * Remove any CRs from the text content of named child elements.
-     *
-     * @param signature The <code>Signature</code> element to process.
-     * @param elementName The element name within the XML DSIG namespace to look for.
-     */
-    private void removeCRsFromNamedChildren(@Nonnull final Element signature, @Nonnull final String elementName) {
-        final NodeList nodes = signature.getElementsByTagNameNS(XMLSignature.XMLNS, elementName);
-        for (int i = 0; i < nodes.getLength(); i++) {
-            final Node node = nodes.item(i);
-            final String text = node.getTextContent();
-            if (text.indexOf('\r') >= 0) {
-                node.setTextContent(text.replaceAll("\\r", ""));
-            }
-        }
-    }
-
     @Override
-    protected void doExecute(@Nonnull final Item<Element> item) throws StageProcessingException {
-        final Element element = item.unwrap();
-        final XMLSignature signature = xmlSigFactory.newXMLSignature(buildSignedInfo(element), buildKeyInfo());
-        try {
-            final XMLSignContext context = new DOMSignContext(privKey, element, element.getFirstChild());
-            
-            // Enable caching reference values if required for debugging.
-            if (isDebugPreDigest() && log.isDebugEnabled()) {
-                context.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE);
-            }
-            
-            // Perform the signature operation
-            signature.sign(context);
-            
-            // Remove any CRs from selected signature elements.
-            if (isRemovingCRsFromSignature()) {
-                final Element signatureElement = ElementSupport.getFirstChildElement(element,
-                        XMLDSIGSupport.SIGNATURE_NAME);
-                removeCRsFromNamedChildren(signatureElement, "SignatureValue");
-                removeCRsFromNamedChildren(signatureElement, "X509Certificate");
-            }
-
-            // Log the pre-digest data for debugging
-            if (isDebugPreDigest() && log.isDebugEnabled()) {
-                final Reference ref = signature.getSignedInfo().getReferences().get(0);
-                final String preDigest = new String(ref.getDigestInputStream().readAllBytes(), StandardCharsets.UTF_8);
-                log.debug("pre digest: {}", preDigest);
-            }
-        } catch (final Exception e) {
-            log.error("Unable to create signature for element", e);
-            throw new StageProcessingException("Unable to create signature for element", e);
-        }
-    }
-
-    /**
-     * Gets the descriptor of signed content.
-     * 
-     * @param target the element that will be signed
-     * 
-     * @return signed content descriptor
-     * 
-     * @throws StageProcessingException thrown if there is a problem creating the signed content descriptor
-     */
-    @Nonnull protected SignedInfo buildSignedInfo(@Nonnull final Element target) throws StageProcessingException {
-        C14NMethodParameterSpec c14nMethodSpec = null;
-        if (c14nAlgo.startsWith(CanonicalizationMethod.EXCLUSIVE) && inclusivePrefixList != null
-                && !inclusivePrefixList.isEmpty()) {
-            c14nMethodSpec = new ExcC14NParameterSpec(inclusivePrefixList);
-        }
-
-        final CanonicalizationMethod c14nMethod;
-        try {
-            c14nMethod = xmlSigFactory.newCanonicalizationMethod(c14nAlgo, c14nMethodSpec);
-        } catch (final Exception e) {
-            final String errMsg = "Unable to create transform " + c14nAlgo;
-            log.error(errMsg, e);
-            throw new StageProcessingException(errMsg, e);
-        }
-
-        final SignatureMethod sigMethod;
-        try {
-            sigMethod = xmlSigFactory.newSignatureMethod(sigAlgo, null);
-        } catch (final Exception e) {
-            final String errMsg = "Unable to create signature method " + sigAlgo;
-            log.error(errMsg, e);
-            throw new StageProcessingException(errMsg, e);
-        }
-
-        final List<Reference> refs = Collections.singletonList(buildSignatureReference(target));
-
-        return xmlSigFactory.newSignedInfo(c14nMethod, sigMethod, refs);
-    }
-
-    /**
-     * Builds the references to the signed content.
-     * 
-     * @param target the element to be signed
-     * 
-     * @return reference to signed content
-     * 
-     * @throws StageProcessingException thrown if there is a problem creating the reference to the element
-     */
-    @Nonnull protected Reference buildSignatureReference(@Nonnull final Element target)
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection)
             throws StageProcessingException {
-        final String id = getElementId(target);
-        final String refUri;
-        if (id == null) {
-            refUri = "";
-        } else {
-            refUri = "#" + id;
-        }
-
-        DigestMethod digestMethod = null;
-        try {
-            final DigestMethodParameterSpec digestMethodSpec = null;
-            digestMethod = xmlSigFactory.newDigestMethod(digestAlgo, digestMethodSpec);
-        } catch (final Exception e) {
-            final String errMsg = "Unable to create digest method " + digestAlgo;
-            log.error(errMsg, e);
-            throw new StageProcessingException(errMsg, e);
-        }
-
-        TransformParameterSpec transformSpec;
-        final ArrayList<Transform> transforms = new ArrayList<>();
-
-        try {
-            transformSpec = null;
-            transforms.add(xmlSigFactory.newTransform(Transform.ENVELOPED, transformSpec));
-        } catch (final Exception e) {
-            final String errMsg = "Unable to create transform " + Transform.ENVELOPED;
-            log.error(errMsg, e);
-            throw new StageProcessingException(errMsg, e);
-        }
-
-        try {
-            if (c14nAlgo.startsWith(CanonicalizationMethod.EXCLUSIVE) && inclusivePrefixList != null
-                    && !inclusivePrefixList.isEmpty()) {
-                transformSpec = new ExcC14NParameterSpec(inclusivePrefixList);
-            }
-            transforms.add(xmlSigFactory.newTransform(c14nAlgo, transformSpec));
-        } catch (final Exception e) {
-            final String errMsg = "Unable to create transform " + c14nAlgo;
-            log.error(errMsg, e);
-            throw new StageProcessingException(errMsg, e);
-        }
-
-        return xmlSigFactory.newReference(refUri, digestMethod, transforms, null, null);
-    }
-
-    /**
-     * Determines the ID for the element to be signed. To determine the ID first, all the element attributes are
-     * inspected, if one matches the provided {@link #idAttributeNames} then the value of the attribute is used as the
-     * ID value. If no ID attribute names are given, or none of the given ones match, and one or more of the attributes
-     * is marked as an ID attribute (i.e. {@link Attr#isId()} is true), then the value of one of those attributes is
-     * used.
-     * 
-     * @param target an element to be referenced by the signature
-     * 
-     * @return the ID value for the element, or null
-     */
-    @Nullable protected String getElementId(@Nonnull final Element target) {
-        final NamedNodeMap attributes = target.getAttributes();
-        if (attributes == null || attributes.getLength() < 1) {
-            return null;
-        }
-
-        if (idAttributeNames != null && !idAttributeNames.isEmpty()) {
-            for (int i = 0; i < attributes.getLength(); i++) {
-                final Attr attribute = (Attr) attributes.item(i);
-                if (idAttributeNames.contains(QNameSupport.getNodeQName(attribute))) {
-                    // mark the attribute as an ID attribute so that it can be referenced by the signature
-                    target.setIdAttributeNode(attribute, true);
-                    final String value = StringSupport.trimOrNull(attribute.getValue());
-                    if (value != null) {
-                        return value;
-                    }
-                }
-            }
-        }
-
-        for (int i = 0; i < attributes.getLength(); i++) {
-            final Attr attribute = (Attr) attributes.item(i);
-            if (attribute.isId()) {
-                final String value = StringSupport.trimOrNull(attribute.getValue());
-                if (value != null) {
-                    return value;
-                }
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * Builds the KeyInfo element to be included in the signature.
-     * 
-     * @return KeyInfo element to be included in the signature
-     * 
-     * @throws StageProcessingException thrown if there is a problem creating the KeyInfo descriptor
-     */
-    @Nullable protected KeyInfo buildKeyInfo() throws StageProcessingException {
-        final List<XMLStructure> keyInfoItems = new ArrayList<>();
-
-        addKeyNames(keyInfoItems);
-        addKeyValue(keyInfoItems);
-        addX509Data(keyInfoItems);
-
-        if (keyInfoItems.isEmpty()) {
-            return null;
-        }
-        return keyInfoFactory.newKeyInfo(keyInfoItems);
-    }
-
-    /**
-     * Adds key names to the KeyInfo, if key names are to be included.
-     * 
-     * @param keyInfoItems collector for KeyInfo children
-     * 
-     * @throws StageProcessingException thrown if there is a problem creating the KeyName content
-     */
-    protected void addKeyNames(@Nonnull @NonnullElements @Live final List<XMLStructure> keyInfoItems)
-            throws StageProcessingException {
-        if (!includeKeyNames) {
-            return;
-        }
-
-        if (keyNames != null && !keyNames.isEmpty()) {
-            for (final String name : keyNames) {
-                keyInfoItems.add(keyInfoFactory.newKeyName(name));
-            }
-        }
-    }
-
-    /**
-     * Adds raw key values to the KeyInfo if key values are to be included.
-     * 
-     * @param keyInfoItems collector for KeyInfo children
-     * 
-     * @throws StageProcessingException thrown if there is a problem creating the KeyValue content
-     */
-    protected void addKeyValue(@Nonnull @NonnullElements @Live final List<XMLStructure> keyInfoItems)
-            throws StageProcessingException {
-        if (!includeKeyValue) {
-            return;
-        }
-
-        PublicKey key = publicKey;
-        
-        // If we have no explicit public key, we can extract one from a certificate, if we have one.
-        if (key == null && !certificates.isEmpty()) {
-            key = certificates.get(0).getPublicKey();
-        }
-
-        if (key != null) {
-            try {
-                keyInfoItems.add(keyInfoFactory.newKeyValue(key));
-            } catch (final Exception e) {
-                log.error("Unable to create KeyValue", e);
-                throw new StageProcessingException("Unable to create KeyValue", e);
-            }
-        }
-    }
-
-    /**
-     * Adds X509 data (subject names, certificates, CRLs, and Issuer/Serial) set to be included, into the key info.
-     * 
-     * @param keyInfoItems collector for KeyInfo children
-     */
-    protected void addX509Data(@Nonnull @NonnullElements @Live final List<XMLStructure> keyInfoItems) {
-        final List<Object> x509Data = new ArrayList<>();
-
-        if (certificates != null && !certificates.isEmpty()) {
-            final X509Certificate endEntityCert = certificates.get(0);
-
-            if (includeX509SubjectName) {
-                final X500Principal subjectDn = endEntityCert.getSubjectX500Principal();
-                x509Data.add(subjectDn.getName(X500Principal.RFC2253));
-            }
-
-            if (includeX509Certificates) {
-                x509Data.addAll(certificates);
-            }
-
-            if (includeX509IssuerSerial) {
-                final X500Principal issuerDn = endEntityCert.getIssuerX500Principal();
-                final BigInteger serialNumber = endEntityCert.getSerialNumber();
-                x509Data.add(keyInfoFactory.newX509IssuerSerial(issuerDn.getName(X500Principal.RFC2253), serialNumber));
-            }
-        }
-
-        if (includeX509Crls && crls != null && !crls.isEmpty()) {
-            x509Data.add(crls);
-        }
-
-        if (!x509Data.isEmpty()) {
-            keyInfoItems.add(keyInfoFactory.newX509Data(x509Data));
+        final var signer = new XMLSignatureSigner(this, log);
+        for (final Item<Element> item : itemCollection) {
+            signer.sign(item);
         }
     }
 
     @Override
     protected void doDestroy() {
-        xmlSigFactory = null;
-        keyInfoFactory = null;
         privKey = null;
         publicKey = null;
         certificates = null;
         crls = null;
-        sigAlgo = null;
-        digestAlgo = null;
-        c14nAlgo = null;
         inclusivePrefixList = null;
         idAttributeNames = null;
         keyNames = null;
@@ -891,49 +538,7 @@ public class XMLSignatureSigningStage extends AbstractIteratingStage<Element> {
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
-        if (!Init.isInitialized()) {
-            Init.init();
-        }
-
-        xmlSigFactory = XMLSignatureFactory.getInstance();
-        keyInfoFactory = xmlSigFactory.getKeyInfoFactory();
-
-        switch (shaVariant) {
-            case SHA1:
-                sigAlgo = SignatureMethod.RSA_SHA1;
-                digestAlgo = DigestMethod.SHA1;
-                break;
-
-            case SHA384:
-                sigAlgo = SignatureMethod.RSA_SHA384;
-                digestAlgo = DigestMethod.SHA384;
-                break;
-
-            case SHA512:
-                sigAlgo = SignatureMethod.RSA_SHA512;
-                digestAlgo = DigestMethod.SHA512;
-                break;
-
-            case SHA256:
-            default:
-                sigAlgo = SignatureMethod.RSA_SHA256;
-                digestAlgo = DigestMethod.SHA256;
-                break;
-        }
-
-        if (c14nExclusive) {
-            if (c14nWithComments) {
-                c14nAlgo = CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS;
-            } else {
-                c14nAlgo = CanonicalizationMethod.EXCLUSIVE;
-            }
-        } else {
-            if (c14nWithComments) {
-                c14nAlgo = CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS;
-            } else {
-                c14nAlgo = CanonicalizationMethod.INCLUSIVE;
-            }
-        }
-
+        Init.init();
     }
+
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureValidationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureValidationStage.java
index 02ea65c..be6d7a5 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureValidationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureValidationStage.java
@@ -24,6 +24,7 @@ import java.util.Set;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.apache.xml.security.Init;
@@ -34,9 +35,10 @@ import org.w3c.dom.Element;
 import net.shibboleth.metadata.ErrorStatus;
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.WarningStatus;
-import net.shibboleth.metadata.dom.XMLSignatureValidator.ValidationException;
-import net.shibboleth.metadata.pipeline.AbstractIteratingStage;
-import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.metadata.dom.impl.XMLSignatureValidator;
+import net.shibboleth.metadata.dom.impl.XMLSignatureValidator.ValidationException;
+import net.shibboleth.metadata.pipeline.AbstractStage;
+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.Unmodifiable;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -60,41 +62,45 @@ import net.shibboleth.utilities.java.support.xml.SerializeSupport;
  * valid and an Element signature is found to be invalid than an {@link WarningStatus} is set on the Element.
  */
 @ThreadSafe
-public class XMLSignatureValidationStage extends AbstractIteratingStage<Element> {
+public class XMLSignatureValidationStage extends AbstractStage<Element> {
 
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(XMLSignatureValidationStage.class);
 
     /** Whether Elements are required to be signed. */
+    @GuardedBy("this")
     private boolean signatureRequired = true;
 
     /** Whether the signature on a Elements is required to be valid. Default value: <code>true</code> */
+    @GuardedBy("this")
     private boolean validSignatureRequired = true;
 
     /** Certificate whose public key is used to verify the Element signature. */
+    @Nullable @GuardedBy("this")
     private Certificate verificationCertificate;
 
     /** Public key used to verify the Element signature. */
+    @NonnullAfterInit @GuardedBy("this")
     private PublicKey verificationKey;
     
     /** Set of blacklisted digest URIs. Default value: empty set. */
-    @Nonnull @NonnullElements private Set<String> blacklistedDigests = Set.of();
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
+    private Set<String> blacklistedDigests = Set.of();
     
     /** Set of blacklisted signature method URIs. Default value: empty set. */
-    @Nonnull @NonnullElements private Set<String> blacklistedSignatureMethods = Set.of();
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
+    private Set<String> blacklistedSignatureMethods = Set.of();
     
     /** Option to determine whether empty references are to be permitted.  Default value: <code>true</code>. */
+    @GuardedBy("this")
     private boolean permittingEmptyReferences = true;
-    
-    /** Validator used for all signatures validated by this stage instance. */
-    private XMLSignatureValidator validator;
 
     /**
      * Gets whether the Element is required to be signed.
      * 
      * @return whether the Element is required to be signed
      */
-    public boolean isSignatureRequired() {
+    public final synchronized boolean isSignatureRequired() {
         return signatureRequired;
     }
 
@@ -113,7 +119,7 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
      * 
      * @return whether the signature on a Element element is required to be valid
      */
-    public boolean isValidSignatureRequired() {
+    public final synchronized boolean isValidSignatureRequired() {
         return validSignatureRequired;
     }
 
@@ -128,43 +134,43 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
     }
 
     /**
-     * Gets the key used to verify the signature.
+     * Gets the certificate whose public key is used to verify the signed Element.
      * 
-     * @return key used to verify the signature
+     * @return certificate whose public key is used to verify the signed Element
      */
-    @Nullable public PublicKey getVerificationKey() {
-        return verificationKey;
+    @Nullable public final synchronized Certificate getVerificationCertificate() {
+        return verificationCertificate;
     }
 
     /**
-     * Sets the key used to verify the signature.
+     * Set the key, included in a certificate, used to verify the signature. This method will also set
+     * {@link #verificationKey} with the public key of the certificate.
      * 
-     * @param key key used to verify the signature
+     * @param certificate certificate containing the key used to verify the signature
      */
-    public synchronized void setVerificationKey(@Nonnull final PublicKey key) {
+    public synchronized void setVerificationCertificate(@Nonnull final Certificate certificate) {
         throwSetterPreconditionExceptions();
-        verificationKey = Constraint.isNotNull(key, "Public key can not be null");
+        verificationCertificate = Constraint.isNotNull(certificate, "Certificate can not be null");
+        verificationKey = verificationCertificate.getPublicKey();
     }
 
     /**
-     * Gets the certificate whose public key is used to verify the signed Element.
+     * Gets the key used to verify the signature.
      * 
-     * @return certificate whose public key is used to verify the signed Element
+     * @return key used to verify the signature
      */
-    @Nullable public Certificate getVerificationCertificate() {
-        return verificationCertificate;
+    @NonnullAfterInit public final synchronized PublicKey getVerificationKey() {
+        return verificationKey;
     }
 
     /**
-     * Set the key, included in a certificate, used to verify the signature. This method will also set
-     * {@link #verificationKey} with the public key of the certificate.
+     * Sets the key used to verify the signature.
      * 
-     * @param certificate certificate containing the key used to verify the signature
+     * @param key key used to verify the signature
      */
-    public synchronized void setVerificationCertificate(@Nonnull final Certificate certificate) {
+    public synchronized void setVerificationKey(@Nonnull final PublicKey key) {
         throwSetterPreconditionExceptions();
-        verificationCertificate = Constraint.isNotNull(certificate, "Certificate can not be null");
-        verificationKey = verificationCertificate.getPublicKey();
+        verificationKey = Constraint.isNotNull(key, "Public key can not be null");
     }
     
     /**
@@ -172,7 +178,8 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
      * 
      * @param identifiers collection of identifiers to be blacklisted
      */
-    public void setBlacklistedDigests(@Nonnull @NonnullElements @Unmodifiable final Collection<String> identifiers) {
+    public synchronized void setBlacklistedDigests(
+            @Nonnull @NonnullElements @Unmodifiable final Collection<String> identifiers) {
         throwSetterPreconditionExceptions();
         blacklistedDigests = Set.copyOf(identifiers);
     }
@@ -182,7 +189,7 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
      * 
      * @return the set of blacklisted digest algorithm identifiers
      */
-    @Nonnull @NonnullElements @Unmodifiable public Set<String> getBlacklistedDigests() {
+    @Nonnull @NonnullElements @Unmodifiable public final synchronized Set<String> getBlacklistedDigests() {
         return blacklistedDigests;
     }
 
@@ -191,7 +198,7 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
      * 
      * @param identifiers collection of identifiers to be blacklisted
      */
-    public void setBlacklistedSignatureMethods(
+    public synchronized void setBlacklistedSignatureMethods(
             @Nonnull @NonnullElements @Unmodifiable final Collection<String> identifiers) {
         throwSetterPreconditionExceptions();
         blacklistedSignatureMethods = Set.copyOf(identifiers);
@@ -202,7 +209,7 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
      * 
      * @return the set of blacklisted signature method identifiers
      */
-    @Nonnull @NonnullElements @Unmodifiable public Set<String> getBlacklistedSignatureMethods() {
+    @Nonnull @NonnullElements @Unmodifiable public final synchronized Set<String> getBlacklistedSignatureMethods() {
         return blacklistedSignatureMethods;
     }
 
@@ -211,7 +218,7 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
      * 
      * @return whether empty references are permitted
      */
-    public boolean isPermittingEmptyReferences() {
+    public final synchronized boolean isPermittingEmptyReferences() {
         return permittingEmptyReferences;
     }
 
@@ -225,8 +232,13 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
         permittingEmptyReferences = permit;
     }
 
-    @Override
-    protected void doExecute(@Nonnull final Item<Element> item) throws StageProcessingException {
+    /**
+     * Validate an individual {@link Item} using the provided validator.
+     *
+     * @param item the {@link Item} to validate
+     * @param validator {@link XMLSignatureValidator} to use for the validation
+     */
+    protected void validateItem(@Nonnull final Item<Element> item, @Nonnull final XMLSignatureValidator validator) {
         
         final Element docElement = item.unwrap();
         
@@ -235,7 +247,7 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
         try {
             signatureElement = validator.getSignatureElement(docElement);
             if (signatureElement == null) {
-                if (signatureRequired) {
+                if (isSignatureRequired()) {
                     log.debug("DOM Element was not signed and signature is required");
                     item.getItemMetadata().put(
                             new ErrorStatus(getId(), "DOM Element was not signed but signatures are required"));
@@ -260,7 +272,7 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
         } catch (final ValidationException e) {
             final String message = "element signature is invalid: " + e.getMessage();
             log.debug("setting status: ", message);
-            if (validSignatureRequired) {
+            if (isValidSignatureRequired()) {
                 item.getItemMetadata().put(new ErrorStatus(getId(), message));
             } else {
                 item.getItemMetadata().put(new WarningStatus(getId(), message));
@@ -268,11 +280,22 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
         }
     }
 
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection) {
+        // Create a single non-thread-safe validator
+        final var validator = new XMLSignatureValidator(getVerificationKey(),
+                getBlacklistedDigests(), getBlacklistedSignatureMethods(), isPermittingEmptyReferences());
+
+        // Use it to validate each item in turn
+        for (@Nonnull final var item : itemCollection) {
+            validateItem(item, validator);
+        }
+    }
+
     @Override
     protected void doDestroy() {
         verificationCertificate = null;
         verificationKey = null;
-        validator = null;
         blacklistedDigests = null;
         blacklistedSignatureMethods = null;
 
@@ -283,16 +306,13 @@ public class XMLSignatureValidationStage extends AbstractIteratingStage<Element>
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
+        Init.init();
+
         if (verificationKey == null) {
             throw new ComponentInitializationException("Unable to initialize " + getId()
                     + ", no verification key was specified");
         }
 
-        validator = new XMLSignatureValidator(verificationKey,
-                blacklistedDigests, blacklistedSignatureMethods, permittingEmptyReferences);
-
-        if (!Init.isInitialized()) {
-            Init.init();
-        }
     }
+
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XPathFilteringStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XPathFilteringStage.java
index bfcc53a..77559c8 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XPathFilteringStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XPathFilteringStage.java
@@ -22,6 +22,7 @@ import java.util.Iterator;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.namespace.NamespaceContext;
 import javax.xml.xpath.XPath;
@@ -62,10 +63,11 @@ public class XPathFilteringStage extends AbstractStage<Element> {
     private final Logger log = LoggerFactory.getLogger(XPathFilteringStage.class);
 
     /** The XPath expression to execute on each {@link DOMElementItem}. */
+    @NonnullAfterInit @NotEmpty @GuardedBy("this")
     private String xpathExpression;
 
     /** The {@link NamespaceContext} to use in interpreting the XPath expression. */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private NamespaceContext namespaceContext = new SimpleNamespaceContext();
 
     /**
@@ -73,7 +75,7 @@ public class XPathFilteringStage extends AbstractStage<Element> {
      * 
      * @return XPath expression to execute on each {@link DOMElementItem}
      */
-    @NonnullAfterInit @NotEmpty public String getXPathExpression() {
+    @NonnullAfterInit @NotEmpty public final synchronized String getXPathExpression() {
         return xpathExpression;
     }
 
@@ -93,7 +95,7 @@ public class XPathFilteringStage extends AbstractStage<Element> {
      * 
      * @return {@link NamespaceContext} to use in interpreting the XPath expression
      */
-    @Nonnull public NamespaceContext getNamespaceContext() {
+    @Nonnull public final synchronized NamespaceContext getNamespaceContext() {
         return namespaceContext;
     }
 
@@ -116,11 +118,11 @@ public class XPathFilteringStage extends AbstractStage<Element> {
             throws StageProcessingException {
         final XPathFactory factory = XPathFactory.newInstance();
         final XPath xpath = factory.newXPath();
-        xpath.setNamespaceContext(namespaceContext);
+        xpath.setNamespaceContext(getNamespaceContext());
 
         final XPathExpression compiledExpression;
         try {
-            compiledExpression = xpath.compile(xpathExpression);
+            compiledExpression = xpath.compile(getXPathExpression());
         } catch (final XPathExpressionException e) {
             // This should never occur, as we attempted the same operation at initialization time.
             throw new StageProcessingException("error compiling XPath expression", e);
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XPathItemSelectionStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XPathItemSelectionStrategy.java
index ce3277a..26b1f3d 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XPathItemSelectionStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XPathItemSelectionStrategy.java
@@ -20,6 +20,7 @@ package net.shibboleth.metadata.dom;
 import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.namespace.NamespaceContext;
 import javax.xml.xpath.XPath;
@@ -50,11 +51,9 @@ public class XPathItemSelectionStrategy implements Predicate<Item<Element>> {
      * {@link XPathExpression} objects are reusable but are not thread-safe, so access to the compiled expression must
      * be protected.
      */
+    @Nonnull @GuardedBy("this")
     private final XPathExpression compiledExpression;
 
-    /** The {@link NamespaceContext} to use in interpreting the XPath expression. */
-    private final NamespaceContext namespaceContext;
-
     /**
      * Constructor.
      * 
@@ -64,6 +63,7 @@ public class XPathItemSelectionStrategy implements Predicate<Item<Element>> {
      */
     public XPathItemSelectionStrategy(@Nonnull @NotEmpty final String expression,
             @Nonnull final NamespaceContext context) throws XPathExpressionException {
+        final NamespaceContext namespaceContext;
         if (context == null) {
             namespaceContext = new SimpleNamespaceContext();
         } else {
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XSLValidationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XSLValidationStage.java
index 203253d..0fcb020 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XSLValidationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XSLValidationStage.java
@@ -42,8 +42,8 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElemen
 @ThreadSafe
 public class XSLValidationStage extends AbstractXSLProcessingStage {
 
-    /** {@inheritDoc} */
-    @Override protected void executeTransformer(@Nonnull final Transformer transformer,
+    @Override
+    protected void executeTransformer(@Nonnull final Transformer transformer,
             @Nonnull @NonnullElements final Collection<Item<Element>> itemCollection) throws StageProcessingException,
             TransformerConfigurationException {
 
@@ -56,4 +56,4 @@ public class XSLValidationStage extends AbstractXSLProcessingStage {
             throw new StageProcessingException("Unable to validate DOM Element", e);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ds/X509ValidationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ds/X509ValidationStage.java
index efd10ed..98e53d8 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ds/X509ValidationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/ds/X509ValidationStage.java
@@ -25,6 +25,8 @@ import java.util.HashMap;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.crypto.dsig.XMLSignature;
 
 import org.w3c.dom.Element;
@@ -33,6 +35,7 @@ import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.dom.AbstractDOMValidationStage;
 import net.shibboleth.metadata.dom.SimpleDOMTraversalContext;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.codec.Base64Support;
 import net.shibboleth.utilities.java.support.codec.DecodingException;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -44,6 +47,7 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
  *
  * @since 0.9.0
  */ 
+ at ThreadSafe
 public class X509ValidationStage extends AbstractDOMValidationStage<X509Certificate, X509ValidationStage.Context> {
 
     /** Context class for this kind of traversal. */
@@ -91,6 +95,7 @@ public class X509ValidationStage extends AbstractDOMValidationStage<X509Certific
     }
 
     /** Certificate factory to use to convert to X.509 certificates. */
+    @NonnullAfterInit @GuardedBy("this")
     private CertificateFactory factory;
 
     @Override
@@ -99,7 +104,7 @@ public class X509ValidationStage extends AbstractDOMValidationStage<X509Certific
     }
 
     @Override
-    protected boolean applicable(@Nonnull final Element e) {
+    protected boolean applicable(@Nonnull final Element e, @Nonnull final Context context) {
         return XMLSignature.XMLNS.equals(e.getNamespaceURI()) &&
                 "X509Certificate".equals(e.getLocalName());
     }
@@ -110,8 +115,12 @@ public class X509ValidationStage extends AbstractDOMValidationStage<X509Certific
         final String text = element.getTextContent();        
         try {
             final byte[] data = Base64Support.decode(text);
-            final X509Certificate cert =
-                    (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(data));
+
+            final X509Certificate cert;
+            synchronized (this) {
+                    cert = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(data));
+            }
+
             // only process each certificate once per item
             if (!context.haveSeen(cert)) {
                 context.add(cert);
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/impl/XMLSignatureSigner.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/impl/XMLSignatureSigner.java
new file mode 100644
index 0000000..88df0d6
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/impl/XMLSignatureSigner.java
@@ -0,0 +1,545 @@
+/*
+ * 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 net.shibboleth.metadata.dom.impl;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509CRL;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.security.auth.x500.X500Principal;
+import javax.xml.crypto.XMLStructure;
+import javax.xml.crypto.dsig.CanonicalizationMethod;
+import javax.xml.crypto.dsig.DigestMethod;
+import javax.xml.crypto.dsig.Reference;
+import javax.xml.crypto.dsig.SignatureMethod;
+import javax.xml.crypto.dsig.SignedInfo;
+import javax.xml.crypto.dsig.Transform;
+import javax.xml.crypto.dsig.XMLSignContext;
+import javax.xml.crypto.dsig.XMLSignature;
+import javax.xml.crypto.dsig.XMLSignatureFactory;
+import javax.xml.crypto.dsig.dom.DOMSignContext;
+import javax.xml.crypto.dsig.keyinfo.KeyInfo;
+import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
+import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
+import javax.xml.crypto.dsig.spec.DigestMethodParameterSpec;
+import javax.xml.crypto.dsig.spec.ExcC14NParameterSpec;
+import javax.xml.crypto.dsig.spec.TransformParameterSpec;
+import javax.xml.namespace.QName;
+
+import org.slf4j.Logger;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.dom.XMLSignatureSigningStage;
+import net.shibboleth.metadata.dom.ds.XMLDSIGSupport;
+import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.xml.ElementSupport;
+import net.shibboleth.utilities.java.support.xml.QNameSupport;
+
+/**
+ * A class that encapsulates the various stages in signing an XML document.
+ * 
+ * <p>
+ * This implementation is not thread safe, but can be used sequentially to sign
+ * a number of different documents.
+ * </p>
+ * 
+ * <p>
+ * As an adjunct to an {@link XMLSignatureSigningStage} instance, it snapshots its
+ * parameters during construction so that they can be used without synchronization
+ * during operation.
+ * </p>
+ */
+ at NotThreadSafe
+public class XMLSignatureSigner {
+
+    /* Fields passed from the calling class. */
+
+    /** Logger provided by the calling class. */
+    @Nonnull private final Logger log;
+
+    /** Private key used to sign data. */
+    @Nonnull private final PrivateKey privKey;
+
+    /** Public key associated with the given private key. */
+    @Nullable private final PublicKey publicKey;
+
+    /** Inclusive prefix list used with exclusive canonicalization. */
+    @Nonnull @NonnullElements @Unmodifiable
+    private final List<String> inclusivePrefixList;
+
+    /**
+     * Names of attributes to treat as ID attributes for signature referencing. Default value: list containing the
+     * non-namespace-qualified attributes 'ID', 'Id', 'id'
+     */
+    @Nonnull @NonnullElements @Unmodifiable
+    private final List<QName> idAttributeNames;
+
+    /** Explicit names to associate with the given signing key. */
+    @Nonnull @NonnullElements @Unmodifiable
+    private final List<String> keyNames;
+
+    /** Certificate chain, with end entity certificate as element 0, to be included with the signature. */
+    @Nonnull @NonnullElements @Unmodifiable
+    private final List<X509Certificate> certificates;
+
+    /** CRLs to be included with the signature. */
+    @Nonnull @NonnullElements @Unmodifiable
+    private final List<X509CRL> crls;
+
+    /** Whether key names should be included in the signature's KeyInfo. */
+    private final boolean includeKeyNames;
+
+    /** Whether the public key should be included in the signature's KeyInfo. */
+    private final boolean includeKeyValue;
+
+    /** Whether the end-entity certificate's subject name should be included in the signature's KeyInfo. */
+    private final boolean includeX509SubjectName;
+
+    /** Whether the certificates chain should be included in the signature's KeyInfo. */
+    private final boolean includeX509Certificates;
+
+    /** Whether the CRLs should be included in the signature's KeyInfo. */
+    private final boolean includeX509Crls;
+
+    /** Whether the end-entity certificate's issuer and serial number should be included in the signature's KeyInfo. */
+    private final boolean includeX509IssuerSerial;
+
+    /** Whether to debug digest operations by logging the pre-digest data stream. */
+    private final boolean debugPreDigest;
+
+    /** Whether to remove CR characters from generated signatures. */
+    private final boolean removingCRsFromSignature;
+
+    /* Fields derived from the calling class. */
+
+    /**
+     * Canonicalization algorithm to use. This is determined from the {@code c14nExclusive} and
+     * {@code c14nWithComments} properties.
+     */
+    @Nonnull private final String c14nAlgo;
+
+    /** Signature algorithm used. */
+    @Nonnull private final String sigAlgo;
+
+    /** Digest algorithm used. */
+    @Nonnull private final String digestAlgo;
+
+    /* Fields created during construction. */
+
+    /** Factory used to create XML signature objects. */
+    @Nonnull private final XMLSignatureFactory xmlSigFactory = XMLSignatureFactory.getInstance();
+
+    /** Factory used to create KeyInfo objects. */
+    @Nonnull private KeyInfoFactory keyInfoFactory = xmlSigFactory.getKeyInfoFactory();
+
+    /**
+     * Constructor.
+     *
+     * @param stage the {@link XMLSignatureSigningStage} we are acting as an adjunct to
+     * @param logger the logger for the calling stage
+     */
+    public XMLSignatureSigner(@Nonnull final XMLSignatureSigningStage stage, @Nonnull final Logger logger) {
+        log = logger;
+
+        /*
+         * Snapshot parameters while holding the lock on the stage, reducing the
+         * cost of the synchronization on the individual getters.
+         */
+        synchronized (stage) {
+            /* Whether to use exclusive canonicalization */
+            final boolean c14nExclusive = stage.isC14nExclusive();
+
+            /* Whether to include comments in the canonicalized data. */
+            final boolean c14nWithComments = stage.isC14nWithComments();
+
+            if (c14nExclusive) {
+                if (c14nWithComments) {
+                    c14nAlgo = CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS;
+                } else {
+                    c14nAlgo = CanonicalizationMethod.EXCLUSIVE;
+                }
+            } else {
+                if (c14nWithComments) {
+                    c14nAlgo = CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS;
+                } else {
+                    c14nAlgo = CanonicalizationMethod.INCLUSIVE;
+                }
+            }
+
+            switch (stage.getShaVariant()) {
+                case SHA1:
+                    sigAlgo = SignatureMethod.RSA_SHA1;
+                    digestAlgo = DigestMethod.SHA1;
+                    break;
+
+                case SHA384:
+                    sigAlgo = SignatureMethod.RSA_SHA384;
+                    digestAlgo = DigestMethod.SHA384;
+                    break;
+
+                case SHA512:
+                    sigAlgo = SignatureMethod.RSA_SHA512;
+                    digestAlgo = DigestMethod.SHA512;
+                    break;
+
+                case SHA256:
+                default:
+                    sigAlgo = SignatureMethod.RSA_SHA256;
+                    digestAlgo = DigestMethod.SHA256;
+                    break;
+            }
+
+            privKey = stage.getPrivateKey();
+            publicKey = stage.getPublicKey();
+            inclusivePrefixList = stage.getInclusivePrefixList();
+            idAttributeNames = stage.getIdAttributeNames();
+            keyNames = stage.getKeyNames();
+            certificates = stage.getCertificates();
+            crls = stage.getCrls();
+            includeKeyNames = stage.isIncludeKeyNames();
+            includeKeyValue = stage.isIncludeKeyValue();
+            includeX509SubjectName = stage.isIncludeX509SubjectName();
+            includeX509Certificates = stage.isIncludeX509Certificates();
+            includeX509Crls = stage.isIncludeX509Crls();
+            includeX509IssuerSerial = stage.isIncludeX509IssuerSerial();
+            debugPreDigest = stage.isDebugPreDigest() & log.isDebugEnabled();
+            removingCRsFromSignature = stage.isRemovingCRsFromSignature();
+        }
+
+    }
+    
+    /**
+     * Sign an individual item.
+     * 
+     * @param item the {@link Item} to sign
+     * @throws StageProcessingException if an error occurs preventing the signature from being made
+     */
+    public void sign(@Nonnull final Item<Element> item) throws StageProcessingException {
+        final Element element = item.unwrap();
+        final var signature = xmlSigFactory.newXMLSignature(buildSignedInfo(element), buildKeyInfo());
+        
+        final XMLSignContext context = new DOMSignContext(privKey, element, element.getFirstChild());
+
+        // Enable caching reference values if required for debugging.
+        if (debugPreDigest) {
+            context.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE);
+        }
+
+        // Perform the signature operation
+        try {
+            signature.sign(context);
+        } catch (final Exception e) {
+            throw new StageProcessingException("Unable to create signature for element", e);
+        }
+
+        // Log the pre-digest data for debugging
+        try {
+            if (debugPreDigest) {
+                final Reference ref = signature.getSignedInfo().getReferences().get(0);
+                final String preDigest = new String(ref.getDigestInputStream().readAllBytes(), StandardCharsets.UTF_8);
+                log.debug("pre digest: {}", preDigest);
+            }
+        } catch (final IOException e) {
+            throw new StageProcessingException("Unable to log pre-digest data", e);
+        }
+
+        // Remove any CRs from selected signature elements.
+        if (removingCRsFromSignature) {
+            final Element signatureElement = ElementSupport.getFirstChildElement(element,
+                    XMLDSIGSupport.SIGNATURE_NAME);
+            removeCRsFromNamedChildren(signatureElement, "SignatureValue");
+            removeCRsFromNamedChildren(signatureElement, "X509Certificate");
+        }
+    }
+
+    /**
+     * Gets the descriptor of signed content.
+     * 
+     * @param target the element that will be signed
+     * 
+     * @return signed content descriptor
+     * 
+     * @throws StageProcessingException thrown if there is a problem creating the signed content descriptor
+     */
+    @Nonnull protected SignedInfo buildSignedInfo(@Nonnull final Element target) throws StageProcessingException {
+        C14NMethodParameterSpec c14nMethodSpec = null;
+        if (c14nAlgo.startsWith(CanonicalizationMethod.EXCLUSIVE) && inclusivePrefixList != null
+                && !inclusivePrefixList.isEmpty()) {
+            c14nMethodSpec = new ExcC14NParameterSpec(inclusivePrefixList);
+        }
+
+        final CanonicalizationMethod c14nMethod;
+        try {
+            c14nMethod = xmlSigFactory.newCanonicalizationMethod(c14nAlgo, c14nMethodSpec);
+        } catch (final Exception e) {
+            final String errMsg = "Unable to create transform " + c14nAlgo;
+            throw new StageProcessingException(errMsg, e);
+        }
+
+        final SignatureMethod sigMethod;
+        try {
+            sigMethod = xmlSigFactory.newSignatureMethod(sigAlgo, null);
+        } catch (final Exception e) {
+            final String errMsg = "Unable to create signature method " + sigAlgo;
+            throw new StageProcessingException(errMsg, e);
+        }
+
+        final List<Reference> refs = Collections.singletonList(buildSignatureReference(target));
+
+        return xmlSigFactory.newSignedInfo(c14nMethod, sigMethod, refs);
+    }
+
+    /**
+     * Builds the references to the signed content.
+     * 
+     * @param target the element to be signed
+     * 
+     * @return reference to signed content
+     * 
+     * @throws StageProcessingException thrown if there is a problem creating the reference to the element
+     */
+    @Nonnull protected Reference buildSignatureReference(@Nonnull final Element target)
+            throws StageProcessingException {
+        final String id = getElementId(target);
+        final String refUri;
+        if (id == null) {
+            refUri = "";
+        } else {
+            refUri = "#" + id;
+        }
+
+        DigestMethod digestMethod = null;
+        try {
+            final DigestMethodParameterSpec digestMethodSpec = null;
+            digestMethod = xmlSigFactory.newDigestMethod(digestAlgo, digestMethodSpec);
+        } catch (final Exception e) {
+            final String errMsg = "Unable to create digest method " + digestAlgo;
+            throw new StageProcessingException(errMsg, e);
+        }
+
+        TransformParameterSpec transformSpec;
+        final ArrayList<Transform> transforms = new ArrayList<>();
+
+        try {
+            transformSpec = null;
+            transforms.add(xmlSigFactory.newTransform(Transform.ENVELOPED, transformSpec));
+        } catch (final Exception e) {
+            final String errMsg = "Unable to create transform " + Transform.ENVELOPED;
+            throw new StageProcessingException(errMsg, e);
+        }
+
+        try {
+            if (c14nAlgo.startsWith(CanonicalizationMethod.EXCLUSIVE) && inclusivePrefixList != null
+                    && !inclusivePrefixList.isEmpty()) {
+                transformSpec = new ExcC14NParameterSpec(inclusivePrefixList);
+            }
+            transforms.add(xmlSigFactory.newTransform(c14nAlgo, transformSpec));
+        } catch (final Exception e) {
+            final String errMsg = "Unable to create transform " + c14nAlgo;
+            throw new StageProcessingException(errMsg, e);
+        }
+
+        return xmlSigFactory.newReference(refUri, digestMethod, transforms, null, null);
+    }
+
+    /**
+     * Determines the ID for the element to be signed. To determine the ID first, all the element attributes are
+     * inspected, if one matches the provided {@link #idAttributeNames} then the value of the attribute is used as the
+     * ID value. If no ID attribute names are given, or none of the given ones match, and one or more of the attributes
+     * is marked as an ID attribute (i.e. {@link Attr#isId()} is true), then the value of one of those attributes is
+     * used.
+     * 
+     * @param target an element to be referenced by the signature
+     * 
+     * @return the ID value for the element, or null
+     */
+    @Nullable protected String getElementId(@Nonnull final Element target) {
+        final NamedNodeMap attributes = target.getAttributes();
+        if (attributes == null || attributes.getLength() < 1) {
+            return null;
+        }
+
+        if (idAttributeNames != null && !idAttributeNames.isEmpty()) {
+            for (int i = 0; i < attributes.getLength(); i++) {
+                final Attr attribute = (Attr) attributes.item(i);
+                if (idAttributeNames.contains(QNameSupport.getNodeQName(attribute))) {
+                    // mark the attribute as an ID attribute so that it can be referenced by the signature
+                    target.setIdAttributeNode(attribute, true);
+                    final String value = StringSupport.trimOrNull(attribute.getValue());
+                    if (value != null) {
+                        return value;
+                    }
+                }
+            }
+        }
+
+        for (int i = 0; i < attributes.getLength(); i++) {
+            final Attr attribute = (Attr) attributes.item(i);
+            if (attribute.isId()) {
+                final String value = StringSupport.trimOrNull(attribute.getValue());
+                if (value != null) {
+                    return value;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Builds the KeyInfo element to be included in the signature.
+     * 
+     * @return KeyInfo element to be included in the signature
+     * 
+     * @throws StageProcessingException thrown if there is a problem creating the KeyInfo descriptor
+     */
+    @Nullable protected KeyInfo buildKeyInfo() throws StageProcessingException {
+        final List<XMLStructure> keyInfoItems = new ArrayList<>();
+
+        addKeyNames(keyInfoItems);
+        addKeyValue(keyInfoItems);
+        addX509Data(keyInfoItems);
+
+        if (keyInfoItems.isEmpty()) {
+            return null;
+        }
+        return keyInfoFactory.newKeyInfo(keyInfoItems);
+    }
+
+    /**
+     * Adds key names to the KeyInfo, if key names are to be included.
+     * 
+     * @param keyInfoItems collector for KeyInfo children
+     * 
+     * @throws StageProcessingException thrown if there is a problem creating the KeyName content
+     */
+    protected void addKeyNames(@Nonnull @NonnullElements @Live final List<XMLStructure> keyInfoItems)
+            throws StageProcessingException {
+        if (!includeKeyNames) {
+            return;
+        }
+
+        if (keyNames != null && !keyNames.isEmpty()) {
+            for (final String name : keyNames) {
+                keyInfoItems.add(keyInfoFactory.newKeyName(name));
+            }
+        }
+    }
+
+    /**
+     * Adds raw key values to the KeyInfo if key values are to be included.
+     * 
+     * @param keyInfoItems collector for KeyInfo children
+     * 
+     * @throws StageProcessingException thrown if there is a problem creating the KeyValue content
+     */
+    protected void addKeyValue(@Nonnull @NonnullElements @Live final List<XMLStructure> keyInfoItems)
+            throws StageProcessingException {
+        if (!includeKeyValue) {
+            return;
+        }
+
+        PublicKey key = publicKey;
+        
+        // If we have no explicit public key, we can extract one from a certificate, if we have one.
+        if (key == null && !certificates.isEmpty()) {
+            key = certificates.get(0).getPublicKey();
+        }
+
+        if (key != null) {
+            try {
+                keyInfoItems.add(keyInfoFactory.newKeyValue(key));
+            } catch (final Exception e) {
+                throw new StageProcessingException("Unable to create KeyValue", e);
+            }
+        }
+    }
+
+    /**
+     * Adds X509 data (subject names, certificates, CRLs, and Issuer/Serial) set to be included, into the key info.
+     * 
+     * @param keyInfoItems collector for KeyInfo children
+     */
+    protected void addX509Data(@Nonnull @NonnullElements @Live final List<XMLStructure> keyInfoItems) {
+        final List<Object> x509Data = new ArrayList<>();
+
+        if (certificates != null && !certificates.isEmpty()) {
+            final X509Certificate endEntityCert = certificates.get(0);
+
+            if (includeX509SubjectName) {
+                final X500Principal subjectDn = endEntityCert.getSubjectX500Principal();
+                x509Data.add(subjectDn.getName(X500Principal.RFC2253));
+            }
+
+            if (includeX509Certificates) {
+                x509Data.addAll(certificates);
+            }
+
+            if (includeX509IssuerSerial) {
+                final X500Principal issuerDn = endEntityCert.getIssuerX500Principal();
+                final BigInteger serialNumber = endEntityCert.getSerialNumber();
+                x509Data.add(keyInfoFactory.newX509IssuerSerial(issuerDn.getName(X500Principal.RFC2253), serialNumber));
+            }
+        }
+
+        if (includeX509Crls && crls != null && !crls.isEmpty()) {
+            x509Data.add(crls);
+        }
+
+        if (!x509Data.isEmpty()) {
+            keyInfoItems.add(keyInfoFactory.newX509Data(x509Data));
+        }
+    }
+    
+    /**
+     * Remove any CRs from the text content of named child elements.
+     *
+     * @param signature The <code>Signature</code> element to process.
+     * @param elementName The element name within the XML DSIG namespace to look for.
+     */
+    private void removeCRsFromNamedChildren(@Nonnull final Element signature, @Nonnull final String elementName) {
+        final NodeList nodes = signature.getElementsByTagNameNS(XMLSignature.XMLNS, elementName);
+        for (int i = 0; i < nodes.getLength(); i++) {
+            final Node node = nodes.item(i);
+            final String text = node.getTextContent();
+            if (text.indexOf('\r') >= 0) {
+                node.setTextContent(text.replaceAll("\\r", ""));
+            }
+        }
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureValidator.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/impl/XMLSignatureValidator.java
similarity index 98%
rename from aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureValidator.java
rename to aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/impl/XMLSignatureValidator.java
index a2e903a..98e33e3 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/XMLSignatureValidator.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/impl/XMLSignatureValidator.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.metadata.dom;
+package net.shibboleth.metadata.dom.impl;
 
 import java.security.PublicKey;
 import java.util.Collections;
@@ -53,13 +53,17 @@ import org.w3c.dom.Node;
 /**
  * A class that encapsulates the various stages in validation of XML signatures as methods.
  * 
+ * <p>
  * Failures, and the reason for the failure, are represented by exceptions.
+ * </p>
  * 
+ * <p>
  * This code largely derives from XmlSecTool V1.2, with the abrupt program termination
  * method of reporting issues replaced by throwing an exception. The intention is
  * that this code might be reintegrated with XmlSecTool in a later release.
+ * </p>
  */
-final class XMLSignatureValidator {
+public final class XMLSignatureValidator {
 
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(XMLSignatureValidator.class);
@@ -84,7 +88,7 @@ final class XMLSignatureValidator {
      *  @param blacklistSignatureMethods set of blacklisted signature method URIs, or <code>null</code>
      *  @param emptyRefPermitted true if empty references are permitted
      */
-    XMLSignatureValidator(@Nonnull final PublicKey key, @Nullable final Set<String> blacklistDigests,
+    public XMLSignatureValidator(@Nonnull final PublicKey key, @Nullable final Set<String> blacklistDigests,
             @Nullable final Set<String> blacklistSignatureMethods,
             final boolean emptyRefPermitted) {
         Constraint.isNotNull(key, "public key can not be null");
@@ -246,7 +250,6 @@ final class XMLSignatureValidator {
             final String alg = ref.getMessageDigestAlgorithm().getAlgorithmURI();
             log.debug("blacklist checking digest {}", alg);
             if (blacklistedDigests.contains(alg)) {
-                log.error("Digest algorithm {} is blacklisted", alg);
                 throw new ValidationException("Digest algorithm " + alg + " is blacklisted");
             }
         } catch (final XMLSignatureException e) {
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemIdentificationStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/impl/package-info.java
similarity index 67%
copy from aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemIdentificationStrategy.java
copy to aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/impl/package-info.java
index 1a3de26..bca8461 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/ItemIdentificationStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/impl/package-info.java
@@ -15,20 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.metadata;
-
-import javax.annotation.Nonnull;
-
-/** Strategy for determining a displayable identifier for a given {@link Item}. */
-public interface ItemIdentificationStrategy {
-
-    /**
-     * Gets an identifier for the item.
-     * 
-     * @param item the item
-     * 
-     * @return the identifier, never null
-     */
-    String getItemIdentifier(@Nonnull final Item<?> item);
-
-}
+/**
+ * Implementation classes related to processing DOM based metadata.
+ */
+package net.shibboleth.metadata.dom.impl;
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AbstractSAMLTraversalStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AbstractSAMLTraversalStage.java
index 28dcffd..e0c9404 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AbstractSAMLTraversalStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AbstractSAMLTraversalStage.java
@@ -77,11 +77,10 @@ public abstract class AbstractSAMLTraversalStage <C extends DOMTraversalContext>
             final Attr id = entity.getAttributeNode("ID");
             if (id != null) {
                 return id.getTextContent() + ": ";
-            } else {
-                final Attr entityID = entity.getAttributeNode("entityID");
-                if (entityID != null) {
-                    return entityID.getTextContent() + ": ";
-                }
+            }
+            final Attr entityID = entity.getAttributeNode("entityID");
+            if (entityID != null) {
+                return entityID.getTextContent() + ": ";
             }
         }
         return "";
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeElementMaker.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeElementMaker.java
index 2b1973b..ea9e3aa 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeElementMaker.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeElementMaker.java
@@ -18,7 +18,7 @@
 package net.shibboleth.metadata.dom.saml;
 
 import javax.annotation.Nonnull;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 
 import org.w3c.dom.Element;
 
@@ -32,7 +32,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  *
  * @since 0.10.0
  */
- at ThreadSafe
+ at Immutable
 public class AttributeElementMaker extends ElementMaker {
 
     /** Value for the <code>Name</code> XML attribute. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeElementMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeElementMatcher.java
index ab8d425..5accfa3 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeElementMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeElementMatcher.java
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.dom.saml;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 import org.w3c.dom.Element;
 
@@ -31,6 +32,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  *
  * @since 0.10.0
  */
+ at Immutable
 public class AttributeElementMatcher extends ElementMatcher {
 
     /** <code>NameFormat</code> attribute value to match. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeValueElementMaker.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeValueElementMaker.java
index 9439f26..7758db7 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeValueElementMaker.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeValueElementMaker.java
@@ -18,7 +18,7 @@
 package net.shibboleth.metadata.dom.saml;
 
 import javax.annotation.Nonnull;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 
 import org.w3c.dom.Element;
 
@@ -30,7 +30,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  * A class for constructing SAML <code>AttributeValue</code> elements
  * for use with the {@link Container} system.
  */
- at ThreadSafe
+ at Immutable
 public class AttributeValueElementMaker extends ElementMaker {
 
     /** Value for the attribute. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeValueElementMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeValueElementMatcher.java
index d8551c0..3ccbac9 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeValueElementMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/AttributeValueElementMatcher.java
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.dom.saml;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 import org.w3c.dom.Element;
 
@@ -29,6 +30,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  * text values,
  * for use with the {@link net.shibboleth.metadata.dom.Container} system.
  */
+ at Immutable
 public class AttributeValueElementMatcher extends ElementMatcher {
 
     /** <code>Attribute</code> value to match. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/ContactPersonFilterStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/ContactPersonFilterStage.java
index a7612c4..86827d0 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/ContactPersonFilterStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/ContactPersonFilterStage.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
@@ -71,19 +72,19 @@ public class ContactPersonFilterStage extends AbstractIteratingStage<Element> {
     /** 'other' person type constant. */
     public static final String OTHER = "other";
 
-    /** Class logger. */
-    private final Logger log = LoggerFactory.getLogger(ContactPersonFilterStage.class);
-
     /** Allowed contact person types. */
     @Nonnull @NonnullElements @Unmodifiable
-    private final Set<String> allowedTypes = Set.of(TECHNICAL, SUPPORT, ADMINISTRATIVE, BILLING, OTHER);
+    private static final Set<String> ALLOWED_TYPES = Set.of(TECHNICAL, SUPPORT, ADMINISTRATIVE, BILLING, OTHER);
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(ContactPersonFilterStage.class);
 
     /** Person types which are white/black listed depending on the value of {@link #whitelistingTypes}. */
-    @Nonnull @NonnullElements @Unmodifiable
-    private Set<String> designatedTypes = Set.copyOf(allowedTypes);
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
+    private Set<String> designatedTypes = Set.copyOf(ALLOWED_TYPES);
 
     /** Whether {@link #designatedTypes} should be considered a whitelist. Default value: true */
-    private boolean whitelistingTypes = true;
+    @GuardedBy("this") private boolean whitelistingTypes = true;
 
     /**
      * Gets the list of designated person types.
@@ -91,7 +92,7 @@ public class ContactPersonFilterStage extends AbstractIteratingStage<Element> {
      * @return list of designated person types
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<String> getDesignatedTypes() {
+    public final synchronized Collection<String> getDesignatedTypes() {
         return designatedTypes;
     }
 
@@ -106,7 +107,7 @@ public class ContactPersonFilterStage extends AbstractIteratingStage<Element> {
 
         final Set<String> checkedTypes = new HashSet<>();
         for (final String type : types) {
-            if (allowedTypes.contains(type)) {
+            if (ALLOWED_TYPES.contains(type)) {
                 checkedTypes.add(type);
             } else {
                 log.debug("Stage {}: {} is not an allowed contact person type and so has been ignored", getId(),
@@ -122,7 +123,7 @@ public class ContactPersonFilterStage extends AbstractIteratingStage<Element> {
      * 
      * @return true if the designated roles should be considered a whitelist, false otherwise
      */
-    public boolean isWhitelistingTypes() {
+    public final synchronized boolean isWhitelistingTypes() {
         return whitelistingTypes;
     }
 
@@ -205,18 +206,18 @@ public class ContactPersonFilterStage extends AbstractIteratingStage<Element> {
             return false;
         }
 
-        if (!allowedTypes.contains(type)) {
+        if (!ALLOWED_TYPES.contains(type)) {
             log.debug("The following ContactPerson contained an invalid contactType, it will be removed:\n{}",
                     SerializeSupport.prettyPrintXML(contactPerson));
             return false;
         }
 
-        if (isWhitelistingTypes() && designatedTypes.contains(type)) {
+        if (isWhitelistingTypes() && getDesignatedTypes().contains(type)) {
             // if we're whitelisting types and the person's type appears in the designated type list, keep them
             return true;
         }
 
-        if (!isWhitelistingTypes() && !designatedTypes.contains(type)) {
+        if (!isWhitelistingTypes() && !getDesignatedTypes().contains(type)) {
             // if we're blacklisting types and the person's type does not appear in the designated type list, keep them
             return true;
         }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer.java
index d677c7a..a029cd7 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer.java
@@ -26,9 +26,9 @@ import java.util.Map;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.json.Json;
 import javax.json.stream.JsonGenerator;
-import javax.json.stream.JsonGeneratorFactory;
 import javax.xml.namespace.QName;
 
 import org.w3c.dom.Element;
@@ -38,9 +38,7 @@ import net.shibboleth.metadata.ItemCollectionSerializer;
 import net.shibboleth.metadata.dom.saml.mdattr.MDAttrSupport;
 import net.shibboleth.metadata.dom.saml.mdui.MDUISupport;
 import net.shibboleth.metadata.pipeline.impl.BaseInitializableComponent;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.xml.ElementSupport;
 
 /**
@@ -52,12 +50,8 @@ import net.shibboleth.utilities.java.support.xml.ElementSupport;
 public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
     implements ItemCollectionSerializer<Element> {
 
-    /** Configured JSON generator factory. */
-    @NonnullAfterInit
-    private JsonGeneratorFactory factory;
-
     /** Whether to pretty-print the resulting JSON. Default: <code>false</code> */
-    private boolean prettyPrinting;
+    @GuardedBy("this") private boolean prettyPrinting;
 
     /**
      * Whether to include legacy display names if none are found in the
@@ -65,17 +59,17 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
      * 
      * Default: <code>false</code>
      */
-    private boolean includingLegacyDisplayNames;
+    @GuardedBy("this") private boolean includingLegacyDisplayNames;
     
     /** Whether to include entity attributes. Default: <code>false</code> */
-    private boolean includingEntityAttributes;
+    @GuardedBy("this") private boolean includingEntityAttributes;
 
     /**
      * Returns whether output is being pretty-printed.
      * 
      * @return whether output is being pretty-printed
      */
-    public boolean isPrettyPrinting() {
+    public final synchronized boolean isPrettyPrinting() {
         return prettyPrinting;
     }
 
@@ -84,7 +78,7 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
      * 
      * @param pretty whether to pretty-print the output
      */
-    public void setPrettyPrinting(final boolean pretty) {
+    public synchronized void setPrettyPrinting(final boolean pretty) {
         prettyPrinting = pretty;
     }
 
@@ -93,7 +87,7 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
      * 
      * @return whether output includes legacy display names
      */
-    public boolean isIncludingLegacyDisplayNames() {
+    public final synchronized boolean isIncludingLegacyDisplayNames() {
         return includingLegacyDisplayNames;
     }
 
@@ -102,7 +96,7 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
      * 
      * @param includeLegacyDisplayNames whether to include legacy display names
      */
-    public void setIncludingLegacyDisplayNames(final boolean includeLegacyDisplayNames) {
+    public synchronized void setIncludingLegacyDisplayNames(final boolean includeLegacyDisplayNames) {
         includingLegacyDisplayNames = includeLegacyDisplayNames;
     }
 
@@ -111,7 +105,7 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
      * 
      * @return whether output includes entity attributes
      */
-    public boolean isIncludingEntityAttributes() {
+    public final synchronized boolean isIncludingEntityAttributes() {
         return includingEntityAttributes;
     }
 
@@ -120,7 +114,7 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
      * 
      * @param includeEntityAttributes whether to include entity attributes.
      */
-    public void setIncludingEntityAttributes(final boolean includeEntityAttributes) {
+    public synchronized void setIncludingEntityAttributes(final boolean includeEntityAttributes) {
         includingEntityAttributes = includeEntityAttributes;
     }
 
@@ -262,7 +256,7 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
         }
         
         // Attempt to find display names elsewhere
-        if (includingLegacyDisplayNames) {
+        if (isIncludingLegacyDisplayNames()) {
             final var org = ElementSupport.getFirstChildElement(entity, SAMLMetadataSupport.ORGANIZATION_NAME);
             if (org != null) {
                 final var displayNames =
@@ -278,6 +272,11 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
     public void serializeCollection(@Nonnull @NonnullElements final Collection<Item<Element>> items,
             @Nonnull final OutputStream output) throws IOException {
         throwComponentStateExceptions();
+        final Map<String, String> generatorConfig = new HashMap<>();
+        if (isPrettyPrinting()) {
+            generatorConfig.put(JsonGenerator.PRETTY_PRINTING, "true");
+        }
+        final var factory = Json.createGeneratorFactory(generatorConfig);
         final JsonGenerator gen = factory.createGenerator(output);
         gen.writeStartArray();
             for (final Item<Element> item : items) {
@@ -298,7 +297,7 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
                                         MDUISupport.PRIVACYSTATEMENTURL_NAME, "PrivacyStatementURLs");
                                 writeLogos(gen, uiInfo);
                             }
-                            if (includingEntityAttributes) {
+                            if (isIncludingEntityAttributes()) {
                                 writeEntityAttributes(gen, entity);
                             }
                         gen.writeEnd();
@@ -309,20 +308,4 @@ public class DiscoFeedCollectionSerializer extends BaseInitializableComponent
         gen.close();
     }
 
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        final Map<String, String> generatorConfig = new HashMap<>();
-        if (prettyPrinting) {
-            generatorConfig.put(JsonGenerator.PRETTY_PRINTING, "true");
-        }
-        factory = Json.createGeneratorFactory(generatorConfig);
-    }
-
-    @Override
-    protected void doDestroy() {
-        factory = null;
-        super.doDestroy();
-    }
-
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntitiesDescriptorAssemblerStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntitiesDescriptorAssemblerStage.java
index 3e2a6ba..854523b 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntitiesDescriptorAssemblerStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntitiesDescriptorAssemblerStage.java
@@ -22,6 +22,7 @@ import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.namespace.QName;
 
@@ -52,7 +53,7 @@ import net.shibboleth.utilities.java.support.xml.NamespaceSupport;
 public class EntitiesDescriptorAssemblerStage extends AbstractStage<Element> {
 
     /** Name of the EntitiesDescriptor's Name attribute. */
-    public static final QName NAME_ATTRIB_NAME = new QName("Name");
+    private static final QName NAME_ATTRIB_NAME = new QName("Name");
 
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(EntitiesDescriptorAssemblerStage.class);
@@ -61,13 +62,14 @@ public class EntitiesDescriptorAssemblerStage extends AbstractStage<Element> {
      * Whether attempting to turn an empty item collection, which would result in a schema-invalid childless
      * EntitiesDescriptor, should be treated as processing error. Default value: <code>false</code>
      */
-    private boolean noChildrenAProcessingError;
+    @GuardedBy("this") private boolean noChildrenAProcessingError;
 
     /** Strategy used to order a collection of Items. The default strategy performs no ordering. */
-    @Nonnull
-    private ItemOrderingStrategy<Element> orderingStrategy = new NoOpItemOrderingStrategy<>();
+    @Nonnull @GuardedBy("this")
+    private ItemOrderingStrategy<Element> itemOrderingStrategy = new NoOpItemOrderingStrategy<>();
 
     /** Name to use for the EntitiesDescriptor. */
+    @Nullable @GuardedBy("this")
     private String descriptorName;
 
     /**
@@ -76,7 +78,7 @@ public class EntitiesDescriptorAssemblerStage extends AbstractStage<Element> {
      * 
      * @return whether attempting to turn an empty item collection should be treated as processing error
      */
-    public boolean isNoChildrenAProcessingError() {
+    public final synchronized boolean isNoChildrenAProcessingError() {
         return noChildrenAProcessingError;
     }
 
@@ -96,8 +98,8 @@ public class EntitiesDescriptorAssemblerStage extends AbstractStage<Element> {
      * 
      * @return strategy used to order a collection of Items
      */
-    @Nonnull public ItemOrderingStrategy<Element> getItemOrderingStrategy() {
-        return orderingStrategy;
+    @Nonnull public final synchronized ItemOrderingStrategy<Element> getItemOrderingStrategy() {
+        return itemOrderingStrategy;
     }
 
     /**
@@ -107,7 +109,7 @@ public class EntitiesDescriptorAssemblerStage extends AbstractStage<Element> {
      */
     public synchronized void setItemOrderingStrategy(@Nonnull final ItemOrderingStrategy<Element> strategy) {
         throwSetterPreconditionExceptions();
-        orderingStrategy = Constraint.isNotNull(strategy, "Item ordering strategy can not be null");
+        itemOrderingStrategy = Constraint.isNotNull(strategy, "Item ordering strategy can not be null");
     }
 
     /**
@@ -115,7 +117,7 @@ public class EntitiesDescriptorAssemblerStage extends AbstractStage<Element> {
      * 
      * @return Name used for the generated descriptor, may be null
      */
-    @Nullable public String getDescriptorName() {
+    @Nullable public final synchronized String getDescriptorName() {
         return descriptorName;
     }
 
@@ -129,16 +131,15 @@ public class EntitiesDescriptorAssemblerStage extends AbstractStage<Element> {
         descriptorName = StringSupport.trimOrNull(name);
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection)
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection)
             throws StageProcessingException {
         if (itemCollection.isEmpty()) {
-            if (noChildrenAProcessingError) {
+            if (isNoChildrenAProcessingError()) {
                 throw new StageProcessingException("Unable to assemble EntitiesDescriptor from an empty collection");
-            } else {
-                log.debug("Unable to assemble EntitiesDescriptor from an empty collection");
-                return;
             }
+            log.debug("Unable to assemble EntitiesDescriptor from an empty collection");
+            return;
         }
 
         final DOMImplementation domImpl =
@@ -156,7 +157,7 @@ public class EntitiesDescriptorAssemblerStage extends AbstractStage<Element> {
         // Put a newline between the start and end tags
         ElementSupport.appendTextContent(entitiesDescriptor, "\n");
 
-        final List<Item<Element>> orderedItems = orderingStrategy.order(itemCollection);
+        final List<Item<Element>> orderedItems = getItemOrderingStrategy().order(itemCollection);
         Element descriptor;
         for (final Item<Element> item : orderedItems) {
             descriptor = item.unwrap();
@@ -181,14 +182,15 @@ public class EntitiesDescriptorAssemblerStage extends AbstractStage<Element> {
      * @param entitiesDescriptor the entity descriptor to which the
      */
     protected void addDescriptorName(@Nonnull final Element entitiesDescriptor) {
-        if (descriptorName != null) {
-            AttributeSupport.appendAttribute(entitiesDescriptor, NAME_ATTRIB_NAME, descriptorName);
+        final var name = getDescriptorName();
+        if (name != null) {
+            AttributeSupport.appendAttribute(entitiesDescriptor, NAME_ATTRIB_NAME, name);
         }
     }
 
     @Override
     protected void doDestroy() {
-        orderingStrategy = null;
+        itemOrderingStrategy = null;
         descriptorName = null;
 
         super.doDestroy();
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntitiesDescriptorDisassemblerStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntitiesDescriptorDisassemblerStage.java
index 311d7be..2c7b4c1 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntitiesDescriptorDisassemblerStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntitiesDescriptorDisassemblerStage.java
@@ -45,8 +45,8 @@ public class EntitiesDescriptorDisassemblerStage extends AbstractStage<Element>
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(EntitiesDescriptorDisassemblerStage.class);
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection) {
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection) {
         // make a copy of the input collection and clear it so that we can iterate over
         // the copy and add to the provided collection
         final ArrayList<Item<Element>> items = new ArrayList<>(itemCollection);
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntityFilterStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntityFilterStage.java
index 2047047..ef59845 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntityFilterStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntityFilterStage.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
@@ -43,21 +44,22 @@ public class EntityFilterStage extends AbstractFilteringStage<Element> {
     private final Logger log = LoggerFactory.getLogger(EntityFilterStage.class);
 
     /** Entities which are white/black listed depending on the value of {@link #whitelistingEntities}. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Set<String> designatedEntities = Set.of();
 
     /** Whether {@link #designatedEntities} should be considered a whitelist or a blacklist. Default value: false */
-    private boolean whitelistingEntities;
+    @GuardedBy("this") private boolean whitelistingEntities;
 
     /** Whether EntitiesDescriptor that do not contain EntityDescriptors should be removed. Default value: true */
-    private boolean removingEntitylessEntitiesDescriptor = true;
+    @GuardedBy("this") private boolean removingEntitylessEntitiesDescriptor = true;
 
     /**
      * Gets the list of designated entity IDs.
      * 
      * @return list of designated entity IDs, never null
      */
-    @Nonnull @NonnullElements @Unmodifiable public Collection<String> getDesignatedEntities() {
+    @Nonnull @NonnullElements @Unmodifiable
+    public final synchronized Collection<String> getDesignatedEntities() {
         return designatedEntities;
     }
 
@@ -77,7 +79,7 @@ public class EntityFilterStage extends AbstractFilteringStage<Element> {
      * 
      * @return true if the designated entities should be considered a whitelist, false otherwise
      */
-    public boolean isWhitelistingEntities() {
+    public final synchronized boolean isWhitelistingEntities() {
         return whitelistingEntities;
     }
 
@@ -96,7 +98,7 @@ public class EntityFilterStage extends AbstractFilteringStage<Element> {
      * 
      * @return whether EntitiesDescriptor that do not contain EntityDescriptors should be removed
      */
-    public boolean isRemovingEntitylessEntitiesDescriptor() {
+    public final synchronized boolean isRemovingEntitylessEntitiesDescriptor() {
         return removingEntitylessEntitiesDescriptor;
     }
 
@@ -110,13 +112,6 @@ public class EntityFilterStage extends AbstractFilteringStage<Element> {
         removingEntitylessEntitiesDescriptor = remove;
     }
 
-    @Override
-    protected void doDestroy() {
-        designatedEntities = null;
-
-        super.doDestroy();
-    }
-
     @Override
     protected boolean doExecute(@Nonnull final Item<Element> item) {
         final Element descriptor = item.unwrap();
@@ -169,8 +164,8 @@ public class EntityFilterStage extends AbstractFilteringStage<Element> {
             }
         }
 
-        if (removingEntitylessEntitiesDescriptor && childEntitiesDescriptors.isEmpty()
-                && childEntityDescriptors.isEmpty()) {
+        if (childEntitiesDescriptors.isEmpty() && childEntityDescriptors.isEmpty()
+                && isRemovingEntitylessEntitiesDescriptor()) {
             return true;
         }
 
@@ -188,13 +183,13 @@ public class EntityFilterStage extends AbstractFilteringStage<Element> {
         final String entityId = entityDescriptor.getAttributeNS(null, "entityID");
 
         // if we're whitelisting entities and this entity isn't in the list, kick it out
-        if (isWhitelistingEntities() && !designatedEntities.contains(entityId)) {
+        if (isWhitelistingEntities() && !getDesignatedEntities().contains(entityId)) {
             log.debug("{} pipeline stage removing entity {} because it wasn't on the whitelist", getId(), entityId);
             return true;
         }
 
         // if we're backlisting entities and this entity is in the list, kick it out
-        if (!isWhitelistingEntities() && designatedEntities.contains(entityId)) {
+        if (!isWhitelistingEntities() && getDesignatedEntities().contains(entityId)) {
             log.debug("{} pipeline stage removing entity {} because it was on the blacklist", getId(), entityId);
             return true;
         }
@@ -202,4 +197,12 @@ public class EntityFilterStage extends AbstractFilteringStage<Element> {
         // entity has been filtered and made it through, don't kick it out
         return false;
     }
+
+    @Override
+    protected void doDestroy() {
+        designatedEntities = null;
+
+        super.doDestroy();
+    }
+
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntityRoleFilterStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntityRoleFilterStage.java
index 7a54182..8888bea 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntityRoleFilterStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/EntityRoleFilterStage.java
@@ -24,6 +24,7 @@ import java.util.Objects;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.namespace.QName;
 
@@ -48,36 +49,36 @@ import net.shibboleth.utilities.java.support.xml.QNameSupport;
 @ThreadSafe
 public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
 
-    /** Class logger. */
-    private final Logger log = LoggerFactory.getLogger(EntityRoleFilterStage.class);
-
     /**
      * Set containing the SAML-defined, named role descriptors: {@link SAMLMetadataSupport#IDP_SSO_DESCRIPTOR_NAME},
      * {@link SAMLMetadataSupport#SP_SSO_DESCRIPTOR_NAME}, {@link SAMLMetadataSupport#AUTHN_AUTHORITY_DESCRIPTOR_NAME},
      * {@link SAMLMetadataSupport#ATTRIBUTE_AUTHORITY_DESCRIPTOR_NAME}, {@link SAMLMetadataSupport#PDP_DESCRIPTOR_NAME}.
      */
-    @Nonnull @NonnullElements @Unmodifiable
-    private final Set<QName> namedRoles = Set.of(SAMLMetadataSupport.IDP_SSO_DESCRIPTOR_NAME,
+    @Nonnull  @NonnullElements @Unmodifiable
+    private static final Set<QName> NAMED_ROLES = Set.of(SAMLMetadataSupport.IDP_SSO_DESCRIPTOR_NAME,
             SAMLMetadataSupport.SP_SSO_DESCRIPTOR_NAME,
             SAMLMetadataSupport.AUTHN_AUTHORITY_DESCRIPTOR_NAME,
             SAMLMetadataSupport.ATTRIBUTE_AUTHORITY_DESCRIPTOR_NAME,
             SAMLMetadataSupport.PDP_DESCRIPTOR_NAME);
 
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(EntityRoleFilterStage.class);
+
     /** Role element or type names which are white/black listed depending on the value of {@link #whitelistingRoles}. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Set<QName> designatedRoles = Set.of();
 
     /** Whether {@link #designatedRoles} should be considered a whitelist or a blacklist. Default value: false */
-    private boolean whitelistingRoles;
+    @GuardedBy("this") private boolean whitelistingRoles;
 
     /**
      * Whether EntityDescriptor elements that do not contain roles, after filtering, should be removed. Default value:
      * true
      */
-    private boolean removingRolelessEntities = true;
+    @GuardedBy("this") private boolean removingRolelessEntities = true;
 
     /** Whether EntitiesDescriptor that do not contain EntityDescriptors should be removed. Default value: true */
-    private boolean removingEntitylessEntitiesDescriptor = true;
+    @GuardedBy("this") private boolean removingEntitylessEntitiesDescriptor = true;
 
     /**
      * Gets the list of designated entity roles. The list may contain either role element names or schema types.
@@ -85,7 +86,7 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
      * @return list of designated entity roles, never null
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<QName> getDesignatedRoles() {
+    public final synchronized Collection<QName> getDesignatedRoles() {
         return designatedRoles;
     }
 
@@ -104,7 +105,7 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
      * 
      * @return true if the designated roles should be considered a whitelist, false otherwise
      */
-    public boolean isWhitelistingRoles() {
+    public final synchronized boolean isWhitelistingRoles() {
         return whitelistingRoles;
     }
 
@@ -123,7 +124,7 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
      * 
      * @return true if EntityDescriptors without roles (after filtering) should be removed, false otherwise
      */
-    public boolean isRemovingRolelessEntities() {
+    public final synchronized boolean isRemovingRolelessEntities() {
         return removingRolelessEntities;
     }
 
@@ -142,7 +143,7 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
      * 
      * @return whether EntitiesDescriptor that do not contain EntityDescriptors should be removed
      */
-    public boolean isRemovingEntitylessEntitiesDescriptor() {
+    public final synchronized boolean isRemovingEntitylessEntitiesDescriptor() {
         return removingEntitylessEntitiesDescriptor;
     }
 
@@ -156,13 +157,6 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
         removingEntitylessEntitiesDescriptor = remove;
     }
 
-    @Override
-    protected void doDestroy() {
-        designatedRoles = null;
-
-        super.doDestroy();
-    }
-
     @Override
     protected boolean doExecute(@Nonnull final Item<Element> item) {
         final Element descriptor = item.unwrap();
@@ -191,13 +185,12 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
      */
     protected boolean processEntitiesDescriptor(@Nonnull final Element entitiesDescriptor) {
         Iterator<Element> descriptorItr;
-        Element descriptor;
 
         final List<Element> childEntitiesDescriptors =
                 ElementSupport.getChildElements(entitiesDescriptor, SAMLMetadataSupport.ENTITIES_DESCRIPTOR_NAME);
         descriptorItr = childEntitiesDescriptors.iterator();
         while (descriptorItr.hasNext()) {
-            descriptor = descriptorItr.next();
+            final var descriptor = descriptorItr.next();
             if (processEntitiesDescriptor(descriptor)) {
                 entitiesDescriptor.removeChild(descriptor);
                 descriptorItr.remove();
@@ -208,15 +201,15 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
                 ElementSupport.getChildElements(entitiesDescriptor, SAMLMetadataSupport.ENTITY_DESCRIPTOR_NAME);
         descriptorItr = childEntityDescriptors.iterator();
         while (descriptorItr.hasNext()) {
-            descriptor = descriptorItr.next();
+            final var descriptor = descriptorItr.next();
             if (processEntityDescriptor(descriptor)) {
                 entitiesDescriptor.removeChild(descriptor);
                 descriptorItr.remove();
             }
         }
 
-        if (removingEntitylessEntitiesDescriptor && childEntitiesDescriptors.isEmpty()
-                && childEntityDescriptors.isEmpty()) {
+        if (childEntitiesDescriptors.isEmpty() && childEntityDescriptors.isEmpty()
+                && isRemovingEntitylessEntitiesDescriptor()) {
             return true;
         }
 
@@ -232,7 +225,7 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
      * @return true if the entity descriptor should be removed, false otherwise
      */
     protected boolean processEntityDescriptor(@Nonnull final Element entityDescriptor) {
-        if (designatedRoles.isEmpty()) {
+        if (getDesignatedRoles().isEmpty()) {
             return false;
         }
 
@@ -241,7 +234,7 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
         log.debug("{} pipeline stage filtering roles from EntityDescriptor {}", getId(), entityId);
 
         final List<Element> roles = getFilteredRoles(entityId, entityDescriptor);
-        if (removingRolelessEntities && roles.isEmpty()) {
+        if (roles.isEmpty() && isRemovingRolelessEntities()) {
             return true;
         }
 
@@ -260,25 +253,21 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
         final List<Element> childElements = ElementSupport.getChildElements(entityDescriptor);
 
         final Iterator<Element> childItr = childElements.iterator();
-
-        Element child;
-        QName childQName;
-        QName roleIdentifier;
         while (childItr.hasNext()) {
-            child = childItr.next();
-            childQName = QNameSupport.getNodeQName(child);
-            roleIdentifier = null;
+            final Element child = childItr.next();
+            final QName childQName = QNameSupport.getNodeQName(child);
 
+            final QName roleIdentifier;
             if (Objects.equals(childQName, SAMLMetadataSupport.ROLE_DESCRIPTOR_NAME)) {
                 roleIdentifier = DOMTypeSupport.getXSIType(child);
-            } else if (namedRoles.contains(childQName)) {
+            } else if (NAMED_ROLES.contains(childQName)) {
                 roleIdentifier = childQName;
             } else {
                 childItr.remove();
                 continue;
             }
 
-            final boolean isDesignatedRole = designatedRoles.contains(roleIdentifier);
+            final boolean isDesignatedRole = getDesignatedRoles().contains(roleIdentifier);
             if (roleIdentifier != null) {
                 if ((isWhitelistingRoles() && !isDesignatedRole) || (!isWhitelistingRoles() && isDesignatedRole)) {
                     log.debug("{} pipeline stage removing role {} from EntityDescriptor {}", new Object[] {getId(),
@@ -294,4 +283,12 @@ public class EntityRoleFilterStage extends AbstractFilteringStage<Element> {
 
         return childElements;
     }
+
+    @Override
+    protected void doDestroy() {
+        designatedRoles = null;
+
+        super.doDestroy();
+    }
+
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/GenerateIdStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/GenerateIdStage.java
index 352a7c6..9291620 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/GenerateIdStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/GenerateIdStage.java
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.dom.saml;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 import javax.xml.namespace.QName;
 
 import org.w3c.dom.Attr;
@@ -32,15 +33,17 @@ import net.shibboleth.utilities.java.support.security.impl.Type4UUIDIdentifierGe
 import net.shibboleth.utilities.java.support.xml.AttributeSupport;
 
 /** A stage that populates the ID attribute of an EntitiesDescriptor or EntityDescriptor. */
+ at ThreadSafe
 public class GenerateIdStage extends AbstractIteratingStage<Element> {
 
     /** QName of the ID attribute added to the descriptor. */
     public static final QName ID_ATTRIB = new QName("ID");
 
     /** Strategy used to generate identifiers. */
+    @Nonnull
     private final IdentifierGenerationStrategy idGenerator;
 
-    /** Constructor. Initialized the {@link #idGenerator} to a {@link Type4UUIDIdentifierGenerationStrategy}. */
+    /** Constructor. Initialize the {@link #idGenerator} to a {@link Type4UUIDIdentifierGenerationStrategy}. */
     public GenerateIdStage() {
         idGenerator = new Type4UUIDIdentifierGenerationStrategy();
     }
@@ -67,6 +70,7 @@ public class GenerateIdStage extends AbstractIteratingStage<Element> {
             element.setAttributeNode(idAttribute);
         }
 
+        // Don't need to synchronize; field initialized by constructor
         idAttribute.setValue(idGenerator.generateIdentifier());
     }
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/PullUpCacheDurationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/PullUpCacheDurationStage.java
index 0eadedd..550c923 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/PullUpCacheDurationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/PullUpCacheDurationStage.java
@@ -22,6 +22,7 @@ import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Attr;
@@ -43,9 +44,11 @@ import net.shibboleth.utilities.java.support.xml.ElementSupport;
 public class PullUpCacheDurationStage extends AbstractIteratingStage<Element> {
 
     /** The minimum cache duration. Default value: <code>0</code> */
+    @Nonnull @GuardedBy("this")
     private Duration minCacheDuration = Duration.ZERO;
 
     /** The maximum cache duration. Default value: {@value java.lang.Long#MAX_VALUE} */
+    @Nonnull @GuardedBy("this")
     private Duration maxCacheDuration = Duration.ofMillis(Long.MAX_VALUE);
 
     /**
@@ -53,7 +56,8 @@ public class PullUpCacheDurationStage extends AbstractIteratingStage<Element> {
      * 
      * @return minimum cache duration, always 0 or greater
      */
-    public Duration getMinimumCacheDuration() {
+    @Nonnull
+    public final synchronized Duration getMinimumCacheDuration() {
         return minCacheDuration;
     }
 
@@ -76,7 +80,8 @@ public class PullUpCacheDurationStage extends AbstractIteratingStage<Element> {
      * 
      * @return maximum cache duration, always greater than 0
      */
-    public Duration getMaximumCacheDuration() {
+    @Nonnull
+    public final synchronized Duration getMaximumCacheDuration() {
         return maxCacheDuration;
     }
 
@@ -106,6 +111,7 @@ public class PullUpCacheDurationStage extends AbstractIteratingStage<Element> {
      * @return the shortest cache duration from the descriptor and its descendants or null if the descriptor does not
      *         contain a cache duration
      */
+    @Nullable
     protected Duration getShortestCacheDuration(@Nonnull final Element descriptor) {
         Duration shortestCacheDuration = null;
         if (!SAMLMetadataSupport.isEntityOrEntitiesDescriptor(descriptor)) {
@@ -162,12 +168,12 @@ public class PullUpCacheDurationStage extends AbstractIteratingStage<Element> {
             return;
         }
 
-        if (cacheDuration.compareTo(minCacheDuration) < 0) {
+        if (cacheDuration.compareTo(getMinimumCacheDuration()) < 0) {
             AttributeSupport.appendDurationAttribute(descriptor, SAMLMetadataSupport.CACHE_DURATION_ATTRIB_NAME,
-                    minCacheDuration);
-        } else if (cacheDuration.compareTo(maxCacheDuration) > 0) {
+                    getMinimumCacheDuration());
+        } else if (cacheDuration.compareTo(getMaximumCacheDuration()) > 0) {
             AttributeSupport.appendDurationAttribute(descriptor, SAMLMetadataSupport.CACHE_DURATION_ATTRIB_NAME,
-                    maxCacheDuration);
+                    getMaximumCacheDuration());
         } else {
             AttributeSupport.appendDurationAttribute(descriptor, SAMLMetadataSupport.CACHE_DURATION_ATTRIB_NAME,
                     cacheDuration);
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/PullUpValidUntilStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/PullUpValidUntilStage.java
index 3b09dc7..4ad8bd9 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/PullUpValidUntilStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/PullUpValidUntilStage.java
@@ -23,6 +23,7 @@ import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Attr;
@@ -44,12 +45,14 @@ import net.shibboleth.utilities.java.support.xml.ElementSupport;
 public class PullUpValidUntilStage extends AbstractIteratingStage<Element> {
 
     /** The minimum amount of time a descriptor may be valid. Default value: 0 */
+    @Nonnull @GuardedBy("this")
     private Duration minValidityDuration = Duration.ZERO;
 
     /**
      * The maximum amount of time a descriptor may be valid. Default value:
      * {@value java.lang.Long#MAX_VALUE}
      */
+    @Nonnull @GuardedBy("this")
     private Duration maxValidityDuration = Duration.ofMillis(Long.MAX_VALUE);
 
     /**
@@ -57,7 +60,8 @@ public class PullUpValidUntilStage extends AbstractIteratingStage<Element> {
      * 
      * @return minimum amount of time a descriptor may be valid, always 0 or greater
      */
-    public Duration getMinimumValidityDuration() {
+    @Nonnull
+    public final synchronized Duration getMinimumValidityDuration() {
         return minValidityDuration;
     }
 
@@ -80,7 +84,8 @@ public class PullUpValidUntilStage extends AbstractIteratingStage<Element> {
      * 
      * @return maximum maximum amount of time a descriptor may be valid, always greater than 0
      */
-    public Duration getMaximumValidityDuration() {
+    @Nonnull
+    public final synchronized Duration getMaximumValidityDuration() {
         return maxValidityDuration;
     }
 
@@ -110,6 +115,7 @@ public class PullUpValidUntilStage extends AbstractIteratingStage<Element> {
      * @return the shortest cache duration from the descriptor and its descendants or null if the descriptor does not
      *         contain a cache duration
      */
+    @Nullable
     protected Instant getNearestValidUntil(@Nonnull final Element descriptor) {
         Instant nearestValidUntil = null;
         if (!SAMLMetadataSupport.isEntityOrEntitiesDescriptor(descriptor)) {
@@ -164,8 +170,8 @@ public class PullUpValidUntilStage extends AbstractIteratingStage<Element> {
         }
 
         final Instant now = Instant.now();
-        final Instant minValidUntil = now.plus(minValidityDuration);
-        final Instant maxValidUntil = now.plus(maxValidityDuration);
+        final Instant minValidUntil = now.plus(getMinimumValidityDuration());
+        final Instant maxValidUntil = now.plus(getMaximumValidityDuration());
 
         final Instant boundedValidUntil;
         if (validUntil.isBefore(minValidUntil)) {
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLMetadataSupport.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLMetadataSupport.java
index 2e2c70b..e44a8c8 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLMetadataSupport.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLMetadataSupport.java
@@ -255,4 +255,4 @@ public final class SAMLMetadataSupport {
 
         ElementSupport.appendChildElement(extensionsElement, extension);
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLStringElementCheckingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLStringElementCheckingStage.java
index d58d326..bcf3b54 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLStringElementCheckingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLStringElementCheckingStage.java
@@ -21,6 +21,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.dom.AbstractElementVisitingStage;
 import net.shibboleth.metadata.dom.DOMTraversalContext;
@@ -35,6 +36,7 @@ import org.w3c.dom.Node;
  *
  * @since 0.10.0
  */
+ at ThreadSafe
 public class SAMLStringElementCheckingStage extends AbstractElementVisitingStage {
 
     /** Regular expression matching a string which contains no non-whitespace characters. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLSupport.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLSupport.java
index 37dd7c6..e0e6d75 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLSupport.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLSupport.java
@@ -86,9 +86,8 @@ public final class SAMLSupport {
         final Attr attr = attribute.getAttributeNode("NameFormat");
         if (attr == null) {
             return ATTRNAME_FORMAT_UNSPECIFIED;
-        } else {
-            return attr.getValue();
         }
+        return attr.getValue();
     }
 
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SetCacheDurationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SetCacheDurationStage.java
index 9f1b859..3091ee2 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SetCacheDurationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SetCacheDurationStage.java
@@ -20,6 +20,7 @@ package net.shibboleth.metadata.dom.saml;
 import java.time.Duration;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Element;
@@ -45,14 +46,15 @@ import net.shibboleth.utilities.java.support.xml.AttributeSupport;
 public class SetCacheDurationStage extends AbstractIteratingStage<Element> {
 
     /** Cache duration that will be set on each metadata element. */
-    @NonnullAfterInit private Duration cacheDuration;
+    @NonnullAfterInit @GuardedBy("this") private Duration cacheDuration;
 
     /**
      * Gets the cache duration that will be set on each metadata element.
      * 
      * @return cache duration
      */
-    public Duration getCacheDuration() {
+    @NonnullAfterInit
+    public final synchronized Duration getCacheDuration() {
         return cacheDuration;
     }
 
@@ -77,7 +79,7 @@ public class SetCacheDurationStage extends AbstractIteratingStage<Element> {
         if (SAMLMetadataSupport.isEntityOrEntitiesDescriptor(descriptor)) {
             AttributeSupport.removeAttribute(descriptor, SAMLMetadataSupport.CACHE_DURATION_ATTRIB_NAME);
             AttributeSupport.appendDurationAttribute(descriptor, SAMLMetadataSupport.CACHE_DURATION_ATTRIB_NAME,
-                    cacheDuration);
+                    getCacheDuration());
         }
     }
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SetValidUntilStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SetValidUntilStage.java
index 2d16739..c4ddad3 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SetValidUntilStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SetValidUntilStage.java
@@ -21,6 +21,7 @@ import java.time.Duration;
 import java.time.Instant;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Element;
@@ -46,14 +47,15 @@ import net.shibboleth.utilities.java.support.xml.AttributeSupport;
 public class SetValidUntilStage extends AbstractIteratingStage<Element> {
 
     /** Amount of time the descriptors will be valid. */
-    @NonnullAfterInit private Duration validityDuration;
+    @NonnullAfterInit @GuardedBy("this") private Duration validityDuration;
 
     /**
      * Gets the amount of time the descriptors will be valid.
      * 
      * @return amount of time the descriptors will be valid
      */
-    public Duration getValidityDuration() {
+    @NonnullAfterInit
+    public final synchronized Duration getValidityDuration() {
         return validityDuration;
     }
 
@@ -78,7 +80,7 @@ public class SetValidUntilStage extends AbstractIteratingStage<Element> {
         if (SAMLMetadataSupport.isEntityOrEntitiesDescriptor(descriptor)) {
             AttributeSupport.removeAttribute(descriptor, SAMLMetadataSupport.VALID_UNTIL_ATTRIB_NAME);
             AttributeSupport.appendDateTimeAttribute(descriptor, SAMLMetadataSupport.VALID_UNTIL_ATTRIB_NAME,
-                    Instant.now().plus(validityDuration));
+                    Instant.now().plus(getValidityDuration()));
         }
     }
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/ValidateValidUntilStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/ValidateValidUntilStage.java
index 319e37e..34b9a2a 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/ValidateValidUntilStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/ValidateValidUntilStage.java
@@ -21,6 +21,7 @@ import java.time.Duration;
 import java.time.Instant;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Element;
@@ -40,20 +41,20 @@ import net.shibboleth.utilities.java.support.xml.AttributeSupport;
 public class ValidateValidUntilStage extends AbstractIteratingStage<Element> {
 
     /** Whether the item is required to have a validUntil attribute. Default value: <code>true</code> */
-    private boolean requireValidUntil = true;
+    @GuardedBy("this") private boolean requireValidUntil = true;
 
     /**
      * Interval from now within which the validUntil date must fall. A value of 0 indicates that no
      * maximum interval is checked. Default value: 1 week
      */
-    @Nonnull private Duration maxValidityInterval = Duration.ofDays(7);
+    @Nonnull @GuardedBy("this") private Duration maxValidityInterval = Duration.ofDays(7);
 
     /**
      * Gets whether the item is required to have a validUntil attribute.
      * 
      * @return whether the item is required to have a validUntil attribute
      */
-    public boolean isRequireValidUntil() {
+    public final synchronized boolean isRequireValidUntil() {
         return requireValidUntil;
     }
 
@@ -72,7 +73,8 @@ public class ValidateValidUntilStage extends AbstractIteratingStage<Element> {
      * 
      * @return Interval from now within which the validUntil date must fall
      */
-    public Duration getMaxValidityInterval() {
+    @Nonnull
+    public final synchronized Duration getMaxValidityInterval() {
         return maxValidityInterval;
     }
 
@@ -104,7 +106,7 @@ public class ValidateValidUntilStage extends AbstractIteratingStage<Element> {
                 AttributeSupport.getDateTimeAttribute(AttributeSupport.getAttribute(element,
                         SAMLMetadataSupport.VALID_UNTIL_ATTRIB_NAME));
         if (validUntil == null) {
-            if (requireValidUntil) {
+            if (isRequireValidUntil()) {
                 item.getItemMetadata().put(new ErrorStatus(getId(), "Item does not include a validUntil attribute"));
             }
         } else {
@@ -113,8 +115,8 @@ public class ValidateValidUntilStage extends AbstractIteratingStage<Element> {
                 item.getItemMetadata().put(new ErrorStatus(getId(), "Item has a validUntil prior to the current time"));
             }
 
-            if (!maxValidityInterval.isZero()) {
-                final var upperBound = lowerBound.plus(maxValidityInterval);
+            if (!getMaxValidityInterval().isZero()) {
+                final var upperBound = lowerBound.plus(getMaxValidityInterval());
                 if (validUntil.isAfter(upperBound)) {
                     item.getItemMetadata().put(
                             new ErrorStatus(getId(), "Item has validUntil larger than the maximum validity interval"));
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/IPHintValidationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/IPHintValidationStage.java
index 1be2480..23c04c6 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/IPHintValidationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/IPHintValidationStage.java
@@ -60,7 +60,7 @@ public class IPHintValidationStage extends AbstractDOMTraversalStage<DOMTraversa
     }
 
     @Override
-    protected boolean applicable(final Element element) {
+    protected boolean applicable(@Nonnull final Element element, @Nonnull final DOMTraversalContext context) {
         return ElementSupport.isElementNamed(element, MDUISupport.IPHINT_NAME);
     }
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SimplePipeline.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SimplePipeline.java
index 2900633..9ff5456 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SimplePipeline.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SimplePipeline.java
@@ -23,6 +23,7 @@ import java.util.Collections;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
@@ -41,12 +42,12 @@ public class SimplePipeline<T> extends BaseIdentifiableInitializableComponent
         implements Pipeline<T> {
 
     /** Stages for this pipeline. */
-    @Nonnull @NonnullElements
+    @Nonnull @NonnullElements @GuardedBy("this")
     private List<Stage<T>> pipelineStages = Collections.emptyList();
 
     @Override
     @Nonnull @NonnullElements @Unmodifiable
-    public List<Stage<T>> getStages() {
+    public final synchronized List<Stage<T>> getStages() {
         return pipelineStages;
     }
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/AcceptAllValidator.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/AcceptAllValidator.java
index 1e984d6..f6a7acf 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/AcceptAllValidator.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/AcceptAllValidator.java
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.validate;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 import net.shibboleth.metadata.Item;
 
@@ -30,6 +31,7 @@ import net.shibboleth.metadata.Item;
  *
  * @since 0.10.0
  */
+ at Immutable
 public class AcceptAllValidator<V> extends BaseValidator implements Validator<V> {
 
     @Override
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/BaseValidator.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/BaseValidator.java
index e758d0c..4f89612 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/BaseValidator.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/BaseValidator.java
@@ -84,9 +84,8 @@ public abstract class BaseValidator extends BaseIdentifiableInitializableCompone
         final String id = getId();
         if (id == null) {
             return stageId;
-        } else {
-            return stageId + "/" + getId();
         }
+        return stageId + "/" + getId();
     }
 
     /**
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/RejectAllValidator.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/RejectAllValidator.java
index 7663d97..874a907 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/RejectAllValidator.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/RejectAllValidator.java
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.validate;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 import net.shibboleth.metadata.Item;
 
@@ -33,6 +34,7 @@ import net.shibboleth.metadata.Item;
  *
  * @since 0.10.0
  */
+ at Immutable
 public class RejectAllValidator<V> extends BaseValidator implements Validator<V> {
 
     @Override
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/Validator.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/Validator.java
index c7afade..aedc11b 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/Validator.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/Validator.java
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.validate;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
@@ -27,11 +28,16 @@ import net.shibboleth.utilities.java.support.component.InitializableComponent;
 
 /**
  * Interface for a validator to be applied to an object in the context of a given {@link Item}.
- * 
+ *
+ * <p>
+ * {@code Validator}s <strong>must</strong> be thread-safe.
+ * </p>
+ *
  * @param <V> type of the object to be validated
  *
  * @since 0.9.0
  */
+ at ThreadSafe
 public interface Validator<V> extends DestructableComponent, IdentifiableComponent,
     InitializableComponent {
     
@@ -64,7 +70,7 @@ public interface Validator<V> extends DestructableComponent, IdentifiableCompone
      * @return an indication of whether to process additional validators
      * @throws StageProcessingException if an error occurs during validation
      */
-    Action validate(@Nonnull V e, @Nonnull Item<?> item, @Nonnull String stageId)
+    @Nonnull Action validate(@Nonnull V e, @Nonnull Item<?> item, @Nonnull String stageId)
         throws StageProcessingException;
     
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/ValidatorSequence.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/ValidatorSequence.java
index 6f3a6f6..195a5ec 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/ValidatorSequence.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/ValidatorSequence.java
@@ -20,6 +20,8 @@ package net.shibboleth.metadata.validate;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.pipeline.StageProcessingException;
@@ -39,10 +41,11 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
  *
  * @since 0.10.0
  */
+ at ThreadSafe
 public class ValidatorSequence<V> extends BaseValidator implements Validator<V> {
 
     /** The list of validators to apply. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<Validator<V>> validators = List.of();
 
     /**
@@ -50,7 +53,8 @@ public class ValidatorSequence<V> extends BaseValidator implements Validator<V>
      * 
      * @param newValidators the list of validators to set
      */
-    public void setValidators(@Nonnull @NonnullElements @Unmodifiable final List<Validator<V>> newValidators) {
+    public synchronized void setValidators(
+            @Nonnull @NonnullElements @Unmodifiable final List<Validator<V>> newValidators) {
         throwSetterPreconditionExceptions();
         validators = List.copyOf(newValidators);
     }
@@ -61,14 +65,14 @@ public class ValidatorSequence<V> extends BaseValidator implements Validator<V>
      * @return list of validators
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<Validator<V>> getValidators() {
+    public final synchronized List<Validator<V>> getValidators() {
         return validators;
     }
 
     @Override
     public Action validate(@Nonnull final V value, @Nonnull final Item<?> item, @Nonnull final String stageId)
             throws StageProcessingException {
-        for (final Validator<V> validator: validators) {
+        for (final Validator<V> validator: getValidators()) {
             final Action action = validator.validate(value, item, stageId);
             if (action == Action.DONE) {
                 return action;
@@ -87,7 +91,7 @@ public class ValidatorSequence<V> extends BaseValidator implements Validator<V>
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
-        for (final Validator<V> validator : validators) {
+        for (final Validator<V> validator : getValidators()) {
             if (!validator.isInitialized()) {
                 validator.initialize();
             }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/x509/X509DSADetector.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/x509/X509DSADetector.java
index b331bbb..9a80962 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/x509/X509DSADetector.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/validate/x509/X509DSADetector.java
@@ -108,9 +108,8 @@ public class X509DSADetector extends BaseValidator implements Validator<X509Cert
         if ("DSA".equals(key.getAlgorithm())) {
             addStatus(error, "certificate contains a DSA key", item, stageId);
             return action;
-        } else {
-            return Action.CONTINUE;
         }
+        return Action.CONTINUE;
     }
 
 }
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/AbstractDOMValidationStageTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/AbstractDOMValidationStageTest.java
index 708b8e4..4d8347a 100644
--- a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/AbstractDOMValidationStageTest.java
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/AbstractDOMValidationStageTest.java
@@ -40,7 +40,7 @@ public class AbstractDOMValidationStageTest extends BaseDOMTest {
     private static class StringValidationStage extends AbstractDOMValidationStage<String, DOMTraversalContext> {
 
         @Override
-        protected boolean applicable(Element element) {
+        protected boolean applicable(Element element, DOMTraversalContext context) {
             return "checkedElement".equals(element.getLocalName());
         }
 
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/XMLSignatureSigningStageTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/XMLSignatureSigningStageTest.java
index f657010..74573af 100644
--- a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/XMLSignatureSigningStageTest.java
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/XMLSignatureSigningStageTest.java
@@ -96,7 +96,7 @@ public class XMLSignatureSigningStageTest extends BaseDOMTest {
 
         PrivateKey signingKey = KeyPairUtil.readPrivateKey(XMLSignatureSigningStageTest.class
                 .getResourceAsStream(classRelativeResource("signingKey.pem")));
-        X509Certificate signingCert = (X509Certificate) CertUtil.readCertificate(XMLSignatureSigningStageTest.class
+        X509Certificate signingCert = CertUtil.readCertificate(XMLSignatureSigningStageTest.class
                 .getResourceAsStream(classRelativeResource("signingCert.pem")));
         final List<X509Certificate> certs = new ArrayList<>();
         certs.add(signingCert);
@@ -133,7 +133,7 @@ public class XMLSignatureSigningStageTest extends BaseDOMTest {
 
         final PrivateKey signingKey = KeyPairUtil.readPrivateKey(XMLSignatureSigningStageTest.class
                 .getResourceAsStream(classRelativeResource("signingKey.pem")));
-        final X509Certificate signingCert = (X509Certificate) CertUtil.readCertificate(XMLSignatureSigningStageTest.class
+        final X509Certificate signingCert = CertUtil.readCertificate(XMLSignatureSigningStageTest.class
                 .getResourceAsStream(classRelativeResource("signingCert.pem")));
         final List<X509Certificate> certs = new ArrayList<>();
         certs.add(signingCert);
@@ -188,7 +188,7 @@ public class XMLSignatureSigningStageTest extends BaseDOMTest {
     public void setRemovingCRsFromSignature() throws Exception {
         PrivateKey signingKey = KeyPairUtil.readPrivateKey(XMLSignatureSigningStageTest.class
                 .getResourceAsStream(classRelativeResource("signingKey.pem")));
-        X509Certificate signingCert = (X509Certificate) CertUtil.readCertificate(XMLSignatureSigningStageTest.class
+        X509Certificate signingCert = CertUtil.readCertificate(XMLSignatureSigningStageTest.class
                 .getResourceAsStream(classRelativeResource("signingCert.pem")));
         final List<X509Certificate> certs = new ArrayList<>();
         certs.add(signingCert);
@@ -264,7 +264,7 @@ public class XMLSignatureSigningStageTest extends BaseDOMTest {
         final var md = getInput("input.xml");
         final var stage = new XMLSignatureSigningStage();
         
-        final var signingCert = (X509Certificate) CertUtil.readCertificate(XMLSignatureSigningStageTest.class
+        final var signingCert = CertUtil.readCertificate(XMLSignatureSigningStageTest.class
                 .getResourceAsStream(classRelativeResource("signingCert.pem")));
         final var certs = List.of(signingCert);
 
@@ -308,7 +308,13 @@ public class XMLSignatureSigningStageTest extends BaseDOMTest {
         // If we didn't have a public key or a certificate, we don't expect to see a KeyInfo at all.
         Assert.assertNull(keyInfo);
     }
-    
+
+    @Test
+    public final void testDefaultHash() {
+        final var stage = new XMLSignatureSigningStage();
+        Assert.assertEquals(stage.getShaVariant(), XMLSignatureSigningStage.ShaVariant.SHA256);
+    }
+
     private boolean hasChildNamed(@Nonnull final Element element, @Nonnull final QName name) {
         return !ElementSupport.getChildElements(element, name).isEmpty();
     }
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/SAMLStringElementCheckingStageTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/SAMLStringElementCheckingStageTest.java
index dfeface..a280d63 100644
--- a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/SAMLStringElementCheckingStageTest.java
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/SAMLStringElementCheckingStageTest.java
@@ -57,9 +57,9 @@ public class SAMLStringElementCheckingStageTest extends BaseDOMTest {
         final Item<Element> outItem = items.get(0);
         Assert.assertSame(outItem, item);
         final List<ErrorStatus> errors = item.getItemMetadata().get(ErrorStatus.class);
-        for (final ErrorStatus error : errors) {
-            System.out.println(error.getComponentId() + ": " + error.getStatusMessage());
-        }
+        //for (final ErrorStatus error : errors) {
+        //    System.out.println(error.getComponentId() + ": " + error.getStatusMessage());
+        //}
         Assert.assertEquals(errors.size(), 3);
     }
 }

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


More information about the commits mailing list