[java-metadata-aggregator] branch master updated: MDA-173 - Add DiscoFeedCollectionSerializer

Ian Young ian at iay.org.uk
Tue Apr 14 12:45:29 EDT 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=1834258f90e3f840d707d194f7323340519358f6

The following commit(s) were added to refs/heads/master by this push:
       new  1834258   MDA-173 - Add DiscoFeedCollectionSerializer
1834258 is described below

commit 1834258f90e3f840d707d194f7323340519358f6
Author: Ian Young <ian at iay.org.uk>
AuthorDate: Tue Apr 14 17:45:21 2020 +0100

    MDA-173 - Add DiscoFeedCollectionSerializer
    
    https://issues.shibboleth.net/jira/browse/MDA-173
---
 aggregator-pipeline/pom.xml                        |  10 +
 .../dom/saml/DiscoFeedCollectionSerializer.java    | 328 +++++++++++++++++++++
 .../metadata/dom/saml/SAMLMetadataSupport.java     |  12 +
 .../metadata/dom/saml/mdui/MDUISupport.java        |  55 ++++
 .../metadata/dom/saml/mdui/package-info.java       |  21 ++
 .../resources/net/shibboleth/metadata/beans.xml    |   3 +
 .../saml/DiscoFeedCollectionSerializerTest.java    | 248 ++++++++++++++++
 .../saml/DiscoFeedCollectionSerializer-base.json   |  34 +++
 .../saml/DiscoFeedCollectionSerializer-base.xml    | 121 ++++++++
 .../DiscoFeedCollectionSerializer-entattr.json     |  55 ++++
 .../saml/DiscoFeedCollectionSerializer-entattr.xml | 129 ++++++++
 .../DiscoFeedCollectionSerializer-infourl.json     |  34 +++
 .../saml/DiscoFeedCollectionSerializer-infourl.xml | 121 ++++++++
 .../DiscoFeedCollectionSerializer-keywords.json    |  34 +++
 .../DiscoFeedCollectionSerializer-keywords.xml     | 121 ++++++++
 .../saml/DiscoFeedCollectionSerializer-legacy.json |  18 ++
 .../saml/DiscoFeedCollectionSerializer-legacy.xml  | 117 ++++++++
 .../DiscoFeedCollectionSerializer-logolang.json    |  35 +++
 .../DiscoFeedCollectionSerializer-logolang.xml     | 121 ++++++++
 .../DiscoFeedCollectionSerializer-nolegacy.json    |  12 +
 .../DiscoFeedCollectionSerializer-nolegacy.xml     | 117 ++++++++
 .../DiscoFeedCollectionSerializer-privacyurl.json  |  34 +++
 .../DiscoFeedCollectionSerializer-privacyurl.xml   | 121 ++++++++
 23 files changed, 1901 insertions(+)

diff --git a/aggregator-pipeline/pom.xml b/aggregator-pipeline/pom.xml
index d565139..a3da99a 100644
--- a/aggregator-pipeline/pom.xml
+++ b/aggregator-pipeline/pom.xml
@@ -52,8 +52,18 @@
         </dependency>
 
         <!-- Provided Dependencies -->
+        <dependency>
+            <groupId>jakarta.json</groupId>
+            <artifactId>jakarta.json-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
 
         <!-- Runtime Dependencies -->
+        <dependency>
+            <groupId>org.glassfish</groupId>
+            <artifactId>jakarta.json</artifactId>
+            <scope>runtime</scope>
+        </dependency>
 
         <!-- Test Dependencies -->
         <dependency>
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer.java
new file mode 100644
index 0000000..1332275
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.metadata.dom.saml;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.json.Json;
+import javax.json.stream.JsonGenerator;
+import javax.json.stream.JsonGeneratorFactory;
+import javax.xml.namespace.QName;
+
+import org.w3c.dom.Element;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.ItemCollectionSerializer;
+import net.shibboleth.metadata.dom.saml.mdattr.MDAttrSupport;
+import net.shibboleth.metadata.dom.saml.mdui.MDUISupport;
+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.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.xml.ElementSupport;
+
+/**
+ * A collection serializer that generates an equivalent of the Shibboleth SP's discovery
+ * feed output from a collection of entity descriptors.
+ */
+public class DiscoFeedCollectionSerializer extends AbstractInitializableComponent
+    implements ItemCollectionSerializer<Element> {
+
+    /** Configured JSON generator factory. */
+    @NonnullAfterInit
+    private JsonGeneratorFactory factory;
+
+    /** Whether to pretty-print the resulting JSON. Default: <code>false</code> */
+    private boolean prettyPrinting;
+
+    /**
+     * Whether to include legacy display names if none are found in the
+     * <code>mdui:UIInfo</code> element.
+     * 
+     * Default: <code>false</code>
+     */
+    private boolean includingLegacyDisplayNames;
+    
+    /** Whether to include entity attributes. Default: <code>false</code> */
+    private boolean includingEntityAttributes;
+
+    /**
+     * Returns whether output is being pretty-printed.
+     * 
+     * @return whether output is being pretty-printed
+     */
+    public boolean isPrettyPrinting() {
+        return prettyPrinting;
+    }
+
+    /**
+     * Sets whether to pretty-print the output.
+     * 
+     * @param pretty whether to pretty-print the output
+     */
+    public void setPrettyPrinting(final boolean pretty) {
+        prettyPrinting = pretty;
+    }
+
+    /**
+     * Returns whether output includes legacy display names.
+     * 
+     * @return whether output includes legacy display names
+     */
+    public boolean isIncludingLegacyDisplayNames() {
+        return includingLegacyDisplayNames;
+    }
+
+    /**
+     * Sets whether to include legacy display names.
+     * 
+     * @param includeLegacyDisplayNames whether to include legacy display names
+     */
+    public void setIncludingLegacyDisplayNames(final boolean includeLegacyDisplayNames) {
+        includingLegacyDisplayNames = includeLegacyDisplayNames;
+    }
+
+    /**
+     * Returns whether output includes entity attributes.
+     * 
+     * @return whether output includes entity attributes
+     */
+    public boolean isIncludingEntityAttributes() {
+        return includingEntityAttributes;
+    }
+
+    /**
+     * Sets whether to include entity attributes.
+     * 
+     * @param includeEntityAttributes whether to include entity attributes.
+     */
+    public void setIncludingEntityAttributes(final boolean includeEntityAttributes) {
+        includingEntityAttributes = includeEntityAttributes;
+    }
+
+    /**
+     * Find the first <code>mdui:UIInfo</code> {@link Element} across a list of
+     * <code>md:IDPSSODescriptor</code>s.
+     *
+     * @param idpDescriptors list of <code>md:IDPSSODescriptor</code>s
+     *
+     * @return the first <code>mdui:UIInfo</code> element, or <code>null</code> if none are found
+     */
+    @Nullable
+    private Element findFirstUIInfo(@Nonnull @NonnullElements final List<Element> idpDescriptors) {
+        for (final var idpDescriptor : idpDescriptors) {
+            final var uiInfo = SAMLMetadataSupport.getDescriptorExtension(idpDescriptor, MDUISupport.UIINFO_NAME);
+            if (uiInfo != null) {
+                return uiInfo;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Write a list of element values and language tags to the output
+     * as a JSON array under a given key.
+     * 
+     * @param gen the {@link JsonGenerator} to write the output to
+     * @param elements list of {@link Element}s to write out
+     * @param key key for the JSON array within the containing object
+     */
+    private void writeValueLangList(@Nonnull final JsonGenerator gen,
+            @Nonnull @NonnullElements final List<Element> elements,
+            @Nonnull final String key) {
+        gen.writeStartArray(key);
+        for (final var element : elements) {
+            gen.writeStartObject();
+                gen.write("value", element.getTextContent());
+                gen.write("lang", element.getAttribute("xml:lang"));
+            gen.writeEnd();
+        }
+        gen.writeEnd();
+    }
+
+    /**
+     * Write a value/language list for elements within a <code>mdui:UIInfo</code> container.
+     * 
+     * @param gen the {@link JsonGenerator} to write the output to
+     * @param uiInfo the <code>mdui:UIInfo</code> element
+     * @param elementName name of the elements to collect from
+     * @param key key for the JSON array within the containing object
+     */
+    private void writeValueLangList(@Nonnull final JsonGenerator gen,
+            @Nonnull final Element uiInfo,
+            @Nonnull final QName elementName,
+            @Nonnull final String key) {
+        final var elements = ElementSupport.getChildElements(uiInfo, elementName);
+        if (!elements.isEmpty()) {
+            writeValueLangList(gen, elements, key);
+        }        
+    }
+    
+    /**
+     * Write out an entity's entity attributes.
+     *
+     * @param gen the {@link JsonGenerator} to write the output to
+     * @param entity the <code>md:EntityDescriptor</code> element
+     */
+    private void writeEntityAttributes(@Nonnull final JsonGenerator gen, @Nonnull final Element entity) {
+        final var ext = SAMLMetadataSupport.getDescriptorExtension(entity, MDAttrSupport.ENTITY_ATTRIBUTES_NAME);
+        if (ext != null) {
+            final var attributes = ElementSupport.getChildElements(ext, SAMLSupport.ATTRIBUTE_NAME);
+            if (!attributes.isEmpty()) {
+                gen.writeStartArray("EntityAttributes");
+                    for (final var attribute : attributes) {
+                        final var values = ElementSupport.getChildElements(attribute, SAMLSupport.ATTRIBUTE_VALUE_NAME);
+                        if (!values.isEmpty()) {
+                            gen.writeStartObject();
+                                gen.write("name", attribute.getAttribute("Name"));
+                                gen.writeStartArray("values");
+                                    for (final var value : values) {
+                                        gen.write(value.getTextContent());
+                                    }
+                                gen.writeEnd();
+                            gen.writeEnd();
+                        }
+                    }
+                gen.writeEnd();
+            }
+        }
+    }
+
+    /**
+     * Write the entity's logos to the output.
+     *
+     * @param gen the {@link JsonGenerator} to write the output to
+     * @param uiInfo the <code>mdui:UIInfo</code> element
+     */
+    private void writeLogos(@Nonnull final JsonGenerator gen, @Nonnull final Element uiInfo) {
+        final var logos = ElementSupport.getChildElements(uiInfo, MDUISupport.LOGO_NAME);
+        if (!logos.isEmpty()) {
+            gen.writeStartArray("Logos");
+                for (final var logo: logos) {
+                    gen.writeStartObject();
+                        gen.write("value", logo.getTextContent());
+                        gen.write("height", logo.getAttribute("height"));
+                        gen.write("width", logo.getAttribute("width"));
+                        // xml:lang is optional on mdui:Logo elements
+                        final var lang = logo.getAttributeNode("xml:lang");
+                        if (lang != null) {
+                            gen.write("lang", lang.getTextContent());
+                        }
+                    gen.writeEnd();
+                }
+            gen.writeEnd();
+        }
+    }
+
+    /**
+     * Write the list of display names to the output.
+     *
+     * If we have a <code>mdui:UIInfo</code>, we may be able to find some display names
+     * there. If we can find no <code>mdui:DisplayName</code> elements, we instead
+     * find legacy display names in the <code>md:Organization</code> element.
+     *
+     * @param gen the {@link JsonGenerator} to write the output to
+     * @param entity <code>md:EntityDescriptor</code> element
+     * @param uiInfo <code>mdui:UIInfo</code> if available, or <code>null</code>
+     */
+    private void writeDisplayNames(@Nonnull final JsonGenerator gen, @Nonnull final Element entity,
+            @Nullable final Element uiInfo) {
+        // Attempt to find display names in the mdui:UIInfo element
+        if (uiInfo != null) {
+            final var displayNames = ElementSupport.getChildElements(uiInfo, MDUISupport.DISPLAYNAME_NAME);
+            if (!displayNames.isEmpty()) {
+                writeValueLangList(gen, displayNames, "DisplayNames");
+                // We have found our display names
+                return;
+            }
+        }
+        
+        // Attempt to find display names elsewhere
+        if (includingLegacyDisplayNames) {
+            final var org = ElementSupport.getFirstChildElement(entity, SAMLMetadataSupport.ORGANIZATION_NAME);
+            if (org != null) {
+                final var displayNames =
+                        ElementSupport.getChildElements(org, SAMLMetadataSupport.ORGANIZATIONDISPLAYNAME_NAME);
+                if (!displayNames.isEmpty()) {
+                    writeValueLangList(gen, displayNames, "DisplayNames");
+                }
+            }
+        }
+    }
+
+    @Override
+    public void serializeCollection(@Nonnull @NonnullElements final Collection<Item<Element>> items,
+            @Nonnull final OutputStream output) throws IOException {
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        final JsonGenerator gen = factory.createGenerator(output);
+        gen.writeStartArray();
+            for (final Item<Element> item : items) {
+                final Element entity = item.unwrap();
+                if (SAMLMetadataSupport.isEntityDescriptor(entity)) {
+                    final var idpDescriptors =
+                            ElementSupport.getChildElements(entity, SAMLMetadataSupport.IDP_SSO_DESCRIPTOR_NAME);
+                    if (!idpDescriptors.isEmpty()) {
+                        gen.writeStartObject();
+                            gen.write("entityID", entity.getAttributeNS(null, "entityID"));
+                            final var uiInfo = findFirstUIInfo(idpDescriptors);
+                            writeDisplayNames(gen, entity, uiInfo);
+                            if (uiInfo != null) {
+                                writeValueLangList(gen, uiInfo, MDUISupport.DESCRIPTION_NAME, "Descriptions");
+                                writeValueLangList(gen, uiInfo, MDUISupport.KEYWORDS_NAME, "Keywords");
+                                writeValueLangList(gen, uiInfo, MDUISupport.INFORMATIONURL_NAME, "InformationURLs");
+                                writeValueLangList(gen, uiInfo,
+                                        MDUISupport.PRIVACYSTATEMENTURL_NAME, "PrivacyStatementURLs");
+                                writeLogos(gen, uiInfo);
+                            }
+                            if (includingEntityAttributes) {
+                                writeEntityAttributes(gen, entity);
+                            }
+                        gen.writeEnd();
+                    }
+                }
+            }
+        gen.writeEnd();
+        gen.close();
+    }
+
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        final Map<String, String> generatorConfig = new HashMap<>();
+        if (prettyPrinting) {
+            generatorConfig.put(JsonGenerator.PRETTY_PRINTING, "true");
+        }
+        factory = Json.createGeneratorFactory(generatorConfig);
+    }
+
+    @Override
+    protected void doDestroy() {
+        factory = null;
+        super.doDestroy();
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLMetadataSupport.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLMetadataSupport.java
index 6b2556d..3108ca9 100644
--- a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLMetadataSupport.java
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLMetadataSupport.java
@@ -76,6 +76,18 @@ public final class SAMLMetadataSupport {
     /** QName of the PDPDescriptor element. */
     public static final QName PDP_DESCRIPTOR_NAME = new QName(MD_NS, "PDPDescriptor");
 
+    /** QName of the Organization element. */
+    public static final QName ORGANIZATION_NAME = new QName(MD_NS, "Organization", MD_PREFIX);
+
+    /** QName of the OrganizationName element. */
+    public static final QName ORGANIZATIONNAME_NAME = new QName(MD_NS, "OrganizationName", MD_PREFIX);
+
+    /** QName of the OrganizationDisplayName element. */
+    public static final QName ORGANIZATIONDISPLAYNAME_NAME = new QName(MD_NS, "OrganizationDisplayName", MD_PREFIX);
+
+    /** QName of the OrganizationURL element. */
+    public static final QName ORGANIZATIONURL_NAME = new QName(MD_NS, "OrganizationURL", MD_PREFIX);
+
     /** Constructor. */
     private SAMLMetadataSupport() {
 
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/MDUISupport.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/MDUISupport.java
new file mode 100644
index 0000000..046542b
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/MDUISupport.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.metadata.dom.saml.mdui;
+
+import javax.annotation.concurrent.ThreadSafe;
+import javax.xml.namespace.QName;
+
+/** Support class for dealing with MDUI metadata. */
+ at ThreadSafe
+public final class MDUISupport {
+
+    /** MDUI namespace. */
+    public static final String MDUI_NS = "urn:oasis:names:tc:SAML:metadata:ui";
+
+    /** {@link QName} representing an <code>mdui:DisplayName</code>. */
+    public static final QName DISPLAYNAME_NAME = new QName(MDUISupport.MDUI_NS, "DisplayName");
+    
+    /** {@link QName} representing an <code>mdui:UIInfo</code>. */
+    public static final QName UIINFO_NAME = new QName(MDUISupport.MDUI_NS, "UIInfo");
+    
+    /** {@link QName} representing an <code>mdui:Description</code>. */
+    public static final QName DESCRIPTION_NAME = new QName(MDUISupport.MDUI_NS, "Description");
+    
+    /** {@link QName} representing an <code>mdui:Keywords</code>. */
+    public static final QName KEYWORDS_NAME = new QName(MDUISupport.MDUI_NS, "Keywords");
+    
+    /** {@link QName} representing an <code>mdui:Logo</code>. */
+    public static final QName LOGO_NAME = new QName(MDUISupport.MDUI_NS, "Logo");
+    
+    /** {@link QName} representing an <code>mdui:InformationURL</code>. */
+    public static final QName INFORMATIONURL_NAME = new QName(MDUISupport.MDUI_NS, "InformationURL");
+    
+    /** {@link QName} representing an <code>mdui:PrivacyStatementURL</code>. */
+    public static final QName PRIVACYSTATEMENTURL_NAME = new QName(MDUISupport.MDUI_NS, "PrivacyStatementURL");
+    
+    /** Constructor. */
+    private MDUISupport() {
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/package-info.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/package-info.java
new file mode 100644
index 0000000..6b84d0a
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdui/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Aggregator classes for the MDUI specification.
+ */
+package net.shibboleth.metadata.dom.saml.mdui;
diff --git a/aggregator-pipeline/src/main/resources/net/shibboleth/metadata/beans.xml b/aggregator-pipeline/src/main/resources/net/shibboleth/metadata/beans.xml
index 0fd6d15..0a18970 100644
--- a/aggregator-pipeline/src/main/resources/net/shibboleth/metadata/beans.xml
+++ b/aggregator-pipeline/src/main/resources/net/shibboleth/metadata/beans.xml
@@ -117,6 +117,9 @@
     <bean id="mda.ContactPersonFilterStage" abstract="true" parent="mda.stage_parent"
         class="net.shibboleth.metadata.dom.saml.ContactPersonFilterStage"/>
 
+    <bean id="mda.DiscoFeedCollectionSerializer" abstract="true" parent="mda.component_parent"
+        class="net.shibboleth.metadata.dom.saml.DiscoFeedCollectionSerializer"/>
+
     <bean id="mda.EntitiesDescriptorAssemblerStage" abstract="true" parent="mda.stage_parent"
         class="net.shibboleth.metadata.dom.saml.EntitiesDescriptorAssemblerStage"/>
 
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializerTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializerTest.java
new file mode 100644
index 0000000..10c3ed7
--- /dev/null
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializerTest.java
@@ -0,0 +1,248 @@
+
+package net.shibboleth.metadata.dom.saml;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import org.w3c.dom.Element;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.ItemCollectionSerializer;
+import net.shibboleth.metadata.dom.BaseDOMTest;
+import net.shibboleth.utilities.java.support.xml.XMLParserException;
+
+public class DiscoFeedCollectionSerializerTest extends BaseDOMTest {
+
+    protected DiscoFeedCollectionSerializerTest() {
+        super(DiscoFeedCollectionSerializer.class);
+    }
+
+    @Test
+    public void testEmptyCollection() throws Exception {
+        final var ser = new DiscoFeedCollectionSerializer();
+        ser.initialize();
+        final String output;
+        try (final var out = new ByteArrayOutputStream()) {
+            ser.serializeCollection(List.of(), out);
+            output = out.toString();
+        }
+        final var read = Json.createReader(new StringReader(output));
+        final var array = read.readArray();
+        Assert.assertEquals(array.size(), 0);
+    }
+
+    private JsonArray fetchJSONArray(@Nonnull final String path) throws IOException {
+        try (var in = BaseDOMTest.class.getResourceAsStream(classRelativeResource(path))) {
+            try (var reader = Json.createReader(in)) {
+                return reader.readArray();
+            }
+        }        
+    }
+
+    private void checkEntity(@Nonnull final JsonObject entity) {
+        Assert.assertEquals(entity.getString("entityID"), "https://idp.example.com/idp/shibboleth");
+    }
+
+    private void checkEntityArray(@Nonnull final JsonArray array) {
+        Assert.assertEquals(array.size(), 1);
+        checkEntity((JsonObject)array.get(0));
+    }
+
+    @Test
+    public void testPreCooked() throws Exception {
+        var entity = fetchJSONArray("base.json");
+        checkEntityArray(entity);
+    }
+
+    private void compareCollections(@Nonnull final JsonArray actual, @Nonnull final JsonArray expected) {
+        // The two collections must match as to size
+        if (actual.size() != expected.size()) {
+            Assert.assertEquals(actual.size(), expected.size(), "array sizes differ");
+        }
+        // The two collections must match entity by entity
+        for (int i = 0; i < actual.size(); i++) {
+            final var actualEntity = (JsonObject)actual.get(i);
+            final var expectedEntity = (JsonObject)expected.get(i);
+
+            // Get an identifier for the entity
+            final String id = "entity " + i + " ('" + expectedEntity.getString("entityID") + "')";
+
+            // Compare the key sets
+            final var actualKeys = actualEntity.keySet();
+            final var expectedKeys = expectedEntity.keySet();
+            Assert.assertEquals(actualKeys, expectedKeys, "key sets differ for " + id);
+
+            // Key sets are the same... compare each key value
+            for (final var key : actualKeys) {
+                final var actualValue = actualEntity.get(key);
+                final var expectedValue = expectedEntity.get(key);
+                Assert.assertEquals(actualValue, expectedValue, "key " + key + " differs for " + id);
+            }
+
+            // Backstop
+            Assert.assertEquals(actualEntity, expectedEntity, id + " differs:");
+        }
+    }
+
+    /**
+     * Checks the serialization of a single entity using a given serializer
+     * configuration.
+     * 
+     * @param name base name for the resources
+     * @param ser configured and initialized serializer to use
+     * 
+     * @return the single {@link JsonObject} for further tests
+     * 
+     * @throws IOException if there are parsing issues with the JSON file
+     * @throws XMLParserException if there are parsing issues with the XML file
+     */
+    private JsonObject checkSingle(@Nonnull final String name, @Nonnull final ItemCollectionSerializer ser)
+            throws IOException, XMLParserException {
+        final String output;
+        try (final var out = new ByteArrayOutputStream()) {
+            ser.serializeCollection(List.of(readDOMItem(name + ".xml")), out);
+            output = out.toString();
+        }
+        //System.out.println(output);
+        final var read = Json.createReader(new StringReader(output));
+        final var array = read.readArray();
+        Assert.assertEquals(array.size(), 1);
+        
+        final var entity = (JsonObject)array.get(0);
+        checkEntity(entity);
+        final var expected = fetchJSONArray(name + ".json");
+        compareCollections(array, expected);
+        Assert.assertEquals(array, expected);
+        
+        return entity;
+    }
+
+    // Test the base case, derived from a single standard IdP
+    // with default generator settings.
+    @Test
+    public void testBase() throws Exception {
+        final var ser = new DiscoFeedCollectionSerializer();
+        ser.initialize();
+        final var entity = checkSingle("base", ser);
+        Assert.assertTrue(entity.containsKey("Descriptions"));
+        Assert.assertTrue(entity.containsKey("DisplayNames"));
+        Assert.assertFalse(entity.containsKey("InformationURLs"));
+        Assert.assertFalse(entity.containsKey("PrivacyStatementURLs"));
+    }
+
+    // Test the absence of an mdui:DisplayName
+    // with default generator settings.
+    @Test
+    public void testNoLegacy() throws Exception {
+        final var ser = new DiscoFeedCollectionSerializer();
+        ser.initialize();
+        final var entity = checkSingle("nolegacy", ser);
+        Assert.assertFalse(entity.containsKey("Descriptions"));
+        Assert.assertFalse(entity.containsKey("DisplayNames"));
+    }
+
+    // Test the inclusion of a legacy DisplayName when the
+    // serializer is set to allow this.
+    @Test
+    public void testLegacy() throws Exception {
+        final var ser = new DiscoFeedCollectionSerializer();
+        ser.setIncludingLegacyDisplayNames(true);
+        ser.setPrettyPrinting(true);
+        ser.initialize();
+        final var entity = checkSingle("legacy", ser);
+        Assert.assertFalse(entity.containsKey("Descriptions"));
+        Assert.assertTrue(entity.containsKey("DisplayNames"));
+    }
+
+    // Test an entity with a couple of InformationURLs.
+    @Test
+    public void testInformationURLs() throws Exception {
+        final var ser = new DiscoFeedCollectionSerializer();
+        ser.initialize();
+        final var entity = checkSingle("infourl", ser);
+        Assert.assertTrue(entity.containsKey("Descriptions"));
+        Assert.assertTrue(entity.containsKey("DisplayNames"));
+        Assert.assertTrue(entity.containsKey("InformationURLs"));
+    }
+
+    // Test an entity with a couple of PrivacyStatementURLs.
+    @Test
+    public void testPrivacyStatementURLs() throws Exception {
+        final var ser = new DiscoFeedCollectionSerializer();
+        ser.initialize();
+        final var entity = checkSingle("privacyurl", ser);
+        Assert.assertTrue(entity.containsKey("Descriptions"));
+        Assert.assertTrue(entity.containsKey("DisplayNames"));
+        Assert.assertTrue(entity.containsKey("PrivacyStatementURLs"));
+    }
+
+    // Test the optional nature of xml:lang on mdui:Logo elements.
+    @Test
+    public void testLogoLang() throws Exception {
+        final var ser = new DiscoFeedCollectionSerializer();
+        ser.initialize();
+        final var entity = checkSingle("logolang", ser);
+        final var logos = entity.getJsonArray("Logos");
+        Assert.assertEquals(logos.size(), 3);
+        Assert.assertFalse(logos.getJsonObject(0).containsKey("lang"));
+        Assert.assertTrue(logos.getJsonObject(1).containsKey("lang"));
+        Assert.assertEquals(logos.getJsonObject(1).getString("lang"), "en");
+        Assert.assertFalse(logos.getJsonObject(2).containsKey("lang"));
+    }
+    
+    // Test entity attributes present (base tests absent)
+    @Test
+    public void testEntityAttributes() throws Exception {
+        final var ser = new DiscoFeedCollectionSerializer();
+        ser.setIncludingEntityAttributes(true);
+        ser.initialize();
+        final var entity = checkSingle("entattr", ser);
+        final var attrs = entity.getJsonArray("EntityAttributes");
+        Assert.assertEquals(attrs.size(), 3);
+        Assert.assertEquals(attrs.getJsonObject(2).getString("name"), "something");
+        Assert.assertEquals(attrs.getJsonObject(2).getJsonArray("values").size(), 1);
+        Assert.assertEquals(attrs.getJsonObject(2).getJsonArray("values").getString(0), "whatever");
+    }
+
+    // Test a large aggregate. This test is normally disabled, but it
+    // can be brought back to life if necessary to test a new corpus.
+    @Test(enabled=false)
+    public void testAll() throws Exception {
+        final var ser = new DiscoFeedCollectionSerializer();
+        ser.initialize();
+
+        // Build a collection to serialize.
+        final var item = readDOMItem("all.xml");
+        final var items = new ArrayList<Item<Element>>();
+        items.add(item);
+        
+        // Disaggregate the collection into individual entities
+        final var disassembler = new EntitiesDescriptorDisassemblerStage();
+        disassembler.setId("disassemble");
+        disassembler.initialize();
+        disassembler.execute(items);
+        
+        // Now generate the disco feed
+        final String output;
+        try (final var out = new ByteArrayOutputStream()) {
+            ser.serializeCollection(items, out);
+            output = out.toString();
+        }
+        final var read = Json.createReader(new StringReader(output));
+        final var array = read.readArray();
+
+        final var expected = fetchJSONArray("all.json");
+        compareCollections(array, expected);
+        Assert.assertEquals(array, expected);
+    }
+}
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-base.json b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-base.json
new file mode 100644
index 0000000..9a8f5ec
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-base.json
@@ -0,0 +1,34 @@
+[
+{
+ "entityID": "https://idp.example.com/idp/shibboleth",
+ "DisplayNames": [
+  {
+  "value": "Example Organization",
+  "lang": "en"
+  }
+ ],
+ "Descriptions": [
+  {
+  "value": "This is the identity provider for The Example Organization.",
+  "lang": "en"
+  }
+ ],
+ "Logos": [
+  {
+  "value": "https://idp.example.com/images/heads_80x80.jpg",
+  "height": "80",
+  "width": "80"
+  },
+  {
+  "value": "https://idp.example.com/images/heads_100x43.jpg",
+  "height": "43",
+  "width": "100"
+  },
+  {
+  "value": "https://idp.example.com/images/heads_240x104.jpg",
+  "height": "104",
+  "width": "240"
+  }
+ ]
+}
+]
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-base.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-base.xml
new file mode 100644
index 0000000..2c7579f
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-base.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+    xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+    xmlns:remd="http://refeds.org/metadata"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    entityID="https://idp.example.com/idp/shibboleth">
+    <Extensions>
+        <shibmd:Scope regexp="false">example.com</shibmd:Scope>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2007-03-30T16:36:00Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+            <saml:Attribute Name="http://macedir.org/entity-category-support"
+                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://refeds.org/category/research-and-scholarship</saml:AttributeValue>
+            </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <IDPSSODescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+            <mdui:UIInfo>
+                <mdui:DisplayName xml:lang="en">Example Organization</mdui:DisplayName>
+                <mdui:Description xml:lang="en">This is the identity provider for The Example Organization.</mdui:Description>
+                <mdui:Logo height="80" width="80">https://idp.example.com/images/heads_80x80.jpg</mdui:Logo>
+                <mdui:Logo height="43" width="100">https://idp.example.com/images/heads_100x43.jpg</mdui:Logo>
+                <mdui:Logo height="104" width="240">https://idp.example.com/images/heads_240x104.jpg</mdui:Logo>
+            </mdui:UIInfo>
+        </Extensions>
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+        <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"
+            Location="https://idp.example.com/idp/profile/Shibboleth/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://idp.example.com/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
+            Location="https://idp.example.com/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+            Location="https://idp.example.com/idp/profile/SAML2/Redirect/SSO"/>
+    </IDPSSODescriptor>
+    <AttributeAuthorityDescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/AttributeQuery"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    </AttributeAuthorityDescriptor>
+    <Organization>
+        <OrganizationName xml:lang="en">Example Organization</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">The Example Organization</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">http://example.com/</OrganizationURL>
+    </Organization>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-entattr.json b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-entattr.json
new file mode 100644
index 0000000..2fe0a6c
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-entattr.json
@@ -0,0 +1,55 @@
+[
+{
+ "entityID": "https://idp.example.com/idp/shibboleth",
+ "DisplayNames": [
+  {
+  "value": "Example Organization",
+  "lang": "en"
+  }
+ ],
+ "Descriptions": [
+  {
+  "value": "This is the identity provider for The Example Organization.",
+  "lang": "en"
+  }
+ ],
+ "Logos": [
+  {
+  "value": "https://idp.example.com/images/heads_80x80.jpg",
+  "height": "80",
+  "width": "80"
+  },
+  {
+  "value": "https://idp.example.com/images/heads_100x43.jpg",
+  "height": "43",
+  "width": "100"
+  },
+  {
+  "value": "https://idp.example.com/images/heads_240x104.jpg",
+  "height": "104",
+  "width": "240"
+  }
+ ],
+ "EntityAttributes": [
+  {
+  "name": "http://macedir.org/entity-category-support",
+  "values": [
+     "http://refeds.org/category/research-and-scholarship"
+  ]
+  },
+  {
+  "name": "urn:oasis:names:tc:SAML:attribute:assurance-certification",
+  "values": [
+     "https://refeds.org/sirtfi",
+     "second-assurance-value"
+  ]
+  },
+  {
+  "name": "something",
+  "values": [
+     "whatever"
+  ]
+  }
+ ]
+}
+]
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-entattr.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-entattr.xml
new file mode 100644
index 0000000..3af7a0b
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-entattr.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+    xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+    xmlns:remd="http://refeds.org/metadata"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    entityID="https://idp.example.com/idp/shibboleth">
+    <Extensions>
+        <shibmd:Scope regexp="false">example.com</shibmd:Scope>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2007-03-30T16:36:00Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+            <saml:Attribute Name="http://macedir.org/entity-category-support"
+                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://refeds.org/category/research-and-scholarship</saml:AttributeValue>
+            </saml:Attribute>
+	    <saml:Attribute Name="urn:oasis:names:tc:SAML:attribute:assurance-certification"
+			    NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+	      <saml:AttributeValue>https://refeds.org/sirtfi</saml:AttributeValue>
+	      <saml:AttributeValue>second-assurance-value</saml:AttributeValue>
+	    </saml:Attribute>
+	    <saml:Attribute Name="something" NameFormat="something-else">
+	      <saml:AttributeValue>whatever</saml:AttributeValue>
+	    </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <IDPSSODescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+            <mdui:UIInfo>
+                <mdui:DisplayName xml:lang="en">Example Organization</mdui:DisplayName>
+                <mdui:Description xml:lang="en">This is the identity provider for The Example Organization.</mdui:Description>
+                <mdui:Logo height="80" width="80">https://idp.example.com/images/heads_80x80.jpg</mdui:Logo>
+                <mdui:Logo height="43" width="100">https://idp.example.com/images/heads_100x43.jpg</mdui:Logo>
+                <mdui:Logo height="104" width="240">https://idp.example.com/images/heads_240x104.jpg</mdui:Logo>
+            </mdui:UIInfo>
+        </Extensions>
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+        <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"
+            Location="https://idp.example.com/idp/profile/Shibboleth/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://idp.example.com/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
+            Location="https://idp.example.com/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+            Location="https://idp.example.com/idp/profile/SAML2/Redirect/SSO"/>
+    </IDPSSODescriptor>
+    <AttributeAuthorityDescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/AttributeQuery"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    </AttributeAuthorityDescriptor>
+    <Organization>
+        <OrganizationName xml:lang="en">Example Organization</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">The Example Organization</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">http://example.com/</OrganizationURL>
+    </Organization>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-infourl.json b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-infourl.json
new file mode 100644
index 0000000..68fa5c3
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-infourl.json
@@ -0,0 +1,34 @@
+[
+{
+ "entityID": "https://idp.example.com/idp/shibboleth",
+ "DisplayNames": [
+  {
+  "value": "Example Organization",
+  "lang": "en"
+  }
+ ],
+ "Descriptions": [
+  {
+  "value": "This is the identity provider for The Example Organization.",
+  "lang": "en"
+  }
+ ],
+ "InformationURLs": [
+  {
+  "value": "https://example.org/info-english",
+  "lang": "en"
+  },
+  {
+  "value": "https://example.org/info-welsh",
+  "lang": "cy"
+  }
+ ],
+ "Logos": [
+  {
+  "value": "https://idp.example.com/images/heads_80x80.jpg",
+  "height": "80",
+  "width": "80"
+  }
+ ]
+}
+]
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-infourl.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-infourl.xml
new file mode 100644
index 0000000..5ac5c80
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-infourl.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+    xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+    xmlns:remd="http://refeds.org/metadata"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    entityID="https://idp.example.com/idp/shibboleth">
+    <Extensions>
+        <shibmd:Scope regexp="false">example.com</shibmd:Scope>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2007-03-30T16:36:00Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+            <saml:Attribute Name="http://macedir.org/entity-category-support"
+                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://refeds.org/category/research-and-scholarship</saml:AttributeValue>
+            </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <IDPSSODescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+          <mdui:UIInfo>
+	    <mdui:InformationURL xml:lang="en">https://example.org/info-english</mdui:InformationURL>
+            <mdui:DisplayName xml:lang="en">Example Organization</mdui:DisplayName>
+            <mdui:Description xml:lang="en">This is the identity provider for The Example Organization.</mdui:Description>
+            <mdui:Logo height="80" width="80">https://idp.example.com/images/heads_80x80.jpg</mdui:Logo>
+	    <mdui:InformationURL xml:lang="cy">https://example.org/info-welsh</mdui:InformationURL>
+          </mdui:UIInfo>
+        </Extensions>
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+        <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"
+            Location="https://idp.example.com/idp/profile/Shibboleth/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://idp.example.com/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
+            Location="https://idp.example.com/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+            Location="https://idp.example.com/idp/profile/SAML2/Redirect/SSO"/>
+    </IDPSSODescriptor>
+    <AttributeAuthorityDescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/AttributeQuery"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    </AttributeAuthorityDescriptor>
+    <Organization>
+        <OrganizationName xml:lang="en">Example Organization</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">The Example Organization</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">http://example.com/</OrganizationURL>
+    </Organization>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-keywords.json b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-keywords.json
new file mode 100644
index 0000000..8197031
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-keywords.json
@@ -0,0 +1,34 @@
+[
+{
+ "entityID": "https://idp.example.com/idp/shibboleth",
+ "DisplayNames": [
+  {
+  "value": "Example Organization",
+  "lang": "en"
+  }
+ ],
+ "Descriptions": [
+  {
+  "value": "This is the identity provider for The Example Organization.",
+  "lang": "en"
+  }
+ ],
+ "Keywords": [
+  {
+  "value": "the quick brown English fox",
+  "lang": "en"
+  },
+  {
+  "value": "the quick brown Welsh fox",
+  "lang": "cy"
+  }
+ ],
+ "Logos": [
+  {
+  "value": "https://idp.example.com/images/heads_80x80.jpg",
+  "height": "80",
+  "width": "80"
+  }
+ ]
+}
+]
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-keywords.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-keywords.xml
new file mode 100644
index 0000000..151b3ed
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-keywords.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+    xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+    xmlns:remd="http://refeds.org/metadata"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    entityID="https://idp.example.com/idp/shibboleth">
+    <Extensions>
+        <shibmd:Scope regexp="false">example.com</shibmd:Scope>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2007-03-30T16:36:00Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+            <saml:Attribute Name="http://macedir.org/entity-category-support"
+                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://refeds.org/category/research-and-scholarship</saml:AttributeValue>
+            </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <IDPSSODescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+          <mdui:UIInfo>
+	    <mdui:Keywords xml:lang="en">the quick brown English fox</mdui:Keywords>
+            <mdui:DisplayName xml:lang="en">Example Organization</mdui:DisplayName>
+            <mdui:Description xml:lang="en">This is the identity provider for The Example Organization.</mdui:Description>
+            <mdui:Logo height="80" width="80">https://idp.example.com/images/heads_80x80.jpg</mdui:Logo>
+	    <mdui:Keywords xml:lang="cy">the quick brown Welsh fox</mdui:Keywords>
+          </mdui:UIInfo>
+        </Extensions>
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+        <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"
+            Location="https://idp.example.com/idp/profile/Shibboleth/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://idp.example.com/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
+            Location="https://idp.example.com/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+            Location="https://idp.example.com/idp/profile/SAML2/Redirect/SSO"/>
+    </IDPSSODescriptor>
+    <AttributeAuthorityDescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/AttributeQuery"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    </AttributeAuthorityDescriptor>
+    <Organization>
+        <OrganizationName xml:lang="en">Example Organization</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">The Example Organization</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">http://example.com/</OrganizationURL>
+    </Organization>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-legacy.json b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-legacy.json
new file mode 100644
index 0000000..ba93a54
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-legacy.json
@@ -0,0 +1,18 @@
+[
+{
+ "entityID": "https://idp.example.com/idp/shibboleth",
+ "Logos": [
+  {
+  "value": "https://idp.example.com/images/heads_80x80.jpg",
+  "height": "80",
+  "width": "80"
+  }
+ ],
+ "DisplayNames": [
+  {
+  "value": "The Example Organization",
+  "lang": "en"
+  }
+ ]
+}
+]
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-legacy.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-legacy.xml
new file mode 100644
index 0000000..584126f
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-legacy.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+    xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+    xmlns:remd="http://refeds.org/metadata"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    entityID="https://idp.example.com/idp/shibboleth">
+    <Extensions>
+        <shibmd:Scope regexp="false">example.com</shibmd:Scope>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2007-03-30T16:36:00Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+            <saml:Attribute Name="http://macedir.org/entity-category-support"
+                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://refeds.org/category/research-and-scholarship</saml:AttributeValue>
+            </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <IDPSSODescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+            <mdui:UIInfo>
+                <mdui:Logo height="80" width="80">https://idp.example.com/images/heads_80x80.jpg</mdui:Logo>
+            </mdui:UIInfo>
+        </Extensions>
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+        <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"
+            Location="https://idp.example.com/idp/profile/Shibboleth/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://idp.example.com/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
+            Location="https://idp.example.com/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+            Location="https://idp.example.com/idp/profile/SAML2/Redirect/SSO"/>
+    </IDPSSODescriptor>
+    <AttributeAuthorityDescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/AttributeQuery"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    </AttributeAuthorityDescriptor>
+    <Organization>
+        <OrganizationName xml:lang="en">Example Organization</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">The Example Organization</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">http://example.com/</OrganizationURL>
+    </Organization>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-logolang.json b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-logolang.json
new file mode 100644
index 0000000..4b7b8c3
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-logolang.json
@@ -0,0 +1,35 @@
+[
+{
+ "entityID": "https://idp.example.com/idp/shibboleth",
+ "DisplayNames": [
+  {
+  "value": "Example Organization",
+  "lang": "en"
+  }
+ ],
+ "Descriptions": [
+  {
+  "value": "This is the identity provider for The Example Organization.",
+  "lang": "en"
+  }
+ ],
+ "Logos": [
+  {
+  "value": "https://idp.example.com/images/heads_80x80.jpg",
+  "height": "80",
+  "width": "80"
+  },
+  {
+  "value": "https://idp.example.com/images/heads_100x43.jpg",
+  "height": "43",
+  "width": "100",
+  "lang": "en"
+  },
+  {
+  "value": "https://idp.example.com/images/heads_240x104.jpg",
+  "height": "104",
+  "width": "240"
+  }
+ ]
+}
+]
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-logolang.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-logolang.xml
new file mode 100644
index 0000000..badd37e
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-logolang.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+    xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+    xmlns:remd="http://refeds.org/metadata"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    entityID="https://idp.example.com/idp/shibboleth">
+    <Extensions>
+        <shibmd:Scope regexp="false">example.com</shibmd:Scope>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2007-03-30T16:36:00Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+            <saml:Attribute Name="http://macedir.org/entity-category-support"
+                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://refeds.org/category/research-and-scholarship</saml:AttributeValue>
+            </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <IDPSSODescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+            <mdui:UIInfo>
+                <mdui:DisplayName xml:lang="en">Example Organization</mdui:DisplayName>
+                <mdui:Description xml:lang="en">This is the identity provider for The Example Organization.</mdui:Description>
+                <mdui:Logo height="80" width="80">https://idp.example.com/images/heads_80x80.jpg</mdui:Logo>
+                <mdui:Logo height="43" width="100" xml:lang="en">https://idp.example.com/images/heads_100x43.jpg</mdui:Logo>
+                <mdui:Logo height="104" width="240">https://idp.example.com/images/heads_240x104.jpg</mdui:Logo>
+            </mdui:UIInfo>
+        </Extensions>
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+        <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"
+            Location="https://idp.example.com/idp/profile/Shibboleth/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://idp.example.com/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
+            Location="https://idp.example.com/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+            Location="https://idp.example.com/idp/profile/SAML2/Redirect/SSO"/>
+    </IDPSSODescriptor>
+    <AttributeAuthorityDescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/AttributeQuery"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    </AttributeAuthorityDescriptor>
+    <Organization>
+        <OrganizationName xml:lang="en">Example Organization</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">The Example Organization</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">http://example.com/</OrganizationURL>
+    </Organization>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-nolegacy.json b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-nolegacy.json
new file mode 100644
index 0000000..7e36278
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-nolegacy.json
@@ -0,0 +1,12 @@
+[
+{
+ "entityID": "https://idp.example.com/idp/shibboleth",
+ "Logos": [
+  {
+  "value": "https://idp.example.com/images/heads_80x80.jpg",
+  "height": "80",
+  "width": "80"
+  }
+ ]
+}
+]
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-nolegacy.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-nolegacy.xml
new file mode 100644
index 0000000..584126f
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-nolegacy.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+    xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+    xmlns:remd="http://refeds.org/metadata"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    entityID="https://idp.example.com/idp/shibboleth">
+    <Extensions>
+        <shibmd:Scope regexp="false">example.com</shibmd:Scope>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2007-03-30T16:36:00Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+            <saml:Attribute Name="http://macedir.org/entity-category-support"
+                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://refeds.org/category/research-and-scholarship</saml:AttributeValue>
+            </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <IDPSSODescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+            <mdui:UIInfo>
+                <mdui:Logo height="80" width="80">https://idp.example.com/images/heads_80x80.jpg</mdui:Logo>
+            </mdui:UIInfo>
+        </Extensions>
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+        <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"
+            Location="https://idp.example.com/idp/profile/Shibboleth/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://idp.example.com/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
+            Location="https://idp.example.com/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+            Location="https://idp.example.com/idp/profile/SAML2/Redirect/SSO"/>
+    </IDPSSODescriptor>
+    <AttributeAuthorityDescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/AttributeQuery"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    </AttributeAuthorityDescriptor>
+    <Organization>
+        <OrganizationName xml:lang="en">Example Organization</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">The Example Organization</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">http://example.com/</OrganizationURL>
+    </Organization>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-privacyurl.json b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-privacyurl.json
new file mode 100644
index 0000000..6a56293
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-privacyurl.json
@@ -0,0 +1,34 @@
+[
+{
+ "entityID": "https://idp.example.com/idp/shibboleth",
+ "DisplayNames": [
+  {
+  "value": "Example Organization",
+  "lang": "en"
+  }
+ ],
+ "Descriptions": [
+  {
+  "value": "This is the identity provider for The Example Organization.",
+  "lang": "en"
+  }
+ ],
+ "PrivacyStatementURLs": [
+  {
+  "value": "https://example.org/priv-english",
+  "lang": "en"
+  },
+  {
+  "value": "https://example.org/priv-welsh",
+  "lang": "cy"
+  }
+ ],
+ "Logos": [
+  {
+  "value": "https://idp.example.com/images/heads_80x80.jpg",
+  "height": "80",
+  "width": "80"
+  }
+ ]
+}
+]
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-privacyurl.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-privacyurl.xml
new file mode 100644
index 0000000..734b902
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/DiscoFeedCollectionSerializer-privacyurl.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+    xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+    xmlns:remd="http://refeds.org/metadata"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    entityID="https://idp.example.com/idp/shibboleth">
+    <Extensions>
+        <shibmd:Scope regexp="false">example.com</shibmd:Scope>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2007-03-30T16:36:00Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+            <saml:Attribute Name="http://macedir.org/entity-category-support"
+                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://refeds.org/category/research-and-scholarship</saml:AttributeValue>
+            </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <IDPSSODescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+          <mdui:UIInfo>
+	    <mdui:PrivacyStatementURL xml:lang="en">https://example.org/priv-english</mdui:PrivacyStatementURL>
+            <mdui:DisplayName xml:lang="en">Example Organization</mdui:DisplayName>
+            <mdui:Description xml:lang="en">This is the identity provider for The Example Organization.</mdui:Description>
+            <mdui:Logo height="80" width="80">https://idp.example.com/images/heads_80x80.jpg</mdui:Logo>
+	    <mdui:PrivacyStatementURL xml:lang="cy">https://example.org/priv-welsh</mdui:PrivacyStatementURL>
+          </mdui:UIInfo>
+        </Extensions>
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+        <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"
+            Location="https://idp.example.com/idp/profile/Shibboleth/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://idp.example.com/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
+            Location="https://idp.example.com/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+            Location="https://idp.example.com/idp/profile/SAML2/Redirect/SSO"/>
+    </IDPSSODescriptor>
+    <AttributeAuthorityDescriptor
+        protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIDSTCCAjGgAwIBAgIhAMSPOSGN+3UUTXSKV+2EBOuF3x/pwPX/TD9GfyEkzLp+
+                        MA0GCSqGSIb3DQEBBQUAMFgxGDAWBgNVBAMMD2lkcDIuaWF5Lm9yZy51azETMBEG
+                        CgmSJomT8ixkARkWA2lheTETMBEGCgmSJomT8ixkARkWA29yZzESMBAGCgmSJomT
+                        8ixkARkWAnVrMB4XDTA4MDIyNTEwMzAxNFoXDTI4MDIyNTEwMzAxNFowWDEYMBYG
+                        A1UEAwwPaWRwMi5pYXkub3JnLnVrMRMwEQYKCZImiZPyLGQBGRYDaWF5MRMwEQYK
+                        CZImiZPyLGQBGRYDb3JnMRIwEAYKCZImiZPyLGQBGRYCdWswggEiMA0GCSqGSIb3
+                        DQEBAQUAA4IBDwAwggEKAoIBAQCb6ts48g10XHTnpy+23huzR184aahkrG0AoeUl
+                        FVlomPjoFDk6czq0S3Qyd+ceF7tMRu3XzS7cMmtVH53O9d+wCs8aPQcPXxHQ5gLk
+                        L7Gu6eJ+3N3jXhpt7/DDPhnzFPNW3EVMueHJ/0IzyspTvq2LPbNWXJ86NKJ+gesZ
+                        QftskwXScOjpoJEIP0EA890QYd4WdYtQPqVV+LPKtnYBoGOnuRhSAM1D/EhCbeb0
+                        lCmRGcdGbDFBchiPO4VLGl85sLa0EhjxMIPAOKXcj8bBlO9Ww9kkG06kQp6eLHwm
+                        Jmt7VNKveCGhyF2QH/CvmdUaPv3gcp1UjrlqFN9LBVSaTIL/AgMBAAEwDQYJKoZI
+                        hvcNAQEFBQADggEBAG+jDBAtlKoHaEBB+l6PpW5zuiDjyHG4zZZYqX77mZ9xP/xe
+                        Kn0yJ18ZLjS3b9WztGLYyC4SJHSF2okq1K02bqsCv9YeP+UWpw2uRR8jt96lLWxZ
+                        jTjoko2v8jBtzDk8LZsqw58m4vZ0AGNZjKeGIywKhxnepwREguyj3bjBpZAGgl0M
+                        HQuXoO/BDC9yKyZslE5CpWp5xP4XzY2/LrorrkwOJLnFuk1sox4/gvkDQukUx/jr
+                        YRbrWfOjcNBx3LE/HI6RNLINicK7yUwerDE86nix5Zc3hskVcCykW+r6HbY6bx7P
+                        YmNKYMZhQAgDtXIjFHOy+WbyVTidmJvxM9UeYCY=
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+            Location="https://idp.example.com:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://idp.example.com:8443/idp/profile/SAML2/SOAP/AttributeQuery"/>
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    </AttributeAuthorityDescriptor>
+    <Organization>
+        <OrganizationName xml:lang="en">Example Organization</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">The Example Organization</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">http://example.com/</OrganizationURL>
+    </Organization>
+</EntityDescriptor>

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


More information about the commits mailing list