/*
 *  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 java.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;

/**
 * A service-provider loader.
 *
 * <p>A service provider is a factory for creating all known implementations of a particular
 * class or interface {@code S}. The known implementations are read from a configuration file in
 * {@code META-INF/services/}. The file's name should match the class' binary name (such as
 * {@code java.util.Outer$Inner}).
 *
 * <p>The file format is as follows.
 * The file's character encoding must be UTF-8.
 * Whitespace is ignored, and {@code #} is used to begin a comment that continues to the
 * next newline.
 * Lines that are empty after comment removal and whitespace trimming are ignored.
 * Otherwise, each line contains the binary name of one implementation class.
 * Duplicate entries are ignored, but entries are otherwise returned in order (that is, the file
 * is treated as an ordered set).
 *
 * <p>Given these classes:
 * <pre>
 * package a.b.c;
 * public interface MyService { ... }
 * public class MyImpl1 implements MyService { ... }
 * public class MyImpl2 implements MyService { ... }
 * </pre>
 * And this configuration file (stored as {@code META-INF/services/a.b.c.MyService}):
 * <pre>
 * # Known MyService providers.
 * a.b.c.MyImpl1  # The original implementation for handling "bar"s.
 * a.b.c.MyImpl2  # A later implementation for "foo"s.
 * </pre>
 * You might use {@code ServiceProvider} something like this:
 * <pre>
 *   for (MyService service : ServiceLoader<MyService>.load(MyService.class)) {
 *     if (service.supports(o)) {
 *       return service.handle(o);
 *     }
 *   }
 * </pre>
 *
 * <p>Note that each iteration creates new instances of the various service implementations, so
 * any heavily-used code will likely want to cache the known implementations itself and reuse them.
 * Note also that the candidate classes are instantiated lazily as you call {@code next} on the
 * iterator: construction of the iterator itself does not instantiate any of the providers.
 *
 * @param <S> the service class or interface
 * @since 1.6
 */
public final class ServiceLoader<S> implements Iterable<S> {
    private final Class<S> service;
    private final ClassLoader classLoader;
    private final Set<URL> services;

    private ServiceLoader(Class<S> service, ClassLoader classLoader) {
        // It makes no sense for service to be null.
        // classLoader is null if you want the system class loader.
        if (service == null) {
            throw new NullPointerException();
        }
        this.service = service;
        this.classLoader = classLoader;
        this.services = new HashSet<URL>();
        reload();
    }

    /**
     * Invalidates the cache of known service provider class names.
     */
    public void reload() {
        internalLoad();
    }

    /**
     * Returns an iterator over all the service providers offered by this service loader.
     * Note that {@code hasNext} and {@code next} may throw if the configuration is invalid.
     *
     * <p>Each iterator will return new instances of the classes it iterates over, so callers
     * may want to cache the results of a single call to this method rather than call it
     * repeatedly.
     *
     * <p>The returned iterator does not support {@code remove}.
     */
    public Iterator<S> iterator() {
        return new ServiceIterator(this);
    }

    /**
     * Constructs a service loader. If {@code classLoader} is null, the system class loader
     * is used.
     *
     * @param service the service class or interface
     * @param classLoader the class loader
     * @return a new ServiceLoader
     */
    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader classLoader) {
        if (classLoader == null) {
            classLoader = ClassLoader.getSystemClassLoader();
        }
        return new ServiceLoader<S>(service, classLoader);
    }

    private void internalLoad() {
        services.clear();
        try {
            String name = "META-INF/services/" + service.getName();
            services.addAll(Collections.list(classLoader.getResources(name)));
        } catch (IOException e) {
            return;
        }
    }

    /**
     * Constructs a service loader, using the current thread's context class loader.
     *
     * @param service the service class or interface
     * @return a new ServiceLoader
     */
    public static <S> ServiceLoader<S> load(Class<S> service) {
        return ServiceLoader.load(service, Thread.currentThread().getContextClassLoader());
    }

    /**
     * Constructs a service loader, using the extension class loader.
     *
     * @param service the service class or interface
     * @return a new ServiceLoader
     */
    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        if (cl != null) {
            while (cl.getParent() != null) {
                cl = cl.getParent();
            }
        }
        return ServiceLoader.load(service, cl);
    }

    /**
     * Internal API to support built-in SPIs that check a system property first.
     * Returns an instance specified by a property with the class' binary name, or null if
     * no such property is set.
     * @hide
     */
    public static <S> S loadFromSystemProperty(final Class<S> service) {
        return AccessController.doPrivileged(new PrivilegedAction<S>() {
            public S run() {
                try {
                    final String className = System.getProperty(service.getName());
                    if (className != null) {
                        Class<?> c = ClassLoader.getSystemClassLoader().loadClass(className);
                        return (S) c.newInstance();
                    }
                    return null;
                } catch (Exception e) {
                    throw new Error(e);
                }
            }
        });
    }

    @Override
    public String toString() {
        return "ServiceLoader for " + service.getName();
    }

    private class ServiceIterator implements Iterator<S> {
        private final ClassLoader classLoader;
        private final Class<S> service;
        private final Set<URL> services;

        private boolean isRead = false;

        private LinkedList<String> queue = new LinkedList<String>();

        public ServiceIterator(ServiceLoader<S> sl) {
            this.classLoader = sl.classLoader;
            this.service = sl.service;
            this.services = sl.services;
        }

        public boolean hasNext() {
            if (!isRead) {
                readClass();
            }
            return (queue != null && !queue.isEmpty());
        }

        @SuppressWarnings("unchecked")
        public S next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            String className = queue.remove();
            try {
                return service.cast(classLoader.loadClass(className).newInstance());
            } catch (Exception e) {
                throw new ServiceConfigurationError("Couldn't instantiate class " + className, e);
            }
        }

        private void readClass() {
            for (URL url : services) {
                BufferedReader reader = null;
                try {
                    reader = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"));
                    String line;
                    while ((line = reader.readLine()) != null) {
                        // Strip comments and whitespace...
                        int commentStart = line.indexOf('#');
                        if (commentStart != -1) {
                            line = line.substring(0, commentStart);
                        }
                        line = line.trim();
                        // Ignore empty lines.
                        if (line.isEmpty()) {
                            continue;
                        }
                        String className = line;
                        checkValidJavaClassName(className);
                        if (!queue.contains(className)) {
                            queue.add(className);
                        }
                    }
                    isRead = true;
                } catch (Exception e) {
                    throw new ServiceConfigurationError("Couldn't read " + url, e);
                } finally {
                    try {
                        if (reader != null) {
                            reader.close();
                        }
                    } catch (IOException ex) {
                    }
                }
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

        private void checkValidJavaClassName(String className) {
            for (int i = 0; i < className.length(); ++i) {
                char ch = className.charAt(i);
                if (!Character.isJavaIdentifierPart(ch) && ch != '.') {
                    throw new ServiceConfigurationError("Bad character '" + ch + "' in class name");
                }
            }
        }
    }
}
