/**
 * Copyright 2007-2016, Kaazing Corporation. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kaazing.gateway.server.context.resolve;

import static org.kaazing.gateway.resource.address.uri.URIUtils.buildURIAsString;
import static org.kaazing.gateway.resource.address.uri.URIUtils.getAuthority;
import static org.kaazing.gateway.resource.address.uri.URIUtils.getCanonicalURI;
import static org.kaazing.gateway.resource.address.uri.URIUtils.getFragment;
import static org.kaazing.gateway.resource.address.uri.URIUtils.getHost;
import static org.kaazing.gateway.resource.address.uri.URIUtils.getPath;
import static org.kaazing.gateway.resource.address.uri.URIUtils.getPort;
import static org.kaazing.gateway.resource.address.uri.URIUtils.getQuery;
import static org.kaazing.gateway.resource.address.uri.URIUtils.getScheme;
import static org.kaazing.gateway.service.util.ServiceUtils.LIST_SEPARATOR;
import static org.kaazing.gateway.util.feature.EarlyAccessFeatures.LOGIN_MODULE_EXPIRING_STATE;
import static org.kaazing.gateway.util.feature.EarlyAccessFeatures.TCP_REALM_EXTENSION;

import java.io.File;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.Key;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import javax.annotation.Resource;
import javax.management.MBeanServer;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
import javax.security.auth.login.Configuration;

import org.kaazing.gateway.resource.address.ResourceAddressFactory;
import org.kaazing.gateway.security.AuthenticationContext;
import org.kaazing.gateway.security.CrossSiteConstraintContext;
import org.kaazing.gateway.security.RealmContext;
import org.kaazing.gateway.security.SecurityContext;
import org.kaazing.gateway.security.auth.BasicLoginModule;
import org.kaazing.gateway.security.auth.NegotiateLoginModule;
import org.kaazing.gateway.security.auth.TimeoutLoginModule;
import org.kaazing.gateway.server.ConfigurationObserver;
import org.kaazing.gateway.server.Gateway;
import org.kaazing.gateway.server.Launcher;
import org.kaazing.gateway.server.config.SchemeConfig;
import org.kaazing.gateway.server.config.june2016.AuthenticationType;
import org.kaazing.gateway.server.config.june2016.AuthorizationConstraintType;
import org.kaazing.gateway.server.config.june2016.ClusterConnectOptionsType;
import org.kaazing.gateway.server.config.june2016.ClusterType;
import org.kaazing.gateway.server.config.june2016.CrossSiteConstraintType;
import org.kaazing.gateway.server.config.june2016.GatewayConfigDocument;
import org.kaazing.gateway.server.config.june2016.LoginModuleOptionsType;
import org.kaazing.gateway.server.config.june2016.LoginModuleType;
import org.kaazing.gateway.server.config.june2016.MimeMappingType;
import org.kaazing.gateway.server.config.june2016.RealmType;
import org.kaazing.gateway.server.config.june2016.SecurityType;
import org.kaazing.gateway.server.config.june2016.ServiceAcceptOptionsType;
import org.kaazing.gateway.server.config.june2016.ServiceConnectOptionsType;
import org.kaazing.gateway.server.config.june2016.ServiceDefaultsType;
import org.kaazing.gateway.server.config.june2016.ServicePropertiesType;
import org.kaazing.gateway.server.config.june2016.ServiceType;
import org.kaazing.gateway.server.config.parse.DefaultSchemeConfig;
import org.kaazing.gateway.server.context.DependencyContext;
import org.kaazing.gateway.server.context.GatewayContext;
import org.kaazing.gateway.server.service.ServiceRegistry;
import org.kaazing.gateway.server.spi.security.ExpiringState;
import org.kaazing.gateway.server.spi.security.LoginModuleOptions;
import org.kaazing.gateway.service.AcceptOptionsContext;
import org.kaazing.gateway.service.ConnectOptionsContext;
import org.kaazing.gateway.service.Service;
import org.kaazing.gateway.service.ServiceContext;
import org.kaazing.gateway.service.ServiceFactory;
import org.kaazing.gateway.service.ServiceProperties;
import org.kaazing.gateway.service.cluster.ClusterConnectOptionsContext;
import org.kaazing.gateway.service.cluster.ClusterContext;
import org.kaazing.gateway.service.cluster.MemberId;
import org.kaazing.gateway.transport.BridgeServiceFactory;
import org.kaazing.gateway.transport.Transport;
import org.kaazing.gateway.transport.TransportFactory;
import org.kaazing.gateway.util.GL;
import org.kaazing.gateway.util.InternalSystemProperty;
import org.kaazing.gateway.util.Utils;
import org.kaazing.gateway.util.aws.AwsUtils;
import org.kaazing.gateway.util.scheduler.SchedulerProvider;
import org.slf4j.Logger;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.hazelcast.core.IMap;

public class GatewayContextResolver {

	public static final String AUTHORIZATION_MODE_CHALLENGE = "challenge";

    /**
     * Prefix to the authentication scheme to indicate that the Kaazing client application will handle the challenge rather than
     * delegate to the browser or the native platform.
     */
    public static final String AUTH_SCHEME_BASIC = "Basic";
    public static final String AUTH_SCHEME_NEGOTIATE = "Negotiate";
    public static final String AUTH_SCHEME_APPLICATION_TOKEN = "Application Token";

    /**
     * Prefix to the authentication scheme to indicate that the Kaazing client application will handle the challenge rather than
     * delegate to the browser or the native platform.
     */
    public static final String AUTH_SCHEME_APPLICATION_PREFIX = "Application ";

    enum HttpMethod {
        OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT
    }


    // This logger logs info and above to std out by default.
    private static final Logger LOGGER = Launcher.getGatewayStartupLogger();

    private static final String SERVICE_TYPE_CLASS_PREFIX = "class:";
    private static final String LOGIN_MODULE_TYPE_CLASS_PREFIX = "class:";

    private static final String EXPIRING_STATE_NAME = "ExpiringState";

    // a map of file-extension to mime-type.  For backward compatibility, we'll
    // hardcode this initial set based on the values in Dragonfire HttpUtils.getContentType().
    // TODO: In 4.0 we may want to remove this and require explicit settings!
    private static final Map<String, String> defaultMimeMappings = new HashMap<>();

    static {
        defaultMimeMappings.put("html", "text/html");
        defaultMimeMappings.put("htm", "text/html");
        defaultMimeMappings.put("js", "text/javascript");
        defaultMimeMappings.put("png", "image/png");
        defaultMimeMappings.put("gif", "image/gif");
        defaultMimeMappings.put("jpg", "image/jpeg");
        defaultMimeMappings.put("jpeg", "image/jpeg");
        defaultMimeMappings.put("css", "text/css");
        defaultMimeMappings.put("swf", "application/x-shockwave-flash");
        defaultMimeMappings.put("xap", "application/x-silverlight-app");
        defaultMimeMappings.put("htc", "text/x-component");
        defaultMimeMappings.put("jnlp", "application/x-java-jnlp-file");
        defaultMimeMappings.put("manifest", "text/cache-manifest");
        defaultMimeMappings.put("appcache", "text/cache-manifest");
        defaultMimeMappings.put("vtt", "text/vtt");
        defaultMimeMappings.put("aspx", "text/html");
    }

    private final Map<String, String> loginModuleClassNames;
    private final Map<String, LoginModuleControlFlag> loginModuleControlFlags;

    private final File webDir;
    private final File tempDir;
    private final MBeanServer jmxMBeanServer;
    private final Map<String, SchemeConfig> schemeConfigsByName;
    private final Map<String, DefaultTransportContext> transportContextsBySchemeName;
    private final Map<String, DefaultTransportContext> transportContextsByName;

    private ContextResolver<SecurityType, DefaultSecurityContext> securityResolver;

    private Map<String, Object> injectables = new HashMap<>();

    private ConfigurationObserver observer;
    public GatewayContextResolver(File configDir, File webDir, File tempDir) {
        this(configDir, webDir, tempDir, null);
    }

    public GatewayContextResolver(File configDir, File webDir, File tempDir, MBeanServer mbeanServer) {
        this(new SecurityContextResolver(configDir, LOGGER), webDir, tempDir, mbeanServer);
    }

    public GatewayContextResolver(ContextResolver<SecurityType, DefaultSecurityContext> securityResolver,
                                  File webDir, File tempDir) {
        this(securityResolver, webDir, tempDir, null);
    }

    public GatewayContextResolver(ContextResolver<SecurityType, DefaultSecurityContext> securityResolver,
                                  File webDir, File tempDir, MBeanServer mbeanServer) {
        this.securityResolver = securityResolver;
        this.webDir = webDir;
        this.tempDir = tempDir;
        this.jmxMBeanServer = mbeanServer;
        this.loginModuleClassNames = new HashMap<>();

        Map<String, LoginModuleControlFlag> loginModuleControlFlags = new HashMap<>();
        loginModuleControlFlags.put("optional", LoginModuleControlFlag.OPTIONAL);
        loginModuleControlFlags.put("required", LoginModuleControlFlag.REQUIRED);
        loginModuleControlFlags.put("requisite", LoginModuleControlFlag.REQUISITE);
        loginModuleControlFlags.put("sufficient", LoginModuleControlFlag.SUFFICIENT);
        this.loginModuleControlFlags = loginModuleControlFlags;

        this.schemeConfigsByName = new HashMap<>();

        this.transportContextsBySchemeName = new HashMap<>();
        this.transportContextsByName = new HashMap<>();
    }

    public GatewayContext resolve(GatewayConfigDocument gatewayConfigDoc)
            throws Exception {
        return resolve(gatewayConfigDoc, System.getProperties());
    }

    public GatewayContext resolve(GatewayConfigDocument gatewayConfigDoc, Properties configuration) throws Exception {
        GatewayConfigDocument.GatewayConfig gatewayConfig = gatewayConfigDoc.getGatewayConfig();
        Collection<? extends SchemeConfig> schemeConfigs = new LinkedList<>();
        SecurityType[] securityConfigs = gatewayConfig.getSecurityArray();
        SecurityType securityConfig = (securityConfigs.length > 0) ? securityConfigs[securityConfigs.length - 1] : null;
        ServiceType[] serviceConfigs = gatewayConfig.getServiceArray();
        ServiceDefaultsType[] serviceDefaultsArray = gatewayConfig.getServiceDefaultsArray();
        ServiceDefaultsType serviceDefaults =
                (serviceDefaultsArray.length > 0) ? serviceDefaultsArray[serviceDefaultsArray.length - 1] : null;
        ClusterType[] clusterConfigs = gatewayConfig.getClusterArray();
        ClusterType clusterConfig = (clusterConfigs.length > 0) ? clusterConfigs[clusterConfigs.length - 1] : null;


        SchedulerProvider schedulerProvider = new SchedulerProvider(configuration);
        ClusterContext clusterContext = resolveCluster(clusterConfig,configuration);
        DefaultSecurityContext securityContext = securityResolver.resolve(securityConfig);
        ExpiringState expiringState = resolveExpiringState(clusterContext);
        RealmsContext realmsContext = resolveRealms(securityConfig, securityContext, configuration, clusterContext, expiringState);
        DefaultServiceDefaultsContext serviceDefaultsContext = resolveServiceDefaults(serviceDefaults, realmsContext);
        ServiceRegistry servicesByURI = new ServiceRegistry();
        Map<String, Object> dependencyContexts = resolveDependencyContext();
        ResourceAddressFactory resourceAddressFactory = resolveResourceAddressFactories();
        TransportFactory transportFactory = TransportFactory.newTransportFactory((Map) configuration);
        ServiceFactory serviceFactory = ServiceFactory.newServiceFactory();
        Collection<ServiceContext> services =
                resolveServices(servicesByURI, webDir, tempDir, serviceConfigs, securityContext,
                        realmsContext, clusterContext, serviceDefaults, schedulerProvider,
                        dependencyContexts,
                        configuration, transportFactory, serviceFactory, resourceAddressFactory, serviceDefaults);
        resolveTransports(transportFactory);

        BridgeServiceFactory bridgeServiceFactory = resolveBridgeServiceFactory(transportFactory);

        Map<String, DefaultSchemeContext> schemeContexts =
                resolveSchemes(services,
                        schemeConfigs,
                        configuration,
                        resourceAddressFactory);

        GatewayContext gatewayContext = new DefaultGatewayContext(schemeContexts,
                transportContextsBySchemeName,
                realmsContext,
                serviceDefaultsContext,
                services,
                servicesByURI,
                webDir,
                tempDir,
                clusterContext,
                schedulerProvider);

        injectables.putAll(dependencyContexts);
        injectables.put("serviceRegistry", servicesByURI);
        injectables.put("realmsContext", realmsContext);
        injectables.put("tempDirectory", tempDir);
        injectables.put("securityContext", securityContext);
        injectables.put("clusterContext", clusterContext);
        injectables.put("gatewayContext", gatewayContext);
        injectables.put("schedulerProvider", schedulerProvider);
        injectables.put("configuration", configuration);
        injectables.put("mbeanServer", jmxMBeanServer);
        injectables.put("bridgeServiceFactory", bridgeServiceFactory);
        injectables.put("resourceAddressFactory", resourceAddressFactory);
        injectables.put("transportFactory", transportFactory);
        injectables.put("expiringState", expiringState);
        gatewayContext.getInjectables().putAll(injectables);

        injectResources(services,
                    bridgeServiceFactory,
                    dependencyContexts,
                    injectables);

        return gatewayContext;
    }

    private BridgeServiceFactory resolveBridgeServiceFactory(TransportFactory transportFactory) {
        return new BridgeServiceFactory(transportFactory);
    }

    private ResourceAddressFactory resolveResourceAddressFactories() {
        return ResourceAddressFactory.newResourceAddressFactory();
    }

    private Map<String, DefaultSchemeContext> resolveSchemes(Collection<? extends ServiceContext> serviceContexts,
                                                             Collection<? extends SchemeConfig> schemeConfigs,
                                                             Properties configuration,
                                                             ResourceAddressFactory resourceAddressFactory)
            throws Exception {

        // load the default scheme information based on service accepts
        Set<String> schemeNames = new HashSet<>();
        for (ServiceContext serviceContext : serviceContexts) {
            for (String acceptURI : serviceContext.getAccepts()) {
                String schemeName = getScheme(acceptURI);
                schemeNames.add(schemeName);
            }
            for (String connectURI : serviceContext.getConnects()) {
                String schemeName = getScheme(connectURI);
                schemeNames.add(schemeName);
            }
            ServiceProperties properties = serviceContext.getProperties();
            String accept = properties.get("accept");
            if (accept != null) {
                accept = accept.trim();
                schemeNames.add(new URI(accept).getScheme());
            }
            String connect = properties.get("connect");
            if (connect != null) {
                connect = connect.trim();
                schemeNames.add(new URI(connect).getScheme());
            }
        }

        // Make sure that presence of ws(s) automatically implies presence of http(s) even if there are no accepts or connects
        // with that scheme.
        if (schemeNames.contains("ws")) {
            schemeNames.add("http");
            schemeNames.add("httpx");
            schemeNames.add("httpxe");
            schemeNames.add("wsn");
            schemeNames.add("wsx");
        }
        if (schemeNames.contains("wss")) {
            schemeNames.add("https");
            schemeNames.add("httpx+ssl");
            schemeNames.add("httpxe+ssl");
            schemeNames.add("wsn+ssl");
            schemeNames.add("wsx+ssl");
        }

        // SSE Schemes
        if (schemeNames.contains("sse")) {
            schemeNames.add("httpxe");
        }
        if (schemeNames.contains("sse+ssl")) {
            schemeNames.add("httpxe+ssl");
        }

        // Always add tcp
        schemeNames.add("tcp");

        // Always add udp
        schemeNames.add("udp");

        // override default scheme configuration
        for (SchemeConfig schemeConfig : schemeConfigs) {
            String schemeName = schemeConfig.getName();
            schemeConfigsByName.put(schemeName, schemeConfig);
            schemeNames.add(schemeName);
        }

        // resolve schemes
        Map<String, DefaultSchemeContext> schemeContexts = new HashMap<>();
        for (String schemeName : schemeNames) {
            DefaultSchemeContext schemeContext = schemeContexts.get(schemeName);
            if (schemeContext == null) {
                SchemeConfig schemeConfig = supplySchemeConfig(schemeName);

                int defaultPort = schemeConfig.getDefaultPort();
                String transportName = schemeConfig.getTransportName();
                DefaultTransportContext transportContext = transportContextsByName.get(transportName);
                if (transportContext == null) {
                    throw new IllegalArgumentException("Missing transport \"" + transportName + "\"");
                }

                schemeContext = new DefaultSchemeContext(schemeName, defaultPort, resourceAddressFactory);
                schemeContexts.put(schemeName, schemeContext);
                transportContextsBySchemeName.put(schemeName, transportContext);
            }
        }

        return schemeContexts;
    }

    private SchemeConfig supplySchemeConfig(String schemeName) {
        SchemeConfig schemeConfig = schemeConfigsByName.get(schemeName);
        if (schemeConfig == null) {
            if (schemeName.contains("tls")) {
                schemeName = schemeName.replace("tls", "ssl");
            }
            schemeConfig = findSchemeConfig(schemeName);
            if (schemeConfig == null) {
                throw new IllegalArgumentException("Missing scheme \"" + schemeName + "\"");
            }
            if (schemeConfig != null) {
                schemeConfigsByName.put(schemeName, schemeConfig);
            }
        }
        return schemeConfig;
    }

    // Resolve service defaults into a config object so we can expose it as its
    // own object to management.
    private DefaultServiceDefaultsContext resolveServiceDefaults(ServiceDefaultsType serviceDefaults, RealmsContext realmsContext) {

        if (serviceDefaults == null) {
            return null;
        }

        DefaultAcceptOptionsContext acceptOptions = null;

        ServiceAcceptOptionsType serviceAcceptOptions = serviceDefaults.getAcceptOptions();
        if (serviceAcceptOptions != null) {
            acceptOptions = new DefaultAcceptOptionsContext(null, serviceAcceptOptions);
        }

        DefaultConnectOptionsContext connectOptions = null;
        ServiceConnectOptionsType serviceConnectOptions = serviceDefaults.getConnectOptions();
        if (serviceConnectOptions != null) {
            connectOptions = new DefaultConnectOptionsContext(null, serviceConnectOptions);
        }

        Map<String, String> mimeMappings = null;

        MimeMappingType[] mimeMappingTypes = serviceDefaults.getMimeMappingArray();
        if (mimeMappingTypes != null) {
            mimeMappings = new HashMap<>();
            for (MimeMappingType mmt : mimeMappingTypes) {
                mimeMappings.put(mmt.getExtension(), mmt.getMimeType());
            }
        }

        return new DefaultServiceDefaultsContext(acceptOptions, connectOptions, mimeMappings);
    }

    @SuppressWarnings("unchecked")
    private Collection<ServiceContext> resolveServices(ServiceRegistry serviceRegistry,
                                                       File webDir,
                                                       File tempDir,
                                                       ServiceType[] serviceConfigs,
                                                       SecurityContext securityContext,
                                                       RealmsContext realmsContext,
                                                       ClusterContext clusterContext,
                                                       ServiceDefaultsType defaultServiceConfig,
                                                       SchedulerProvider schedulerProvider,
                                                       Map<String, Object> dependencyContexts,
                                                       Properties configuration,
                                                       TransportFactory transportFactory,
                                                       ServiceFactory serviceFactory,
                                                       ResourceAddressFactory resourceAddressFactory,
                                                       ServiceDefaultsType serviceDefaults)
            throws Exception {

//        Map<String, Class<? extends Service>> serviceClasses = new HashMap<String, Class<? extends Service>>();
        Collection<ServiceContext> serviceContexts = new HashSet<>();

        // The list of mime mappings for a given service is a combination of the defaults we hardcoded,
        // any <mime-mapping> blocks from <service-defaults>, and any from <service>.
        Map<String, String> serviceDefaultsMimeMappings = new HashMap<>();
        serviceDefaultsMimeMappings.putAll(defaultMimeMappings);

        if (defaultServiceConfig != null) {
            // Note that we add the extensions as lower case for search consistency.
            for (MimeMappingType mimeMappingType : defaultServiceConfig.getMimeMappingArray()) {
                serviceDefaultsMimeMappings.put(mimeMappingType.getExtension().toLowerCase(), mimeMappingType.getMimeType());
            }
        }

        // Used by client access policy xml. This parameter is not fully initialized until after the service c
        List<Map<String, Map<String, CrossSiteConstraintContext>>> authorityToSetOfAcceptConstraintsByURI =
                new ArrayList<>();

        for (ServiceType serviceConfig : serviceConfigs) {
            String serviceName = serviceConfig.getName();
            String serviceDescription = serviceConfig.getDescription();
            String[] acceptStrings = serviceConfig.getAcceptArray();
            String[] balanceStrings = serviceConfig.getBalanceArray();
            String[] connectStrings = serviceConfig.getConnectArray();
            String serviceType = serviceConfig.getType();
            Service serviceInstance;

            Class<? extends Service> serviceClass;
            if (serviceType.startsWith(SERVICE_TYPE_CLASS_PREFIX)) {
                String className = serviceType.substring(SERVICE_TYPE_CLASS_PREFIX.length());
                try {
                    Class<?> clazz = Class.forName(className);
                    if (!Service.class.isAssignableFrom(clazz)) {
                        throw new IllegalArgumentException("Incompatible gateway service class: " + className);
                    }
                    serviceClass = (Class<? extends Service>) clazz;
                    serviceInstance = serviceClass.newInstance();
                } catch (ClassNotFoundException e) {
                    throw new IllegalArgumentException("Unknown gateway service class: " + className);
                }
            } else {
                serviceInstance = serviceFactory.newService(serviceType);
                if (serviceInstance == null) {
                    throw new IllegalArgumentException("Unrecognized service type: " + serviceType);
                }
            }

            ServicePropertiesType propertiesType = serviceConfig.getProperties();
            DefaultServiceProperties properties = parsePropertiesType(propertiesType);

            // default ports
            Collection<String> acceptURIs = resolveURIs(acceptStrings);
            Collection<String> balanceURIs = resolveURIs(balanceStrings);
            Collection<String> connectURIs = resolveURIs(connectStrings);

            String acceptProperty = properties.get("accept");
            if (acceptProperty != null) {
                acceptProperty = acceptProperty.trim();
                acceptProperty = resolveURI(getCanonicalURI(acceptProperty, false));
                properties.put("accept", acceptProperty);
            }

            String connectProperty = properties.get("connect");
            if (connectProperty != null) {
                connectProperty = connectProperty.trim();
                properties.remove("connect");
                connectURIs.add(resolveURI(getCanonicalURI(connectProperty, true)));
            }

            Collection<String> requireRolesCollection = new LinkedList<>();
            for (AuthorizationConstraintType authConstraint : serviceConfig.getAuthorizationConstraintArray()) {
                Collections.addAll(requireRolesCollection, authConstraint.getRequireRoleArray());
            }
            if (requireRolesCollection.isEmpty()) {
                requireRolesCollection.add("*");
            }
            String[] requireRoles = requireRolesCollection.toArray(new String[requireRolesCollection.size()]);

            // Add the service-specific mime mappings on top of the service-defaults+hardcoded.
            Map<String, String> mimeMappings = new HashMap<>();
            mimeMappings.putAll(serviceDefaultsMimeMappings);

            for (MimeMappingType mimeMappingType : serviceConfig.getMimeMappingArray()) {
                mimeMappings.put(mimeMappingType.getExtension().toLowerCase(), mimeMappingType.getMimeType());
            }

            Map<String, Map<String, CrossSiteConstraintContext>> acceptConstraintsByURI =
                    new HashMap<>();
            for (String acceptURI : acceptURIs) {
                int wildcardOriginCount = 0;
                CrossSiteConstraintType[] crossSiteConstraints = serviceConfig.getCrossSiteConstraintArray();
                for (CrossSiteConstraintType crossSiteConstraint : crossSiteConstraints) {
                    String allowOrigin = (String) crossSiteConstraint.getAllowOrigin();
                    String allowMethods = crossSiteConstraint.getAllowMethods();
                    String allowHeaders = crossSiteConstraint.getAllowHeaders();
                    BigInteger maximumAgeBigInt = crossSiteConstraint.getMaximumAge();
                    Integer maximumAge = (maximumAgeBigInt == null) ? null : maximumAgeBigInt.intValue();

                    if (allowOrigin == null) {
                        throw new IllegalArgumentException("Cross-site allow-origin is required");
                    }

                    if ("*".equals(allowOrigin)) {
                        wildcardOriginCount++;
                    } else {
                        String allowOriginURI = getCanonicalURI(allowOrigin, false);
                        allowOrigin = allowOriginURI;
                        String allowOriginScheme = getScheme(allowOriginURI);

                        if (!"http".equals(allowOriginScheme) && !"https".equals(allowOriginScheme)) {
                            throw new IllegalArgumentException(
                                    "Cross-site allow-origin must have URI syntax with http or https scheme");
                        }

                        if (getPath(allowOriginURI) != null && getQuery(allowOriginURI) != null
                                || getFragment(allowOriginURI) != null) {
                            throw new IllegalArgumentException(
                                    "Cross-site allow-origin must have URI syntax without path, query or fragment");
                        }

                        if (getPort(allowOriginURI) == -1) {
                            // default the port
                            if ("http".equals(allowOriginScheme)) {
                                allowOrigin += ":80";
                            } else if ("https".equals(allowOriginScheme)) {
                                allowOrigin += ":443";
                            } else {
                                throw new IllegalArgumentException("Unable to default port for scheme: \"" + allowOriginScheme
                                        + "\"");
                            }
                        }
                    }

                    if (allowMethods != null) {
                        String[] allowMethodsArray = allowMethods.split(",");
                        for (String allowMethod : allowMethodsArray) {
                            HttpMethod.valueOf(allowMethod);
                        }
                    } else {
                        // default the allow methods
                        allowMethods = "GET,POST";
                    }

                    Map<String, CrossSiteConstraintContext> acceptConstraints = acceptConstraintsByURI.get(acceptURI);
                    if (acceptConstraints == null) {
                        acceptConstraints = new HashMap<>();
                        acceptConstraintsByURI.put(acceptURI, acceptConstraints);
                    }

                    // add to authorityToSetOfAcceptConstraintsByURI
                    authorityToSetOfAcceptConstraintsByURI.add(acceptConstraintsByURI);

                    CrossSiteConstraintContext acceptConstraint = new DefaultCrossSiteConstraintContext(allowOrigin,
                            allowMethods, allowHeaders, maximumAge);
                    CrossSiteConstraintContext oldAcceptConstraint = acceptConstraints.put(allowOrigin, acceptConstraint);
                    if (oldAcceptConstraint != null) {
                        throw new IllegalArgumentException("Duplicate cross-site-constraint for service " + acceptURI
                                + " with allow-origin " + allowOrigin);
                    }
                }
                // verify that if there's a wildcard <cross-site-constraint> it is the only one
                if (wildcardOriginCount > 0 && crossSiteConstraints.length > 1) {
                    throw new IllegalArgumentException("Conflicting cross site constraints specified for service \"" + acceptURI
                            + "\". Remove the wildcard to specify more restrictive cross site constraints");
                }

                String host = getHost(acceptURI);
                if (host == null || host.isEmpty()) {
                    throw new IllegalArgumentException("Host is required for service \"" + acceptURI + "\".");
                }

                // verify that wild-card is singleton, if present
                if (requireRolesCollection.contains("*") && requireRolesCollection.size() > 1) {
                    throw new IllegalArgumentException("Conflicting security constraints specified for service \"" + acceptURI
                            + "\". Remove the wildcard to specify restricted roles");
                }
            }

            RealmContext serviceRealmContext = null;
            final String realmName = serviceConfig.getRealmName();
            if (serviceConfig.isSetRealmName()) {
