[java-identity-provider] branch master updated: IDP-1170 - REST DataConnector

Scott Cantor cantor.2 at osu.edu
Sat May 20 19:00:17 EDT 2017


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

scantor pushed a commit to branch master
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=39617ee9b094ba61f599fd3d416e2e67e9689a87

The following commit(s) were added to refs/heads/master by this push:
       new  39617ee   IDP-1170 - REST DataConnector
39617ee is described below

commit 39617ee9b094ba61f599fd3d416e2e67e9689a87
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Sat May 20 19:00:15 2017 -0400

    IDP-1170 - REST DataConnector
    
    https://issues.shibboleth.net/jira/browse/IDP-1170
    
    Connector and plugin implementations.
---
 .../dc/http/impl/AbstractHTTPSearchBuilder.java    | 144 +++++++++
 .../resolver/dc/http/impl/HTTPDataConnector.java   |  93 ++++++
 .../dc/http/impl/HTTPResponseMappingStrategy.java  |  34 ++
 .../resolver/dc/http/impl/HTTPSearch.java          |  45 +++
 .../http/impl/ScriptedResponseMappingStrategy.java | 344 +++++++++++++++++++++
 .../resolver/dc/http/impl/TemplatedURLBuilder.java | 194 ++++++++++++
 .../resolver/dc/http/impl/package-info.java        |  22 ++
 .../dc/http/impl/HTTPDataConnectorTest.java        | 187 +++++++++++
 .../idp/attribute/resolver/impl/dc/http/test.js    |  20 ++
 9 files changed, 1083 insertions(+)

diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/AbstractHTTPSearchBuilder.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/AbstractHTTPSearchBuilder.java
new file mode 100644
index 0000000..d3288e1
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/AbstractHTTPSearchBuilder.java
@@ -0,0 +1,144 @@
+/*
+ * 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.idp.attribute.resolver.dc.http.impl;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.http.HttpRequest;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.impl.ExecutableSearchBuilder;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Basis of request builder. Derived classes just have to provide the per request URI but may override
+ * the complete request build if desired, for example to construct a SOAP message or something more
+ * exotic.
+ * 
+ * <p>This is all a bit byzantine to maintain a consistent design with the LDAP/etc. connectors,
+ * which split the work of producing "search objects that execute and return a result" and
+ * "mapping strategies that process a result". The HTTP client supports response handlers
+ * that offload all the connection cleanup and avoid any extra data buffering, so our
+ * facade passes the mapping strategy in as a response handler and just returns the result.</p>
+ */
+public abstract class AbstractHTTPSearchBuilder extends AbstractInitializableComponent implements
+        ExecutableSearchBuilder<HTTPSearch> {
+    
+    /** HTTP client security parameters. */
+    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+    
+    /**
+     * Set the optional client security parameters.
+     * 
+     * @param params the new client security parameters
+     */
+    public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        httpClientSecurityParameters = params;
+    }
+    
+    /** {@inheritDoc} */
+    @Override public HTTPSearch build(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue<?>>> dependencyAttributes) throws ResolutionException {
+        
+        final HttpUriRequest request = getHttpRequest(resolutionContext, dependencyAttributes);
+        final HttpClientContext context = buildHttpContext(request);
+
+        return new HTTPSearch() {
+            /** {@inheritDoc} */
+            @Nonnull public String getResultCacheKey() {
+                Constraint.isTrue(request instanceof HttpGet, "Only GET requests are cacheable");
+                return ((HttpGet) request).getURI().toString();
+            }
+
+            /** {@inheritDoc} */
+            public String toString() {
+                return request.getRequestLine().getUri();
+            }
+            
+            /** {@inheritDoc} */
+            @Nonnull public Map<String,IdPAttribute> execute(@Nonnull final HttpClient client,
+                    @Nonnull final HTTPResponseMappingStrategy mappingStrategy) throws IOException {
+                final Map<String,IdPAttribute> results = client.execute(request, mappingStrategy, context);
+                HttpClientSecuritySupport.checkTLSCredentialEvaluated(context, request.getURI().getScheme());
+                return results;
+            }
+        };
+    }
+
+    /**
+     * Method to return the URL to access via GET.
+     * 
+     * @param resolutionContext the context of the resolution
+     * @param dependencyAttributes made available to the request
+     * 
+     * @return the URL to GET
+     * @throws ResolutionException if an error occurs
+     */
+    @Nonnull @NotEmpty protected abstract String getURL(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String,List<IdPAttributeValue<?>>> dependencyAttributes) throws ResolutionException;
+    
+    /**
+     * Default implementation just supports GET and builds a request around a URL.
+     * 
+     * @param resolutionContext the context of the resolution
+     * @param dependencyAttributes made available to the request
+     * 
+     * @return the {@link HttpRequest} to use
+     * @throws ResolutionException if an error occurs
+     */
+    @Nonnull protected HttpUriRequest getHttpRequest(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String,List<IdPAttributeValue<?>>> dependencyAttributes) throws ResolutionException {
+        
+        // Default just wraps a computed URL into a GET.
+        return new HttpGet(getURL(resolutionContext, dependencyAttributes));
+    }
+    
+    /**
+     * Build the {@link HttpClientContext} instance to be used.
+     * 
+     * @param request the HTTP client request
+     * @return the client context instance
+     */
+    @Nonnull private HttpClientContext buildHttpContext(@Nonnull final HttpUriRequest request) {
+        final HttpClientContext clientContext = HttpClientContext.create();
+        HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, false);
+        HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, request);
+        return clientContext;
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPDataConnector.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPDataConnector.java
new file mode 100644
index 0000000..2eb808f
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPDataConnector.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+/*
+ * Derived from work (c) 2015 CSC, see included license.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.http.impl;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.http.client.HttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.ValidationException;
+import net.shibboleth.idp.attribute.resolver.dc.Validator;
+import net.shibboleth.idp.attribute.resolver.dc.impl.AbstractSearchDataConnector;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** This class implements a {@link DataConnector} that obtains data from an HTTP service. */
+public class HTTPDataConnector extends AbstractSearchDataConnector<HTTPSearch,HTTPResponseMappingStrategy> {
+    
+    /** Class logging. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(HTTPDataConnector.class);
+
+    /** The {@link HttpClient} to use. */
+    @NonnullAfterInit private HttpClient httpClient;
+    
+    /** Constructor. */
+    public HTTPDataConnector() {
+        setValidator(new Validator() {
+            public void validate() throws ValidationException {
+            }
+        });
+    }
+
+    /**
+     * Set the {@link HttpClient} to use.
+     * 
+     * @param client client to use
+     */
+    public void setHttpClient(@Nonnull final HttpClient client) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        httpClient = Constraint.isNotNull(client, "HttpClient cannot be null");
+    }
+        
+    /** {@inheritDoc} */
+    public void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (httpClient == null) {
+            throw new ComponentInitializationException(getLogPrefix() + " HttpClient cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Map<String,IdPAttribute> retrieveAttributes(@Nonnull final HTTPSearch executable)
+            throws ResolutionException {
+
+        try {
+            return getMappingStrategy().map(executable.execute(httpClient, getMappingStrategy()));
+        } catch (final IOException e) {
+            throw new ResolutionException(getLogPrefix() + " HTTP request failed", e);
+        }
+    }
+        
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPResponseMappingStrategy.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPResponseMappingStrategy.java
new file mode 100644
index 0000000..1a27f87
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPResponseMappingStrategy.java
@@ -0,0 +1,34 @@
+/*
+ * 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.idp.attribute.resolver.dc.http.impl;
+
+import java.util.Map;
+
+import org.apache.http.client.ResponseHandler;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.resolver.dc.MappingStrategy;
+
+/**
+ * Strategy for processing an {@link org.apache.http.client.protocol.HttpClient} response into a map of 
+ * {@link net.shibboleth.idp.attribute.IdPAttribute}s.
+ */
+public interface HTTPResponseMappingStrategy extends MappingStrategy<Map<String,IdPAttribute>>,
+    ResponseHandler<Map<String,IdPAttribute>> {
+    
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPSearch.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPSearch.java
new file mode 100644
index 0000000..8bf0ec3
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPSearch.java
@@ -0,0 +1,45 @@
+/*
+ * 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.idp.attribute.resolver.dc.http.impl;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.apache.http.client.HttpClient;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.resolver.dc.impl.ExecutableSearch;
+
+/** An HTTP request that returns attribute data. */
+public interface HTTPSearch extends ExecutableSearch {
+
+    /**
+     * The abstraction that will contact the service and obtain results.
+     * 
+     * @param client the HTTP client
+     * @param mappingStrategy response mapping strategy
+     * 
+     * @return attribute results
+     * @throws IOException if an error occurs
+     */
+    @Nonnull Map<String,IdPAttribute> execute(@Nonnull final HttpClient client,
+            @Nonnull final HTTPResponseMappingStrategy mappingStrategy) throws IOException;
+    
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/ScriptedResponseMappingStrategy.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/ScriptedResponseMappingStrategy.java
new file mode 100644
index 0000000..7dae176
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/ScriptedResponseMappingStrategy.java
@@ -0,0 +1,344 @@
+/*
+ * 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.idp.attribute.resolver.dc.http.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.script.ScriptContext;
+import javax.script.ScriptException;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.ContentType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resource.Resource;
+import net.shibboleth.utilities.java.support.scripting.AbstractScriptEvaluator;
+import net.shibboleth.utilities.java.support.scripting.EvaluableScript;
+
+/**
+ * {@link HTTPResponseMappingStrategy} that relies on a script to map the response to the
+ * attribute set.
+ * 
+ * <p>Well-suited to JSON output formats that can be parsed by the scripting engine.</p>
+ */
+public final class ScriptedResponseMappingStrategy extends AbstractScriptEvaluator
+        implements HTTPResponseMappingStrategy {
+    
+    /** The id of the object where the results go. */
+    @Nonnull public static final String RESULTS_STRING = "connectorResults";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ScriptedResponseMappingStrategy.class);
+
+    /** "Successful" statuses. */
+    @Nonnull private Set<Integer> acceptStatuses;
+    
+    /** Acceptable Content-Types. */
+    @Nonnull private Set<String> acceptTypes;
+    
+    /** Limit on content-length. */
+    private long maxLength;
+    
+    /**
+     * Constructor.
+     *
+     * @param theScript the script to run
+     */
+    private ScriptedResponseMappingStrategy(@Nonnull final EvaluableScript theScript) {
+        super(theScript);
+        
+        acceptStatuses = Collections.singleton(HttpStatus.SC_OK);
+        acceptTypes = Collections.emptySet();
+    }
+    
+    /**
+     * Set the HTTP status codes to treat as successful.
+     * 
+     * @param statuses successful codes
+     */
+    public void setAcceptStatuses(@Nonnull final Collection<Integer> statuses) {
+        Constraint.isNotNull(statuses, "Statuses cannot be null");
+        
+        acceptStatuses = new HashSet<>(Collections2.filter(statuses, Predicates.notNull()));
+    }
+
+    /**
+     * Set the content-types to allow.
+     * 
+     * @param types types to allow
+     */
+    public void setAcceptTypes(@Nonnull final Collection<String> types) {
+        Constraint.isNotNull(types, "Types cannot be null");
+        
+        acceptTypes = new HashSet<>(StringSupport.normalizeStringCollection(types));
+    }
+    
+    /**
+     * Set a limit on content-length.
+     * 
+     * <p>Defaults to 0, allowing any. Setting a limit implies that an unknown length will be rejected.</p>
+     * 
+     * @param len limit on size
+     */
+    public void setMaxLength(final long len) {
+        maxLength = len;
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull public Map<String,IdPAttribute> map(@Nonnull final Map<String,IdPAttribute> results)
+            throws ResolutionException {
+        return results;
+    }
+    
+    /** {@inheritDoc} */
+    public Map<String,IdPAttribute> handleResponse(final HttpResponse response) throws IOException {
+        
+        log.debug("{} Handling HTTP response", getLogPrefix());
+        
+        if (response == null) {
+            log.debug("{} HTTP response was missing", getLogPrefix());
+            throw new IOException(getLogPrefix() + " HTTP response was missing");
+        }
+        
+        checkStatus(response);
+        
+        final HttpEntity entity = response.getEntity();
+        if (entity == null) {
+            log.debug("{} Response body was missing", getLogPrefix());
+            throw new IOException(getLogPrefix() + " Response body was missing");
+        }
+        
+        checkContentType(entity);
+        checkContentLength(entity);
+        
+        try {
+            return (Map<String,IdPAttribute>) evaluate(response);
+        } catch (final RuntimeException e) {
+            throw new IOException(getLogPrefix() + " Script did not run successfully", e);
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void prepareContext(@Nonnull final ScriptContext scriptContext, @Nullable final Object... input) {
+        log.debug("{} Adding to-be-populated attribute set '{}' to script context", getLogPrefix(), RESULTS_STRING);
+        scriptContext.setAttribute(RESULTS_STRING, new HashSet<>(), ScriptContext.ENGINE_SCOPE);
+        
+        scriptContext.setAttribute("response", input[0], ScriptContext.ENGINE_SCOPE);
+        scriptContext.setAttribute("log", log, ScriptContext.ENGINE_SCOPE);
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Object finalizeContext(@Nonnull final ScriptContext scriptContext,
+            @Nullable final Object scriptResult) throws ScriptException {
+        
+        // The real result in our case is a variable in the context.
+        final Object res = scriptContext.getAttribute(RESULTS_STRING);
+
+        if (null == res) {
+            log.error("{} Could not locate output variable '{}' from script", getLogPrefix(), RESULTS_STRING);
+            throw new ScriptException("Could not locate output from script");
+        }
+        if (!(res instanceof Collection)) {
+            log.error("{} Output '{}' was of type '{}', expected '{}'", getLogPrefix(), res.getClass().getName(),
+                    Collection.class.getName());
+            throw new ScriptException("Output was of the wrong type");
+        }
+
+        final Collection outputCollection = (Collection) res;
+        final Map<String, IdPAttribute> outputMap = new HashMap<>(outputCollection.size());
+        for (final Object o : outputCollection) {
+            if (o instanceof IdPAttribute) {
+                final IdPAttribute attribute = (IdPAttribute) o;
+                if (null == attribute.getId()) {
+                    log.warn("{} Anonymous Attribute encountered, ignored", getLogPrefix());
+                } else {
+                    checkValues(attribute);
+                    outputMap.put(attribute.getId(), attribute);
+                }
+            } else {
+                log.warn("{} Output collection contained an object of type '{}', ignored", getLogPrefix(),
+                        o.getClass().getName());
+            }
+        }
+
+        return outputMap;
+    }
+    
+    /**
+     * Enforce any status code requirements.
+     * 
+     * @param response HTTP response
+     * 
+     * @throws IOException if the status is unacceptable
+     */
+    private void checkStatus(@Nonnull final HttpResponse response) throws IOException {
+        if (!acceptStatuses.isEmpty()) {
+            if (response.getStatusLine() == null
+                    || !acceptStatuses.contains(response.getStatusLine().getStatusCode())) {
+                log.debug("{} Unacceptable HTTP status: {}", getLogPrefix(),
+                        response.getStatusLine() != null ? response.getStatusLine().getStatusCode() : "unknown");
+                throw new IOException(getLogPrefix() + " HTTP status unknown or unacceptable");
+            }
+        }
+    }
+
+    /**
+     * Enforce Content-Type requirements.
+     * 
+     * @param entity the entity body
+     * 
+     * @throws IOException if the type is unacceptable
+     */
+    private void checkContentType(@Nonnull final HttpEntity entity) throws IOException {
+        if (!acceptTypes.isEmpty()) {
+            
+            final ContentType contentType = ContentType.get(entity);
+            if (contentType == null || !acceptTypes.contains(contentType.getMimeType())) {
+                log.debug("{} Unacceptable Content-Type: {}", getLogPrefix(),
+                        contentType != null ? contentType.getMimeType() : "unknown");
+                throw new IOException(getLogPrefix() + " Content-Type unknown or unacceptable");
+            }
+        }
+    }
+
+    /**
+     * Check the content length.
+     * 
+     * @param entity the entity body
+     * 
+     * @throws IOException if the length is unacceptable
+     */
+    private void checkContentLength(@Nonnull final HttpEntity entity) throws IOException {
+        if (maxLength > 0) {
+            if (entity.getContentLength() < 0 || entity.getContentLength() > maxLength) {
+                log.debug("{} Unacceptable Content-Length: {}", getLogPrefix(), entity.getContentLength());
+                throw new IOException(getLogPrefix() + " Content-Length exceeded acceptable limits or was unset");
+            }
+        }
+    }
+
+    /**
+     * Ensure that all the values in the attribute are of the correct type.
+     * 
+     * @param attribute the attribute to look at
+     */
+    private void checkValues(final IdPAttribute attribute) {
+
+        if (null == attribute.getValues()) {
+            log.info("{} Attribute '{}' has no values provided.", getLogPrefix(), attribute.getId());
+            attribute.setValues(Collections.<IdPAttributeValue<?>> emptyList());
+            return;
+        }
+        log.debug("{} Attribute '{}' has {} value(s).", getLogPrefix(), attribute.getId(),
+                attribute.getValues().size());
+        final List<IdPAttributeValue<?>> inputValues = attribute.getValues();
+        final List<IdPAttributeValue<?>> outputValues = new ArrayList<>(inputValues.size());
+
+        for (final Object o : inputValues) {
+            if (o instanceof IdPAttributeValue<?>) {
+                outputValues.add((IdPAttributeValue<?>) o);
+            } else {
+                log.error("{} Attribute '{} has attribute value of type {}.  This will be ignored", getLogPrefix(),
+                        attribute.getId(), o.getClass().getName());
+            }
+        }
+        attribute.setValues(outputValues);
+    }
+    
+    /**
+     * Factory to create {@link ScriptedResponseMappingStrategy} from a {@link Resource}.
+     * 
+     * @param engineName the language
+     * @param resource the resource to look at
+     * @return the function
+     * @throws ScriptException if the compile fails
+     * @throws IOException if the file doesn't exist.
+     */
+    @Nonnull static ScriptedResponseMappingStrategy resourceScript(@Nonnull @NotEmpty final String engineName,
+            @Nonnull final Resource resource) throws ScriptException, IOException {
+        try (final InputStream is = resource.getInputStream()) {
+            final EvaluableScript script = new EvaluableScript(engineName, is);
+            return new ScriptedResponseMappingStrategy(script);
+        }
+    }
+
+    /**
+     * Factory to create {@link ScriptedResponseMappingStrategy} from a {@link Resource}.
+     * 
+     * @param resource the resource to look at
+     * @return the function
+     * @throws ScriptException if the compile fails
+     * @throws IOException if the file doesn't exist.
+     */
+    @Nonnull static ScriptedResponseMappingStrategy resourceScript(@Nonnull final Resource resource)
+            throws ScriptException, IOException {
+        return resourceScript(DEFAULT_ENGINE, resource);
+    }
+
+    /**
+     * Factory to create {@link ScriptedResponseMappingStrategy} from inline data.
+     * 
+     * @param scriptSource the script, as a string
+     * @param engineName the language
+     * @return the function
+     * @throws ScriptException if the compile fails
+     */
+    @Nonnull static ScriptedResponseMappingStrategy inlineScript(@Nonnull @NotEmpty final String engineName,
+            @Nonnull @NotEmpty final String scriptSource) throws ScriptException {
+        final EvaluableScript script = new EvaluableScript(engineName, scriptSource);
+        return new ScriptedResponseMappingStrategy(script);
+    }
+
+    /**
+     * Factory to create {@link ScriptedResponseMappingStrategy} from inline data.
+     * 
+     * @param scriptSource the script, as a string
+     * @return the function
+     * @throws ScriptException if the compile fails
+     */
+    @Nonnull static ScriptedResponseMappingStrategy inlineScript(@Nonnull @NotEmpty final String scriptSource)
+            throws ScriptException {
+        return inlineScript(DEFAULT_ENGINE, scriptSource);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/TemplatedURLBuilder.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/TemplatedURLBuilder.java
new file mode 100644
index 0000000..ca3cf8d2
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/TemplatedURLBuilder.java
@@ -0,0 +1,194 @@
+/*
+ * 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.idp.attribute.resolver.dc.http.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.velocity.Template;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+
+/**
+ * An {@link net.shibboleth.idp.attribute.resolver.dc.impl.ExecutableSearchBuilder} that generates the URL to
+ * request by evaluating a {@link Template} against the currently resolved attributes within an
+ * {@link AttributeResolutionContext}.
+ */
+public class TemplatedURLBuilder extends AbstractHTTPSearchBuilder {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(TemplatedURLBuilder.class);
+
+    /** Template to be evaluated. */
+    @NonnullAfterInit private Template template;
+
+    /** Text of template to be evaluated. */
+    @NonnullAfterInit private String templateText;
+
+    /** VelocityEngine. */
+    @NonnullAfterInit private VelocityEngine engine;
+    
+    /** Escaper for form parameters. */
+    @Nonnull private final Escaper paramEscaper;
+
+    /** Escaper for fragments. */
+    @Nonnull private final Escaper fragmentEscaper;
+
+    /** Escaper for path segments. */
+    @Nonnull private final Escaper pathEscaper;
+
+    /** Constructor. */
+    public TemplatedURLBuilder() {
+        paramEscaper = UrlEscapers.urlFormParameterEscaper();
+        fragmentEscaper = UrlEscapers.urlFragmentEscaper();
+        pathEscaper = UrlEscapers.urlPathSegmentEscaper();
+    }
+    
+    /**
+     * Get the template to be evaluated.
+     * 
+     * @return template
+     */
+    @NonnullAfterInit public Template getTemplate() {
+        return template;
+    }
+
+    /**
+     * Get the template text to be evaluated.
+     * 
+     * @return template text
+     */
+    @NonnullAfterInit public String getTemplateText() {
+        return templateText;
+    }
+
+    /**
+     * Set the template to be evaluated.
+     * 
+     * @param text template to be evaluated
+     */
+    public void setTemplateText(@Nullable final String text) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        templateText = StringSupport.trimOrNull(text);
+    }
+
+    /**
+     * Get the {@link VelocityEngine} to be used.
+     * 
+     * @return template engine
+     */
+    @NonnullAfterInit public VelocityEngine getVelocityEngine() {
+        return engine;
+    }
+
+    /**
+     * Set the {@link VelocityEngine} to be used.
+     * 
+     * @param velocityEngine engine to be used
+     */
+    public void setVelocityEngine(@Nonnull final VelocityEngine velocityEngine) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        engine = Constraint.isNotNull(velocityEngine, "Velocity engine cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (null == engine) {
+            throw new ComponentInitializationException("Velocity engine cannot be null");
+        }
+
+        if (null == templateText) {
+            throw new ComponentInitializationException("Template text cannot be null");
+        }
+
+        template = Template.fromTemplate(engine, templateText);
+    }
+
+    /**
+     * Invokes {@link Template#merge(org.apache.velocity.context.Context)} on the supplied context.
+     * 
+     * @param context to merge
+     * 
+     * @return result of the merge operation
+     */
+    @Nonnull @NotEmpty protected String merge(@Nonnull final VelocityContext context) {
+        final String result = template.merge(context);
+        log.debug("Template text {} yields {}", templateText, result);
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected String getURL(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue<?>>> dependencyAttributes) throws ResolutionException {
+
+        final VelocityContext context = new VelocityContext();
+        log.trace("Creating request URL using attribute resolution context {}", resolutionContext);
+        context.put("resolutionContext", resolutionContext);
+
+        context.put("paramEscaper", paramEscaper);
+        context.put("fragmentEscaper", fragmentEscaper);
+        context.put("pathEscaper", pathEscaper);
+
+        // inject dependencies
+        if (dependencyAttributes != null && !dependencyAttributes.isEmpty()) {
+            for (final Map.Entry<String, List<IdPAttributeValue<?>>> entry : dependencyAttributes.entrySet()) {
+                final List<Object> values = new ArrayList<>(entry.getValue().size());
+                for (final IdPAttributeValue<?> value : entry.getValue()) {
+                    values.add(value.getValue());
+                }
+                log.trace("Adding dependency {} to context with {} value(s)", entry.getKey(), values.size());
+                context.put(entry.getKey(), values);
+            }
+        }
+
+        try {
+            return merge(context);
+        } catch (final VelocityException e) {
+            log.error("Error running template engine", e);
+            throw new ResolutionException("Error running template engine", e);
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/package-info.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/package-info.java
new file mode 100644
index 0000000..195491a
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * A {@link net.shibboleth.idp.attribute.resolver.DataConnector} implementation that
+ * reads data from an HTTP web service.
+ */
+package net.shibboleth.idp.attribute.resolver.dc.http.impl;
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPDataConnectorTest.java b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPDataConnectorTest.java
new file mode 100644
index 0000000..054f545
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/http/impl/HTTPDataConnectorTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.idp.attribute.resolver.dc.http.impl;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import javax.script.ScriptException;
+
+import net.shibboleth.ext.spring.resource.ResourceHelper;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.impl.TestCache;
+import net.shibboleth.idp.saml.impl.TestSources;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.repository.RepositorySupport;
+import net.shibboleth.utilities.java.support.velocity.VelocityEngine;
+
+import org.apache.http.HttpStatus;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.impl.SecurityEnhancedHttpClientSupport;
+import org.springframework.core.io.ClassPathResource;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link HTTPDataConnector}
+ */
+public class HTTPDataConnectorTest {
+
+    private static final String TEST_CONNECTOR_NAME = "HTTPConnector";
+    
+    private static final String TEST_URL =
+            RepositorySupport.buildHTTPSResourceURL("java-identity-provider",
+                    "idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/http/test.json");
+
+    private static final String SCRIPT_PATH = "/net/shibboleth/idp/attribute/resolver/impl/dc/http/test.js";
+
+    private HTTPDataConnector connector;
+    
+    @BeforeMethod public void setUp() throws IOException, ScriptException {
+        connector = new HTTPDataConnector();
+        connector.setId(TEST_CONNECTOR_NAME);
+        connector.setHttpClient(
+                HttpClientBuilder.create()
+                    .setSSLSocketFactory(SecurityEnhancedHttpClientSupport.buildTLSSocketFactory(false, false))
+                    .build());
+        
+        final ScriptedResponseMappingStrategy mapping =
+                ScriptedResponseMappingStrategy.resourceScript(ResourceHelper.of(new ClassPathResource(SCRIPT_PATH)));
+        mapping.setLogPrefix(TEST_CONNECTOR_NAME + ":");
+        connector.setMappingStrategy(mapping);
+    }
+    
+    @Test public void test() throws ComponentInitializationException, ResolutionException {
+        final HttpClientSecurityParameters params = new HttpClientSecurityParameters();
+        params.setTLSProtocols(Collections.singleton("TLSv1"));
+        final TemplatedURLBuilder builder = new TemplatedURLBuilder();
+        builder.setTemplateText(TEST_URL);
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.setHttpClientSecurityParameters(params);
+        builder.initialize();
+        
+        connector.setExecutableSearchBuilder(builder);
+        ((ScriptedResponseMappingStrategy) connector.getMappingStrategy()).setAcceptStatuses(Collections.singleton(HttpStatus.SC_OK));
+        ((ScriptedResponseMappingStrategy) connector.getMappingStrategy()).setAcceptTypes(Collections.singleton("application/json"));
+        connector.initialize();
+        
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        
+        final Map<String,IdPAttribute> attrs = connector.resolve(context);
+
+        Assert.assertEquals(attrs.size(), 2);
+        
+        Assert.assertEquals(attrs.get("foo").getValues().size(), 1);
+        Assert.assertEquals(attrs.get("foo").getValues().get(0).getValue(), "foo1");
+        
+        Assert.assertEquals(attrs.get("bar").getValues().size(), 2);
+        Assert.assertEquals(attrs.get("bar").getValues().get(0).getValue(), "bar1");
+        Assert.assertEquals(attrs.get("bar").getValues().get(1).getValue(), "bar2");
+    }
+
+    @Test(expectedExceptions=ResolutionException.class) public void testBadProtocol()
+            throws ComponentInitializationException, ResolutionException {
+        final HttpClientSecurityParameters params = new HttpClientSecurityParameters();
+        params.setTLSProtocols(Collections.singleton("SSLv3"));
+        final TemplatedURLBuilder builder = new TemplatedURLBuilder();
+        builder.setTemplateText(TEST_URL);
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.setHttpClientSecurityParameters(params);
+        builder.initialize();
+        
+        connector.setExecutableSearchBuilder(builder);
+        connector.initialize();
+        
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        
+        connector.resolve(context);
+    }
+
+    @Test(expectedExceptions=ResolutionException.class) public void testMissing()
+            throws ComponentInitializationException, ResolutionException {
+        final TemplatedURLBuilder builder = new TemplatedURLBuilder();
+        builder.setTemplateText("https://shibboleth.net/test.json");
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.initialize();
+        
+        connector.setExecutableSearchBuilder(builder);
+        connector.initialize();
+        
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        
+        connector.resolve(context);
+    }
+
+    @Test public void testMissingOk() throws ComponentInitializationException, ResolutionException, ScriptException {
+        final TemplatedURLBuilder builder = new TemplatedURLBuilder();
+        builder.setTemplateText("https://shibboleth.net/test.json");
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.initialize();
+                
+        connector.setExecutableSearchBuilder(builder);
+        
+        final ScriptedResponseMappingStrategy mapping = ScriptedResponseMappingStrategy.inlineScript("1");
+        mapping.setLogPrefix(TEST_CONNECTOR_NAME + ":");
+        mapping.setAcceptStatuses(Collections.singleton(HttpStatus.SC_NOT_FOUND));
+        connector.setMappingStrategy(mapping);
+        
+        connector.initialize();
+        
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        
+        final Map<String,IdPAttribute> attrs = connector.resolve(context);
+
+        Assert.assertTrue(attrs == null || attrs.isEmpty());
+    }
+    
+    @Test public void resolveWithCache() throws ComponentInitializationException, ResolutionException {
+        final TemplatedURLBuilder builder = new TemplatedURLBuilder();
+        builder.setTemplateText(TEST_URL);
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.initialize();
+        
+        connector.setExecutableSearchBuilder(builder);
+        
+        final TestCache cache = new TestCache();
+        connector.setResultsCache(cache);
+        connector.initialize();
+
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        
+        Assert.assertTrue(cache.size() == 0);
+        final Map<String,IdPAttribute> optional = connector.resolve(context);
+        Assert.assertTrue(cache.size() == 1);
+        Assert.assertEquals(cache.iterator().next(), optional);
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/http/test.js b/idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/http/test.js
new file mode 100644
index 0000000..1c1fbe2
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/http/test.js
@@ -0,0 +1,20 @@
+var EntityHelper = Java.type("org.apache.http.util.EntityUtils");
+var body = EntityHelper.toString(response.getEntity(), "UTF-8");
+var result = JSON.parse(body.toString("UTF-8"));
+
+var IdPAttribute = Java.type("net.shibboleth.idp.attribute.IdPAttribute");
+var StringValue = Java.type("net.shibboleth.idp.attribute.StringAttributeValue");
+var HashSet = Java.type("java.util.HashSet");
+
+for (var i=0; i<result.length; i++) {
+
+    var attr = new IdPAttribute(result[i].name);
+    var values = new HashSet();
+    
+    for (var j=0; j<result[i].values.length; j++) {
+        values.add(new StringValue(result[i].values[j]));
+    }
+    
+    attr.setValues(values);
+    connectorResults.add(attr);
+}

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


More information about the commits mailing list