Java tutorial
/* * 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 org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Predicate; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.karaf.features.FeaturesService; import org.apache.karaf.kar.KarService; import org.osgi.framework.Bundle; import org.osgi.service.cm.ConfigurationAdmin; import org.pentaho.di.core.Result; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.exception.KettleXMLException; import org.pentaho.di.core.logging.LogLevel; import org.pentaho.di.core.parameters.UnknownParamException; import org.pentaho.di.job.Job; import org.pentaho.di.job.JobMeta; 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.entities.serialization.IMarketplaceXmlSerializer; 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.helpers.Util; import org.pentaho.marketplace.domain.services.interfaces.IRemotePluginProvider; import org.pentaho.platform.api.engine.IApplicationContext; import org.pentaho.platform.api.engine.IPentahoSession; import org.pentaho.platform.api.engine.IPluginManager; import org.pentaho.platform.api.engine.ISecurityHelper; import org.pentaho.platform.engine.core.system.PentahoSessionHolder; import org.pentaho.platform.engine.core.system.PentahoSystem; import org.pentaho.platform.util.VersionHelper; import org.pentaho.platform.util.VersionInfo; import org.pentaho.telemetry.ITelemetryService; import org.springframework.security.Authentication; import org.springframework.security.GrantedAuthority; import org.xml.sax.InputSource; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Map; /** * Plugin service implementation for the BA server */ public class BaPluginService extends BasePluginService { //region Constants private static final String CLOSE_METHOD_NAME = "close"; private static final String PROPERTY_COLLECTION_SEPARATOR = ","; private static final String INSTALL_JOB_NAME = "download_and_install_plugin.kjb"; private static final String UNINSTALL_JOB_NAME = "uninstall_plugin.kjb"; private static final String CACHE_FOLDER = "system/plugin-cache/"; private static final String DOWNLOAD_CACHE_FOLDER = CACHE_FOLDER + "downloads/"; private static final String BACKUP_CACHE_FOLDER = CACHE_FOLDER + "backups/"; private static final String STAGING_CACHE_FOLDER = CACHE_FOLDER + "staging/"; private static final String MARKETPLACE_FOLDER = "system/marketplace"; private static final String SYSTEM_FOLDER = "system/"; private static final String PLUGIN_XML_FILE = "plugin.xml"; //endregion //region Properties public IMarketplaceXmlSerializer getXmlSerializer() { return this.xmlPluginsSerializer; } protected BaPluginService setXmlSerializer(IMarketplaceXmlSerializer serializer) { this.xmlPluginsSerializer = serializer; return this; } private IMarketplaceXmlSerializer xmlPluginsSerializer; public ISecurityHelper getSecurityHelper() { return this.securityHelper; } protected BaPluginService setSecurityHelper(ISecurityHelper securityHelper) { this.securityHelper = securityHelper; return this; } private ISecurityHelper securityHelper; @Override protected String getServerVersion() { if (super.getServerVersion() == null) { VersionInfo versionInfo = VersionHelper.getVersionInfo(PentahoSystem.class); this.setServerVersion(versionInfo.getVersionNumber()); } return super.getServerVersion(); } // TODO: see if there is a better way to encapsulate this public IPluginManager getPluginManager(IPentahoSession session) { return PentahoSystem.get(IPluginManager.class, session); } // TODO: see if there is a better way to encapsulate this protected IApplicationContext getApplicationContext() { if (this.applicationContext == null) { return PentahoSystem.getApplicationContext(); } return this.applicationContext; } protected BaPluginService setApplicationContext(IApplicationContext applicationContext) { this.applicationContext = applicationContext; return this; } private IApplicationContext applicationContext; // TODO: see if there is a better way to encapsulate this. // Probably just pass in the session in the methods that require it. protected IPentahoSession getCurrentSession() { return PentahoSessionHolder.getSession(); } protected BaPluginService setCurrentSession(IPentahoSession session) { PentahoSessionHolder.setSession(session); return this; } /** * Gets the Roles which are authorized to install / unintall plugins * @return */ public Collection<String> getAuthorizedRoles() { return this.authorizedRoles; } /** * Sets the Roles which are authorized to install / unintall plugins * @return */ public void setAuthorizedRoles(Collection<String> authorizedRoles) { if (authorizedRoles == null) { authorizedRoles = Collections.emptyList(); } this.authorizedRoles = authorizedRoles; } private Collection<String> authorizedRoles = Collections.emptyList(); /** * Sets roles which authorized to install / uninstall plugins from a string of comma separated values. * @param authorizedRolesString Comma separated string of roles ( e.g.: "roleA, roleB, roleC" ) */ public void setAuthorizedRoles(String authorizedRolesString) { this.setAuthorizedRoles(this.parseStringCollection(authorizedRolesString, PROPERTY_COLLECTION_SEPARATOR)); } /** * Gets the user names which are authorized to install / unintall plugins * @return */ public Collection<String> getAuthorizedUsernames() { return authorizedUsernames; } /** * Sets the user names which are authorized to install / unintall plugins * @return */ public void setAuthorizedUsernames(Collection<String> authorizedUsernames) { if (authorizedUsernames == null) { authorizedUsernames = Collections.emptyList(); } this.authorizedUsernames = authorizedUsernames; } private Collection<String> authorizedUsernames = Collections.emptyList(); /** * Sets the user names which authorized to install / uninstall plugins from a string of comma separated values. * @param authorizedUsernamesString Comma separated string of user names ( e.g.: "Jack, Lilly, Joe" ) */ public void setAuthorizedUsernames(String authorizedUsernamesString) { this.setAuthorizedUsernames( this.parseStringCollection(authorizedUsernamesString, PROPERTY_COLLECTION_SEPARATOR)); } /** * Gets the OSGI bundle this service belongs to * @return */ public Bundle getBundle() { return this.bundle; } /** * Sets the OSGI bundle this service belongs to * @return */ public void setBundle(Bundle bundle) { this.bundle = bundle; } private Bundle bundle; /** * Gets the relative path of the folder where the marketplace kettle transformations / jobs are stored. This path is * relative to the bundle base folder supplied by {@link Bundle#getLocation()} * * @return */ public String getRelativeKettleExecutionFolderPath() { return relativeKettleExecutionFolderPath; } /** * Sets the relative path of the folder where the marketplace kettle transformations / jobs are stored This path is * relative to the bundle base folder supplied by {@link Bundle#getLocation()} * * @param folderPath */ public void setRelativeKettleExecutionFolderPath(String folderPath) { this.relativeKettleExecutionFolderPath = folderPath; } private String relativeKettleExecutionFolderPath; /** * Gets the absolute path to the folder where the marketplace transformations / jobs are stored and executed.* * @return */ public Path getAbsoluteKettleExecutionFolderPath() { return this.getMarketplaceFolder().resolve(this.getRelativeKettleExecutionFolderPath()); } /** * Gets the path to where the kettle files are within the Bundle. This path is relative to the bundle root. * @return */ public String getAbsoluteKettleResourcesSourcePath() { return this.absoluteKettleResourcesSourcePath; } public void setAbsoluteKettleResourcesSourcePath(String path) { this.absoluteKettleResourcesSourcePath = path; } private String absoluteKettleResourcesSourcePath; protected Path getMarketplaceFolder() { String marketplacePath = this.getApplicationContext().getSolutionPath(MARKETPLACE_FOLDER); return Paths.get(marketplacePath).toAbsolutePath(); } private JobMeta getInstallJobMeta() { return this.getJobMeta(INSTALL_JOB_NAME); } private JobMeta getUninstallJobMeta() { return this.getJobMeta(UNINSTALL_JOB_NAME); } private JobMeta getJobMeta(String jobFileName) { String jobFilePath = this.getAbsoluteKettleExecutionFolderPath().resolve(jobFileName).toString(); JobMeta meta = null; try { meta = new JobMeta(jobFilePath, null); } catch (KettleXMLException e) { this.getLogger().error("Unable to create job meta from file path " + jobFilePath, e); } return meta; } //endregion //region Constructors public BaPluginService(IRemotePluginProvider metadataPluginsProvider, IVersionDataFactory versionDataFactory, IPluginVersionFactory pluginVersionFactory, KarService karService, FeaturesService featuresService, ConfigurationAdmin configurationAdmin, ITelemetryService telemetryService, IDomainStatusMessageFactory domainStatusMessageFactory, IMarketplaceXmlSerializer pluginsSerializer, ISecurityHelper securityHelper, Bundle bundle) { super(metadataPluginsProvider, versionDataFactory, pluginVersionFactory, karService, featuresService, configurationAdmin, telemetryService, domainStatusMessageFactory); //initialize dependencies this.setXmlSerializer(pluginsSerializer); this.setSecurityHelper(securityHelper); this.setBundle(bundle); } /** * Called after class is instantiated by Dependency Injector */ public void init() { this.copyKettleFilesToExecutionFolder(); } /** * Called on object destruction by Dependency Injector */ public void destroy() { this.deleteKettleFilesFromExecutionFolder(); } //endregion //region Methods @Override public Map<String, IPlugin> getPlugins() { Map<String, IPlugin> plugins = super.getPlugins(); // remove non BA plugins CollectionUtils.filter(plugins.entrySet(), new Predicate() { @Override public boolean evaluate(Object mapEntry) { Map.Entry<String, IPlugin> mapEntryCasted = (Map.Entry<String, IPlugin>) mapEntry; return mapEntryCasted.getValue().getType() == MarketEntryType.Platform; } }); return plugins; } @Override protected boolean hasMarketplacePermission() { Collection<String> authorizedRoles = this.getAuthorizedRoles(); Collection<String> authorizedUsernames = this.getAuthorizedUsernames(); if (authorizedRoles.isEmpty() && authorizedUsernames.isEmpty()) { // If it's true, we'll just check if the user is admin return this.getSecurityHelper().isPentahoAdministrator(this.getCurrentSession()); } Authentication authentication = this.getSecurityHelper().getAuthentication(this.getCurrentSession(), true); Collection<String> userRoles = this.getRoles(authentication); String userName = authentication.getName(); return authorizedUsernames.contains(userName) || CollectionUtils.containsAny(authorizedRoles, userRoles); } private Collection<String> getRoles(Authentication authentication) { Collection<String> roles = new ArrayList<>(); for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { roles.add(grantedAuthority.getAuthority()); } return roles; } @Override protected IPluginVersion getInstalledNonOsgiPluginVersion(IPlugin plugin) { String versionPath = this.getApplicationContext() .getSolutionPath("system/" + plugin.getId() + "/version.xml"); FileReader reader = null; try { File file = new File(versionPath); if (!file.exists()) { return null; } reader = new FileReader(versionPath); IPluginVersion version = this.getXmlSerializer().getInstalledVersion(new InputSource(reader)); version.setIsOsgi(false); return version; } catch (Exception e) { e.printStackTrace(); } finally { try { if (reader != null) { reader.close(); } } catch (Exception e) { // do nothing } } return null; } private boolean isLegacyPlugin(String pluginId) { String pluginConfigPath = this.getApplicationContext() .getSolutionPath(SYSTEM_FOLDER + File.separator + pluginId + File.separator + PLUGIN_XML_FILE); return (new File(pluginConfigPath).isFile()); } @Override protected Collection<String> getInstalledNonOsgiPluginIds() { Collection<String> plugins = new HashSet<>(); // search and add ids for non-OSGi legacy plugins File systemDir = new File(this.getApplicationContext().getSolutionPath(SYSTEM_FOLDER)); String[] dirs = systemDir.list(DirectoryFileFilter.INSTANCE); for (String dir : dirs) { if (isLegacyPlugin(dir) && !plugins.contains(dir)) { plugins.add(dir); } } return plugins; } @Override protected void unloadPlugin(IPlugin plugin) { String pluginId = plugin.getId(); IPluginManager pluginManager = this.getPluginManager(this.getCurrentSession()); ClassLoader cl = pluginManager.getClassLoader(pluginId); if (cl != null && cl instanceof URLClassLoader) { try { URLClassLoader cl1 = (URLClassLoader) cl; Util.closeURLClassLoader(cl1); Method closeMethod = cl1.getClass().getMethod(BaPluginService.CLOSE_METHOD_NAME); closeMethod.invoke(cl1); } catch (Throwable e) { if (e instanceof NoSuchMethodException) { logger.debug("Probably running in java 6 so close method on URLClassLoader is not available"); } else if (e instanceof IOException) { logger.error("Unable to close class loader for plugin. Will try uninstalling plugin anyway", e); } else { logger.error("Error while closing class loader", e); } } } } @Override protected boolean executeNonOsgiInstall(IPlugin plugin, IPluginVersion version) { try { Result result = this.executeInstallPluginJob(plugin.getId(), version.getDownloadUrl(), version.getSamplesDownloadUrl(), version.getVersion()); if (result == null || result.getNrErrors() > 0) { return false; } } catch (KettleException e) { logger.error(e.getMessage(), e); return false; } return true; } @Override protected boolean executeNonOsgiUninstall(IPlugin plugin) { try { Result result = this.executeUninstallPluginJob(plugin.getId()); if (result == null || result.getNrErrors() > 0) { return false; } } catch (KettleException e) { logger.error(e.getMessage(), e); return false; } return true; } private Result executeInstallPluginJob(String pluginId, String downloadUrl, String samplesDownloadUrl, String availableVersion) throws UnknownParamException { JobMeta installMeta = this.getInstallJobMeta(); if (installMeta == null) { this.getLogger().error("Unable to find install job meta."); return null; } Job job = new Job(null, installMeta); File file = new File(this.getApplicationContext().getSolutionPath(DOWNLOAD_CACHE_FOLDER)); file.mkdirs(); file = new File(this.getApplicationContext().getSolutionPath(BACKUP_CACHE_FOLDER)); file.mkdirs(); file = new File(this.getApplicationContext().getSolutionPath(STAGING_CACHE_FOLDER)); file.mkdirs(); job.getJobMeta().setParameterValue("downloadUrl", downloadUrl); if (samplesDownloadUrl != null) { job.getJobMeta().setParameterValue("samplesDownloadUrl", samplesDownloadUrl); job.getJobMeta().setParameterValue("samplesDir", "/public/plugin-samples"); job.getJobMeta().setParameterValue("samplesTargetDestination", this.getApplicationContext().getSolutionPath("plugin-samples/" + pluginId)); job.getJobMeta().setParameterValue("samplesTargetBackup", this.getApplicationContext() .getSolutionPath(BACKUP_CACHE_FOLDER + pluginId + "_samples_" + new Date().getTime())); job.getJobMeta().setParameterValue("samplesDownloadDestination", this.getApplicationContext().getSolutionPath(DOWNLOAD_CACHE_FOLDER + pluginId + "-samples-" + availableVersion + "_" + new Date().getTime() + ".zip")); job.getJobMeta().setParameterValue("samplesStagingDestination", this.getApplicationContext().getSolutionPath("system/plugin-cache/staging_samples")); job.getJobMeta().setParameterValue("samplesStagingDestinationAndDir", this.getApplicationContext() .getSolutionPath("system/plugin-cache/staging_samples/" + pluginId)); } job.getJobMeta().setParameterValue("downloadDestination", this.getApplicationContext().getSolutionPath("system/plugin-cache/downloads/" + pluginId + "-" + availableVersion + "_" + new Date().getTime() + ".zip")); job.getJobMeta().setParameterValue("stagingDestination", this.getApplicationContext().getSolutionPath(STAGING_CACHE_FOLDER)); job.getJobMeta().setParameterValue("stagingDestinationAndDir", this.getApplicationContext().getSolutionPath(STAGING_CACHE_FOLDER + pluginId)); job.getJobMeta().setParameterValue("targetDestination", this.getApplicationContext().getSolutionPath("system/" + pluginId)); job.getJobMeta().setParameterValue("targetBackup", this.getApplicationContext() .getSolutionPath(BACKUP_CACHE_FOLDER + pluginId + "_" + new Date().getTime())); job.copyParametersFrom(job.getJobMeta()); job.setLogLevel(LogLevel.DETAILED); job.activateParameters(); job.start(); job.waitUntilFinished(); Result result = job.getResult(); // Execute the selected job. return result; } private Result executeUninstallPluginJob(String pluginId) throws UnknownParamException { JobMeta uninstallJobMeta = this.getUninstallJobMeta(); if (uninstallJobMeta == null) { this.getLogger().error("Unable to find uninstall job meta."); return null; } Job job = new Job(null, uninstallJobMeta); File file = new File(this.getApplicationContext().getSolutionPath(BACKUP_CACHE_FOLDER)); file.mkdirs(); String uninstallBackup = this.getApplicationContext() .getSolutionPath(BACKUP_CACHE_FOLDER + pluginId + "_" + new Date().getTime()); job.getJobMeta().setParameterValue("uninstallLocation", this.getApplicationContext().getSolutionPath("system/" + pluginId)); job.getJobMeta().setParameterValue("uninstallBackup", uninstallBackup); job.getJobMeta().setParameterValue("samplesDir", "/public/plugin-samples/" + pluginId); job.copyParametersFrom(job.getJobMeta()); job.activateParameters(); job.start(); job.waitUntilFinished(); Result result = job.getResult(); // Execute the selected job. return result; } private void copyKettleFilesToExecutionFolder() { Path kettleExecutionFolderPath = this.getAbsoluteKettleExecutionFolderPath(); File targetKettleFilesFolder = new File(kettleExecutionFolderPath.toUri()); if (!targetKettleFilesFolder.exists() && !targetKettleFilesFolder.mkdirs()) { this.getLogger().error("Failed to create temporary folder for marketplace kettle transformations at " + targetKettleFilesFolder.toString()); } String kettleResourcesSourcePath = this.getAbsoluteKettleResourcesSourcePath(); Iterable<String> kettleResourcePaths = Collections.list(bundle.getEntryPaths(kettleResourcesSourcePath)); for (String kettleResourcePath : kettleResourcePaths) { this.writeResourceToFolder(kettleResourcePath, kettleExecutionFolderPath); } } private void deleteKettleFilesFromExecutionFolder() { URI kettleExecutionFolder = this.getAbsoluteKettleExecutionFolderPath().toUri(); File targetKettleFilesFolder = new File(kettleExecutionFolder); if (targetKettleFilesFolder.exists()) { try { FileUtils.deleteDirectory(targetKettleFilesFolder); } catch (IOException e) { this.getLogger().error("Unable to delete marketplace temporary kettle execution folder: " + targetKettleFilesFolder.toString(), e); } } } private void writeResourceToFolder(URL resourceUrl, Path destinationFolder) { try { InputStream inputStream = resourceUrl.openConnection().getInputStream(); String fileName = FilenameUtils.getName(resourceUrl.toString()); Path destinationFile = destinationFolder.resolve(fileName); Files.copy(inputStream, destinationFile, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { this.getLogger().error( "Error copying " + resourceUrl.toString() + " to destination folder " + destinationFolder, e); } } private void writeResourceToFolder(String resourceUrl, Path destinationFolder) { URL url = this.getBundle().getResource(resourceUrl); this.writeResourceToFolder(url, destinationFolder); } private Collection<String> parseStringCollection(String string, String valueSeparator) { String[] splitString = string.split(valueSeparator); Collection<String> parsedValues = new ArrayList<>(splitString.length); for (String authorizedRole : splitString) { if (authorizedRole != null) { authorizedRole = authorizedRole.trim(); if (!authorizedRole.isEmpty()) { parsedValues.add(authorizedRole); } } } return parsedValues; } //endregion }