io.apiman.gateway.engine.impl.DefaultPluginRegistry.java Source code

Java tutorial

Introduction

Here is the source code for io.apiman.gateway.engine.impl.DefaultPluginRegistry.java

Source

/*
 * Copyright 2015 JBoss Inc
 *
 * Licensed 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 io.apiman.gateway.engine.impl;

import io.apiman.common.plugin.Plugin;
import io.apiman.common.plugin.PluginClassLoader;
import io.apiman.common.plugin.PluginCoordinates;
import io.apiman.common.plugin.PluginSpec;
import io.apiman.common.plugin.PluginUtils;
import io.apiman.gateway.engine.IPluginRegistry;
import io.apiman.gateway.engine.async.AsyncResultImpl;
import io.apiman.gateway.engine.async.IAsyncResult;
import io.apiman.gateway.engine.async.IAsyncResultHandler;
import io.apiman.gateway.engine.i18n.Messages;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

/**
 * A simple plugin registry that stores plugins in a temporary location.  This
 * implementation shouldn't really be used except for testing and perhaps getting
 * started with embedding the policy engine.  The reasons to not use this
 * implementation include:
 *
 * 1) not truly asynchronous (not good if embedding in a true async platform)
 * 2) stores downloaded plugins in java.io.tmp
 * 3) does not remember where it put plugins, so will re-download them often
 *
 * @author eric.wittmann@redhat.com
 */
public class DefaultPluginRegistry implements IPluginRegistry {
    private File pluginsDir;
    private final Map<PluginCoordinates, Plugin> pluginCache = new HashMap<>();
    private Map<PluginCoordinates, Throwable> errorCache = new HashMap<>();
    private Set<URI> pluginRepositories;

    /**
     * Constructor.
     */
    public DefaultPluginRegistry() {
        this(createTempPluginsDir(), PluginUtils.getDefaultMavenRepositories());
    }

    /**
     * Constructor.
     * @param configMap the configuration map
     */
    public DefaultPluginRegistry(Map<String, String> configMap) {
        this(getConfiguredPluginsDir(configMap), getConfiguredPluginRepositories(configMap));
    }

    /**
     * Constructor.
     * @param pluginsDir the plugins directory
     */
    public DefaultPluginRegistry(File pluginsDir) {
        this(pluginsDir, PluginUtils.getDefaultMavenRepositories());
    }

    /**
     * Constructor.
     * @param pluginsDir the plugins directory
     * @param pluginRepositories the plugin repositories
     */
    public DefaultPluginRegistry(File pluginsDir, Set<URI> pluginRepositories) {
        this.pluginsDir = pluginsDir;
        this.pluginRepositories = pluginRepositories;
    }

    private static final File createTempPluginsDir() {
        // TODO log a warning here
        try {
            @SuppressWarnings("nls")
            File tempDir = File.createTempFile("_apiman", "plugins");
            tempDir.delete();
            tempDir.mkdir();
            return tempDir;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static File getConfiguredPluginsDir(Map<String, String> configMap) {
        String pluginsDirPath = configMap.get("pluginsDir"); //$NON-NLS-1$
        if (pluginsDirPath != null) {
            File file = new File(pluginsDirPath).getAbsoluteFile();
            if (!file.exists()) {
                file.mkdirs();
            } else if (file.isFile()) {
                throw new RuntimeException("Invalid plugins directory: " + file.toString()); //$NON-NLS-1$
            }
            return file;
        } else {
            return createTempPluginsDir();
        }
    }

    private static Set<URI> getConfiguredPluginRepositories(Map<String, String> configMap) {
        Set<URI> rval = new HashSet<>();
        rval.addAll(PluginUtils.getDefaultMavenRepositories());
        String repositories = configMap.get("pluginRepositories"); //$NON-NLS-1$
        if (repositories != null) {
            String[] split = repositories.split(","); //$NON-NLS-1$
            for (String repository : split) {
                try {
                    String trimmedRepo = repository.trim();
                    if (!trimmedRepo.isEmpty()) {
                        if (trimmedRepo.startsWith("file:")) { //$NON-NLS-1$
                            trimmedRepo = trimmedRepo.replace('\\', '/');
                        }
                        rval.add(new URI(trimmedRepo));
                    }
                } catch (URISyntaxException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return rval;
    }

    /**
     * @see io.apiman.gateway.engine.IPluginRegistry#loadPlugin(io.apiman.common.plugin.PluginCoordinates, io.apiman.gateway.engine.async.IAsyncResultHandler)
     */
    @Override
    public Future<IAsyncResult<Plugin>> loadPlugin(final PluginCoordinates coordinates,
            final IAsyncResultHandler<Plugin> userHandler) {
        final PluginFuture future = new PluginFuture();
        final boolean isSnapshot = PluginUtils.isSnapshot(coordinates);

        // Wrap the user provided handler so we can hook into the response.  We want to cache
        // the result (regardless of whether it's a success or failure)
        final IAsyncResultHandler<Plugin> handler = (IAsyncResult<Plugin> result) -> {
            synchronized (pluginCache) {
                if (result.isError()) {
                    // Don't cache errors if the plugin is a snapshot version.
                    if (!isSnapshot) {
                        errorCache.put(coordinates, result.getError());
                    }
                } else {
                    // Make sure we *always* use whatever is in the cache.  This resolves a
                    // race condition where multiple threads could ask for the plugin at the
                    // same time, resulting in two or more threads downloading the plugin.
                    // This is OK as long as we make sure we only ever use one.
                    if (pluginCache.containsKey(coordinates)) {
                        try {
                            result.getResult().getLoader().close();
                        } catch (IOException e) {
                        }
                        result = AsyncResultImpl.create(pluginCache.get(coordinates));
                    } else {
                        pluginCache.put(coordinates, result.getResult());
                    }
                }
            }
            if (userHandler != null) {
                userHandler.handle(result);
            }
            future.setResult(result);
        };

        boolean handled = false;
        synchronized (pluginCache) {

            // First check the cache.
            if (pluginCache.containsKey(coordinates)) {
                if (isSnapshot) {
                    removeFromCache(coordinates);
                } else {
                    // Invoke the user handler directly - we know we don't need to re-cache it.
                    AsyncResultImpl<Plugin> result = AsyncResultImpl.create(pluginCache.get(coordinates));
                    if (userHandler != null) {
                        userHandler.handle(result);
                    }
                    future.setResult(result);
                    handled = true;
                }
            }

            // Check the error cache - don't keep trying again and again for a failure.
            if (!handled && errorCache.containsKey(coordinates)) {
                // Invoke the user handle directly - we know we don't need to re-cache it.
                AsyncResultImpl<Plugin> result = AsyncResultImpl.create(errorCache.get(coordinates), Plugin.class);
                if (userHandler != null) {
                    userHandler.handle(result);
                }
                future.setResult(result);
                handled = true;
            }
        }

        String pluginRelativePath = PluginUtils.getPluginRelativePath(coordinates);
        File pluginDir = new File(pluginsDir, pluginRelativePath);
        if (!pluginDir.exists()) {
            pluginDir.mkdirs();
        }
        File pluginFile = new File(pluginDir, "plugin." + coordinates.getType()); //$NON-NLS-1$

        // Next try to load it from the plugin file registry
        if (!handled && pluginFile.isFile()) {
            if (isSnapshot) {
                try {
                    FileUtils.deleteDirectory(pluginDir);
                } catch (IOException e) {
                }
            } else {
                handled = true;
                try {
                    handler.handle(AsyncResultImpl.create(readPluginFile(coordinates, pluginFile)));
                } catch (Exception error) {
                    handler.handle(AsyncResultImpl.<Plugin>create(error));
                }
            }
        }

        // Next try to load it from the user's .m2 directory (copy it into the plugin file
        // registry first though)
        if (!handled) {
            File m2Dir = PluginUtils.getUserM2Repository();
            String m2Override = System.getProperty("apiman.gateway.m2-repository-path"); //$NON-NLS-1$
            if (m2Override != null) {
                m2Dir = new File(m2Override).getAbsoluteFile();
            }
            if (m2Dir != null) {
                File artifactFile = PluginUtils.getM2Path(m2Dir, coordinates);
                if (artifactFile.isFile()) {
                    handled = true;
                    try {
                        FileUtils.copyFile(artifactFile, pluginFile);
                        handler.handle(AsyncResultImpl.create(readPluginFile(coordinates, pluginFile)));
                    } catch (Exception error) {
                        handler.handle(AsyncResultImpl.<Plugin>create(error));
                    }
                }
            }
        }

        // Last effort - try to download it from a remote maven repository.  If this fails, then
        // we have to simply report "plugin not found".
        if (!handled) {
            downloadPlugin(coordinates, (IAsyncResult<File> result) -> {
                if (result.isSuccess()) {
                    File downloadedArtifactFile = result.getResult();
                    if (downloadedArtifactFile == null || !downloadedArtifactFile.isFile()) {
                        handler.handle(AsyncResultImpl.<Plugin>create(
                                new Exception(Messages.i18n.format("DefaultPluginRegistry.PluginNotFound")))); //$NON-NLS-1$
                    } else {
                        try {
                            String pluginRelativePath1 = PluginUtils.getPluginRelativePath(coordinates);
                            File pluginDir1 = new File(pluginsDir, pluginRelativePath1);
                            if (!pluginDir1.exists()) {
                                pluginDir1.mkdirs();
                            }
                            File pluginFile1 = new File(pluginDir1, "plugin." + coordinates.getType()); //$NON-NLS-1$
                            if (!pluginFile1.exists()) {
                                FileUtils.copyFile(downloadedArtifactFile, pluginFile1);
                                FileUtils.deleteQuietly(downloadedArtifactFile);
                            } else {
                                FileUtils.deleteQuietly(downloadedArtifactFile);
                            }
                            handler.handle(AsyncResultImpl.create(readPluginFile(coordinates, pluginFile1)));
                        } catch (Exception error) {
                            handler.handle(AsyncResultImpl.<Plugin>create(error));
                        }
                    }
                } else {
                    handler.handle(AsyncResultImpl.<Plugin>create(result.getError()));
                }
            });
        }

        return future;
    }

    /**
     * @param coordinates the coordinates
     */
    private void removeFromCache(PluginCoordinates coordinates) {
        Plugin plugin = pluginCache.remove(coordinates);
        try {
            plugin.getLoader().close();
        } catch (IOException e) {
        }

    }

    /**
     * Reads the plugin into an object.  This method will fail if the plugin is not valid.
     * This could happen if the file is not a java archive, or if the plugin spec file is
     * missing from the archive, etc.
     */
    protected Plugin readPluginFile(PluginCoordinates coordinates, File pluginFile) throws Exception {
        try {
            PluginClassLoader pluginClassLoader = createPluginClassLoader(pluginFile);
            URL specFile = pluginClassLoader.getResource(PluginUtils.PLUGIN_SPEC_PATH);
            if (specFile == null) {
                throw new Exception(Messages.i18n.format("DefaultPluginRegistry.MissingPluginSpecFile", //$NON-NLS-1$
                        PluginUtils.PLUGIN_SPEC_PATH));
            } else {
                PluginSpec spec = PluginUtils.readPluginSpecFile(specFile);
                Plugin plugin = new Plugin(spec, coordinates, pluginClassLoader);
                // TODO use logger when available
                System.out.println("Read apiman plugin: " + spec); //$NON-NLS-1$
                return plugin;
            }
        } catch (Exception e) {
            throw new Exception(
                    Messages.i18n.format("DefaultPluginRegistry.InvalidPlugin", pluginFile.getAbsolutePath()), e); //$NON-NLS-1$
        }
    }

    /**
     * Creates a plugin classloader for the given plugin file.
     */
    protected PluginClassLoader createPluginClassLoader(final File pluginFile) throws IOException {
        return new PluginClassLoader(pluginFile, Thread.currentThread().getContextClassLoader()) {
            @Override
            protected File createWorkDir(File pluginArtifactFile) throws IOException {
                File workDir = new File(pluginFile.getParentFile(), ".work"); //$NON-NLS-1$
                workDir.mkdirs();
                return workDir;
            }
        };
    }

    /**
     * Downloads the plugin via its maven GAV information.  This will first look in the local
     * .m2 directory.  If the plugin is not found there, then it will try to download the
     * plugin from one of the configured remote maven repositories.
     */
    protected void downloadPlugin(final PluginCoordinates coordinates, final IAsyncResultHandler<File> handler) {
        if (pluginRepositories.isEmpty()) {
            // Didn't find it - no repositories configured!
            handler.handle(AsyncResultImpl.create((File) null));
            return;
        }

        final Iterator<URI> iterator = pluginRepositories.iterator();
        URI repoUrl = iterator.next();
        final IAsyncResultHandler<File> handler2 = new IAsyncResultHandler<File>() {
            @Override
            public void handle(IAsyncResult<File> result) {
                if (result.isSuccess() && result.getResult() == null && iterator.hasNext()) {
                    downloadFromMavenRepo(coordinates, iterator.next(), this);
                } else {
                    handler.handle(result);
                }
            }
        };
        downloadFromMavenRepo(coordinates, repoUrl, handler2);
    }

    /**
     * Tries to download the plugin from the given remote maven repository.
     */
    protected void downloadFromMavenRepo(PluginCoordinates coordinates, URI mavenRepoUrl,
            IAsyncResultHandler<File> handler) {
        String artifactSubPath = PluginUtils.getMavenPath(coordinates);
        try {
            File tempArtifactFile = File.createTempFile("_plugin", "dwn"); //$NON-NLS-1$ //$NON-NLS-2$
            URL artifactUrl = new URL(mavenRepoUrl.toURL(), artifactSubPath);
            downloadArtifactTo(artifactUrl, tempArtifactFile, handler);
        } catch (Exception e) {
            handler.handle(AsyncResultImpl.<File>create(e));
        }
    }

    /**
     * Download the artifact at the given URL and store it locally into the given
     * plugin file path.
     */
    protected void downloadArtifactTo(URL artifactUrl, File pluginFile, IAsyncResultHandler<File> handler) {
        InputStream istream = null;
        OutputStream ostream = null;
        try {
            URLConnection connection = artifactUrl.openConnection();
            connection.connect();
            if (connection instanceof HttpURLConnection) {
                HttpURLConnection httpConnection = (HttpURLConnection) connection;
                if (httpConnection.getResponseCode() != 200) {
                    handler.handle(AsyncResultImpl.create(null));
                    return;
                }
            }
            istream = connection.getInputStream();
            ostream = new FileOutputStream(pluginFile);
            IOUtils.copy(istream, ostream);
            ostream.flush();
            handler.handle(AsyncResultImpl.create(pluginFile));
        } catch (Exception e) {
            handler.handle(AsyncResultImpl.<File>create(e));
        } finally {
            IOUtils.closeQuietly(istream);
            IOUtils.closeQuietly(ostream);
        }
    }

    private static final class PluginFuture implements Future<IAsyncResult<Plugin>> {

        private IAsyncResult<Plugin> result;
        private CountDownLatch latch = new CountDownLatch(1);

        /**
         * Constructor.
         */
        public PluginFuture() {
        }

        /**
         * @see java.util.concurrent.Future#cancel(boolean)
         */
        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            return false;
        }

        /**
         * @see java.util.concurrent.Future#isCancelled()
         */
        @Override
        public boolean isCancelled() {
            return false;
        }

        /**
         * @see java.util.concurrent.Future#isDone()
         */
        @Override
        public boolean isDone() {
            return result != null;
        }

        /**
         * @see java.util.concurrent.Future#get()
         */
        @Override
        public IAsyncResult<Plugin> get() throws InterruptedException, ExecutionException {
            latch.await();
            return result;
        }

        /**
         * @see java.util.concurrent.Future#get(long, java.util.concurrent.TimeUnit)
         */
        @Override
        public IAsyncResult<Plugin> get(long timeout, TimeUnit unit)
                throws InterruptedException, ExecutionException, TimeoutException {
            if (latch.await(timeout, unit)) {
                return result;
            } else {
                throw new TimeoutException();
            }
        }

        /**
         * @param result the result to set
         */
        public void setResult(IAsyncResult<Plugin> result) {
            this.result = result;
            latch.countDown();
        }

    }

}