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

Ian Young ian at iay.org.uk
Tue Jul 14 16:10:32 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=8e3d404d9a6a9185da103ae881dc909e69d14851

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

commit 8e3d404d9a6a9185da103ae881dc909e69d14851
Author: Ian Young <ian at iay.org.uk>
AuthorDate: Tue Jul 14 17:10:28 2020 +0100

    MDA-242 - Review for thread safety
    
    https://issues.shibboleth.net/jira/browse/MDA-242
    
    Second tranche: dom.saml.mdattr, dom.saml.mdrpi, dom.saml.mdui, pipeline, pipeline.impl.
---
 .../mdattr/AbstractEntityAttributeMatcher.java     |   4 +-
 .../dom/saml/mdattr/AbstractExactValueMatcher.java |  13 +-
 .../saml/mdattr/AssuranceCertificationMatcher.java |   4 +-
 .../saml/mdattr/EntityAttributeAddingStage.java    | 142 ++++++++++-----------
 .../saml/mdattr/EntityAttributeFilteringStage.java |  32 +++--
 .../dom/saml/mdattr/EntityCategoryMatcher.java     |   4 +-
 .../saml/mdattr/EntityCategorySupportMatcher.java  |   4 +-
 .../dom/saml/mdattr/MultiPredicateMatcher.java     |  35 ++---
 .../saml/mdattr/RegistrationAuthorityMatcher.java  |   7 +-
 .../EntityRegistrationAuthorityFilterStage.java    |  47 +++----
 ...trationAuthorityItemIdentificationStrategy.java |  20 +--
 .../dom/saml/mdui/IPHintValidationStage.java       |   9 +-
 .../AbstractItemMetadataSelectionStage.java        |  23 ++--
 .../metadata/pipeline/AbstractStage.java           |   9 +-
 .../pipeline/AtLeastCollectionPredicate.java       |  12 +-
 .../metadata/pipeline/CompositeStage.java          |   7 +-
 .../FilesInDirectoryMultiOutputStrategy.java       |  35 ++---
 .../metadata/pipeline/FutureSupport.java           |   7 +-
 .../metadata/pipeline/ItemIdTransformStage.java    |   7 +-
 .../metadata/pipeline/ItemMetadataAddingStage.java |   9 +-
 .../metadata/pipeline/ItemMetadataFilterStage.java |   6 +-
 .../pipeline/ItemMetadataTerminationStage.java     |   6 +-
 .../metadata/pipeline/ItemOrderingStage.java       |   7 +-
 .../metadata/pipeline/ItemOrderingStrategy.java    |   6 +
 .../pipeline/MultiOutputSerializationStage.java    |  12 +-
 .../net/shibboleth/metadata/pipeline/Pipeline.java |   4 +
 .../metadata/pipeline/PipelineCallable.java        |   4 +-
 .../pipeline/PipelineDemultiplexerStage.java       |  21 +--
 .../metadata/pipeline/PipelineMergeStage.java      |  22 ++--
 .../metadata/pipeline/ScriptletStage.java          |  17 ++-
 .../metadata/pipeline/SerializationStage.java      |  33 ++---
 .../metadata/pipeline/SplitMergeStage.java         |  51 ++++----
 .../net/shibboleth/metadata/pipeline/Stage.java    |   4 +-
 .../metadata/pipeline/StaticItemSourceStage.java   |   5 +-
 .../pipeline/StatusMetadataLoggingStage.java       |  12 +-
 .../pipeline/impl/NoOpItemOrderingStrategy.java    |   2 +
 36 files changed, 346 insertions(+), 296 deletions(-)

diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractEntityAttributeMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractEntityAttributeMatcher.java
index 92f9aff..838107f 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractEntityAttributeMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractEntityAttributeMatcher.java
@@ -21,7 +21,7 @@ import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 
 import net.shibboleth.metadata.dom.saml.mdattr.EntityAttributeFilteringStage.EntityAttributeContext;
 
@@ -37,7 +37,7 @@ import net.shibboleth.metadata.dom.saml.mdattr.EntityAttributeFilteringStage.Ent
  *
  * @since 0.9.0
  */
- at ThreadSafe
+ at Immutable
 public abstract class AbstractEntityAttributeMatcher implements Predicate<EntityAttributeContext> {
 
     /**
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractExactValueMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractExactValueMatcher.java
index c814330..1a0bfed 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractExactValueMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractExactValueMatcher.java
@@ -19,7 +19,7 @@ package net.shibboleth.metadata.dom.saml.mdattr;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
@@ -30,17 +30,17 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  *
  * @since 0.9.0
  */
- at ThreadSafe
+ at Immutable
 public abstract class AbstractExactValueMatcher extends AbstractEntityAttributeMatcher {
 
     /** The attribute value to match. */
-    private final String value;
+    @Nonnull private final String value;
 
     /** The attribute <code>Name</code> to match. */
-    private final String name;
+    @Nonnull private final String name;
     
     /** The attribute <code>NameFormat</code> to match. */
-    private final String nameFormat;
+    @Nonnull private final String nameFormat;
     
     /** Registration authority to match against, or <code>null</code>. */
     @Nullable
@@ -84,9 +84,8 @@ public abstract class AbstractExactValueMatcher extends AbstractEntityAttributeM
         if (registrationAuthority == null) {
             // ignore the context's registration authority value
             return true;
-        } else {
-            return registrationAuthority.equals(inputRegistrationAuthority);
         }
+        return registrationAuthority.equals(inputRegistrationAuthority);
     }
 
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AssuranceCertificationMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AssuranceCertificationMatcher.java
index 7c1e3ab..aeed636 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AssuranceCertificationMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AssuranceCertificationMatcher.java
@@ -19,14 +19,14 @@ package net.shibboleth.metadata.dom.saml.mdattr;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 
 /**
  * An entity attribute matcher which matches a given assurance certification value.
  *
  * @since 0.10.0
  */
- at ThreadSafe
+ at Immutable
 public class AssuranceCertificationMatcher extends AbstractExactValueMatcher {
 
     /** Attribute Name value for assurance certifications. */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeAddingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeAddingStage.java
index 8701af1..24e846a 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeAddingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeAddingStage.java
@@ -17,11 +17,13 @@
 
 package net.shibboleth.metadata.dom.saml.mdattr;
 
+import java.util.Collection;
 import java.util.List;
-import java.util.function.Function;
 import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -35,8 +37,8 @@ import net.shibboleth.metadata.dom.saml.AttributeValueElementMaker;
 import net.shibboleth.metadata.dom.saml.AttributeValueElementMatcher;
 import net.shibboleth.metadata.dom.saml.SAMLMetadataSupport;
 import net.shibboleth.metadata.dom.saml.SAMLSupport;
-import net.shibboleth.metadata.pipeline.AbstractIteratingStage;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.metadata.pipeline.AbstractStage;
+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;
 
@@ -45,7 +47,8 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  *
  * @since 0.10.0
  */
-public class EntityAttributeAddingStage extends AbstractIteratingStage<Element> {
+ at ThreadSafe
+public class EntityAttributeAddingStage extends AbstractStage<Element> {
 
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(EntityAttributeAddingStage.class);
@@ -55,7 +58,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * 
      * The default value is for an entity attribute specifying an entity category.
      */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private String attributeName = EntityCategorySupport.EC_CATEGORY_ATTR_NAME;
 
     /**
@@ -63,11 +66,11 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * 
      * The default value is suitable for most entity attributes.
      */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private String attributeNameFormat = EntityCategorySupport.EC_ATTR_NAME_FORMAT;
 
     /** The value of the attribute to be added. */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private String attributeValue;
 
     /**
@@ -76,23 +79,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * 
      * Default: <code>false</code> (add as last child).
      */
-    private boolean addingFirstChild;
-
-    /** {@link Predicate} used to match existing Attribute elements. */
-    @NonnullAfterInit
-    private Predicate<Element> attributeMatcher;
-
-    /** {@link Function} used to create new Attribute elements. */
-    @NonnullAfterInit
-    private Function<Container, Element> attributeMaker;
-
-    /** {@link Predicate} used to match existing AttributeValue elements. */
-    @NonnullAfterInit
-    private Predicate<Element> attributeValueMatcher;
-
-    /** {@link Function} used to create new AttributeValue elements. */
-    @NonnullAfterInit
-    private Function<Container, Element> attributeValueMaker;
+    @GuardedBy("this") private boolean addingFirstChild;
 
     /**
      * Returns the attribute name.
@@ -100,7 +87,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * @return the attributeName
      */
     @Nonnull
-    public String getAttributeName() {
+    public final synchronized String getAttributeName() {
         return attributeName;
     }
 
@@ -109,7 +96,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * 
      * @param name the attributeName to set
      */
-    public void setAttributeName(@Nonnull final String name) {
+    public synchronized void setAttributeName(@Nonnull final String name) {
         throwSetterPreconditionExceptions();
         attributeName = Constraint.isNotNull(name, "attributeName must not be null");
     }
@@ -120,7 +107,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * @return the attributeNameFormat
      */
     @Nonnull
-    public String getAttributeNameFormat() {
+    public final synchronized String getAttributeNameFormat() {
         return attributeNameFormat;
     }
 
@@ -129,7 +116,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * 
      * @param nameFormat the attributeNameFormat to set
      */
-    public void setAttributeNameFormat(@Nonnull final String nameFormat) {
+    public synchronized void setAttributeNameFormat(@Nonnull final String nameFormat) {
         throwSetterPreconditionExceptions();
         attributeNameFormat = Constraint.isNotNull(nameFormat, "attributeNameFormat must not be null");
     }
@@ -140,7 +127,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * @return the attributeValue
      */
     @Nonnull
-    public String getAttributeValue() {
+    public final synchronized String getAttributeValue() {
         return attributeValue;
     }
 
@@ -149,7 +136,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * 
      * @param value the attributeValue to set
      */
-    public void setAttributeValue(@Nonnull final String value) {
+    public synchronized void setAttributeValue(@Nonnull final String value) {
         throwSetterPreconditionExceptions();
         attributeValue = Constraint.isNotNull(value, "attributeValue must not be null");
     }
@@ -160,7 +147,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * 
      * @return <code>true</code> if adding as the first child, <code>false</code> if the last
      */
-    public boolean isAddingFirstChild() {
+    public final synchronized boolean isAddingFirstChild() {
         return addingFirstChild;
     }
 
@@ -170,7 +157,7 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * 
      * @param addFirst <code>true</code> to add as the first child, <code>false</code> as the last
      */
-    public void setAddingFirstChild(final boolean addFirst) {
+    public synchronized void setAddingFirstChild(final boolean addFirst) {
         throwSetterPreconditionExceptions();
         addingFirstChild = addFirst;
     }
@@ -180,11 +167,13 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
      * Attribute container elements.
      * 
      * @param attributes {@link List} of Attribute {@link Container}s
+     * @param matcher 
      * @return true iff the value appears somewhere in the list of containers
      */
-    private boolean attributeValuePresent(@Nonnull final List<Container> attributes) {
+    private boolean attributeValuePresent(@Nonnull final List<Container> attributes,
+            @Nonnull final Predicate<Element> matcher) {
         for (final Container attribute : attributes) {
-            if (attribute.findChild(attributeValueMatcher) != null) {
+            if (attribute.findChild(matcher) != null) {
                 return true;
             }
         }
@@ -192,37 +181,49 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
     }
 
     @Override
-    protected void doExecute(@Nonnull final Item<Element> item) {
-        final Element entity = item.unwrap();
-        if (SAMLMetadataSupport.isEntityDescriptor(entity)) {
-            // Start from the entity
-            final Container entityContainer = new Container(entity);
-
-            // Dig down to <Extensions>
-            final Container extensionsContainer =
-                    entityContainer.locateChild(SAMLSupport.EXTENSIONS_MATCHER,
-                            SAMLSupport.EXTENSIONS_MAKER, Container.FIRST_CHILD);
-
-            // Dig down to <EntityAttributes>
-            final Container attributesContainer =
-                    extensionsContainer.locateChild(MDAttrSupport.ENTITY_ATTRIBUTES_MATCHER,
-                            MDAttrSupport.ENTITY_ATTRIBUTES_MAKER,
-                            addingFirstChild ? Container.FIRST_CHILD : Container.LAST_CHILD);
-
-            // Collect all matching <Attribute> containers
-            final List<Container> attributes =
-                    attributesContainer.findChildren(attributeMatcher);
-
-            // If any of the existing attribute values match our value, we're done
-            if (attributeValuePresent(attributes)) {
-                log.debug("attribute value '{}' already present", attributeValue);
-                return;
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<Element>> itemCollection) {
+        final var name = getAttributeName();
+        final var format = getAttributeNameFormat();
+        final var attributeMatcher = new AttributeElementMatcher(name, format);
+        final var attributeMaker = new AttributeElementMaker(name, format);
+
+        final var value = getAttributeValue();
+        final var attributeValueMatcher = new AttributeValueElementMatcher(value);
+        final var attributeValueMaker = new AttributeValueElementMaker(value);
+
+        for (final var item : itemCollection) {
+            final Element entity = item.unwrap();
+    
+            if (SAMLMetadataSupport.isEntityDescriptor(entity)) {
+                // Start from the entity
+                final Container entityContainer = new Container(entity);
+    
+                // Dig down to <Extensions>
+                final Container extensionsContainer =
+                        entityContainer.locateChild(SAMLSupport.EXTENSIONS_MATCHER,
+                                SAMLSupport.EXTENSIONS_MAKER, Container.FIRST_CHILD);
+    
+                // Dig down to <EntityAttributes>
+                final Container attributesContainer =
+                        extensionsContainer.locateChild(MDAttrSupport.ENTITY_ATTRIBUTES_MATCHER,
+                                MDAttrSupport.ENTITY_ATTRIBUTES_MAKER,
+                                isAddingFirstChild() ? Container.FIRST_CHILD : Container.LAST_CHILD);
+    
+                // Collect all matching <Attribute> containers
+                final List<Container> attributes =
+                        attributesContainer.findChildren(attributeMatcher);
+    
+                // If any of the existing attribute values match our value, we're done
+                if (attributeValuePresent(attributes, attributeValueMatcher)) {
+                    log.debug("attribute value '{}' already present", attributeValue);
+                    return;
+                }
+    
+                // If not already present, re-locate an <Attribute> and add it in there.
+                final Container attribute =
+                        attributesContainer.locateChild(attributeMatcher, attributeMaker, Container.LAST_CHILD);
+                attribute.addChild(attributeValueMaker, Container.LAST_CHILD);
             }
-
-            // If not already present, re-locate an <Attribute> and add it in there.
-            final Container attribute =
-                    attributesContainer.locateChild(attributeMatcher, attributeMaker, Container.LAST_CHILD);
-            attribute.addChild(attributeValueMaker, Container.LAST_CHILD);
         }
     }
 
@@ -234,19 +235,6 @@ public class EntityAttributeAddingStage extends AbstractIteratingStage<Element>
             throw new ComponentInitializationException("attributeValue property must be supplied");
         }
 
-        attributeMatcher = new AttributeElementMatcher(attributeName, attributeNameFormat);
-        attributeMaker = new AttributeElementMaker(attributeName, attributeNameFormat);
-        attributeValueMatcher = new AttributeValueElementMatcher(attributeValue);
-        attributeValueMaker = new AttributeValueElementMaker(attributeValue);
-    }
-
-    @Override
-    protected void doDestroy() {
-        attributeMatcher = null;
-        attributeMaker = null;
-        attributeValueMatcher = null;
-        attributeValueMaker = null;
-        super.doDestroy();
     }
 
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage.java
index c392a44..2604492 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage.java
@@ -22,6 +22,9 @@ import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -55,6 +58,7 @@ import net.shibboleth.utilities.java.support.xml.ElementSupport;
  *
  * @since 0.9.0
  */
+ at ThreadSafe
 public class EntityAttributeFilteringStage extends AbstractIteratingStage<Element> {
 
     /** Class logger. */
@@ -106,6 +110,7 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
     /**
      * A simple immutable implementation of {@link EntityAttributeContext}.
      */
+    @Immutable
     static class ContextImpl implements EntityAttributeContext {
 
         /** The attribute's value. */
@@ -198,21 +203,21 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
      * This amounts to an implicit ORing of the individual rules, with early
      * termination.
      */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<Predicate<EntityAttributeContext>> rules = List.of();
 
     /** Mode of operation: whitelisting or blacklisting. Default: whitelisting. */
-    private boolean whitelisting = true;
+    @GuardedBy("this") private boolean whitelisting = true;
 
     /** Whether we add status metadata to the item when entity attributes are removed. Default: no. */
-    private boolean recordingRemovals;
+    @GuardedBy("this") private boolean recordingRemovals;
 
     /**
      * Sets the {@link List} of rules to be used to match attribute values.
      * 
      * @param newRules new {@link List} of rules
      */
-    public void setRules(
+    public synchronized void setRules(
             @Nonnull @NonnullElements @Unmodifiable final List<Predicate<EntityAttributeContext>> newRules) {
         throwSetterPreconditionExceptions();
         rules = List.copyOf(Constraint.isNotNull(newRules, "rules property may not be null"));
@@ -224,7 +229,7 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
      * @return the {@link List} of rules
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<Predicate<EntityAttributeContext>> getRules() {
+    public final synchronized List<Predicate<EntityAttributeContext>> getRules() {
         return rules;
     }
     
@@ -234,7 +239,7 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
      * @param newValue <code>true</code> to whitelist (default),
      *                 <code>false</code> to blacklist
      */
-    public void setWhitelisting(final boolean newValue) {
+    public synchronized void setWhitelisting(final boolean newValue) {
         throwSetterPreconditionExceptions();
         whitelisting = newValue;
     }
@@ -245,7 +250,7 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
      * @return <code>true</code> if whitelisting (default),
      *         <code>false</code> if blacklisting
      */
-    public boolean isWhitelisting() {
+    public final synchronized boolean isWhitelisting() {
         return whitelisting;
     }
 
@@ -254,7 +259,7 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
      *
      * @param newValue whether to remove recorded entity attributes
      */
-    public void setRecordingRemovals(final boolean newValue) {
+    public synchronized void setRecordingRemovals(final boolean newValue) {
         throwSetterPreconditionExceptions();
         recordingRemovals = newValue;
     }
@@ -265,7 +270,7 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
      * @return <code>true</code> if recording,
      *         <code>false</code> if not (default)
      */
-    public boolean isRecordingRemovals() {
+    public synchronized boolean isRecordingRemovals() {
         return recordingRemovals;
     }
 
@@ -277,7 +282,7 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
      *  otherwise <code>false</code>
      */
     private boolean applyRules(final EntityAttributeContext ctx) {
-        for (final Predicate<EntityAttributeContext> rule : rules) {
+        for (final Predicate<EntityAttributeContext> rule : getRules()) {
             if (rule.test(ctx)) {
                 return true;
             }
@@ -295,9 +300,8 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
         final List<RegistrationAuthority> regAuthList = item.getItemMetadata().get(RegistrationAuthority.class);
         if (regAuthList.isEmpty()) {
             return null;
-        } else {
-            return regAuthList.get(0).getRegistrationAuthority();
         }
+        return regAuthList.get(0).getRegistrationAuthority();
     }
     
     /**
@@ -328,9 +332,9 @@ public class EntityAttributeFilteringStage extends AbstractIteratingStage<Elemen
                     new ContextImpl(attributeValue, attributeName,
                             attributeNameFormat, registrationAuthority);            
             final boolean matched = applyRules(ctx);
-            if (matched ^ whitelisting) {
+            if (matched ^ isWhitelisting()) {
                 log.debug("removing {}", ctx);
-                if (recordingRemovals) {
+                if (isRecordingRemovals()) {
                     item.getItemMetadata().put(new WarningStatus(getId(),
                             "removing '" + ctx.getName() + "' = '" + ctx.getValue() + "'"));
                 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcher.java
index ab0f18c..5b4eced 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcher.java
@@ -19,14 +19,14 @@ package net.shibboleth.metadata.dom.saml.mdattr;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 
 /**
  * An entity attribute matcher which matches a given entity category.
  *
  * @since 0.9.0
  */
- at ThreadSafe
+ at Immutable
 public class EntityCategoryMatcher extends AbstractExactValueMatcher {
     
     /**
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcher.java
index 2a2cc21..8b355ad 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcher.java
@@ -19,14 +19,14 @@ package net.shibboleth.metadata.dom.saml.mdattr;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 
 /**
  * An entity attribute matcher which matches a given entity support category.
  *
  * @since 0.9.0
  */
- at ThreadSafe
+ at Immutable
 public class EntityCategorySupportMatcher extends AbstractExactValueMatcher {
     
     /**
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcher.java
index 9ca1707..f634c22 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcher.java
@@ -21,6 +21,7 @@ import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -40,19 +41,19 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
 
     /** {@link Predicate} to use to match the context's attribute value. */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private Predicate<CharSequence> valuePredicate = x -> true;
     
     /** {@link Predicate} to use to match the context's attribute name. */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private Predicate<CharSequence> namePredicate = x -> true;
     
     /** {@link Predicate} to use to match the context's attribute name format. */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private Predicate<CharSequence> nameFormatPredicate = x -> true;
     
     /** {@link Predicate} to use to match the context's registration authority. */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private Predicate<CharSequence> registrationAuthorityPredicate = x -> true;
     
     /**
@@ -61,7 +62,7 @@ public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
      * @return the {@link Predicate} being used to match the context's attribute value
      */
     @Nonnull
-    public Predicate<CharSequence> getValuePredicate() {
+    public final synchronized Predicate<CharSequence> getValuePredicate() {
         return valuePredicate;
     }
     
@@ -70,7 +71,7 @@ public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
      * 
      * @param predicate new {@link Predicate} to use to match the context's attribute value
      */
-    public void setValuePredicate(@Nonnull final Predicate<CharSequence> predicate) {
+    public synchronized void setValuePredicate(@Nonnull final Predicate<CharSequence> predicate) {
         valuePredicate = Constraint.isNotNull(predicate, "value predicate may not be null");
     }
     
@@ -80,7 +81,7 @@ public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
      * @return the {@link Predicate} being used to match the context's attribute name
      */
     @Nonnull
-    public Predicate<CharSequence> getNamePredicate() {
+    public final synchronized Predicate<CharSequence> getNamePredicate() {
         return namePredicate;
     }
     
@@ -89,7 +90,7 @@ public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
      * 
      * @param predicate new {@link Predicate} to use to match the context's attribute name
      */
-    public void setNamePredicate(@Nonnull final Predicate<CharSequence> predicate) {
+    public synchronized void setNamePredicate(@Nonnull final Predicate<CharSequence> predicate) {
         namePredicate = Constraint.isNotNull(predicate, "name predicate may not be null");
     }
     
@@ -99,7 +100,7 @@ public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
      * @return the {@link Predicate} being used to match the context's attribute name format
      */
     @Nonnull
-    public Predicate<CharSequence> getNameFormatPredicate() {
+    public final synchronized Predicate<CharSequence> getNameFormatPredicate() {
         return nameFormatPredicate;
     }
     
@@ -108,7 +109,7 @@ public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
      * 
      * @param predicate new {@link Predicate} to use to match the context's attribute name format
      */
-    public void setNameFormatPredicate(@Nonnull final Predicate<CharSequence> predicate) {
+    public synchronized void setNameFormatPredicate(@Nonnull final Predicate<CharSequence> predicate) {
         nameFormatPredicate = Constraint.isNotNull(predicate, "name format predicate may not be null");
     }
     
@@ -118,8 +119,8 @@ public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
      * @return the {@link Predicate} being used to match the context's registration authority
      */
     @Nonnull
-    public Predicate<CharSequence> getRegistrationAuthorityPredicate() {
-        return valuePredicate;
+    public final synchronized Predicate<CharSequence> getRegistrationAuthorityPredicate() {
+        return registrationAuthorityPredicate;
     }
     
     /**
@@ -127,29 +128,29 @@ public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
      * 
      * @param predicate new {@link Predicate} to use to match the context's registration authority
      */
-    public void setRegistrationAuthorityPredicate(@Nonnull final Predicate<CharSequence> predicate) {
+    public synchronized void setRegistrationAuthorityPredicate(@Nonnull final Predicate<CharSequence> predicate) {
         registrationAuthorityPredicate = Constraint.isNotNull(predicate,
                 "registration authority predicate may not be null");
     }
     
     @Override
     protected boolean matchAttributeValue(@Nonnull final String inputValue) {
-        return valuePredicate.test(inputValue);
+        return getValuePredicate().test(inputValue);
     }
 
     @Override
     protected boolean matchAttributeName(@Nonnull final String inputName) {
-         return namePredicate.test(inputName);
+         return getNamePredicate().test(inputName);
     }
 
     @Override
     protected boolean matchAttributeNameFormat(@Nonnull final String inputNameFormat) {
-        return nameFormatPredicate.test(inputNameFormat);
+        return getNameFormatPredicate().test(inputNameFormat);
     }
 
     @Override
     protected boolean matchRegistrationAuthority(@Nullable final String inputRegistrationAuthority) {
-        return registrationAuthorityPredicate.test(inputRegistrationAuthority);
+        return getRegistrationAuthorityPredicate().test(inputRegistrationAuthority);
     }
 
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcher.java
index 621350c..90a962b 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcher.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcher.java
@@ -21,7 +21,7 @@ import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
+import javax.annotation.concurrent.Immutable;
 
 import net.shibboleth.metadata.dom.saml.mdattr.EntityAttributeFilteringStage.EntityAttributeContext;
 
@@ -33,7 +33,7 @@ import net.shibboleth.metadata.dom.saml.mdattr.EntityAttributeFilteringStage.Ent
  *
  * @since 0.9.0
  */
- at ThreadSafe
+ at Immutable
 public class RegistrationAuthorityMatcher implements Predicate<EntityAttributeContext> {
     
     /** Registration authority to match against. */
@@ -54,9 +54,8 @@ public class RegistrationAuthorityMatcher implements Predicate<EntityAttributeCo
         if (registrationAuthority == null) {
             // match entities *without* a registration authority
             return null == input.getRegistrationAuthority();
-        } else {
-            return registrationAuthority.equals(input.getRegistrationAuthority());
         }
+        return registrationAuthority.equals(input.getRegistrationAuthority());
     }
 
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdrpi/EntityRegistrationAuthorityFilterStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdrpi/EntityRegistrationAuthorityFilterStage.java
index f28111b..1cf5cc3 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdrpi/EntityRegistrationAuthorityFilterStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdrpi/EntityRegistrationAuthorityFilterStage.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;
@@ -45,24 +46,24 @@ public class EntityRegistrationAuthorityFilterStage extends AbstractFilteringSta
     private final Logger log = LoggerFactory.getLogger(EntityRegistrationAuthorityFilterStage.class);
 
     /** Whether a descriptor is required to have registration information. Default value: false */
-    private boolean requiringRegistrationInformation;
+    @GuardedBy("this") private boolean requiringRegistrationInformation;
 
     /** Registrars which are white/black listed depending on the value of {@link #whitelistingAuthorities}. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Set<String> designatedAuthorities = Set.of();
 
     /** Whether {@link #designatedAuthorities} should be considered a whitelist or a blacklist. Default value: false */
-    private boolean whitelistingAuthorities;
+    @GuardedBy("this") private boolean whitelistingAuthorities;
 
     /** Whether EntitiesDescriptor that do not contain EntityDescriptors should be removed. Default value: true */
-    private boolean removingEntitylessEntitiesDescriptor = true;
+    @GuardedBy("this") private boolean removingEntitylessEntitiesDescriptor = true;
 
     /**
      * Gets whether a descriptor is required to have registration information.
      * 
      * @return whether a descriptor is required to have registration information
      */
-    public boolean isRequiringRegistrationInformation() {
+    public final synchronized boolean isRequiringRegistrationInformation() {
         return requiringRegistrationInformation;
     }
 
@@ -82,7 +83,7 @@ public class EntityRegistrationAuthorityFilterStage extends AbstractFilteringSta
      * @return list of designated registration authority, never null
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<String> getDesignatedRegistrationAuthorities() {
+    public final synchronized Collection<String> getDesignatedRegistrationAuthorities() {
         return designatedAuthorities;
     }
 
@@ -102,7 +103,7 @@ public class EntityRegistrationAuthorityFilterStage extends AbstractFilteringSta
      * 
      * @return true if the designated registration authority should be considered a whitelist, false otherwise
      */
-    public boolean isWhitelistingRegistrationAuthorities() {
+    public final synchronized boolean isWhitelistingRegistrationAuthorities() {
         return whitelistingAuthorities;
     }
 
@@ -122,7 +123,7 @@ public class EntityRegistrationAuthorityFilterStage extends AbstractFilteringSta
      * 
      * @return whether EntitiesDescriptor that do not contain EntityDescriptors should be removed
      */
-    public boolean isRemovingEntitylessEntitiesDescriptor() {
+    public final synchronized boolean isRemovingEntitylessEntitiesDescriptor() {
         return removingEntitylessEntitiesDescriptor;
     }
 
@@ -136,13 +137,6 @@ public class EntityRegistrationAuthorityFilterStage extends AbstractFilteringSta
         removingEntitylessEntitiesDescriptor = remove;
     }
 
-    @Override
-    protected void doDestroy() {
-        designatedAuthorities = null;
-
-        super.doDestroy();
-    }
-
     @Override
     protected boolean doExecute(@Nonnull final Item<Element> item) {
         final Element descriptor;
@@ -198,8 +192,8 @@ public class EntityRegistrationAuthorityFilterStage extends AbstractFilteringSta
             }
         }
 
-        if (removingEntitylessEntitiesDescriptor && childEntitiesDescriptors.isEmpty()
-                && childEntityDescriptors.isEmpty()) {
+        if (childEntitiesDescriptors.isEmpty() && childEntityDescriptors.isEmpty() &&
+                isRemovingEntitylessEntitiesDescriptor()) {
             return true;
         }
 
@@ -223,15 +217,14 @@ public class EntityRegistrationAuthorityFilterStage extends AbstractFilteringSta
         final Element registrationInfoElement =
                 SAMLMetadataSupport.getDescriptorExtension(descriptor, MDRPIMetadataSupport.MDRPI_REGISTRATION_INFO);
         if (registrationInfoElement == null) {
-            if (requiringRegistrationInformation) {
+            if (isRequiringRegistrationInformation()) {
                 log.debug(
                         "{} pipeline stage removing Item because it did not have " +
                                 "required registration information extension",
                         getId());
                 return true;
-            } else {
-                return false;
             }
+            return false;
         }
 
         final String registrationAuthority =
@@ -244,13 +237,15 @@ public class EntityRegistrationAuthorityFilterStage extends AbstractFilteringSta
             return true;
         }
 
-        if (whitelistingAuthorities && !designatedAuthorities.contains(registrationAuthority)) {
+        if (isWhitelistingRegistrationAuthorities() &&
+                !getDesignatedRegistrationAuthorities().contains(registrationAuthority)) {
             log.debug("{} pipeline stage removing Item because its registration authority was not on the whitelist",
                     getId());
             return true;
         }
 
-        if (!whitelistingAuthorities && designatedAuthorities.contains(registrationAuthority)) {
+        if (!isWhitelistingRegistrationAuthorities() &&
+                getDesignatedRegistrationAuthorities().contains(registrationAuthority)) {
             log.debug("{} pipeline stage removing Item because its registration authority was on the blacklist",
                     getId());
             return true;
@@ -258,4 +253,12 @@ public class EntityRegistrationAuthorityFilterStage extends AbstractFilteringSta
 
         return false;
     }
+
+    @Override
+    protected void doDestroy() {
+        designatedAuthorities = null;
+
+        super.doDestroy();
+    }
+
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdrpi/RegistrationAuthorityItemIdentificationStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdrpi/RegistrationAuthorityItemIdentificationStrategy.java
index af9a83d..539d2af 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdrpi/RegistrationAuthorityItemIdentificationStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdrpi/RegistrationAuthorityItemIdentificationStrategy.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 net.shibboleth.metadata.FirstItemIdItemIdentificationStrategy;
@@ -49,13 +50,13 @@ public class RegistrationAuthorityItemIdentificationStrategy extends FirstItemId
     /**
      * Set of registration authorities to be ignored.
      */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Set<String> ignoredRegistrationAuthorities = Set.of();
     
     /**
      * Replacement display names for registration authorities.
      */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Map<String, String> registrationAuthorityDisplayNames = Map.of();
     
     /**
@@ -64,7 +65,7 @@ public class RegistrationAuthorityItemIdentificationStrategy extends FirstItemId
      * @return {@link Set} of registration authority names.
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<String> getIgnoredRegistrationAuthorities() {
+    public final synchronized Collection<String> getIgnoredRegistrationAuthorities() {
         return ignoredRegistrationAuthorities;
     }
 
@@ -73,7 +74,7 @@ public class RegistrationAuthorityItemIdentificationStrategy extends FirstItemId
      * 
      * @param registrars {@link Set} of registration authority names to ignore.
      */
-    public void setIgnoredRegistrationAuthorities(
+    public synchronized void setIgnoredRegistrationAuthorities(
             @Nonnull @NonnullElements @Unmodifiable final Collection<String> registrars) {
         ignoredRegistrationAuthorities = Set.copyOf(registrars);
     }
@@ -84,7 +85,7 @@ public class RegistrationAuthorityItemIdentificationStrategy extends FirstItemId
      * @return {@link Map} of display names for authorities.
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Map<String, String> getRegistrationAuthorityDisplayNames() {
+    public final synchronized Map<String, String> getRegistrationAuthorityDisplayNames() {
         return registrationAuthorityDisplayNames;
     }
 
@@ -93,7 +94,7 @@ public class RegistrationAuthorityItemIdentificationStrategy extends FirstItemId
      * 
      * @param names {@link Map} of display names for registration authorities.
      */
-    public void setRegistrationAuthorityDisplayNames(
+    public synchronized void setRegistrationAuthorityDisplayNames(
             @Nonnull @NonnullElements @Unmodifiable final Map<String, String> names) {
         registrationAuthorityDisplayNames = Map.copyOf(names);
     }
@@ -117,17 +118,16 @@ public class RegistrationAuthorityItemIdentificationStrategy extends FirstItemId
         final String regAuth = regAuths.get(0).getRegistrationAuthority();
         
         // nothing to return if it's an ignored authority
-        if (ignoredRegistrationAuthorities.contains(regAuth)) {
+        if (getIgnoredRegistrationAuthorities().contains(regAuth)) {
             return null;
         }
         
         // handle mapping it to a simpler form if that's available
-        final String displayName = registrationAuthorityDisplayNames.get(regAuth);
+        final String displayName = getRegistrationAuthorityDisplayNames().get(regAuth);
         if (displayName != null) {
             return displayName;
-        } else {
-            return regAuth;
         }
+        return regAuth;
     }
 
 }
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 23c04c6..61e4be7 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
@@ -18,6 +18,7 @@
 package net.shibboleth.metadata.dom.saml.mdui;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.w3c.dom.Element;
@@ -38,14 +39,14 @@ import net.shibboleth.utilities.java.support.xml.ElementSupport;
 public class IPHintValidationStage extends AbstractDOMTraversalStage<DOMTraversalContext> {
 
     /** Whether to check that the CIDR notation describes a network. Defaults to true. */
-    private boolean checkingNetworks = true;
+    @GuardedBy("this") private boolean checkingNetworks = true;
     
     /**
      * Gets whether the stage is checking for network addresses only.
      * 
      * @return whether the stage is checking for network addresses only
      */
-    public boolean isCheckingNetworks() {
+    public final synchronized boolean isCheckingNetworks() {
         return checkingNetworks;
     }
 
@@ -54,7 +55,7 @@ public class IPHintValidationStage extends AbstractDOMTraversalStage<DOMTraversa
      * 
      * @param check whether to check for network addresses only
      */
-    public void setCheckingNetworks(final boolean check) {
+    public synchronized void setCheckingNetworks(final boolean check) {
         throwSetterPreconditionExceptions();
         this.checkingNetworks = check;
     }
@@ -71,7 +72,7 @@ public class IPHintValidationStage extends AbstractDOMTraversalStage<DOMTraversa
         final String hint = ipHint.getTextContent();
         try {
             final IPRange range = IPRange.parseCIDRBlock(hint);
-            if (checkingNetworks) {
+            if (isCheckingNetworks()) {
                 if (range.getHostAddress() != null) {
                     addError(context.getItem(), ipHint, "invalid IPHint '" + hint +
                             "': CIDR notation represents a host, not a network");
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AbstractItemMetadataSelectionStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AbstractItemMetadataSelectionStage.java
index 9ec78e8..3b17e0c 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AbstractItemMetadataSelectionStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AbstractItemMetadataSelectionStage.java
@@ -25,6 +25,7 @@ import java.util.Map;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.FirstItemIdItemIdentificationStrategy;
@@ -47,10 +48,11 @@ public abstract class AbstractItemMetadataSelectionStage<T> extends AbstractStag
     /**
      * {@link ItemMetadata} classes that, if an item contains them, will cause the {@link Item} to be selected.
      */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private Set<Class<? extends ItemMetadata>> selectionRequirements = Set.of();
 
     /** Strategy used to generate item identifiers for logging purposes. */
+    @Nonnull @GuardedBy("this")
     private ItemIdentificationStrategy identificationStrategy = new FirstItemIdItemIdentificationStrategy();
 
     /**
@@ -61,7 +63,7 @@ public abstract class AbstractItemMetadataSelectionStage<T> extends AbstractStag
      *         selected, never null nor containing null elements
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<Class<? extends ItemMetadata>> getSelectionRequirements() {
+    public final synchronized Collection<Class<? extends ItemMetadata>> getSelectionRequirements() {
         return selectionRequirements;
     }
 
@@ -83,7 +85,7 @@ public abstract class AbstractItemMetadataSelectionStage<T> extends AbstractStag
      * 
      * @return strategy used to generate {@link Item} identifiers for logging purposes
      */
-    @Nonnull public ItemIdentificationStrategy getItemIdentificationStrategy() {
+    @Nonnull public final synchronized ItemIdentificationStrategy getItemIdentificationStrategy() {
         return identificationStrategy;
     }
 
@@ -107,7 +109,7 @@ public abstract class AbstractItemMetadataSelectionStage<T> extends AbstractStag
             final HashMap<Class<? extends ItemMetadata>, List<? extends ItemMetadata>> matchingMetadata =
                     new HashMap<>();
 
-            for (final Class<? extends ItemMetadata> infoClass : selectionRequirements) {
+            for (final Class<? extends ItemMetadata> infoClass : getSelectionRequirements()) {
                 if (item.getItemMetadata().containsKey(infoClass)) {
                     matchingMetadata.put(infoClass, item.getItemMetadata().get(infoClass));
                 }
@@ -136,12 +138,11 @@ public abstract class AbstractItemMetadataSelectionStage<T> extends AbstractStag
      * 
      * @throws StageProcessingException thrown if there is a problem processing the item
      */
-    protected abstract
-            void
-            doExecute(
-                    @Nonnull @NonnullElements final Collection<Item<T>> itemCollection,
-                    @Nonnull final Item<T> matchingItem,
-                    @Nonnull @NonnullElements
-                    final Map<Class<? extends ItemMetadata>, List<? extends ItemMetadata>> matchingMetadata)
+    protected abstract void doExecute(
+            @Nonnull @NonnullElements final Collection<Item<T>> itemCollection,
+            @Nonnull final Item<T> matchingItem,
+            @Nonnull @NonnullElements
+            final Map<Class<? extends ItemMetadata>, List<? extends ItemMetadata>> matchingMetadata)
                     throws StageProcessingException;
+
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AbstractStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AbstractStage.java
index bfffc89..5393f62 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AbstractStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AbstractStage.java
@@ -22,6 +22,7 @@ import java.util.Collection;
 import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
@@ -41,7 +42,7 @@ public abstract class AbstractStage<T> extends BaseIdentifiableInitializableComp
      * The {@link Predicate} applied to the supplied item collection to determine whether the stage will be executed.
      * Default value: always <code>true</code>.
      */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private Predicate<Collection<Item<T>>> collectionPredicate = x -> true;
     
     /**
@@ -51,7 +52,7 @@ public abstract class AbstractStage<T> extends BaseIdentifiableInitializableComp
      * @param pred the {@link Predicate} applied to the supplied item collection to determine
      * whether the stage will be executed
      */
-    public void setCollectionPredicate(@Nonnull final Predicate<Collection<Item<T>>> pred) {
+    public synchronized void setCollectionPredicate(@Nonnull final Predicate<Collection<Item<T>>> pred) {
         throwSetterPreconditionExceptions();
         collectionPredicate = Constraint.isNotNull(pred, "collectionPredicate may not be null");
     }
@@ -64,7 +65,7 @@ public abstract class AbstractStage<T> extends BaseIdentifiableInitializableComp
      * the stage will be executed.
      */
     @Nonnull
-    public Predicate<Collection<Item<T>>> getCollectionPredicate() {
+    public final synchronized Predicate<Collection<Item<T>>> getCollectionPredicate() {
         return collectionPredicate;
     }
 
@@ -75,7 +76,7 @@ public abstract class AbstractStage<T> extends BaseIdentifiableInitializableComp
 
         final var start = Instant.now();
 
-        if (collectionPredicate.test(itemCollection)) {
+        if (getCollectionPredicate().test(itemCollection)) {
             doExecute(itemCollection);
         }
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AtLeastCollectionPredicate.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AtLeastCollectionPredicate.java
index 6433b43..37f4820 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AtLeastCollectionPredicate.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/AtLeastCollectionPredicate.java
@@ -20,6 +20,9 @@ package net.shibboleth.metadata.pipeline;
 import java.util.Collection;
 import java.util.function.Predicate;
 
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
 /**
  * A {@link Predicate} which returns <code>true</code> if and only if the number of elements
  * in the supplied collection is at least the configured minimum value.
@@ -28,12 +31,13 @@ import java.util.function.Predicate;
  *
  * @since 0.9.0
  */
+ at ThreadSafe
 public class AtLeastCollectionPredicate<T> implements Predicate<Collection<T>> {
 
     /**
      * Minimum element count which will satisfy the condition. Default value: 0.
      */
-    private int minimum;
+    @GuardedBy("this") private int minimum;
 
     /**
      * Returns the minimum number of elements which will result in a <code>true</code>
@@ -42,7 +46,7 @@ public class AtLeastCollectionPredicate<T> implements Predicate<Collection<T>> {
      * @return the minimum number of elements which will result in a <code>true</code>
      * result.
      */
-    public int getMinimum() {
+    public final synchronized int getMinimum() {
         return minimum;
     }
     
@@ -53,13 +57,13 @@ public class AtLeastCollectionPredicate<T> implements Predicate<Collection<T>> {
      * @param min minimum number of elements which will result in a <code>true</code>
      * result.
      */
-    public void setMinimum(final int min) {
+    public synchronized void setMinimum(final int min) {
         minimum = min;
     }
     
     @Override
     public boolean test(final Collection<T> input) {
-        return input.size() >= minimum;
+        return input.size() >= getMinimum();
     }
 
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/CompositeStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/CompositeStage.java
index ee4c243..18991b4 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/CompositeStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/CompositeStage.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
@@ -37,7 +38,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 public class CompositeStage<T> extends AbstractStage<T> {
 
     /** Stages which compose this stage. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<Stage<T>> composedStages = List.of();
 
 
@@ -47,7 +48,7 @@ public class CompositeStage<T> extends AbstractStage<T> {
      * @return list the stages that compose this stage, never null nor containing null elements
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<Stage<T>> getComposedStages() {
+    public final synchronized List<Stage<T>> getComposedStages() {
         return composedStages;
     }
 
@@ -65,7 +66,7 @@ public class CompositeStage<T> extends AbstractStage<T> {
     @Override
     protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection)
             throws StageProcessingException {
-        for (final Stage<T> stage : composedStages) {
+        for (final Stage<T> stage : getComposedStages()) {
             stage.execute(itemCollection);
         }
     }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/FilesInDirectoryMultiOutputStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/FilesInDirectoryMultiOutputStrategy.java
index be8adc9..7a546cc 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/FilesInDirectoryMultiOutputStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/FilesInDirectoryMultiOutputStrategy.java
@@ -25,6 +25,9 @@ import java.util.List;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -53,6 +56,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  *
  * @since 0.9.2
  */
+ at ThreadSafe
 public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableComponent
     implements MultiOutputSerializationStage.OutputStrategy<T> {
     
@@ -62,10 +66,11 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
     /**
      * Class implementing the returned {@link MultiOutputSerializationStage.Destination} objects.
      */
+    @Immutable
     private class FileDestination implements MultiOutputSerializationStage.Destination {
 
         /** The destination {@link File}. */
-        private final File file;
+        @Nonnull private final File file;
 
         /**
          * Constructor.
@@ -87,23 +92,23 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
         
     }
     /** String to use as a prefix for file names generated by this strategy. Default value: empty string. */
-    @Nonnull private String namePrefix = "";
+    @Nonnull @GuardedBy("this") private String namePrefix = "";
     
     /** {@link Function} to use to transform the {@link Item}'s {@link ItemId}. Default: identity transform. */
-    @Nonnull private Function<String, String> nameTransformer = x -> x;
+    @Nonnull @GuardedBy("this") private Function<String, String> nameTransformer = x -> x;
 
     /** String to use as a suffix for file names generated by this strategy. Default value: empty string. */
-    @Nonnull private String nameSuffix = "";
+    @Nonnull @GuardedBy("this") private String nameSuffix = "";
     
     /** Directory into which to write files. */
-    @NonnullAfterInit private File directory;
+    @NonnullAfterInit @GuardedBy("this") private File directory;
 
     /**
      * Gets the name prefix in use.
      * 
      * @return the name prefix in use
      */
-    @Nonnull public String getNamePrefix() {
+    @Nonnull public final synchronized String getNamePrefix() {
         return namePrefix;
     }
 
@@ -112,7 +117,7 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
      * 
      * @param prefix the name prefix to use
      */
-    public void setNamePrefix(@Nonnull final String prefix) {
+    public synchronized void setNamePrefix(@Nonnull final String prefix) {
         throwSetterPreconditionExceptions();
         namePrefix = Constraint.isNotNull(prefix, "name prefix may not be null");
     }
@@ -122,7 +127,7 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
      * 
      * @return the name transformer in use.
      */
-    @Nonnull public Function<String, String> getNameTransformer() {
+    @Nonnull public final synchronized Function<String, String> getNameTransformer() {
         return nameTransformer;
     }
 
@@ -131,7 +136,7 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
      * 
      * @param transformer the name transformer to use
      */
-    public void setNameTransformer(@Nonnull final Function<String, String> transformer) {
+    public synchronized void setNameTransformer(@Nonnull final Function<String, String> transformer) {
         throwSetterPreconditionExceptions();
         nameTransformer = Constraint.isNotNull(transformer,
                 "name transformer may not be null");
@@ -142,7 +147,7 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
      * 
      * @return the name suffix in use
      */
-    @Nonnull public String getNameSuffix() {
+    @Nonnull public final synchronized String getNameSuffix() {
         return nameSuffix;
     }
 
@@ -151,7 +156,7 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
      * 
      * @param suffix the name suffix to use
      */
-    public void setNameSuffix(@Nonnull final String suffix) {
+    public synchronized void setNameSuffix(@Nonnull final String suffix) {
         throwSetterPreconditionExceptions();
         nameSuffix = Constraint.isNotNull(suffix, "name suffix may not be null");
     }
@@ -161,7 +166,7 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
      * 
      * @return the directory in use
      */
-    @Nonnull public File getDirectory() {
+    @Nonnull public final synchronized File getDirectory() {
         return directory;
     }
 
@@ -170,7 +175,7 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
      * 
      * @param dir the directory to use
      */
-    public void setDirectory(@Nonnull final File dir) {
+    public synchronized void setDirectory(@Nonnull final File dir) {
         throwSetterPreconditionExceptions();
         directory = Constraint.isNotNull(dir, "directory may not be null");
     }
@@ -185,11 +190,11 @@ public class FilesInDirectoryMultiOutputStrategy<T> extends BaseInitializableCom
         final ItemId id = ids.get(0);
 
         // Construct file name
-        final String name = namePrefix + nameTransformer.apply(id.getId()) + nameSuffix;
+        final String name = getNamePrefix() + getNameTransformer().apply(id.getId()) + getNameSuffix();
         log.debug("id mapped {} -> {}", id.getId(), name);
         
         // Locate the output file within the directory
-        final File outputFile = new File(directory, name);
+        final File outputFile = new File(getDirectory(), name);
 
         return new FileDestination(outputFile);
     }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/FutureSupport.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/FutureSupport.java
index e4c00f0..d2bd558 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/FutureSupport.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/FutureSupport.java
@@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
@@ -43,6 +44,7 @@ public final class FutureSupport {
      *
      * @param <T> type of "future" object to return
      */
+    @Immutable
     private static class FutureNow<T> implements Future<T> {
     
         /**
@@ -130,10 +132,9 @@ public final class FutureSupport {
             if (e.getCause() instanceof StageProcessingException) {
                 // UN-wrap our own exceptions so as to propagate them
                 throw (StageProcessingException) e.getCause();
-            } else {
-                // Wrap other exceptions
-                throw new StageProcessingException("ExecutionException during processing", e);
             }
+            // Wrap other exceptions
+            throw new StageProcessingException("ExecutionException during processing", e);
         } catch (final InterruptedException e) {
             LOG.debug("Execution service was interrupted", e);
             throw new StageProcessingException("Execution service was interrupted", e);
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemIdTransformStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemIdTransformStage.java
index e527bb9..04ab05b 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemIdTransformStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemIdTransformStage.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
@@ -43,7 +44,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 public class ItemIdTransformStage<T> extends AbstractIteratingStage<T> {
 
     /** Transformers used on IDs. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<Function<String, String>> idTransformers = List.of();
 
     /**
@@ -52,7 +53,7 @@ public class ItemIdTransformStage<T> extends AbstractIteratingStage<T> {
      * @return transforms used to produce the transformed entity IDs, never null
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<Function<String, String>> getIdTransformers() {
+    public final synchronized Collection<Function<String, String>> getIdTransformers() {
         return idTransformers;
     }
 
@@ -73,7 +74,7 @@ public class ItemIdTransformStage<T> extends AbstractIteratingStage<T> {
 
         final List<ItemId> transformedIds = new ArrayList<>();
         for (final ItemId id : ids) {
-            for (final Function<String, String> idTransform : idTransformers) {
+            for (final Function<String, String> idTransform : getIdTransformers()) {
                 final String transformedId = idTransform.apply(id.getId());
                 transformedIds.add(new ItemId(transformedId));
             }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataAddingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataAddingStage.java
index c6f0893..8854afd 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataAddingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataAddingStage.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
@@ -40,7 +41,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 public class ItemMetadataAddingStage<T> extends AbstractIteratingStage<T> {
 
     /** {@link ItemMetadata} objects to add to each {@link Item}'s item metadata. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<ItemMetadata> additionalItemMetadata = List.of();
 
     /**
@@ -49,7 +50,7 @@ public class ItemMetadataAddingStage<T> extends AbstractIteratingStage<T> {
      * @return the {@link ItemMetadata} being added to each {@link Item}'s item metadata
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<ItemMetadata> getAdditionalItemMetadata() {
+    public final synchronized Collection<ItemMetadata> getAdditionalItemMetadata() {
         return additionalItemMetadata;
     }
 
@@ -58,7 +59,7 @@ public class ItemMetadataAddingStage<T> extends AbstractIteratingStage<T> {
      * 
      * @param metadata the {@link ItemMetadata} to be added to each {@link Item}'s item metadata
      */
-    public void setAdditionalItemMetadata(
+    public synchronized void setAdditionalItemMetadata(
             @Nonnull @NonnullElements @Unmodifiable final Collection<ItemMetadata> metadata) {
         throwSetterPreconditionExceptions();
 
@@ -68,6 +69,6 @@ public class ItemMetadataAddingStage<T> extends AbstractIteratingStage<T> {
 
     @Override
     protected void doExecute(@Nonnull final Item<T> item) throws StageProcessingException {
-        item.getItemMetadata().putAll(additionalItemMetadata);
+        item.getItemMetadata().putAll(getAdditionalItemMetadata());
     }
 }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataFilterStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataFilterStage.java
index 1a26925..cf7607e 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataFilterStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataFilterStage.java
@@ -45,8 +45,8 @@ public class ItemMetadataFilterStage<T> extends AbstractItemMetadataSelectionSta
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(ItemMetadataFilterStage.class);
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection,
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection,
             final Item<T> matchingItem,
             final Map<Class<? extends ItemMetadata>, List<? extends ItemMetadata>> matchingMetadata)
             throws StageProcessingException {
@@ -56,4 +56,4 @@ public class ItemMetadataFilterStage<T> extends AbstractItemMetadataSelectionSta
 
         itemCollection.remove(matchingItem);
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataTerminationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataTerminationStage.java
index 65f811c..04fe865 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataTerminationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemMetadataTerminationStage.java
@@ -43,8 +43,8 @@ public class ItemMetadataTerminationStage<T> extends AbstractItemMetadataSelecti
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(ItemMetadataTerminationStage.class);
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection,
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection,
             final Item<T> matchingItem,
             final Map<Class<? extends ItemMetadata>, List<? extends ItemMetadata>> matchingMetadata)
             throws TerminationException {
@@ -56,4 +56,4 @@ public class ItemMetadataTerminationStage<T> extends AbstractItemMetadataSelecti
         throw new TerminationException("Item " + itemId + " marked with metadata of type "
                 + matchingMetadata.keySet());
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemOrderingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemOrderingStage.java
index 00fd3c2..d30b0c2 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemOrderingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemOrderingStage.java
@@ -20,6 +20,7 @@ package net.shibboleth.metadata.pipeline;
 import java.util.Collection;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
@@ -39,7 +40,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 public class ItemOrderingStage<T> extends AbstractStage<T> {
 
     /** Strategy used to order a collection of Items. The default strategy performs no ordering. */
-    @Nonnull
+    @Nonnull @GuardedBy("this")
     private ItemOrderingStrategy<T> orderingStrategy = new NoOpItemOrderingStrategy<>();
 
     /**
@@ -47,7 +48,7 @@ public class ItemOrderingStage<T> extends AbstractStage<T> {
      * 
      * @return strategy used to order a collection of Items
      */
-    @Nonnull public ItemOrderingStrategy<T> getItemOrderingStrategy() {
+    @Nonnull public final synchronized ItemOrderingStrategy<T> getItemOrderingStrategy() {
         return orderingStrategy;
     }
 
@@ -64,7 +65,7 @@ public class ItemOrderingStage<T> extends AbstractStage<T> {
     @Override
     protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection)
             throws StageProcessingException {
-        final var orderedItems = orderingStrategy.order(itemCollection);
+        final var orderedItems = getItemOrderingStrategy().order(itemCollection);
         itemCollection.clear();
         itemCollection.addAll(orderedItems);
     }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemOrderingStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemOrderingStrategy.java
index ef3b45d..81c409c 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemOrderingStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ItemOrderingStrategy.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
@@ -28,11 +29,16 @@ import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 
 /**
  * A strategy that defines how to order a {@link net.shibboleth.metadata.Item} collection.
+ * 
+ * <p>
+ * All implementations of this interface <strong>must</strong> be thread-safe.
+ * </p>
  *
  * @param <T> type of item to be handled
  *
  * @since 0.10.0
  */
+ at ThreadSafe
 public interface ItemOrderingStrategy<T> {
 
     /**
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/MultiOutputSerializationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/MultiOutputSerializationStage.java
index ee16e91..85a42e5 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/MultiOutputSerializationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/MultiOutputSerializationStage.java
@@ -23,10 +23,12 @@ import java.io.OutputStream;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.metadata.ItemSerializer;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
@@ -92,9 +94,11 @@ public class MultiOutputSerializationStage<T> extends AbstractIteratingStage<T>
     }
 
     /** Strategy used to determine where to serialize the item. */
+    @NonnullAfterInit @GuardedBy("this")
     private OutputStrategy<T> outputStrategy;
 
     /** Serializer used to write the collection to the output stream. */
+    @NonnullAfterInit @GuardedBy("this")
     private ItemSerializer<T> serializer;
 
     /**
@@ -102,7 +106,7 @@ public class MultiOutputSerializationStage<T> extends AbstractIteratingStage<T>
      * 
      * @return the output strategy function
      */
-    @Nullable public OutputStrategy<T> getOutputStrategy() {
+    @Nullable public final synchronized OutputStrategy<T> getOutputStrategy() {
         return outputStrategy;
     }
 
@@ -121,7 +125,7 @@ public class MultiOutputSerializationStage<T> extends AbstractIteratingStage<T>
      * 
      * @return serializer used to write item to the output file
      */
-    @Nullable public ItemSerializer<T> getSerializer() {
+    @Nullable public final synchronized ItemSerializer<T> getSerializer() {
         return serializer;
     }
 
@@ -138,9 +142,9 @@ public class MultiOutputSerializationStage<T> extends AbstractIteratingStage<T>
     @Override
     protected void doExecute(@Nonnull final Item<T> item)
             throws StageProcessingException {
-        try (final Destination destination = outputStrategy.getDestination(item);
+        try (final Destination destination = getOutputStrategy().getDestination(item);
                 final OutputStream stream = destination.getOutputStream()) {
-            serializer.serialize(item, stream);
+            getSerializer().serialize(item, stream);
         } catch (final IOException e) {
             throw new StageProcessingException("Error writing to output location", e);
         }
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/Pipeline.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/Pipeline.java
index 304d91c..7e10c34 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/Pipeline.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/Pipeline.java
@@ -35,11 +35,15 @@ import net.shibboleth.utilities.java.support.component.InitializableComponent;
  * first stage is responsible for populating the collection with an initial set of Items which subsequent stages further
  * modify.
  * 
+ * <p>
  * Each pipeline must be initialized, via the {@link #initialize()} method, before use. After a pipeline has been
  * initialized it may never be re-initialized. A pipeline is not considered initialized until all of its {@link Stage},
  * have been initialized.
+ * </p>
  * 
+ * <p>
  * Pipelines are reusable and threadsafe.
+ * </p>
  * 
  * @param <T> type of item which is processed by this pipeline
  */
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineCallable.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineCallable.java
index 3199479..a8539c1 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineCallable.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineCallable.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.concurrent.Callable;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 import net.shibboleth.metadata.Item;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
@@ -34,6 +35,7 @@ import org.slf4j.LoggerFactory;
  * 
  * @param <T> type of the items processed by the pipeline
  */
+ at Immutable
 public class PipelineCallable<T> implements Callable<Collection<Item<T>>> {
 
     /** Class logger. */
@@ -65,4 +67,4 @@ public class PipelineCallable<T> implements Callable<Collection<Item<T>>> {
         pipeline.execute(itemCollection);
         return itemCollection;
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineDemultiplexerStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineDemultiplexerStage.java
index d09eedd..618bf4b 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineDemultiplexerStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineDemultiplexerStage.java
@@ -27,6 +27,7 @@ import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
@@ -61,6 +62,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 public class PipelineDemultiplexerStage<T> extends AbstractStage<T> {
 
     /** Service used to execute the selected and/or non-selected item pipelines. */
+    @Nonnull @GuardedBy("this")
     private ExecutorService executorService = Executors.newSingleThreadExecutor();
 
     /**
@@ -68,13 +70,14 @@ public class PipelineDemultiplexerStage<T> extends AbstractStage<T> {
      *
      * Default: <code>true</code>.
      */
-    private boolean waitingForPipelines = true;
+    @GuardedBy("this") private boolean waitingForPipelines = true;
 
     /** Factory used to create the Item collection that is then given to the pipelines. */
+    @Nonnull @GuardedBy("this")
     private Supplier<Collection<Item<T>>> collectionFactory = new SimpleItemCollectionFactory<>();
 
     /** The pipelines through which items are sent and the selection strategy used for that pipeline. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<Pair<Pipeline<T>, Predicate<Item<T>>>> pipelineAndStrategies = List.of();
 
     /**
@@ -82,7 +85,7 @@ public class PipelineDemultiplexerStage<T> extends AbstractStage<T> {
      * 
      * @return executor service used to run the selected and non-selected item pipelines
      */
-    @Nonnull public ExecutorService getExecutorService() {
+    @Nonnull public final synchronized ExecutorService getExecutorService() {
         return executorService;
     }
 
@@ -101,7 +104,7 @@ public class PipelineDemultiplexerStage<T> extends AbstractStage<T> {
      * 
      * @return whether this child waits for all the invoked pipelines to complete before proceeding
      */
-    public boolean isWaitingForPipelines() {
+    public final synchronized boolean isWaitingForPipelines() {
         return waitingForPipelines;
     }
 
@@ -120,7 +123,7 @@ public class PipelineDemultiplexerStage<T> extends AbstractStage<T> {
      * 
      * @return factory used to create the Item collection that is then given to the pipelines
      */
-    @Nonnull public Supplier<Collection<Item<T>>> getCollectionFactory() {
+    @Nonnull public final synchronized Supplier<Collection<Item<T>>> getCollectionFactory() {
         return collectionFactory;
     }
 
@@ -139,7 +142,7 @@ public class PipelineDemultiplexerStage<T> extends AbstractStage<T> {
      * 
      * @return pipeline and item selection strategies used to demultiplex item collections within this stage
      */
-    @Nonnull @NonnullElements @Unmodifiable public List<Pair<Pipeline<T>, Predicate<Item<T>>>>
+    @Nonnull @NonnullElements @Unmodifiable public final synchronized List<Pair<Pipeline<T>, Predicate<Item<T>>>>
             getPipelineAndSelectionStrategies() {
         return pipelineAndStrategies;
     }
@@ -166,10 +169,10 @@ public class PipelineDemultiplexerStage<T> extends AbstractStage<T> {
             throws StageProcessingException {
         final ArrayList<Future<Collection<Item<T>>>> pipelineFutures = new ArrayList<>();
 
-        for (final Pair<Pipeline<T>, Predicate<Item<T>>> pipelineAndStrategy : pipelineAndStrategies) {
+        for (final Pair<Pipeline<T>, Predicate<Item<T>>> pipelineAndStrategy : getPipelineAndSelectionStrategies()) {
             final Pipeline<T> pipeline = pipelineAndStrategy.getFirst();
             final Predicate<Item<T>> selectionStrategy = pipelineAndStrategy.getSecond();
-            final Collection<Item<T>> selectedItems = collectionFactory.get();
+            final Collection<Item<T>> selectedItems = getCollectionFactory().get();
 
             for (final Item<T> item : itemCollection) {
                 if (selectionStrategy.test(item)) {
@@ -179,7 +182,7 @@ public class PipelineDemultiplexerStage<T> extends AbstractStage<T> {
                 }
             }
 
-            pipelineFutures.add(executorService.submit(new PipelineCallable<>(pipeline, selectedItems)));
+            pipelineFutures.add(getExecutorService().submit(new PipelineCallable<>(pipeline, selectedItems)));
         }
 
         if (isWaitingForPipelines()) {
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineMergeStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineMergeStage.java
index aaac44e..b49fede 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineMergeStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/PipelineMergeStage.java
@@ -26,6 +26,7 @@ import java.util.concurrent.Future;
 import java.util.function.Supplier;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.CollectionMergeStrategy;
@@ -52,19 +53,22 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 public class PipelineMergeStage<T> extends AbstractStage<T> {
 
     /** Service used to execute the pipelines whose results will be merged. */
+    @Nonnull @GuardedBy("this")
     private ExecutorService executorService = Executors.newSingleThreadExecutor();
 
     /**
      * The factory used to create the item returned by this source. Default implementation is
      * {@link SimpleItemCollectionFactory}.
      */
+    @Nonnull @GuardedBy("this")
     private Supplier<Collection<Item<T>>> collectionFactory = new SimpleItemCollectionFactory<>();
 
     /** Strategy used to merge all the joined pipeline results in to the final Item collection. */
+    @Nonnull @GuardedBy("this")
     private CollectionMergeStrategy mergeStrategy = new SimpleCollectionMergeStrategy();
 
     /** Pipelines whose results become the output of this source. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<Pipeline<T>> mergedPipelines = List.of();
 
     /**
@@ -72,7 +76,7 @@ public class PipelineMergeStage<T> extends AbstractStage<T> {
      * 
      * @return executor service used to run the selected and non-selected item pipelines
      */
-    @Nonnull public ExecutorService getExecutorService() {
+    @Nonnull public final synchronized ExecutorService getExecutorService() {
         return executorService;
     }
 
@@ -92,7 +96,7 @@ public class PipelineMergeStage<T> extends AbstractStage<T> {
      * @return unmodifiable set of pipelines used by this stage
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public List<Pipeline<T>> getMergedPipelines() {
+    public final synchronized List<Pipeline<T>> getMergedPipelines() {
         return mergedPipelines;
     }
 
@@ -112,7 +116,7 @@ public class PipelineMergeStage<T> extends AbstractStage<T> {
      * 
      * @return factory used to create the {@link Item} collection produced by this source
      */
-    @Nonnull public Supplier<Collection<Item<T>>> getCollectionFactory() {
+    @Nonnull public final synchronized Supplier<Collection<Item<T>>> getCollectionFactory() {
         return collectionFactory;
     }
 
@@ -131,7 +135,7 @@ public class PipelineMergeStage<T> extends AbstractStage<T> {
      * 
      * @return strategy used to merge all the joined pipeline results in to the final Item collection, never null
      */
-    @Nonnull public CollectionMergeStrategy getCollectionMergeStrategy() {
+    @Nonnull public final synchronized CollectionMergeStrategy getCollectionMergeStrategy() {
         return mergeStrategy;
     }
 
@@ -151,9 +155,9 @@ public class PipelineMergeStage<T> extends AbstractStage<T> {
             throws StageProcessingException {
         final ArrayList<Future<Collection<Item<T>>>> pipelineResultFutures = new ArrayList<>();
 
-        for (final Pipeline<T> pipeline : mergedPipelines) {
-            pipelineResultFutures.add(executorService.submit(
-                    new PipelineCallable<>(pipeline, collectionFactory.get())));
+        for (final Pipeline<T> pipeline : getMergedPipelines()) {
+            pipelineResultFutures.add(getExecutorService().submit(
+                    new PipelineCallable<>(pipeline, getCollectionFactory().get())));
         }
 
         final ArrayList<Collection<Item<T>>> pipelineResults = new ArrayList<>();
@@ -161,7 +165,7 @@ public class PipelineMergeStage<T> extends AbstractStage<T> {
             pipelineResults.add(FutureSupport.futureItems(future));
         }
 
-        mergeStrategy.mergeCollection(itemCollection, pipelineResults);
+        getCollectionMergeStrategy().mergeCollection(itemCollection, pipelineResults);
     }
 
     @Override
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ScriptletStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ScriptletStage.java
index bc61d7a..ac9500e 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ScriptletStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/ScriptletStage.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.script.ScriptException;
 import javax.script.SimpleScriptContext;
@@ -29,6 +30,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 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.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -60,6 +62,7 @@ public class ScriptletStage<T> extends AbstractStage<T> {
     private final Logger log = LoggerFactory.getLogger(ScriptletStage.class);
 
     /** Script executed by this stage. */
+    @NonnullAfterInit @GuardedBy("this")
     private EvaluableScript script;
 
     /**
@@ -67,7 +70,7 @@ public class ScriptletStage<T> extends AbstractStage<T> {
      * 
      * @return the script executed by this stage
      */
-    @Nullable public EvaluableScript getScript() {
+    @Nullable public final synchronized EvaluableScript getScript() {
         return script;
     }
 
@@ -81,14 +84,14 @@ public class ScriptletStage<T> extends AbstractStage<T> {
         script = Constraint.isNotNull(stageScript, "Stage script can not be null");
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection)
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection)
             throws StageProcessingException {
         final SimpleScriptContext context = new SimpleScriptContext();
         context.setAttribute(ITEMS, itemCollection, SimpleScriptContext.ENGINE_SCOPE);
 
         try {
-            script.eval(context);
+            getScript().eval(context);
         } catch (final ScriptException e) {
             final String errMsg = getId() + " pipeline stage unable to execute script";
             log.error(errMsg, e);
@@ -96,12 +99,12 @@ public class ScriptletStage<T> extends AbstractStage<T> {
         }
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doInitialize() throws ComponentInitializationException {
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
         if (script == null) {
             throw new ComponentInitializationException("Unable to initialize " + getId() + ", script may not be null");
         }
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SerializationStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SerializationStage.java
index 0db44ca..e693073 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SerializationStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SerializationStage.java
@@ -25,6 +25,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;
@@ -57,13 +58,14 @@ public class SerializationStage<T> extends AbstractStage<T> {
     private final Logger log = LoggerFactory.getLogger(SerializationStage.class);
 
     /** File to which the item will be written. */
-    @NonnullAfterInit
+    @NonnullAfterInit @GuardedBy("this")
     private File outputFile;
 
     /** Whether an existing output file should be overwritten. Default value: <code>true</code> */
-    private boolean overwritingExistingOutputFile = true;
+    @GuardedBy("this") private boolean overwritingExistingOutputFile = true;
 
     /** Serializer used to write the collection to the output stream. */
+    @NonnullAfterInit @GuardedBy("this")
     private ItemCollectionSerializer<T> serializer;
 
     /**
@@ -71,7 +73,7 @@ public class SerializationStage<T> extends AbstractStage<T> {
      * 
      * @return file to which the item will be written
      */
-    @NonnullAfterInit public File getOutputFile() {
+    @NonnullAfterInit public final synchronized File getOutputFile() {
         return outputFile;
     }
 
@@ -90,7 +92,7 @@ public class SerializationStage<T> extends AbstractStage<T> {
      * 
      * @return whether an existing output file should be overwritten
      */
-    public boolean isOverwritingExistingOutputFile() {
+    public final synchronized boolean isOverwritingExistingOutputFile() {
         return overwritingExistingOutputFile;
     }
 
@@ -109,7 +111,7 @@ public class SerializationStage<T> extends AbstractStage<T> {
      * 
      * @return serializer used to write item to the output file
      */
-    @Nullable public ItemCollectionSerializer<T> getSerializer() {
+    @Nullable public final synchronized ItemCollectionSerializer<T> getSerializer() {
         return serializer;
     }
 
@@ -123,26 +125,27 @@ public class SerializationStage<T> extends AbstractStage<T> {
         serializer = Constraint.isNotNull(itemSerializer, "Item collection serializer can not be null");
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection)
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection)
             throws StageProcessingException {
-        try (OutputStream stream = new FileOutputStream(outputFile)) {
-            serializer.serializeCollection(itemCollection, stream);
+        try (OutputStream stream = new FileOutputStream(getOutputFile())) {
+            getSerializer().serializeCollection(itemCollection, stream);
         } catch (final IOException e) {
-            throw new StageProcessingException("Error writing to output file " + outputFile.getAbsolutePath(), e);
+            throw new StageProcessingException("Error writing to output file " +
+                    getOutputFile().getAbsolutePath(), e);
         }
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doDestroy() {
+    @Override
+    protected void doDestroy() {
         outputFile = null;
         serializer = null;
 
         super.doDestroy();
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doInitialize() throws ComponentInitializationException {
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
         if (outputFile == null) {
@@ -181,4 +184,4 @@ public class SerializationStage<T> extends AbstractStage<T> {
         }
 
     }
-}
\ No newline at end of file
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SplitMergeStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SplitMergeStage.java
index a5626f9..0752cb9 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SplitMergeStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/SplitMergeStage.java
@@ -27,6 +27,7 @@ import java.util.function.Supplier;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
@@ -69,21 +70,27 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
     private final Logger log = LoggerFactory.getLogger(SplitMergeStage.class);
 
     /** Service used to execute the selected and/or non-selected item pipelines. */
+    @Nonnull @GuardedBy("this")
     private ExecutorService executorService = Executors.newSingleThreadExecutor();
 
     /** Factory used to create the Item collection that is then given to the pipelines. */
+    @Nonnull @GuardedBy("this")
     private Supplier<Collection<Item<T>>> collectionFactory = new SimpleItemCollectionFactory<>();
 
     /** Strategy used to split the given item collection. */
+    @Nonnull @GuardedBy("this")
     private Predicate<Item<T>> selectionStrategy = x -> false;
 
     /** Pipeline that receives the selected items. */
+    @Nullable @GuardedBy("this")
     private Pipeline<T> selectedItemPipeline;
 
     /** Pipeline that receives the non-selected items. */
+    @Nullable @GuardedBy("this")
     private Pipeline<T> nonselectedItemPipeline;
 
     /** Strategy used to merge all the joined pipeline results in to the final Item collection. */
+    @Nonnull @GuardedBy("this")
     private CollectionMergeStrategy mergeStrategy = new SimpleCollectionMergeStrategy();
 
     /**
@@ -91,7 +98,7 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
      * 
      * @return executor service used to run the selected and non-selected item pipelines
      */
-    @Nonnull public ExecutorService getExecutorService() {
+    @Nonnull public final synchronized ExecutorService getExecutorService() {
         return executorService;
     }
 
@@ -110,7 +117,7 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
      * 
      * @return factory used to create the Item collection that is then given to the pipelines
      */
-    @Nonnull public Supplier<Collection<Item<T>>> getCollectionFactory() {
+    @Nonnull public final synchronized Supplier<Collection<Item<T>>> getCollectionFactory() {
         return collectionFactory;
     }
 
@@ -129,7 +136,7 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
      * 
      * @return strategy used to split the given item collection
      */
-    @Nonnull public Predicate<Item<T>> getSelectionStrategy() {
+    @Nonnull public final synchronized Predicate<Item<T>> getSelectionStrategy() {
         return selectionStrategy;
     }
 
@@ -148,7 +155,7 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
      * 
      * @return pipeline that receives the selected items
      */
-    @Nullable public Pipeline<T> getSelectedItemPipeline() {
+    @Nullable public final synchronized Pipeline<T> getSelectedItemPipeline() {
         return selectedItemPipeline;
     }
 
@@ -167,7 +174,7 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
      * 
      * @return pipeline that receives the non-selected items
      */
-    @Nullable public Pipeline<T> getNonselectedItemPipeline() {
+    @Nullable public final synchronized Pipeline<T> getNonselectedItemPipeline() {
         return nonselectedItemPipeline;
     }
 
@@ -186,7 +193,7 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
      * 
      * @return strategy used to merge all the joined pipeline results in to the final Item collection, never null
      */
-    @Nonnull public CollectionMergeStrategy getCollectionMergeStrategy() {
+    @Nonnull public final synchronized CollectionMergeStrategy getCollectionMergeStrategy() {
         return mergeStrategy;
     }
 
@@ -201,27 +208,25 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
         mergeStrategy = Constraint.isNotNull(strategy, "Collection merge strategy can not be null");
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection)
+    @Override
+    protected void doExecute(@Nonnull @NonnullElements final Collection<Item<T>> itemCollection)
             throws StageProcessingException {
-        final Collection<Item<T>> selectedItems = collectionFactory.get();
-        final Collection<Item<T>> nonselectedItems = collectionFactory.get();
+        final Collection<Item<T>> selectedItems = getCollectionFactory().get();
+        final Collection<Item<T>> nonselectedItems = getCollectionFactory().get();
 
+        final var strategy = getSelectionStrategy();
         for (final Item<T> item : itemCollection) {
-            if (item == null) {
-                continue;
-            }
-
-            if (selectionStrategy.test(item)) {
+            if (strategy.test(item)) {
                 selectedItems.add(item);
             } else {
                 nonselectedItems.add(item);
             }
         }
 
-        final Future<Collection<Item<T>>> selectedItemFuture = executePipeline(selectedItemPipeline, selectedItems);
+        final Future<Collection<Item<T>>> selectedItemFuture =
+                executePipeline(getSelectedItemPipeline(), selectedItems);
         final Future<Collection<Item<T>>> nonselectedItemFuture =
-                executePipeline(nonselectedItemPipeline, nonselectedItems);
+                executePipeline(getNonselectedItemPipeline(), nonselectedItems);
 
         final ArrayList<Collection<Item<T>>> pipelineResults = new ArrayList<>();
         
@@ -230,7 +235,7 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
         pipelineResults.add(FutureSupport.futureItems(nonselectedItemFuture));
 
         itemCollection.clear();
-        mergeStrategy.mergeCollection(itemCollection, pipelineResults);
+        getCollectionMergeStrategy().mergeCollection(itemCollection, pipelineResults);
     }
 
     /**
@@ -253,11 +258,11 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
         }
 
         final PipelineCallable<T> callable = new PipelineCallable<>(pipeline, items);
-        return executorService.submit(callable);
+        return getExecutorService().submit(callable);
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doDestroy() {
+    @Override
+    protected void doDestroy() {
         executorService = null;
         collectionFactory = null;
         selectionStrategy = null;
@@ -268,8 +273,8 @@ public class SplitMergeStage<T> extends AbstractStage<T> {
         super.doDestroy();
     }
 
-    /** {@inheritDoc} */
-    @Override protected void doInitialize() throws ComponentInitializationException {
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
         if (selectedItemPipeline == null && nonselectedItemPipeline == null) {
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/Stage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/Stage.java
index a46f548..b85df67 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/Stage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/Stage.java
@@ -31,8 +31,10 @@ import net.shibboleth.utilities.java.support.component.InitializableComponent;
 /**
  * A stage in a {@link Pipeline} that operates upon a collection {@link Item} in a particular manner.
  * 
+ * <p>
  * Stages must be thread safe and reusable.
- * 
+ * </p>
+ *
  * @param <T> type of metadata upon which the stage operates
  */
 @ThreadSafe
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/StaticItemSourceStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/StaticItemSourceStage.java
index 420ae62..140e33d 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/StaticItemSourceStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/StaticItemSourceStage.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.metadata.Item;
@@ -36,7 +37,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 public class StaticItemSourceStage<T> extends AbstractStage<T> {
 
     /** Collection of static Items added to each Item collection by {@link #execute(Collection)}. */
-    @Nonnull @NonnullElements @Unmodifiable
+    @Nonnull @NonnullElements @Unmodifiable @GuardedBy("this")
     private List<Item<T>> source = List.of();
 
     /**
@@ -45,7 +46,7 @@ public class StaticItemSourceStage<T> extends AbstractStage<T> {
      * @return collection of static Items added to the Item collection by this stage
      */
     @Nonnull @NonnullElements @Unmodifiable
-    public Collection<Item<T>> getSourceItems() {
+    public final synchronized Collection<Item<T>> getSourceItems() {
         return source;
     }
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/StatusMetadataLoggingStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/StatusMetadataLoggingStage.java
index 7f2b09b..b158452 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/StatusMetadataLoggingStage.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/StatusMetadataLoggingStage.java
@@ -46,12 +46,12 @@ public class StatusMetadataLoggingStage<T> extends AbstractItemMetadataSelection
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(StatusMetadataLoggingStage.class);
 
-    /** {@inheritDoc} */
-    @Override protected void doExecute(
-                    @Nonnull @NonnullElements final Collection<Item<T>> itemCollection,
-                    @Nonnull final Item<T> matchingItem,
-                    @Nonnull @NonnullElements final Map<Class<? extends ItemMetadata>,
-                        List<? extends ItemMetadata>> matchingMetadata)
+    @Override
+    protected void doExecute(
+            @Nonnull @NonnullElements final Collection<Item<T>> itemCollection,
+            @Nonnull final Item<T> matchingItem,
+            @Nonnull @NonnullElements final Map<Class<? extends ItemMetadata>,
+            List<? extends ItemMetadata>> matchingMetadata)
                     throws StageProcessingException {
 
         final String itemId = getItemIdentificationStrategy().getItemIdentifier(matchingItem);
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/impl/NoOpItemOrderingStrategy.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/impl/NoOpItemOrderingStrategy.java
index 4856753..7f667b3 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/impl/NoOpItemOrderingStrategy.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/pipeline/impl/NoOpItemOrderingStrategy.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.metadata.Item;
 import net.shibboleth.metadata.pipeline.ItemOrderingStrategy;
@@ -34,6 +35,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
  *
  * @since 0.10.0
  */
+ at Immutable
 public class NoOpItemOrderingStrategy<T> implements ItemOrderingStrategy<T> {
 
     @Override

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


More information about the commits mailing list