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

com.ofcoder.klein.spi.ExtensionLoader Maven / Gradle / Ivy

The newest version!
/*
 * 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.ofcoder.klein.spi;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The type Extension loader.
 * This is done by loading the properties file.
 *
 * @param  the type parameter
 * @see ExtensionLoader
 */
@SuppressWarnings("all")
public final class ExtensionLoader {

    private static final Logger LOG = LoggerFactory.getLogger(ExtensionLoader.class);

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

    private static final Map, ExtensionLoader> LOADERS = new ConcurrentHashMap<>();

    private final Class clazz;

    private final ClassLoader classLoader;

    private final Holder> cachedClasses = new Holder<>();

    private final Map> cachedInstances = new ConcurrentHashMap<>();

    private final Map, Object> joinInstances = new ConcurrentHashMap<>();

    private String cachedDefaultName;
    private AtomicReference globalName = new AtomicReference<>(null);

    private final Comparator> holderComparator = (o1, o2) -> {
        if (o1.getOrder() > o2.getOrder()) {
            return 1;
        } else if (o1.getOrder() < o2.getOrder()) {
            return -1;
        } else {
            return 0;
        }
    };

    private final Comparator classEntityComparator = (o1, o2) -> {
        if (o1.getOrder() > o2.getOrder()) {
            return 1;
        } else if (o1.getOrder() < o2.getOrder()) {
            return -1;
        } else {
            return 0;
        }
    };

    /**
     * Instantiates a new Extension loader.
     *
     * @param clazz the clazz.
     */
    private ExtensionLoader(final Class clazz, final ClassLoader cl) {
        this.clazz = clazz;
        this.classLoader = cl;
        if (!Objects.equals(clazz, ExtensionFactory.class)) {
            ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getExtensionClassesEntity();
        }
    }

    /**
     * Gets extension loader.
     *
     * @param    the type parameter
     * @param clazz the clazz
     * @param cl    the cl
     * @return the extension loader.
     */
    public static  ExtensionLoader getExtensionLoader(final Class clazz, final ClassLoader cl) {

        Objects.requireNonNull(clazz, "extension clazz is null");

        if (!clazz.isInterface()) {
            throw new IllegalArgumentException("extension clazz (" + clazz + ") is not interface!");
        }
        if (!clazz.isAnnotationPresent(SPI.class)) {
            throw new IllegalArgumentException("extension clazz (" + clazz + ") without @" + SPI.class + " Annotation");
        }
        ExtensionLoader extensionLoader = (ExtensionLoader) LOADERS.get(clazz);
        if (Objects.nonNull(extensionLoader)) {
            return extensionLoader;
        }
        LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz, cl));
        return (ExtensionLoader) LOADERS.get(clazz);
    }

    /**
     * Gets extension loader.
     *
     * @param    the type parameter
     * @param clazz the clazz
     * @return the extension loader
     */
    public static  ExtensionLoader getExtensionLoader(final Class clazz) {
        return getExtensionLoader(clazz, ExtensionLoader.class.getClassLoader());
    }

    /**
     * Get default join.
     *
     * @return the default join.
     */
    public T getDefaultJoin() {
        getExtensionClassesEntity();
        if (StringUtils.isBlank(cachedDefaultName)) {
            return null;
        }
        return getJoin(cachedDefaultName);
    }

    /**
     * get join use global.
     *
     * @param name join name
     * @param args construction parameters
     * @return join
     */
    public T register(final String name, final Object... args) {
        globalName.compareAndSet(null, name);

        Holder objectHolder = cachedInstances.get(name);
        if (Objects.isNull(objectHolder)) {
            cachedInstances.putIfAbsent(name, new Holder<>());
            objectHolder = cachedInstances.get(name);
        }
        Object value = objectHolder.getValue();
        if (Objects.isNull(value)) {
            synchronized (cachedInstances) {
                value = objectHolder.getValue();
                if (Objects.isNull(value)) {
                    Holder pair = createExtension(name, args);
                    value = pair.getValue();
                    int order = pair.getOrder();
                    objectHolder.setValue(value);
                    objectHolder.setOrder(order);
                }
            }
        }
        return (T) value;
    }

    /**
     * Get global join.
     *
     * @return the default join.
     */
    public T getJoin() {
        return getJoin(globalName.get());
    }

    /**
     * Get join.
     *
     * @param name the name
     * @return the join.
     */
    public T getJoin(final String name) {
        if (StringUtils.isBlank(name)) {
            throw new NullPointerException("get join name is null");
        }
        Holder objectHolder = cachedInstances.get(name);
        if (Objects.isNull(objectHolder)) {
            throw new RuntimeException("the join not register, name is " + name);
        }
        Object value = objectHolder.getValue();
        if (Objects.isNull(value)) {
            throw new RuntimeException("the join is registing, name is " + name);
        }
        return (T) value;
    }

    /**
     * get all join spi.
     *
     * @return list. joins
     */
    public List getJoins() {
        Map extensionClassesEntity = this.getExtensionClassesEntity();
        if (extensionClassesEntity.isEmpty()) {
            return Collections.emptyList();
        }
        if (Objects.equals(extensionClassesEntity.size(), cachedInstances.size())) {
            return (List) this.cachedInstances.values().stream()
                    .sorted(holderComparator)
                    .map(e -> {
                        return e.getValue();
                    }).collect(Collectors.toList());
        }
        List joins = new ArrayList<>();
        List classEntities = extensionClassesEntity.values().stream()
                .sorted(classEntityComparator).collect(Collectors.toList());
        classEntities.forEach(v -> {
            T join = this.getJoin(v.getName());
            joins.add(join);
        });
        return joins;
    }

    @SuppressWarnings("unchecked")
    private Holder createExtension(final String name, final Object... args) {
        ClassEntity classEntity = getExtensionClassesEntity().get(name);
        if (Objects.isNull(classEntity)) {
            throw new IllegalArgumentException("name is error");
        }
        Class aClass = classEntity.getClazz();
        Object o = joinInstances.get(aClass);
        if (Objects.isNull(o)) {
            try {
                Class[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
                Constructor constructor = aClass.getConstructor(paramTypes);
                joinInstances.putIfAbsent(aClass, constructor.newInstance(args));
                o = joinInstances.get(aClass);
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException
                     | NoSuchMethodException e) {
                throw new IllegalStateException("Extension instance(name: " + name + ", class: "
                        + aClass + ") could not be instantiated: " + e.getMessage(), e);

            }
        }
        Holder objectHolder = new Holder<>();
        objectHolder.setOrder(classEntity.getOrder());
        objectHolder.setValue((T) o);
        return objectHolder;
    }

    /**
     * Gets extension classes.
     *
     * @return the extension classes
     */
    public Map> getExtensionClasses1() {
        Map classes = this.getExtensionClassesEntity();
        return classes.values().stream().collect(Collectors.toMap(e -> e.getName(), x -> x.getClazz(), (a, b) -> a));
    }

    private Map getExtensionClassesEntity() {
        Map classes = cachedClasses.getValue();
        if (Objects.isNull(classes)) {
            synchronized (cachedClasses) {
                classes = cachedClasses.getValue();
                if (Objects.isNull(classes)) {
                    classes = loadExtensionClass();
                    cachedClasses.setValue(classes);
                    cachedClasses.setOrder(0);
                }
            }
        }
        return classes;
    }

    private Map loadExtensionClass() {
        SPI annotation = clazz.getAnnotation(SPI.class);
        if (Objects.nonNull(annotation)) {
            String value = annotation.value();
            if (StringUtils.isNotBlank(value)) {
                cachedDefaultName = value;
            }
        }
        Map classes = new HashMap<>(16);
        loadDirectory(classes);
        return classes;
    }

    /**
     * Load files under DIRECTORY.
     */
    private void loadDirectory(final Map classes) {
        String fileName = DIRECTORY + clazz.getName();
        try {
            Enumeration urls = Objects.nonNull(this.classLoader) ? classLoader.getResources(fileName)
                    : ClassLoader.getSystemResources(fileName);
            if (Objects.nonNull(urls)) {
                while (urls.hasMoreElements()) {
                    URL url = urls.nextElement();
                    loadResources(classes, url);
                }
            }
        } catch (IOException t) {
            LOG.error("load extension class error {}", fileName, t);
        }
    }

    private void loadResources(final Map classes, final URL url) throws IOException {
        try (InputStream inputStream = url.openStream()) {
            Properties properties = new Properties();
            properties.load(inputStream);
            properties.forEach((k, v) -> {
                String name = (String) k;
                String classPath = (String) v;
                if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
                    try {
                        loadClass(classes, name, classPath);
                    } catch (ClassNotFoundException e) {
                        throw new IllegalStateException("load extension resources error", e);
                    }
                }
            });
        } catch (IOException e) {
            throw new IllegalStateException("load extension resources error", e);
        }
    }

    private void loadClass(final Map classes,
                           final String name, final String classPath) throws ClassNotFoundException {
        Class subClass = Objects.nonNull(this.classLoader) ? Class.forName(classPath, true, this.classLoader) : Class.forName(classPath);
        if (!clazz.isAssignableFrom(subClass)) {
            throw new IllegalStateException("load extension resources error," + subClass + " subtype is not of " + clazz);
        }
        if (!subClass.isAnnotationPresent(Join.class)) {
            throw new IllegalStateException("load extension resources error," + subClass + " without @" + Join.class + " annotation");
        }
        ClassEntity oldClassEntity = classes.get(name);
        if (Objects.isNull(oldClassEntity)) {
            Join joinAnnotation = subClass.getAnnotation(Join.class);
            ClassEntity classEntity = new ClassEntity(name, joinAnnotation.order(), subClass);
            classes.put(name, classEntity);
        } else if (!Objects.equals(oldClassEntity.getClazz(), subClass)) {
            throw new IllegalStateException("load extension resources error,Duplicate class " + clazz.getName() + " name "
                    + name + " on " + oldClassEntity.getClazz().getName() + " or " + subClass.getName());
        }
    }

    /**
     * The type Holder.
     *
     * @param  the type parameter.
     */
    public static class Holder {

        private volatile T value;

        private Integer order;

        /**
         * Gets value.
         *
         * @return the value
         */
        public T getValue() {
            return value;
        }

        /**
         * Sets value.
         *
         * @param value the value
         */
        public void setValue(final T value) {
            this.value = value;
        }

        /**
         * set order.
         *
         * @param order order.
         */
        public void setOrder(final Integer order) {
            this.order = order;
        }

        /**
         * get order.
         *
         * @return order.
         */
        public Integer getOrder() {
            return order;
        }
    }

    static final class ClassEntity {

        /**
         * name.
         */
        private String name;

        /**
         * order.
         */
        private Integer order;

        /**
         * class.
         */
        private Class clazz;

        private ClassEntity(final String name, final Integer order, final Class clazz) {
            this.name = name;
            this.order = order;
            this.clazz = clazz;
        }

        /**
         * get class.
         *
         * @return class.
         */
        public Class getClazz() {
            return clazz;
        }

        /**
         * set class.
         *
         * @param clazz class.
         */
        public void setClazz(final Class clazz) {
            this.clazz = clazz;
        }

        /**
         * get name.
         *
         * @return name.
         */
        public String getName() {
            return name;
        }

        /**
         * get order.
         *
         * @return order.
         */
        public Integer getOrder() {
            return order;
        }
    }
}