/** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * http://bitbucket.org/sdorra/scm-manager * */ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- import com.github.legman.Subscribe; import com.google.common.base.Predicate; import com.google.common.io.Files; import com.google.inject.Inject; import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfigurationChangedEvent; import sonia.scm.io.ZipUnArchiver; import sonia.scm.util.AssertUtil; import sonia.scm.util.IOUtil; import sonia.scm.util.SystemUtil; import sonia.scm.util.Util; import sonia.scm.version.Version; //~--- JDK imports ------------------------------------------------------------ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import javax.xml.bind.JAXB; import sonia.scm.net.ahc.AdvancedHttpClient; import static sonia.scm.plugin.PluginCenterDtoMapper.*; /** * TODO replace aether stuff. * TODO check AdvancedPluginConfiguration from 1.x * * @author Sebastian Sdorra */ @Singleton public class DefaultPluginManager implements PluginManager { /** Field description */ public static final String CACHE_NAME = "sonia.cache.plugins"; /** Field description */ public static final String ENCODING = "UTF-8"; /** the logger for DefaultPluginManager */ private static final Logger logger = LoggerFactory.getLogger(DefaultPluginManager.class); /** enable or disable remote plugins */ private static final boolean REMOTE_PLUGINS_ENABLED = true; /** Field description */ public static final Predicate FILTER_UPDATES = new StatePluginPredicate(PluginState.UPDATE_AVAILABLE); //~--- constructors --------------------------------------------------------- /** * Constructs ... * * @param context * @param configuration * @param pluginLoader * @param cacheManager * @param httpClient */ @Inject public DefaultPluginManager(SCMContextProvider context, ScmConfiguration configuration, PluginLoader pluginLoader, CacheManager cacheManager, AdvancedHttpClient httpClient) { this.context = context; this.configuration = configuration; this.cache = cacheManager.getCache(CACHE_NAME); this.httpClient = httpClient; installedPlugins = new HashMap<>(); for (InstalledPlugin wrapper : pluginLoader.getInstalledPlugins()) { InstalledPluginDescriptor plugin = wrapper.getPlugin(); PluginInformation info = plugin.getInformation(); if ((info != null) && info.isValid()) { installedPlugins.put(info.getId(), plugin); } } } //~--- methods -------------------------------------------------------------- /** * Method description * */ @Override public void clearCache() { if (logger.isDebugEnabled()) { logger.debug("clear plugin cache"); } cache.clear(); } /** * Method description * * * @param config */ @Subscribe public void configChanged(ScmConfigurationChangedEvent config) { clearCache(); } /** * Method description * * * @param id */ @Override public void install(String id) { PluginPermissions.manage().check(); PluginCenter center = getPluginCenter(); for (PluginInformation plugin : center.getPlugins()) { String pluginId = plugin.getId(); if (Util.isNotEmpty(pluginId) && pluginId.equals(id)) { plugin.setState(PluginState.INSTALLED); // ugly workaround InstalledPluginDescriptor newPlugin = new InstalledPluginDescriptor(); // TODO check // newPlugin.setInformation(plugin); installedPlugins.put(id, newPlugin); } } } /** * Method description * * * @param packageStream * * @throws IOException */ @Override public void installPackage(InputStream packageStream) throws IOException { PluginPermissions.manage().check(); File tempDirectory = Files.createTempDir(); try { new ZipUnArchiver().extractArchive(packageStream, tempDirectory); InstalledPluginDescriptor plugin = JAXB.unmarshal(new File(tempDirectory, "plugin.xml"), InstalledPluginDescriptor.class); PluginCondition condition = plugin.getCondition(); if ((condition != null) &&!condition.isSupported()) { throw new PluginConditionFailedException(condition); } /* * AetherPluginHandler aph = new AetherPluginHandler(this, context, * configuration); * Collection repositories = * Sets.newHashSet(new PluginRepository("package-repository", * "file://".concat(tempDirectory.getAbsolutePath()))); * * aph.setPluginRepositories(repositories); * * aph.install(plugin.getInformation().getId()); */ plugin.getInformation().setState(PluginState.INSTALLED); installedPlugins.put(plugin.getInformation().getId(), plugin); } finally { IOUtil.delete(tempDirectory); } } /** * Method description * * * @param id */ @Override public void uninstall(String id) { PluginPermissions.manage().check(); InstalledPluginDescriptor plugin = installedPlugins.get(id); if (plugin == null) { String pluginPrefix = getPluginIdPrefix(id); for (String nid : installedPlugins.keySet()) { if (nid.startsWith(pluginPrefix)) { id = nid; plugin = installedPlugins.get(nid); break; } } } if (plugin == null) { throw new PluginNotInstalledException(id.concat(" is not install")); } /* * if (pluginHandler == null) * { * getPluginCenter(); * } * * pluginHandler.uninstall(id); */ installedPlugins.remove(id); preparePlugins(getPluginCenter()); } /** * Method description * * * @param id */ @Override public void update(String id) { PluginPermissions.manage().check(); String[] idParts = id.split(":"); String name = idParts[0]; PluginInformation installed = null; for (PluginInformation info : getInstalled()) { if (name.equals(info.getName())) { installed = info; break; } } if (installed == null) { StringBuilder msg = new StringBuilder(name); msg.append(" is not install"); throw new PluginNotInstalledException(msg.toString()); } uninstall(installed.getId()); install(id); } //~--- get methods ---------------------------------------------------------- /** * Method description * * * @param id * * @return */ @Override public PluginInformation get(String id) { PluginPermissions.read().check(); PluginInformation result = null; for (PluginInformation info : getPluginCenter().getPlugins()) { if (id.equals(info.getId())) { result = info; break; } } return result; } /** * Method description * * * @param predicate * * @return */ @Override public Set get(Predicate predicate) { AssertUtil.assertIsNotNull(predicate); PluginPermissions.read().check(); Set infoSet = new HashSet<>(); filter(infoSet, getInstalled(), predicate); filter(infoSet, getPluginCenter().getPlugins(), predicate); return infoSet; } /** * Method description * * * @return */ @Override public Collection getAll() { PluginPermissions.read().check(); Set infoSet = getInstalled(); infoSet.addAll(getPluginCenter().getPlugins()); return infoSet; } /** * Method description * * * @return */ @Override public Collection getAvailable() { PluginPermissions.read().check(); Set availablePlugins = new HashSet<>(); Set centerPlugins = getPluginCenter().getPlugins(); for (PluginInformation info : centerPlugins) { if (!installedPlugins.containsKey(info.getName())) { availablePlugins.add(info); } } return availablePlugins; } /** * Method description * * * @return */ @Override public Set getAvailableUpdates() { PluginPermissions.read().check(); return get(FILTER_UPDATES); } /** * Method description * * * @return */ @Override public Set getInstalled() { PluginPermissions.read().check(); Set infoSet = new LinkedHashSet<>(); for (InstalledPluginDescriptor plugin : installedPlugins.values()) { infoSet.add(plugin.getInformation()); } return infoSet; } //~--- methods -------------------------------------------------------------- /** * Method description * * * * @param url * @return */ private String buildPluginUrl(String url) { String os = SystemUtil.getOS(); String arch = SystemUtil.getArch(); try { os = URLEncoder.encode(os, ENCODING); } catch (UnsupportedEncodingException ex) { logger.error(ex.getMessage(), ex); } return url.replace("{version}", context.getVersion()).replace("{os}", os).replace("{arch}", arch); } /** * Method description * * * @param target * @param source * @param predicate */ private void filter(Set target, Collection source, Predicate predicate) { for (PluginInformation info : source) { if (predicate.apply(info)) { target.add(info); } } } /** * Method description * * * @param available */ private void preparePlugin(PluginInformation available) { PluginState state = PluginState.AVAILABLE; for (PluginInformation installed : getInstalled()) { if (isSamePlugin(available, installed)) { if (installed.getVersion().equals(available.getVersion())) { state = PluginState.INSTALLED; } else if (isNewer(available, installed)) { state = PluginState.UPDATE_AVAILABLE; } else { state = PluginState.NEWER_VERSION_INSTALLED; } break; } } available.setState(state); } /** * Method description * * * @param pc */ private void preparePlugins(PluginCenter pc) { Set infoSet = pc.getPlugins(); if (infoSet != null) { Iterator pit = infoSet.iterator(); while (pit.hasNext()) { PluginInformation available = pit.next(); if (isCorePluging(available)) { pit.remove(); } else { preparePlugin(available); } } } } //~--- get methods ---------------------------------------------------------- /** * Method description * * * @return */ private PluginCenter getPluginCenter() { PluginCenter center = cache.get(PluginCenter.class.getName()); if (center == null) { synchronized (DefaultPluginManager.class) { String pluginUrl = buildPluginUrl(configuration.getPluginUrl()); logger.info("fetch plugin information from {}", pluginUrl); if (REMOTE_PLUGINS_ENABLED && Util.isNotEmpty(pluginUrl)) { try { center = new PluginCenter(); PluginCenterDto pluginCenterDto = httpClient.get(pluginUrl).request().contentFromJson(PluginCenterDto.class); Set pluginInformationSet = map(pluginCenterDto.getEmbedded().getPlugins()); center.setPlugins(pluginInformationSet); preparePlugins(center); cache.put(PluginCenter.class.getName(), center); } catch (IOException ex) { logger.error("could not load plugins from plugin center", ex); } } } if(center == null) { center = new PluginCenter(); } } return center; } /** * Method description * * * @param pluginId * * @return */ private String getPluginIdPrefix(String pluginId) { return pluginId.substring(0, pluginId.lastIndexOf(':')); } /** * Method description * * * @param available * * @return */ private boolean isCorePluging(PluginInformation available) { boolean core = false; for (InstalledPluginDescriptor installedPlugin : installedPlugins.values()) { PluginInformation installed = installedPlugin.getInformation(); if (isSamePlugin(available, installed) && (installed.getState() == PluginState.CORE)) { core = true; break; } } return core; } /** * Method description * * * @param available * @param installed * * @return */ private boolean isNewer(PluginInformation available, PluginInformation installed) { boolean result = false; Version version = Version.parse(available.getVersion()); if (version != null) { result = version.isNewer(installed.getVersion()); } return result; } /** * Method description * * * @param p1 * @param p2 * * @return */ private boolean isSamePlugin(PluginInformation p1, PluginInformation p2) { return p1.getName().equals(p2.getName()); } //~--- fields --------------------------------------------------------------- /** Field description */ private final Cache cache; /** Field description */ private final AdvancedHttpClient httpClient; /** Field description */ private final ScmConfiguration configuration; /** Field description */ private final SCMContextProvider context; /** Field description */ private final Map installedPlugins; }