/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF 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 org.apache.xml.security.utils.resolver;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.xml.security.signature.XMLSignatureInput;
import org.apache.xml.security.utils.ClassLoaderUtils;
import org.apache.xml.security.utils.JavaUtils;
import org.apache.xml.security.utils.resolver.implementations.ResolverDirectHTTP;
import org.apache.xml.security.utils.resolver.implementations.ResolverFragment;
import org.apache.xml.security.utils.resolver.implementations.ResolverLocalFilesystem;
import org.apache.xml.security.utils.resolver.implementations.ResolverXPointer;
import org.w3c.dom.Attr;

/**
 * During reference validation, we have to retrieve resources from somewhere.
 * This is done by retrieving a Resolver. The resolver needs two arguments: The
 * URI in which the link to the new resource is defined and the baseURI of the
 * file/entity in which the URI occurs (the baseURI is the same as the SystemId).
 */
public class ResourceResolver {

    private static boolean allowUnsafeResourceResolving =
            AccessController.doPrivileged(
                    (PrivilegedAction<Boolean>) () -> Boolean.getBoolean("org.apache.xml.security.allowUnsafeResourceResolving"));

    private static final org.slf4j.Logger LOG =
        org.slf4j.LoggerFactory.getLogger(ResourceResolver.class);

    /** these are the system-wide resolvers */
    private static final List<ResourceResolver> resolverList = new ArrayList<>();

    /** Field resolverSpi */
    private final ResourceResolverSpi resolverSpi;

    /**
     * Constructor ResourceResolver
     *
     * @param resourceResolver
     */
    public ResourceResolver(ResourceResolverSpi resourceResolver) {
        this.resolverSpi = resourceResolver;
    }

    /**
     * Method getInstance
     *
     * @param uriAttr
     * @param baseURI
     * @param secureValidation
     * @return the instance
     *
     * @throws ResourceResolverException
     */
    public static final ResourceResolver getInstance(
        Attr uriAttr, String baseURI, boolean secureValidation
    ) throws ResourceResolverException {
        ResourceResolverContext context = new ResourceResolverContext(uriAttr, baseURI, secureValidation);
        return internalGetInstance(context);
    }

    private static <N> ResourceResolver internalGetInstance(ResourceResolverContext context)
            throws ResourceResolverException {
        synchronized (resolverList) {
            for (ResourceResolver resolver : resolverList) {
                ResourceResolver resolverTmp = resolver;
                if (!resolver.resolverSpi.engineIsThreadSafe()) {
                    try {
                        resolverTmp =
                            new ResourceResolver(JavaUtils.newInstanceWithEmptyConstructor(resolver.resolverSpi.getClass()));
                    } catch (InstantiationException e) {
                        throw new ResourceResolverException(e, context.uriToResolve, context.baseUri, "");
                    } catch (IllegalAccessException e) {
                        throw new ResourceResolverException(e, context.uriToResolve, context.baseUri, "");
                    }
                }

                LOG.debug("check resolvability by class {}", resolverTmp.getClass().getName());

                if (resolverTmp.canResolve(context)) {
                    // Check to see whether the Resolver is allowed
                    if (context.secureValidation
                        && (resolverTmp.resolverSpi instanceof ResolverLocalFilesystem
                            || resolverTmp.resolverSpi instanceof ResolverDirectHTTP)) {
                        Object exArgs[] = { resolverTmp.resolverSpi.getClass().getName() };
                        throw new ResourceResolverException(
                            "signature.Reference.ForbiddenResolver", exArgs, context.uriToResolve, context.baseUri
                        );
                    }
                    return resolverTmp;
                }
            }
        }

        Object exArgs[] = { context.uriToResolve != null
                ? context.uriToResolve : "null", context.baseUri };

        throw new ResourceResolverException("utils.resolver.noClass", exArgs, context.uriToResolve, context.baseUri);
    }

    /**
     * Method getInstance
     *
     * @param uri
     * @param baseURI
     * @param individualResolvers
     * @return the instance
     *
     * @throws ResourceResolverException
     */
    public static ResourceResolver getInstance(
        Attr uri, String baseURI, List<ResourceResolver> individualResolvers
    ) throws ResourceResolverException {
        return getInstance(uri, baseURI, individualResolvers, true);
    }

    /**
     * Method getInstance
     *
     * @param uri
     * @param baseURI
     * @param individualResolvers
     * @param secureValidation
     * @return the instance
     *
     * @throws ResourceResolverException
     */
    public static ResourceResolver getInstance(
        Attr uri, String baseURI, List<ResourceResolver> individualResolvers, boolean secureValidation
    ) throws ResourceResolverException {
        LOG.debug(
            "I was asked to create a ResourceResolver and got {}",
            (individualResolvers == null ? 0 : individualResolvers.size())
        );

        ResourceResolverContext context = new ResourceResolverContext(uri, baseURI, secureValidation);

        // first check the individual Resolvers
        if (individualResolvers != null) {
            for (int i = 0; i < individualResolvers.size(); i++) {
                ResourceResolver resolver = individualResolvers.get(i);

                if (resolver != null) {
                    String currentClass = resolver.resolverSpi.getClass().getName();
                    LOG.debug("check resolvability by class {}", currentClass);

                    if (resolver.canResolve(context)) {
                        return resolver;
                    }
                }
            }
        }

        return internalGetInstance(context);
    }

    /**
     * Registers a ResourceResolverSpi class. This method LOGs a warning if
     * the class cannot be registered.
     *
     * @param className the name of the ResourceResolverSpi class to be registered
     * @throws SecurityException if a security manager is installed and the
     *    caller does not have permission to register a resource resolver
     */
    @SuppressWarnings("unchecked")
    public static void register(String className) {
        JavaUtils.checkRegisterPermission();
        try {
            Class<ResourceResolverSpi> resourceResolverClass =
                (Class<ResourceResolverSpi>)
                ClassLoaderUtils.loadClass(className, ResourceResolver.class);
            register(resourceResolverClass, false);
        } catch (ClassNotFoundException e) {
            LOG.warn("Error loading resolver " + className + " disabling it");
        }
    }

    /**
     * Registers a ResourceResolverSpi class at the beginning of the provider
     * list. This method LOGs a warning if the class cannot be registered.
     *
     * @param className the name of the ResourceResolverSpi class to be registered
     * @throws SecurityException if a security manager is installed and the
     *    caller does not have permission to register a resource resolver
     */
    @SuppressWarnings("unchecked")
    public static void registerAtStart(String className) {
        JavaUtils.checkRegisterPermission();
        try {
            Class<ResourceResolverSpi> resourceResolverClass =
                (Class<ResourceResolverSpi>)
                ClassLoaderUtils.loadClass(className, ResourceResolver.class);
            register(resourceResolverClass, true);
        } catch (ClassNotFoundException e) {
            LOG.warn("Error loading resolver " + className + " disabling it");
        }
    }

    /**
     * Registers a ResourceResolverSpi class. This method LOGs a warning if the class
     * cannot be registered.
     * @param className
     * @param start
     * @throws SecurityException if a security manager is installed and the
     *    caller does not have permission to register a resource resolver
     */
    public static void register(Class<? extends ResourceResolverSpi> className, boolean start) {
        JavaUtils.checkRegisterPermission();
        try {
            ResourceResolverSpi resourceResolverSpi = JavaUtils.newInstanceWithEmptyConstructor(className);
            register(resourceResolverSpi, start);
        } catch (IllegalAccessException e) {
            LOG.warn("Error loading resolver " + className + " disabling it");
        } catch (InstantiationException e) {
            LOG.warn("Error loading resolver " + className + " disabling it");
        }
    }

    /**
     * Registers a ResourceResolverSpi instance. This method LOGs a warning if the class
     * cannot be registered.
     * @param resourceResolverSpi
     * @param start
     * @throws SecurityException if a security manager is installed and the
     *    caller does not have permission to register a resource resolver
     */
    public static void register(ResourceResolverSpi resourceResolverSpi, boolean start) {
        JavaUtils.checkRegisterPermission();
        synchronized(resolverList) {
            if (start) {
                resolverList.add(0, new ResourceResolver(resourceResolverSpi));
            } else {
                resolverList.add(new ResourceResolver(resourceResolverSpi));
            }
        }
        LOG.debug("Registered resolver: {}", resourceResolverSpi.toString());
    }

    /**
     * This method registers the default resolvers.
     */
    public static void registerDefaultResolvers() {
        synchronized(resolverList) {
            resolverList.add(new ResourceResolver(new ResolverFragment()));
            resolverList.add(new ResourceResolver(new ResolverLocalFilesystem()));
            resolverList.add(new ResourceResolver(new ResolverXPointer()));
            resolverList.add(new ResourceResolver(new ResolverDirectHTTP()));
        }
    }

    /**
     * Method resolve
     *
     * @param uri
     * @param baseURI
     * @return the resource
     *
     * @throws ResourceResolverException
     */
    public XMLSignatureInput resolve(Attr uri, String baseURI, boolean secureValidation)
        throws ResourceResolverException {
        ResourceResolverContext context = new ResourceResolverContext(uri, baseURI, secureValidation);
        return resolverSpi.engineResolveURI(context);
    }

    /**
     * Method setProperty
     *
     * @param key
     * @param value
     */
    public void setProperty(String key, String value) {
        resolverSpi.engineSetProperty(key, value);
    }

    /**
     * Method getProperty
     *
     * @param key
     * @return the value of the property
     */
    public String getProperty(String key) {
        return resolverSpi.engineGetProperty(key);
    }

    /**
     * Method addProperties
     *
     * @param properties
     */
    public void addProperties(Map<String, String> properties) {
        resolverSpi.engineAddProperies(properties);
    }

    /**
     * Method getPropertyKeys
     *
     * @return all property keys.
     */
    public String[] getPropertyKeys() {
        return resolverSpi.engineGetPropertyKeys();
    }

    /**
     * Method understandsProperty
     *
     * @param propertyToTest
     * @return true if the resolver understands the property
     */
    public boolean understandsProperty(String propertyToTest) {
        return resolverSpi.understandsProperty(propertyToTest);
    }

    public static boolean isURISafeToResolve(Attr uriAttr, String baseUri) {
        if (allowUnsafeResourceResolving) {
            return true;
        }
        String uriToResolve = uriAttr != null ? uriAttr.getValue() : null;
        if (uriToResolve != null) {
            if (uriToResolve.startsWith("file:") || uriToResolve.startsWith("http:")) {
                return false;
            }
            if (!uriToResolve.isEmpty() && uriToResolve.charAt(0) != '#' &&
                    baseUri != null && (baseUri.startsWith("file:") || baseUri.startsWith("http:"))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Method canResolve
     *
     * @param context
     * @return true if it can resolve the uri
     */
    private boolean canResolve(ResourceResolverContext context) {
        return this.resolverSpi.engineCanResolveURI(context);
    }
}
