/**
 * OW2 Specifications
 * Copyright (C) 2012 Bull S.A.S.
 * Contact: easybeans@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: ValidationProviderResolverImpl.java 6220 2012-04-02 08:57:39Z benoitf $
 * --------------------------------------------------------------------------
 */

package org.ow2.spec.ee.validation;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.validation.ValidationException;
import javax.validation.ValidationProviderResolver;
import javax.validation.spi.ValidationProvider;

import org.osgi.framework.Bundle;

/**
 * Provider Resolver.
 * @author Florent Benoit
 */
public class ValidationProviderResolverImpl implements ValidationProviderResolver {

    /**
     * provider Services KEY.
     */
    public static final String METAINF_SERVICES_VALIDATION_PROVIDER = "META-INF/services/javax.validation.spi.ValidationProvider";

    /**
     * List of all validation providers found / registered.
     */
    private volatile List<ValidationProvider<?>> osgiValidationProviders = null;

    /**
     * List of mapping between Provider services URL and validation Provider.
     */
    private volatile List<URL> urlsValidationProviders = null;
    
    /**
     * ClassLoaderThread validation Providers.
     */
    private volatile Map<ClassLoader, List<ValidationProvider<?>>> classLoaderProviders = null;

    /**
     * Logger.
     */
    private Logger logger = Logger.getLogger(ValidationProviderResolverImpl.class.getName());

    /**
     * Default constructor.
     */
    public ValidationProviderResolverImpl() {
        this.osgiValidationProviders = new ArrayList<ValidationProvider<?>>();
        // Do not keep strong reference on classloader
        this.classLoaderProviders = new WeakHashMap<ClassLoader, List<ValidationProvider<?>>>();
        this.urlsValidationProviders = new ArrayList<URL>();
    }

    /**
     * Clear cache of OSGi providers.
     */
    public void clearCachedProviders() {
        synchronized (osgiValidationProviders) {
            osgiValidationProviders.clear();
            urlsValidationProviders.clear();
        }
    }


    /**
     * Add a new validation provider.
     * @param validationProvider the given provider
     */
    public void addValidationProvider(URL url, ValidationProvider<?> validationProvider) {
        if (!urlsValidationProviders.contains(url)) {
            if (logger.isLoggable(Level.FINE)) {
                logger.log(Level.FINE, "Adding new provider for URL " + url + ", provider = " + validationProvider);
            }
            synchronized (osgiValidationProviders) {
                this.osgiValidationProviders.add(validationProvider);
            }
            if (url != null) {
                urlsValidationProviders.add(url);
            }
        } else {
            if (logger.isLoggable(Level.FINE)) {
                logger.log(Level.FINE, "Not adding new provider for URL " + url + ", provider = " + validationProvider + " as it already exists");
            }
        }
    }
    
    /**
     * Remove the given validation provider.
     * @param validationProvider the provider
     */
    public void removeValidationProvider(URL url, ValidationProvider<?> validationProvider) {
        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "Removinf validationProvider with URL " + url + ", provider = " + validationProvider);
        }
        synchronized (osgiValidationProviders) {
            this.osgiValidationProviders.remove(validationProvider);
        }
        if (url != null && urlsValidationProviders.contains(url)) {
            urlsValidationProviders.remove(url);
        }
    }
    

    /**
     * Returns a list of the ValidationProvider implementations available in the runtime environment.
     * @return list of the validation providers available in the environment
     */
    public List<ValidationProvider<?>> getValidationProviders() {
        List<ValidationProvider<?>> allProviders = new ArrayList<ValidationProvider<?>>();
        synchronized (osgiValidationProviders) {
            allProviders.addAll(osgiValidationProviders);
        }

        // Now, check if we have providers for the current ClassLoader
        ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();

        List<ValidationProvider<?>> currentClassLoaderProviders = classLoaderProviders.get(currentClassLoader);
        if (currentClassLoaderProviders == null) {
            // Needs to search providers for the current classloader
            currentClassLoaderProviders = searchProviders(currentClassLoader);
            classLoaderProviders.put(currentClassLoader, currentClassLoaderProviders);
        }

        // Add also providers of the classloader
        allProviders.addAll(currentClassLoaderProviders);
        return allProviders;
    }

    /**
     * Gets providers for the given classloader.
     * @param classLoader the classloader to use for the search
     * @return a list (or empty) of providers found for the given classloader
     */
    protected List<ValidationProvider<?>> searchProviders(ClassLoader classLoader) {
        List<ValidationProvider<?>> providers = new ArrayList<ValidationProvider<?>>();

        Enumeration<URL> urls = null;
        try {
            urls = classLoader.getResources(METAINF_SERVICES_VALIDATION_PROVIDER);
        } catch (IOException e) {
            throw new ValidationException("Unable to find services provider", e);
        }
        // Analyze URLs
        if (urls != null) {
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if (!urlsValidationProviders.contains(url)) {
                    ValidationProvider<?> validationProvider = getProvider(url, null, classLoader);
                    if (logger.isLoggable(Level.FINE)) {
                        logger.log(Level.FINE, "Adding a provider with URL = " + url + " and validationProvider " +  validationProvider);
                    }
                    providers.add(validationProvider);
                } else if (logger.isLoggable(Level.FINE)) {
                    logger.log(Level.FINE, "Not adding Validation Provider as already exists for URL '" + url);
                }
            }
        }
        return providers;
    }
    
    
    /**
     * Try to load from the given url the Validation Provider
     * @param url the link to the persistence provider property
     * @param bundle the bundle used to load the class if specified
     * @param classLoader the classloader used to load the class if specified
     * @return
     */
    public ValidationProvider<?> getProvider(URL url, Bundle bundle, ClassLoader classLoader) {

        URLConnection urlConnection = null;
        try {
            urlConnection = url.openConnection();
        } catch (IOException e) {
            throw new ValidationException("Cannot open connection on URL '" + url + "'.", e);
        }

        // avoid lock
        urlConnection.setDefaultUseCaches(false);
        InputStream is = null;
        try {
            is = urlConnection.getInputStream();
            Reader reader = null;
            BufferedReader bufferedReader = null;
            String validationProviderName = null;
            try {
                reader = new InputStreamReader(is);
                bufferedReader = new BufferedReader(reader);
                String line = bufferedReader.readLine();
                if (line == null) {
                    throw new ValidationException("No lines found in the file available at the URL '" + url + "'.");
                }
                // Add the provider
                validationProviderName = line.trim();
                Class<?> validationProviderClass = null;
                // Now, loads the class
                if (bundle != null) {
                    validationProviderClass = bundle.loadClass(validationProviderName);
                }
                if (classLoader != null) {
                    validationProviderClass = classLoader.loadClass(validationProviderName);
                }
                if (validationProviderClass == null) {
                    throw new ValidationException("Unable to load class '" + validationProviderName + "' without any bundle/classloader");
                }
                ValidationProvider<?> validationProvider = (ValidationProvider<?>) validationProviderClass.newInstance();
                return validationProvider;
            } catch (ClassNotFoundException e) {
                throw new ValidationException("Cannot build validationProvider for name '" + validationProviderName + "' with classLoader '" + classLoader + "'.", e);
            } catch (InstantiationException e) {
                throw new ValidationException("Cannot build validationProvider for name '" + validationProviderName + "' with classLoader '" + classLoader + "'.", e);
            } catch (IllegalAccessException e) {
                throw new ValidationException("Cannot build validationProvider for name '" + validationProviderName + "' with classLoader '" + classLoader + "'.", e);
            } finally {
                reader.close();
                bufferedReader.close();
            }
        } catch (IOException e) {
            throw new ValidationException("Cannot get InputStream on URL '" + url + "'.", e);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    logger.log(Level.WARNING, "Cannot close InputStream on URL '" + url + "'.", e);
                }
            }
        }
    }

}
