All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alipay.sofa.jraft.util.JRaftServiceLoader Maven / Gradle / Ivy

/*
 * 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 com.alipay.sofa.jraft.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.ServiceConfigurationError;

/**
 * A simple service-provider loading facility (SPI).
 *
 * @author jiachun.fjc
 */
public final class JRaftServiceLoader implements Iterable {

    private static final String      PREFIX    = "META-INF/services/";

    // the class or interface representing the service being loaded
    private final Class           service;

    // the class loader used to locate, load, and instantiate providers
    private final ClassLoader        loader;

    // cached providers, in instantiation order
    private LinkedHashMap providers = new LinkedHashMap<>();

    // the current lazy-lookup iterator
    private LazyIterator             lookupIterator;

    public static  JRaftServiceLoader load(final Class service) {
        return JRaftServiceLoader.load(service, Thread.currentThread().getContextClassLoader());
    }

    public static  JRaftServiceLoader load(final Class service, final ClassLoader loader) {
        return new JRaftServiceLoader<>(service, loader);
    }

    public List sort() {
        final Iterator it = iterator();
        final List sortList = new ArrayList<>();
        while (it.hasNext()) {
            sortList.add(it.next());
        }

        if (sortList.size() <= 1) {
            return sortList;
        }

        sortList.sort((o1, o2) -> {
            final SPI o1_spi = o1.getClass().getAnnotation(SPI.class);
            final SPI o2_spi = o2.getClass().getAnnotation(SPI.class);

            final int o1_priority = o1_spi == null ? 0 : o1_spi.priority();
            final int o2_priority = o2_spi == null ? 0 : o2_spi.priority();

            return -(o1_priority - o2_priority);
        });

        return sortList;
    }

    public S first() {
        final List sortList = sort();
        if (sortList.isEmpty()) {
            throw fail(this.service, "could not find any implementation for class");
        }
        return sortList.get(0);
    }

    public S find(final String implName) {
        for (final S s : this.providers.values()) {
            final SPI spi = s.getClass().getAnnotation(SPI.class);
            if (spi != null && spi.name().equalsIgnoreCase(implName)) {
                return s;
            }
        }
        while (this.lookupIterator.hasNext()) {
            final Class cls = this.lookupIterator.next();
            final SPI spi = cls.getAnnotation(SPI.class);
            if (spi != null && spi.name().equalsIgnoreCase(implName)) {
                try {
                    final S provider = this.service.cast(cls.newInstance());
                    this.providers.put(cls.getName(), provider);
                    return provider;
                } catch (Throwable x) {
                    throw fail(this.service, "provider " + cls.getName() + " could not be instantiated", x);
                }
            }
        }
        throw fail(this.service, "provider " + implName + " not found");
    }

    public void reload() {
        this.providers.clear();
        this.lookupIterator = new LazyIterator(this.service, this.loader);
    }

    private JRaftServiceLoader(final Class service, final ClassLoader loader) {
        this.service = Requires.requireNonNull(service, "service interface cannot be null");
        this.loader = (loader == null) ? ClassLoader.getSystemClassLoader() : loader;
        reload();
    }

    private static ServiceConfigurationError fail(final Class service, final String msg, final Throwable cause) {
        return new ServiceConfigurationError(service.getName() + ": " + msg, cause);
    }

    private static ServiceConfigurationError fail(final Class service, final String msg) {
        return new ServiceConfigurationError(service.getName() + ": " + msg);
    }

    private static ServiceConfigurationError fail(final Class service, final URL url, final int line,
                                                  final String msg) {
        return fail(service, url + ":" + line + ": " + msg);
    }

    // parse a single line from the given configuration file, adding the name
    // on the line to the names list.
    private int parseLine(final Class service, final URL u, final BufferedReader r, final int lc,
                          final List names) throws IOException, ServiceConfigurationError {

        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        final int ci = ln.indexOf('#');
        if (ci >= 0) {
            ln = ln.substring(0, ci);
        }
        ln = ln.trim();
        final int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) {
                throw fail(service, u, lc, "illegal configuration-file syntax");
            }
            int cp = ln.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp)) {
                throw fail(service, u, lc, "illegal provider-class name: " + ln);
            }
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) {
                    throw fail(service, u, lc, "Illegal provider-class name: " + ln);
                }
            }
            if (!this.providers.containsKey(ln) && !names.contains(ln)) {
                names.add(ln);
            }
        }
        return lc + 1;
    }

    private Iterator parse(final Class service, final URL url) {
        final ArrayList names = new ArrayList<>();
        try (final InputStream in = url.openStream();
                final BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
            int lc = 1;
            // noinspection StatementWithEmptyBody
            while ((lc = parseLine(service, url, r, lc, names)) >= 0)
                ;
        } catch (final IOException x) {
            throw fail(service, "error reading configuration file", x);
        }
        return names.iterator();
    }

    @Override
    public Iterator iterator() {
        return new Iterator() {

            final Iterator> knownProviders = JRaftServiceLoader.this.providers.entrySet()
                                                                    .iterator();

            @Override
            public boolean hasNext() {
                return this.knownProviders.hasNext() || JRaftServiceLoader.this.lookupIterator.hasNext();
            }

            @Override
            public S next() {
                if (this.knownProviders.hasNext()) {
                    return this.knownProviders.next().getValue();
                }
                final Class cls = JRaftServiceLoader.this.lookupIterator.next();
                try {
                    final S provider = JRaftServiceLoader.this.service.cast(cls.newInstance());
                    JRaftServiceLoader.this.providers.put(cls.getName(), provider);
                    return provider;
                } catch (final Throwable x) {
                    throw fail(JRaftServiceLoader.this.service, "provider " + cls.getName()
                                                                + " could not be instantiated", x);
                }
            }

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

    private class LazyIterator implements Iterator> {
        Class         service;
        ClassLoader      loader;
        Enumeration configs  = null;
        Iterator pending  = null;
        String           nextName = null;

        private LazyIterator(Class service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        @Override
        public boolean hasNext() {
            if (this.nextName != null) {
                return true;
            }
            if (this.configs == null) {
                try {
                    final String fullName = PREFIX + this.service.getName();
                    if (this.loader == null) {
                        this.configs = ClassLoader.getSystemResources(fullName);
                    } else {
                        this.configs = this.loader.getResources(fullName);
                    }
                } catch (final IOException x) {
                    throw fail(this.service, "error locating configuration files", x);
                }
            }
            while ((this.pending == null) || !this.pending.hasNext()) {
                if (!this.configs.hasMoreElements()) {
                    return false;
                }
                this.pending = parse(this.service, this.configs.nextElement());
            }
            this.nextName = this.pending.next();
            return true;
        }

        @SuppressWarnings("unchecked")
        @Override
        public Class next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            final String name = this.nextName;
            this.nextName = null;
            final Class cls;
            try {
                cls = Class.forName(name, false, this.loader);
            } catch (final ClassNotFoundException x) {
                throw fail(this.service, "provider " + name + " not found");
            }
            if (!this.service.isAssignableFrom(cls)) {
                throw fail(this.service, "provider " + name + " not a subtype");
            }
            return (Class) cls;
        }

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

    /**
     * Returns a string describing this service.
     */
    @Override
    public String toString() {
        return "com.alipay.sofa.jraft.util.JRaftServiceLoader[" + this.service.getName() + "]";
    }
}