org.pentaho.marketplace.domain.services.DiPluginService.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.marketplace.domain.services.DiPluginService.java

Source

/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program 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.
 *
 * Copyright (c) 2015 Pentaho Corporation. All rights reserved.
 */

package org.pentaho.marketplace.domain.services;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.karaf.features.FeaturesService;
import org.apache.karaf.kar.KarService;
import org.osgi.service.cm.ConfigurationAdmin;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.exception.KettlePluginException;
import org.pentaho.di.core.plugins.KettleURLClassLoader;
import org.pentaho.di.core.plugins.PluginInterface;
import org.pentaho.di.core.plugins.PluginRegistry;
import org.pentaho.di.core.plugins.PluginTypeInterface;
import org.pentaho.di.core.util.StringUtil;
import org.pentaho.di.version.BuildVersion;
import org.pentaho.marketplace.domain.model.entities.MarketEntryType;
import org.pentaho.marketplace.domain.model.entities.interfaces.IPlugin;
import org.pentaho.marketplace.domain.model.entities.interfaces.IPluginVersion;
import org.pentaho.marketplace.domain.model.factories.interfaces.IDomainStatusMessageFactory;
import org.pentaho.marketplace.domain.model.factories.interfaces.IPluginVersionFactory;
import org.pentaho.marketplace.domain.model.factories.interfaces.IVersionDataFactory;
import org.pentaho.marketplace.domain.services.interfaces.IRemotePluginProvider;
import org.pentaho.marketplace.util.web.HttpUtil;
import org.pentaho.telemetry.ITelemetryService;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

public class DiPluginService extends BasePluginService {

    // region Properties
    private static final String BASE_PLUGINS_FOLDER_NAME = "plugins";

    // TODO turn into explicit dependency
    private PluginRegistry getPluginRegistry() {
        return PluginRegistry.getInstance();
    }

    private BuildVersion getBuildVersion() {
        return BuildVersion.getInstance();
    }

    @Override
    protected String getServerVersion() {
        String version = super.getServerVersion();
        if (StringUtil.isEmpty(version)) {
            version = this.getBuildVersion().getVersion();
            this.setServerVersion(version);
        }

        return version;
    }
    // endregion

    // region Constructor
    public DiPluginService(IRemotePluginProvider metadataPluginsProvider, IVersionDataFactory versionDataFactory,
            IPluginVersionFactory pluginVersionFactory, KarService karService, FeaturesService featuresService,
            ConfigurationAdmin configurationAdmin, IDomainStatusMessageFactory domainStatusMessageFactory,
            ITelemetryService telemetryService) {
        super(metadataPluginsProvider, versionDataFactory, pluginVersionFactory, karService, featuresService,
                configurationAdmin, telemetryService, domainStatusMessageFactory);
    }
    // endregion

    @Override
    protected boolean hasMarketplacePermission() {
        return true;
    }

    @Override
    protected void unloadPlugin(IPlugin plugin) {
        String parentFolderName = this.buildPluginsFolderPath(plugin);
        File pluginFolder = new File(parentFolderName + File.separator + plugin.getId());

        PluginRegistry pluginRegistry = this.getPluginRegistry();
        List<PluginInterface> spoonPlugins;
        try {
            // get all spoon plugins provided by the marketplace plugin
            spoonPlugins = pluginRegistry.findPluginsByFolder(pluginFolder.toURI().toURL());
        } catch (MalformedURLException malformedUrlException) {
            this.getLogger().error("Malformed url from folder of plugin to uninstall: " + plugin.getId(),
                    malformedUrlException);
            return;
        }

        for (PluginInterface spoonPlugin : spoonPlugins) {
            // unload plugin
            try {
                ClassLoader cl = pluginRegistry.getClassLoader(spoonPlugin);
                if (cl instanceof KettleURLClassLoader) {
                    ((KettleURLClassLoader) cl).closeClassLoader();
                }
            } catch (KettlePluginException e) {
                this.getLogger().debug("Unable to get classloader for plugin ");
            }
            pluginRegistry.removePlugin(spoonPlugin.getPluginType(), spoonPlugin);
        }
    }

    @Override
    protected IPluginVersion getInstalledNonOsgiPluginVersion(IPlugin plugin) {

        // if plugin folder exists then non osgi plugin exists
        IPluginVersion pluginVersion = this.getPluginVersionFactory().create();
        pluginVersion.setIsOsgi(false);

        String pluginFolder = buildPluginsFolderPath(plugin) + File.separator + plugin.getId();
        File pluginFolderFile = new File(pluginFolder);

        if (!pluginFolderFile.exists()) {
            this.getLogger().debug(
                    "Plugin " + plugin.getId() + " not found at expected folder " + pluginFolderFile.getPath());

            pluginFolder = BASE_PLUGINS_FOLDER_NAME + File.separator + plugin.getId();
            pluginFolderFile = new File(pluginFolder);
            if (!pluginFolderFile.exists()) {
                this.getLogger().debug(
                        "Plugin " + plugin.getId() + " not found at expected folder " + pluginFolderFile.getPath());
                return null;
            }
        }

        String versionPath = pluginFolder + File.separator + "version.xml";
        File file = new File(versionPath);
        if (!file.exists()) {
            return pluginVersion;
        }

        FileReader reader = null;
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbf.newDocumentBuilder();
            reader = new FileReader(versionPath);
            Document dom = db.parse(new InputSource(reader));
            NodeList versionElements = dom.getElementsByTagName("version");
            if (versionElements.getLength() >= 1) {
                Element versionElement = (Element) versionElements.item(0);

                pluginVersion.setBuildId(versionElement.getAttribute("buildId"));
                pluginVersion.setBranch(versionElement.getAttribute("branch"));
                pluginVersion.setVersion(versionElement.getTextContent());
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return pluginVersion;
    }

    @Override
    protected Collection<String> getInstalledNonOsgiPluginIds() {
        // get ids of Non OSGi plugins
        Collection<String> pluginIds = this.getInstalledPluginIdsFromFolders();

        return pluginIds;
    }

    /***
     * Goes to every folder where market entries may be installed and assumes each sub-folder is a market entry id
     * @return
     */
    private Collection<String> getInstalledPluginIdsFromFolders() {
        Collection<String> pluginIds = new HashSet<>();

        for (MarketEntryType type : MarketEntryType.values()) {
            String pluginTypeFolderName = this.getInstallationSubfolder(type);
            pluginTypeFolderName = BASE_PLUGINS_FOLDER_NAME
                    + (pluginTypeFolderName == null ? "" : Const.FILE_SEPARATOR + pluginTypeFolderName);
            File pluginTypeFolder = new File(pluginTypeFolderName);
            File[] files = pluginTypeFolder.listFiles();
            if (files != null) {
                for (File file : files) {
                    if (file.isDirectory()) {
                        String folderNamePotentialPluginId = file.getName();
                        pluginIds.add(folderNamePotentialPluginId);
                    }
                }
            }
        }

        return pluginIds;
    }

    @Override
    protected boolean executeNonOsgiInstall(IPlugin plugin, IPluginVersion version) {
        String parentFolderName = buildPluginsFolderPath(plugin);

        // Until plugin dependencies are implemented, check that the pentaho-big-data-plugin directory exists
        // before installing anything of type HadoopShim
        if (plugin.getType().equals(MarketEntryType.HadoopShim)) {

            File bdPluginFolder = new File(parentFolderName).getParentFile();
            if (bdPluginFolder == null || !bdPluginFolder.exists()) {
                return false;
            }
        }

        File pluginFolder = new File(parentFolderName + File.separator + plugin.getId());
        this.getLogger().info("Installing plugin in folder: " + pluginFolder.getAbsolutePath());

        try {

            if (pluginFolder.exists()) {
                deleteDirectory(pluginFolder);
            }
            unzipMarketEntry(parentFolderName, version.getDownloadUrl());
            createVersionXML(plugin, version);
        } catch (KettleException e) {
            this.getLogger().error("ERROR on delete or create", e);
            return false;
        }

        return true;
    }

    @Override
    protected boolean executeNonOsgiUninstall(IPlugin plugin) {
        String parentFolderName = buildPluginsFolderPath(plugin);
        File pluginFolder = new File(parentFolderName + File.separator + plugin.getId());
        this.getLogger().info("Uninstalling plugin in folder: " + pluginFolder.getAbsolutePath());

        if (!pluginFolder.exists()) {
            // try plugins/plugin-id
            File rootPluginFolder = new File(BASE_PLUGINS_FOLDER_NAME + File.separator + plugin.getId());
            if (!rootPluginFolder.exists()) {
                this.getLogger()
                        .error("No plugin was found in the expected folder : " + pluginFolder.getAbsolutePath());
                return false;
            }
            pluginFolder = rootPluginFolder;
        }

        // delete plugin folder
        try {
            deleteDirectory(pluginFolder);
        } catch (KettleException exception) {
            this.getLogger().error("Error deleting plugin folder on uninstall of plugin " + plugin.getId());
            return false;
        }

        return true;
    }

    @Override
    public Map<String, IPlugin> getPlugins() {
        Map<String, IPlugin> plugins = super.getPlugins();

        // remove non PDI plugins
        CollectionUtils.filter(plugins.entrySet(), new Predicate() {
            @SuppressWarnings("unchecked")
            @Override
            public boolean evaluate(Object mapEntry) {
                Map.Entry<String, IPlugin> mapEntryCasted = (Map.Entry<String, IPlugin>) mapEntry;
                return mapEntryCasted.getValue().getType() != MarketEntryType.Platform;
            }
        });

        return plugins;
    }

    /**
     * Builds and returns the path to the plugins folder.
     *
     * @param marketEntry
     * @return String the path to the plugins folder.
     */
    public String buildPluginsFolderPath(final IPlugin marketEntry) {
        PluginInterface plugin = getPluginObject(marketEntry.getId());
        if (plugin != null && plugin.getPluginDirectory() != null) {
            return new File(plugin.getPluginDirectory().getFile()).getParent();
        } else {
            String subfolder = getInstallationSubfolder(marketEntry.getType());

            // Use current directory (should be the Kettle distribution directory) as the root folder to install plugins
            // This is because plugin types are not guaranteed to search the ~/.kettle folder for plugins.
            return BASE_PLUGINS_FOLDER_NAME + (subfolder == null ? "" : Const.FILE_SEPARATOR + subfolder);
        }
    }

    /**
     * Find the plugin object related to a pluginId.
     *
     * @param pluginId
     *          id of plugin
     * @return plugin object
     */
    private PluginInterface getPluginObject(String pluginId) {
        PluginRegistry pluginRegistry = this.getPluginRegistry();
        for (Class<? extends PluginTypeInterface> pluginType : pluginRegistry.getPluginTypes()) {
            if (pluginRegistry.findPluginWithId(pluginType, pluginId) != null) {
                return pluginRegistry.findPluginWithId(pluginType, pluginId);
            }
        }
        return null;
    }

    /**
     * Returns the folder name for the MarketEntries type.
     *
     * @param marketEntryType
     * @return
     */
    public String getInstallationSubfolder(MarketEntryType marketEntryType) {
        String subfolder;
        switch (marketEntryType) {
        case Step:
            subfolder = "steps";
            break;
        case JobEntry:
            subfolder = "jobentries";
            break;
        case Partitioner:
            subfolder = "steps";
            break;
        case SpoonPlugin:
            subfolder = "spoon";
            break;
        case Database:
            subfolder = "databases";
            break;
        /*
        case Repository:
          subfolder = "repositories";
          break;
          */
        case HadoopShim:
            subfolder = "pentaho-big-data-plugin" + File.separator + "hadoop-configurations";
            break;
        /*
        case General:
        */
        case Mixed:
            subfolder = "";
            break;

        default:
            subfolder = null;
        }
        return subfolder;
    }

    /**
     * This is a copy of method. That method works fine if the plugin is used in the same version it is built from. When
     * the plugin was dropped into PDI 4.2.1 then an invocation target exception was thrown when invoking
     * JarfileGenerator.deleteDirectory().
     *
     * I placed the method here even though the cause of the exception is not that obvious. The
     * JarfileGenerator.deleteDirectory method has not changed since 4.2.1.
     *
     * @param dir
     */
    private static void deleteDirectory(File dir) throws KettleException {
        if (dir != null) {
            File[] files = dir.listFiles();
            if (files != null) {
                for (int i = 0; i < files.length; i++) {
                    if (files[i].isDirectory()) {
                        deleteDirectory(files[i]);
                    } else if (!files[i].delete()) {
                        throw new KettleException("Failed to delete " + files[i]);
                    }
                }
            }
            if (!dir.delete()) {
                throw new KettleException("Failed to delete directory " + dir);
            }
        }
    }

    private void createVersionXML(IPlugin marketEntry, IPluginVersion version) throws KettleException {
        String pluginFolder = buildPluginsFolderPath(marketEntry) + File.separator + marketEntry.getId();
        String versionPath = pluginFolder + File.separator + "version.xml";
        File parentFolder = new File(pluginFolder);
        File file = new File(versionPath);
        if (file != null) {
            BufferedWriter bufferedWriter = null;
            try {
                if (!parentFolder.exists()) {
                    parentFolder.mkdirs();
                }

                FileWriter fw = new FileWriter(file.getAbsoluteFile());
                bufferedWriter = new BufferedWriter(fw);
                bufferedWriter.write("<version " + buildAttribute("branch", version.getBranch()) + " "
                        + buildAttribute("buildId", version.getBuildId()) + ">" + version.getVersion()
                        + "</version>");
            } catch (IOException ioe) {
                throw new KettleException(ioe);
            } finally {
                if (bufferedWriter != null) {
                    try {
                        bufferedWriter.close();
                    } catch (IOException ioe) {
                        throw new KettleException(ioe);
                    }
                }
            }
        }
    }

    private static String buildAttribute(String name, String value) {
        return nullOrEmpty(value) ? "" : name + "='" + value + "'";
    }

    private static boolean nullOrEmpty(String string) {
        return string == null || string.isEmpty();
    }

    /**
     * Unzips the plugin to the file system The passed MarkeyEntry has the URL of the zip file.
     * @throws KettleException
     */
    private static void unzipMarketEntry(String folderName, String packageUrl) throws KettleException {

        // Copy the file locally first
        //
        File tmpFile = null;
        InputStream inputStream = null;
        ZipInputStream zis = null;

        try {
            // using HttpUtil to handle http redirects
            InputStream urlInputStream = HttpUtil.getURLInputStream(packageUrl);
            if (urlInputStream == null) {
                throw new KettleException("Unable get file from " + packageUrl);
            }
            tmpFile = File.createTempFile("plugin", ".zip");
            org.apache.commons.io.FileUtils.copyInputStreamToFile(urlInputStream, tmpFile);

            // Read the package, extract in folder
            //
            inputStream = new FileInputStream(tmpFile);
            zis = new ZipInputStream(inputStream);
            ZipEntry zipEntry = null;
            try {
                zipEntry = zis.getNextEntry();
            } catch (IOException ioe) {
                throw new KettleException(ioe);
            }
            byte[] buffer = new byte[1024];
            int bytesRead = 0;
            FileOutputStream fos = null;

            while (zipEntry != null) {
                try {
                    File file = new File(folderName + File.separator + zipEntry.getName());

                    if (zipEntry.isDirectory()) {
                        file.mkdirs();
                    } else {
                        file.getParentFile().mkdirs();

                        fos = new FileOutputStream(file);
                        while ((bytesRead = zis.read(buffer)) != -1) {
                            fos.write(buffer, 0, bytesRead);
                        }
                    }

                    zipEntry = zis.getNextEntry();
                } catch (FileNotFoundException fnfe) {
                    throw new KettleException(fnfe);
                } catch (IOException ioe) {
                    throw new KettleException(ioe);
                } finally {
                    if (fos != null) {
                        try {
                            fos.close();
                        } catch (IOException e) {
                            // Ignore.
                        }
                    }
                }
            }
        } catch (IOException e) {
            throw new KettleException("Unable to unzip file " + packageUrl, e);
        } finally {
            if (zis != null) {
                tmpFile.delete();
                try {
                    zis.close();
                } catch (Exception e) {
                    throw new KettleException("Unable to close zip file stream (corrupt file?) of file " + tmpFile,
                            e);
                }
            }
        }

    }

}