Merged in feature/install_plugins (pull request #299)

Feature/install plugins
This commit is contained in:
Rene Pfeuffer
2019-08-22 08:51:18 +00:00
78 changed files with 2675 additions and 1970 deletions

View File

@@ -0,0 +1,32 @@
package sonia.scm.plugin;
import com.google.common.base.Preconditions;
public class AvailablePlugin implements Plugin {
private final AvailablePluginDescriptor pluginDescriptor;
private final boolean pending;
public AvailablePlugin(AvailablePluginDescriptor pluginDescriptor) {
this(pluginDescriptor, false);
}
private AvailablePlugin(AvailablePluginDescriptor pluginDescriptor, boolean pending) {
this.pluginDescriptor = pluginDescriptor;
this.pending = pending;
}
@Override
public AvailablePluginDescriptor getDescriptor() {
return pluginDescriptor;
}
public boolean isPending() {
return pending;
}
public AvailablePlugin install() {
Preconditions.checkState(!pending, "installation is already pending");
return new AvailablePlugin(pluginDescriptor, true);
}
}

View File

@@ -0,0 +1,47 @@
package sonia.scm.plugin;
import java.util.Optional;
import java.util.Set;
/**
* @since 2.0.0
*/
public class AvailablePluginDescriptor implements PluginDescriptor {
private final PluginInformation information;
private final PluginCondition condition;
private final Set<String> dependencies;
private final String url;
private final String checksum;
public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set<String> dependencies, String url, String checksum) {
this.information = information;
this.condition = condition;
this.dependencies = dependencies;
this.url = url;
this.checksum = checksum;
}
public String getUrl() {
return url;
}
public Optional<String> getChecksum() {
return Optional.ofNullable(checksum);
}
@Override
public PluginInformation getInformation() {
return information;
}
@Override
public PluginCondition getCondition() {
return condition;
}
@Override
public Set<String> getDependencies() {
return dependencies;
}
}

View File

@@ -36,27 +36,27 @@ package sonia.scm.plugin;
import java.nio.file.Path; import java.nio.file.Path;
/** /**
* Wrapper for a {@link Plugin}. The wrapper holds the directory, * Wrapper for a {@link InstalledPluginDescriptor}. The wrapper holds the directory,
* {@link ClassLoader} and {@link WebResourceLoader} of a plugin. * {@link ClassLoader} and {@link WebResourceLoader} of a plugin.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0
*/ */
public final class PluginWrapper public final class InstalledPlugin implements Plugin
{ {
/** /**
* Constructs a new plugin wrapper. * Constructs a new plugin wrapper.
* *
* @param plugin wrapped plugin * @param descriptor wrapped plugin
* @param classLoader plugin class loader * @param classLoader plugin class loader
* @param webResourceLoader web resource loader * @param webResourceLoader web resource loader
* @param directory plugin directory * @param directory plugin directory
*/ */
public PluginWrapper(Plugin plugin, ClassLoader classLoader, public InstalledPlugin(InstalledPluginDescriptor descriptor, ClassLoader classLoader,
WebResourceLoader webResourceLoader, Path directory) WebResourceLoader webResourceLoader, Path directory)
{ {
this.plugin = plugin; this.descriptor = descriptor;
this.classLoader = classLoader; this.classLoader = classLoader;
this.webResourceLoader = webResourceLoader; this.webResourceLoader = webResourceLoader;
this.directory = directory; this.directory = directory;
@@ -94,18 +94,19 @@ public final class PluginWrapper
*/ */
public String getId() public String getId()
{ {
return plugin.getInformation().getId(); return descriptor.getInformation().getId();
} }
/** /**
* Returns the plugin. * Returns the plugin descriptor.
* *
* *
* @return plugin * @return plugin descriptor
*/ */
public Plugin getPlugin() @Override
public InstalledPluginDescriptor getDescriptor()
{ {
return plugin; return descriptor;
} }
/** /**
@@ -128,7 +129,7 @@ public final class PluginWrapper
private final Path directory; private final Path directory;
/** plugin */ /** plugin */
private final Plugin plugin; private final InstalledPluginDescriptor descriptor;
/** plugin web resource loader */ /** plugin web resource loader */
private final WebResourceLoader webResourceLoader; private final WebResourceLoader webResourceLoader;

View File

@@ -0,0 +1,259 @@
/**
* 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.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "plugin")
@XmlAccessorType(XmlAccessType.FIELD)
public final class InstalledPluginDescriptor extends ScmModule implements PluginDescriptor
{
/**
* Constructs ...
*
*/
InstalledPluginDescriptor() {}
/**
* Constructs ...
*
*
* @param scmVersion
* @param information
* @param resources
* @param condition
* @param childFirstClassLoader
* @param dependencies
*/
public InstalledPluginDescriptor(int scmVersion, PluginInformation information,
PluginResources resources, PluginCondition condition,
boolean childFirstClassLoader, Set<String> dependencies)
{
this.scmVersion = scmVersion;
this.information = information;
this.resources = resources;
this.condition = condition;
this.childFirstClassLoader = childFirstClassLoader;
this.dependencies = dependencies;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param obj
*
* @return
*/
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final InstalledPluginDescriptor other = (InstalledPluginDescriptor) obj;
return Objects.equal(scmVersion, other.scmVersion)
&& Objects.equal(condition, other.condition)
&& Objects.equal(information, other.information)
&& Objects.equal(resources, other.resources)
&& Objects.equal(childFirstClassLoader, other.childFirstClassLoader)
&& Objects.equal(dependencies, other.dependencies);
}
/**
* Method description
*
*
* @return
*/
@Override
public int hashCode()
{
return Objects.hashCode(scmVersion, condition, information, resources,
childFirstClassLoader, dependencies);
}
/**
* Method description
*
*
* @return
*/
@Override
public String toString()
{
//J-
return MoreObjects.toStringHelper(this)
.add("scmVersion", scmVersion)
.add("condition", condition)
.add("information", information)
.add("resources", resources)
.add("childFirstClassLoader", childFirstClassLoader)
.add("dependencies", dependencies)
.toString();
//J+
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override
public PluginCondition getCondition()
{
return condition;
}
/**
* Method description
*
*
* @return
*
* @since 2.0.0
*/
@Override
public Set<String> getDependencies()
{
if (dependencies == null)
{
dependencies = ImmutableSet.of();
}
return dependencies;
}
/**
* Method description
*
*
* @return
*/
@Override
public PluginInformation getInformation()
{
return information;
}
/**
* Method description
*
*
* @return
*/
public PluginResources getResources()
{
return resources;
}
/**
* Method description
*
*
* @return
*/
public int getScmVersion()
{
return scmVersion;
}
/**
* Method description
*
*
* @return
*/
public boolean isChildFirstClassLoader()
{
return childFirstClassLoader;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@XmlElement(name = "child-first-classloader")
private boolean childFirstClassLoader;
/** Field description */
@XmlElement(name = "conditions")
private PluginCondition condition;
/** Field description */
@XmlElement(name = "dependency")
@XmlElementWrapper(name = "dependencies")
private Set<String> dependencies;
/** Field description */
@XmlElement(name = "information")
private PluginInformation information;
/** Field description */
private PluginResources resources;
/** Field description */
@XmlElement(name = "scm-version")
private int scmVersion = 1;
}

View File

@@ -1,255 +1,6 @@
/**
* 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; package sonia.scm.plugin;
//~--- non-JDK imports -------------------------------------------------------- public interface Plugin {
import com.google.common.base.MoreObjects; PluginDescriptor getDescriptor();
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public final class Plugin extends ScmModule
{
/**
* Constructs ...
*
*/
Plugin() {}
/**
* Constructs ...
*
*
* @param scmVersion
* @param information
* @param resources
* @param condition
* @param childFirstClassLoader
* @param dependencies
*/
public Plugin(int scmVersion, PluginInformation information,
PluginResources resources, PluginCondition condition,
boolean childFirstClassLoader, Set<String> dependencies)
{
this.scmVersion = scmVersion;
this.information = information;
this.resources = resources;
this.condition = condition;
this.childFirstClassLoader = childFirstClassLoader;
this.dependencies = dependencies;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param obj
*
* @return
*/
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final Plugin other = (Plugin) obj;
return Objects.equal(scmVersion, other.scmVersion)
&& Objects.equal(condition, other.condition)
&& Objects.equal(information, other.information)
&& Objects.equal(resources, other.resources)
&& Objects.equal(childFirstClassLoader, other.childFirstClassLoader)
&& Objects.equal(dependencies, other.dependencies);
}
/**
* Method description
*
*
* @return
*/
@Override
public int hashCode()
{
return Objects.hashCode(scmVersion, condition, information, resources,
childFirstClassLoader, dependencies);
}
/**
* Method description
*
*
* @return
*/
@Override
public String toString()
{
//J-
return MoreObjects.toStringHelper(this)
.add("scmVersion", scmVersion)
.add("condition", condition)
.add("information", information)
.add("resources", resources)
.add("childFirstClassLoader", childFirstClassLoader)
.add("dependencies", dependencies)
.toString();
//J+
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public PluginCondition getCondition()
{
return condition;
}
/**
* Method description
*
*
* @return
*
* @since 2.0.0
*/
public Set<String> getDependencies()
{
if (dependencies == null)
{
dependencies = ImmutableSet.of();
}
return dependencies;
}
/**
* Method description
*
*
* @return
*/
public PluginInformation getInformation()
{
return information;
}
/**
* Method description
*
*
* @return
*/
public PluginResources getResources()
{
return resources;
}
/**
* Method description
*
*
* @return
*/
public int getScmVersion()
{
return scmVersion;
}
/**
* Method description
*
*
* @return
*/
public boolean isChildFirstClassLoader()
{
return childFirstClassLoader;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@XmlElement(name = "child-first-classloader")
private boolean childFirstClassLoader;
/** Field description */
@XmlElement(name = "conditions")
private PluginCondition condition;
/** Field description */
@XmlElement(name = "dependency")
@XmlElementWrapper(name = "dependencies")
private Set<String> dependencies;
/** Field description */
private PluginInformation information;
/** Field description */
private PluginResources resources;
/** Field description */
@XmlElement(name = "scm-version")
private int scmVersion = 1;
} }

View File

@@ -1,120 +0,0 @@
/**
* 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;
//~--- JDK imports ------------------------------------------------------------
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "plugin-center")
@XmlAccessorType(XmlAccessType.FIELD)
public class PluginCenter implements Serializable
{
/** Field description */
private static final long serialVersionUID = -6414175308610267397L;
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public Set<PluginInformation> getPlugins()
{
return plugins;
}
/**
* Method description
*
*
* @return
*/
public Set<PluginRepository> getRepositories()
{
return repositories;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param plugins
*/
public void setPlugins(Set<PluginInformation> plugins)
{
this.plugins = plugins;
}
/**
* Method description
*
*
* @param repositories
*/
public void setRepositories(Set<PluginRepository> repositories)
{
this.repositories = repositories;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@XmlElement(name = "plugin")
@XmlElementWrapper(name = "plugins")
private Set<PluginInformation> plugins = new HashSet<PluginInformation>();
/** Field description */
@XmlElement(name = "repository")
@XmlElementWrapper(name = "repositories")
private Set<PluginRepository> repositories = new HashSet<PluginRepository>();
}

View File

@@ -0,0 +1,13 @@
package sonia.scm.plugin;
import java.util.Set;
public interface PluginDescriptor {
PluginInformation getInformation();
PluginCondition getCondition();
Set<String> getDependencies();
}

View File

@@ -70,8 +70,6 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea
private String author; private String author;
private String category; private String category;
private String avatarUrl; private String avatarUrl;
private PluginCondition condition;
private PluginState state;
@Override @Override
public PluginInformation clone() { public PluginInformation clone() {
@@ -83,10 +81,6 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea
clone.setAuthor(author); clone.setAuthor(author);
clone.setCategory(category); clone.setCategory(category);
clone.setAvatarUrl(avatarUrl); clone.setAvatarUrl(avatarUrl);
clone.setState(state);
if (condition != null) {
clone.setCondition(condition.clone());
}
return clone; return clone;
} }

View File

@@ -1,101 +0,0 @@
/**
* 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 sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.Serializable;
import java.util.Comparator;
/**
*
* @author Sebastian Sdorra
* @since 1.6
*/
public class PluginInformationComparator
implements Comparator<PluginInformation>, Serializable
{
/** Field description */
public static final PluginInformationComparator INSTANCE =
new PluginInformationComparator();
/** Field description */
private static final long serialVersionUID = -8339752498853225668L;
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param plugin
* @param other
*
* @return
*/
@Override
public int compare(PluginInformation plugin, PluginInformation other)
{
int result = 0;
result = Util.compare(plugin.getName(), other.getName());
if (result == 0)
{
PluginState state = plugin.getState();
PluginState otherState = other.getState();
if ((state != null) && (otherState != null))
{
result = state.getCompareValue() - otherState.getCompareValue();
}
else if ((state == null) && (otherState != null))
{
result = 1;
}
else if ((state != null) && (otherState == null))
{
result = -1;
}
}
return result;
}
}

View File

@@ -68,7 +68,7 @@ public interface PluginLoader
* *
* @return * @return
*/ */
public Collection<PluginWrapper> getInstalledPlugins(); public Collection<InstalledPlugin> getInstalledPlugins();
/** /**
* Returns a {@link ClassLoader} which is able to load classes and resources * Returns a {@link ClassLoader} which is able to load classes and resources

View File

@@ -33,113 +33,56 @@
package sonia.scm.plugin; package sonia.scm.plugin;
//~--- JDK imports ------------------------------------------------------------ import java.util.List;
import java.util.Optional;
import com.google.common.base.Predicate;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
/** /**
* The plugin manager is responsible for plugin related tasks, such as install, uninstall or updating.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public interface PluginManager public interface PluginManager {
{
/** /**
* Method description * Returns the available plugin with the given name.
* * @param name of plugin
* @return optional available plugin.
*/ */
public void clearCache(); Optional<AvailablePlugin> getAvailable(String name);
/** /**
* Method description * Returns the installed plugin with the given name.
* * @param name of plugin
* * @return optional installed plugin.
* @param id
*/ */
public void install(String id); Optional<InstalledPlugin> getInstalled(String name);
/** /**
* Installs a plugin package from a inputstream. * Returns all installed plugins.
* *
* * @return a list of installed plugins.
* @param packageStream package input stream
*
* @throws IOException
* @since 1.21
*/ */
public void installPackage(InputStream packageStream) throws IOException; List<InstalledPlugin> getInstalled();
/** /**
* Method description * Returns all available plugins. The list contains the plugins which are loaded from the plugin center, but without
* the installed plugins.
* *
* * @return a list of available plugins.
* @param id
*/ */
public void uninstall(String id); List<AvailablePlugin> getAvailable();
/** /**
* Method description * Installs the plugin with the given name from the list of available plugins.
* *
* * @param name plugin name
* @param id * @param restartAfterInstallation restart context after plugin installation
*/ */
public void update(String id); void install(String name, boolean restartAfterInstallation);
//~--- get methods ----------------------------------------------------------
/** /**
* Method description * Install all pending plugins and restart the scm context.
*
*
* @param id
*
* @return
*/ */
public PluginInformation get(String id); void installPendingAndRestart();
/**
* Method description
*
*
* @param filter
*
* @return
*/
public Collection<PluginInformation> get(Predicate<PluginInformation> filter);
/**
* Method description
*
*
* @return
*/
public Collection<PluginInformation> getAll();
/**
* Method description
*
*
* @return
*/
public Collection<PluginInformation> getAvailable();
/**
* Method description
*
*
* @return
*/
public Collection<PluginInformation> getAvailableUpdates();
/**
* Method description
*
*
* @return
*/
public Collection<PluginInformation> getInstalled();
} }

View File

@@ -1,160 +0,0 @@
/**
* 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.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import java.io.Serializable;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class PluginRepository implements Serializable
{
/** Field description */
private static final long serialVersionUID = -9504354306304731L;
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
PluginRepository() {}
/**
* Constructs ...
*
*
* @param id
* @param url
*/
public PluginRepository(String id, String url)
{
this.id = id;
this.url = url;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param obj
*
* @return
*/
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final PluginRepository other = (PluginRepository) obj;
return Objects.equal(id, other.id) && Objects.equal(url, other.url);
}
/**
* Method description
*
*
* @return
*/
@Override
public int hashCode()
{
return Objects.hashCode(id, url);
}
/**
* Method description
*
*
* @return
*/
@Override
public String toString()
{
return MoreObjects.toStringHelper(this).add("id", id).add("url",
url).toString();
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getId()
{
return id;
}
/**
* Method description
*
*
* @return
*/
public String getUrl()
{
return url;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String id;
/** Field description */
private String url;
}

View File

@@ -1,74 +0,0 @@
/**
* 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;
/**
*
* @author Sebastian Sdorra
*/
public enum PluginState
{
CORE(100), AVAILABLE(60), INSTALLED(80), NEWER_VERSION_INSTALLED(20),
UPDATE_AVAILABLE(40);
/**
* Constructs ...
*
*
* @param compareValue
*/
private PluginState(int compareValue)
{
this.compareValue = compareValue;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @since 1.6
* @return
*/
public int getCompareValue()
{
return compareValue;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final int compareValue;
}

View File

@@ -65,7 +65,7 @@ public final class Plugins
{ {
try try
{ {
context = JAXBContext.newInstance(Plugin.class, ScmModule.class); context = JAXBContext.newInstance(InstalledPluginDescriptor.class, ScmModule.class);
} }
catch (JAXBException ex) catch (JAXBException ex)
{ {
@@ -91,7 +91,7 @@ public final class Plugins
* *
* @return * @return
*/ */
public static Plugin parsePluginDescriptor(Path path) public static InstalledPluginDescriptor parsePluginDescriptor(Path path)
{ {
return parsePluginDescriptor(Files.asByteSource(path.toFile())); return parsePluginDescriptor(Files.asByteSource(path.toFile()));
} }
@@ -104,15 +104,15 @@ public final class Plugins
* *
* @return * @return
*/ */
public static Plugin parsePluginDescriptor(ByteSource data) public static InstalledPluginDescriptor parsePluginDescriptor(ByteSource data)
{ {
Preconditions.checkNotNull(data, "data parameter is required"); Preconditions.checkNotNull(data, "data parameter is required");
Plugin plugin; InstalledPluginDescriptor plugin;
try (InputStream stream = data.openStream()) try (InputStream stream = data.openStream())
{ {
plugin = (Plugin) context.createUnmarshaller().unmarshal(stream); plugin = (InstalledPluginDescriptor) context.createUnmarshaller().unmarshal(stream);
} }
catch (JAXBException ex) catch (JAXBException ex)
{ {

View File

@@ -206,7 +206,7 @@ public final class SmpArchive
* *
* @throws IOException * @throws IOException
*/ */
public Plugin getPlugin() throws IOException public InstalledPluginDescriptor getPlugin() throws IOException
{ {
if (plugin == null) if (plugin == null)
{ {
@@ -245,9 +245,9 @@ public final class SmpArchive
* *
* @throws IOException * @throws IOException
*/ */
private Plugin createPlugin() throws IOException private InstalledPluginDescriptor createPlugin() throws IOException
{ {
Plugin p = null; InstalledPluginDescriptor p = null;
NonClosingZipInputStream zis = null; NonClosingZipInputStream zis = null;
try try
@@ -412,5 +412,5 @@ public final class SmpArchive
private final ByteSource archive; private final ByteSource archive;
/** Field description */ /** Field description */
private Plugin plugin; private InstalledPluginDescriptor plugin;
} }

View File

@@ -1,78 +0,0 @@
/**
* 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.google.common.base.Predicate;
/**
*
* @author Sebastian Sdorra
*/
public class StatePluginPredicate implements Predicate<PluginInformation>
{
/**
* Constructs ...
*
*
* @param state
*/
public StatePluginPredicate(PluginState state)
{
this.state = state;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param plugin
*
* @return
*/
@Override
public boolean apply(PluginInformation plugin)
{
return state == plugin.getState();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final PluginState state;
}

View File

@@ -0,0 +1,32 @@
package sonia.scm.plugin;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class AvailablePluginTest {
@Mock
private AvailablePluginDescriptor descriptor;
@Test
void shouldReturnNewPendingPluginOnInstall() {
AvailablePlugin plugin = new AvailablePlugin(descriptor);
assertThat(plugin.isPending()).isFalse();
AvailablePlugin installed = plugin.install();
assertThat(installed.isPending()).isTrue();
}
@Test
void shouldThrowIllegalStateExceptionIfAlreadyPending() {
AvailablePlugin plugin = new AvailablePlugin(descriptor).install();
assertThrows(IllegalStateException.class, () -> plugin.install());
}
}

View File

@@ -113,7 +113,7 @@ public class SmpArchiveTest
public void testGetPlugin() throws IOException public void testGetPlugin() throws IOException
{ {
File archive = createArchive("sonia.sample", "1.0"); File archive = createArchive("sonia.sample", "1.0");
Plugin plugin = SmpArchive.create(archive).getPlugin(); InstalledPluginDescriptor plugin = SmpArchive.create(archive).getPlugin();
assertNotNull(plugin); assertNotNull(plugin);

View File

@@ -46,7 +46,8 @@ type Props = {
contentRight?: React.Node, contentRight?: React.Node,
footerLeft: React.Node, footerLeft: React.Node,
footerRight: React.Node, footerRight: React.Node,
link: string, link?: string,
action?: () => void,
// context props // context props
classes: any classes: any
@@ -54,9 +55,11 @@ type Props = {
class CardColumn extends React.Component<Props> { class CardColumn extends React.Component<Props> {
createLink = () => { createLink = () => {
const { link } = this.props; const { link, action } = this.props;
if (link) { if (link) {
return <Link className="overlay-column" to={link} />; return <Link className="overlay-column" to={link} />;
} else if (action) {
return <a className="overlay-column" onClick={e => {e.preventDefault(); action();}} href="#" />;
} }
return null; return null;
}; };

View File

@@ -14,7 +14,7 @@ class ButtonGroup extends React.Component<Props> {
const childWrapper = []; const childWrapper = [];
React.Children.forEach(children, child => { React.Children.forEach(children, child => {
if (child) { if (child) {
childWrapper.push(<p className="control">{child}</p>); childWrapper.push(<p className="control" key={childWrapper.length}>{child}</p>);
} }
}); });

View File

@@ -1,7 +1,6 @@
//@flow //@flow
import type {Collection, Links} from "./hal"; import type {Collection, Links} from "./hal";
export type Plugin = { export type Plugin = {
name: string, name: string,
version: string, version: string,
@@ -10,6 +9,8 @@ export type Plugin = {
author: string, author: string,
category: string, category: string,
avatarUrl: string, avatarUrl: string,
pending: boolean,
dependencies: string[],
_links: Links _links: Links
}; };

View File

@@ -29,7 +29,23 @@
"installedNavLink": "Installiert", "installedNavLink": "Installiert",
"availableNavLink": "Verfügbar" "availableNavLink": "Verfügbar"
}, },
"noPlugins": "Keine Plugins gefunden." "installPending": "Austehende Plugins installieren",
"noPlugins": "Keine Plugins gefunden.",
"modal": {
"title": "{{name}} Plugin installieren",
"restart": "Neustarten um Plugin zu aktivieren",
"install": "Installieren",
"installAndRestart": "Installieren und Neustarten",
"abort": "Abbrechen",
"author": "Autor",
"version": "Version",
"dependencyNotification": "Mit diesem Plugin werden folgende Abhängigkeiten mit installieren wenn sie noch nicht vorhanden sind!",
"dependencies": "Abhängigkeiten",
"successNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:",
"reload": "jetzt new laden",
"restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.",
"installPending": "Die folgenden Plugins werden installiert und anschließend wir der SCM-Manager Kontext neu gestartet."
}
}, },
"repositoryRole": { "repositoryRole": {
"navLink": "Berechtigungsrollen", "navLink": "Berechtigungsrollen",

View File

@@ -6,7 +6,7 @@
"settingsNavLink": "Settings", "settingsNavLink": "Settings",
"generalNavLink": "General" "generalNavLink": "General"
}, },
"info": { "info": {
"currentAppVersion": "Current Application Version", "currentAppVersion": "Current Application Version",
"communityTitle": "Community Support", "communityTitle": "Community Support",
"communityIconAlt": "Community Support Icon", "communityIconAlt": "Community Support Icon",
@@ -29,7 +29,23 @@
"installedNavLink": "Installed", "installedNavLink": "Installed",
"availableNavLink": "Available" "availableNavLink": "Available"
}, },
"noPlugins": "No plugins found." "installPending": "Install pending plugins",
"noPlugins": "No plugins found.",
"modal": {
"title": "Install {{name}} Plugin",
"restart": "Restart to activate",
"install": "Install",
"installAndRestart": "Install and Restart",
"abort": "Abort",
"author": "Author",
"version": "Version",
"dependencyNotification": "With this plugin, the following dependencies are installed if they are not available yet!",
"dependencies": "Dependencies",
"successNotification": "Successful installed plugin. You have to reload the page, to see ui changes:",
"reload": "reload now",
"restartNotification": "Restarting the scm-manager context, should only be done if no one else is currently working with it.",
"installPending": "The following plugins will be installed and after installation the scm-manager context will be restarted."
}
}, },
"repositoryRole": { "repositoryRole": {
"navLink": "Permission Roles", "navLink": "Permission Roles",
@@ -53,7 +69,7 @@
"permissions": "Permissions", "permissions": "Permissions",
"submit": "Save" "submit": "Save"
}, },
"delete" : { "delete": {
"button": "Löschen", "button": "Löschen",
"subtitle": "Berechtigungsrolle löschen", "subtitle": "Berechtigungsrolle löschen",
"confirmAlert": { "confirmAlert": {

View File

@@ -0,0 +1,68 @@
// @flow
import React from "react";
import { Button } from "@scm-manager/ui-components";
import type { PluginCollection } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import InstallPendingModal from "./InstallPendingModal";
type Props = {
collection: PluginCollection,
// context props
t: string => string
};
type State = {
showModal: boolean
};
class InstallPendingAction extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showModal: false
};
}
openModal = () => {
this.setState({
showModal: true
});
};
closeModal = () => {
this.setState({
showModal: false
});
};
renderModal = () => {
const { showModal } = this.state;
const { collection } = this.props;
if (showModal) {
return (
<InstallPendingModal
collection={collection}
onClose={this.closeModal}
/>
);
}
return null;
};
render() {
const { t } = this.props;
return (
<>
{this.renderModal()}
<Button
color="primary"
label={t("plugins.installPending")}
action={this.openModal}
/>
</>
);
}
}
export default translate("admin")(InstallPendingAction);

View File

@@ -0,0 +1,134 @@
// @flow
import React from "react";
import {
apiClient,
Button,
ButtonGroup,
ErrorNotification,
Modal,
Notification
} from "@scm-manager/ui-components";
import type { PluginCollection } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import waitForRestart from "./waitForRestart";
import InstallSuccessNotification from "./InstallSuccessNotification";
type Props = {
onClose: () => void,
collection: PluginCollection,
// context props
t: string => string
};
type State = {
loading: boolean,
success: boolean,
error?: Error
};
class InstallPendingModal extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
loading: false,
success: false
};
}
renderNotifications = () => {
const { t } = this.props;
const { error, success } = this.state;
if (error) {
return <ErrorNotification error={error} />;
} else if (success) {
return <InstallSuccessNotification />;
} else {
return (
<Notification type="warning">
{t("plugins.modal.restartNotification")}
</Notification>
);
}
};
installAndRestart = () => {
const { collection } = this.props;
this.setState({
loading: true
});
apiClient
.post(collection._links.installPending.href)
.then(waitForRestart)
.then(() => {
this.setState({
success: true,
loading: false,
error: undefined
});
})
.catch(error => {
this.setState({
success: false,
loading: false,
error: error
});
});
};
renderBody = () => {
const { collection, t } = this.props;
return (
<>
<div className="media">
<div className="content">
<p>{t("plugins.modal.installPending")}</p>
<ul>
{collection._embedded.plugins
.filter(plugin => plugin.pending)
.map(plugin => (
<li key={plugin.name} className="has-text-weight-bold">
{plugin.name}
</li>
))}
</ul>
</div>
</div>
<div className="media">{this.renderNotifications()}</div>
</>
);
};
renderFooter = () => {
const { onClose, t } = this.props;
const { loading, error, success } = this.state;
return (
<ButtonGroup>
<Button
color="warning"
label={t("plugins.modal.installAndRestart")}
loading={loading}
action={this.installAndRestart}
disabled={error || success}
/>
<Button label={t("plugins.modal.abort")} action={onClose} />
</ButtonGroup>
);
};
render() {
const { onClose, t } = this.props;
return (
<Modal
title={t("plugins.modal.installAndRestart")}
closeFunction={onClose}
body={this.renderBody()}
footer={this.renderFooter()}
active={true}
/>
);
}
}
export default translate("admin")(InstallPendingModal);

View File

@@ -0,0 +1,25 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Notification } from "@scm-manager/ui-components";
type Props = {
// context props
t: string => string
};
class InstallSuccessNotification extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<Notification type="success">
{t("plugins.modal.successNotification")}{" "}
<a onClick={e => window.location.reload(true)}>
{t("plugins.modal.reload")}
</a>
</Notification>
);
}
}
export default translate("admin")(InstallSuccessNotification);

View File

@@ -0,0 +1,30 @@
// @flow
import * as React from "react";
import classNames from "classnames";
import injectSheet from "react-jss";
const styles = {
container: {
border: "2px solid #e9f7fd",
padding: "1em 1em",
marginTop: "2em",
display: "flex",
justifyContent: "center"
}
};
type Props = {
children?: React.Node,
// context props
classes: any
};
class PluginBottomActions extends React.Component<Props> {
render() {
const { children, classes } = this.props;
return <div className={classNames(classes.container)}>{children}</div>;
}
}
export default injectSheet(styles)(PluginBottomActions);

View File

@@ -1,65 +1,118 @@
//@flow //@flow
import React from "react"; import React from "react";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
import type {Plugin} from "@scm-manager/ui-types"; import type { Plugin } from "@scm-manager/ui-types";
import {CardColumn} from "@scm-manager/ui-components"; import { CardColumn } from "@scm-manager/ui-components";
import PluginAvatar from "./PluginAvatar"; import PluginAvatar from "./PluginAvatar";
import PluginModal from "./PluginModal";
import classNames from "classnames";
type Props = { type Props = {
plugin: Plugin, plugin: Plugin,
refresh: () => void,
// context props // context props
classes: any classes: any
}; };
type State = {
showModal: boolean
};
const styles = { const styles = {
link: { link: {
pointerEvents: "cursor" cursor: "pointer",
pointerEvents: "all"
},
spinner: {
position: "absolute",
right: 0,
top: 0
} }
}; };
class PluginEntry extends React.Component<Props> { class PluginEntry extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showModal: false
};
}
createAvatar = (plugin: Plugin) => { createAvatar = (plugin: Plugin) => {
return <PluginAvatar plugin={plugin} />; return <PluginAvatar plugin={plugin} />;
}; };
createContentRight = (plugin: Plugin) => { toggleModal = () => {
this.setState(prevState => ({
showModal: !prevState.showModal
}));
};
createFooterRight = (plugin: Plugin) => {
return <small className="level-item">{plugin.author}</small>;
};
isInstallable = () => {
const { plugin } = this.props;
return plugin._links && plugin._links.install && plugin._links.install.href;
};
createFooterLeft = () => {
const { classes } = this.props; const { classes } = this.props;
if (plugin._links && plugin._links.install && plugin._links.install.href) { if (this.isInstallable()) {
return ( return (
<div className={classes.link} onClick={() => console.log(plugin._links.install.href) /*TODO trigger plugin installation*/}> <span
<i className="fas fa-cloud-download-alt fa-2x has-text-info" /> className={classNames(classes.link, "level-item")}
</div> onClick={this.toggleModal}
>
<i className="fas fa-download has-text-info" />
</span>
); );
} }
}; };
createFooterLeft = (plugin: Plugin) => { createPendingSpinner = () => {
return <small className="level-item">{plugin.author}</small>; const { plugin, classes } = this.props;
}; if (plugin.pending) {
return (
createFooterRight = (plugin: Plugin) => { <span className={classes.spinner}>
return <p className="level-item">{plugin.version}</p>; <i className="fas fa-spinner fa-spin has-text-info" />
</span>
);
}
return null;
}; };
render() { render() {
const { plugin } = this.props; const { plugin, refresh } = this.props;
const { showModal } = this.state;
const avatar = this.createAvatar(plugin); const avatar = this.createAvatar(plugin);
const contentRight = this.createContentRight(plugin); const footerLeft = this.createFooterLeft();
const footerLeft = this.createFooterLeft(plugin);
const footerRight = this.createFooterRight(plugin); const footerRight = this.createFooterRight(plugin);
// TODO: Add link to plugin page below const modal = showModal ? (
return ( <PluginModal
<CardColumn plugin={plugin}
link="#" refresh={refresh}
avatar={avatar} onClose={this.toggleModal}
title={plugin.displayName ? plugin.displayName : plugin.name}
description={plugin.description}
contentRight={contentRight}
footerLeft={footerLeft}
footerRight={footerRight}
/> />
) : null;
return (
<>
<CardColumn
action={this.isInstallable() ? this.toggleModal : null}
avatar={avatar}
title={plugin.displayName ? plugin.displayName : plugin.name}
description={plugin.description}
contentRight={this.createPendingSpinner()}
footerLeft={footerLeft}
footerRight={footerRight}
/>
{modal}
</>
); );
} }
} }

View File

@@ -5,14 +5,15 @@ import type { PluginGroup } from "@scm-manager/ui-types";
import PluginEntry from "./PluginEntry"; import PluginEntry from "./PluginEntry";
type Props = { type Props = {
group: PluginGroup group: PluginGroup,
refresh: () => void
}; };
class PluginGroupEntry extends React.Component<Props> { class PluginGroupEntry extends React.Component<Props> {
render() { render() {
const { group } = this.props; const { group, refresh } = this.props;
const entries = group.plugins.map((plugin, index) => { const entries = group.plugins.map(plugin => {
return <PluginEntry plugin={plugin} key={index} />; return <PluginEntry plugin={plugin} key={plugin.name} refresh={refresh} />;
}); });
return <CardColumnGroup name={group.name} elements={entries} />; return <CardColumnGroup name={group.name} elements={entries} />;
} }

View File

@@ -5,18 +5,19 @@ import PluginGroupEntry from "../components/PluginGroupEntry";
import groupByCategory from "./groupByCategory"; import groupByCategory from "./groupByCategory";
type Props = { type Props = {
plugins: Plugin[] plugins: Plugin[],
refresh: () => void
}; };
class PluginList extends React.Component<Props> { class PluginList extends React.Component<Props> {
render() { render() {
const { plugins } = this.props; const { plugins, refresh } = this.props;
const groups = groupByCategory(plugins); const groups = groupByCategory(plugins);
return ( return (
<div className="content is-plugin-page"> <div className="content is-plugin-page">
{groups.map(group => { {groups.map(group => {
return <PluginGroupEntry group={group} key={group.name} />; return <PluginGroupEntry group={group} key={group.name} refresh={refresh} />;
})} })}
</div> </div>
); );

View File

@@ -0,0 +1,270 @@
//@flow
import React from "react";
import { compose } from "redux";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
import type { Plugin } from "@scm-manager/ui-types";
import {
apiClient,
Button,
ButtonGroup,
Checkbox,
ErrorNotification,
Modal,
Notification
} from "@scm-manager/ui-components";
import classNames from "classnames";
import waitForRestart from "./waitForRestart";
import InstallSuccessNotification from "./InstallSuccessNotification";
type Props = {
plugin: Plugin,
refresh: () => void,
onClose: () => void,
// context props
classes: any,
t: (key: string, params?: Object) => string
};
type State = {
success: boolean,
restart: boolean,
loading: boolean,
error?: Error
};
const styles = {
userLabelAlignment: {
textAlign: "left",
marginRight: 0,
minWidth: "5.5em"
},
userFieldFlex: {
flexGrow: 4
}
};
class PluginModal extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
loading: false,
restart: false,
success: false
};
}
onInstallSuccess = () => {
const { restart } = this.state;
const { refresh, onClose } = this.props;
const newState = {
loading: false,
error: undefined
};
if (restart) {
waitForRestart()
.then(() => {
this.setState({
...newState,
success: true
});
})
.catch(error => {
this.setState({
loading: false,
success: false,
error
});
});
} else {
this.setState(newState, () => {
refresh();
onClose();
});
}
};
install = (e: Event) => {
const { restart } = this.state;
const { plugin } = this.props;
this.setState({
loading: true
});
e.preventDefault();
apiClient
.post(plugin._links.install.href + "?restart=" + restart.toString())
.then(this.onInstallSuccess)
.catch(error => {
this.setState({
loading: false,
error: error
});
});
};
footer = () => {
const { onClose, t } = this.props;
const { loading, error, restart, success } = this.state;
let color = "primary";
let label = "plugins.modal.install";
if (restart) {
color = "warning";
label = "plugins.modal.installAndRestart";
}
return (
<ButtonGroup>
<Button
label={t(label)}
color={color}
action={this.install}
loading={loading}
disabled={!!error || success}
/>
<Button label={t("plugins.modal.abort")} action={onClose} />
</ButtonGroup>
);
};
renderDependencies() {
const { plugin, classes, t } = this.props;
let dependencies = null;
if (plugin.dependencies && plugin.dependencies.length > 0) {
dependencies = (
<div className="media">
<Notification type="warning">
<strong>{t("plugins.modal.dependencyNotification")}</strong>
<ul className={classes.listSpacing}>
{plugin.dependencies.map((dependency, index) => {
return <li key={index}>{dependency}</li>;
})}
</ul>
</Notification>
</div>
);
}
return dependencies;
}
renderNotifications = () => {
const { t } = this.props;
const { restart, error, success } = this.state;
if (error) {
return (
<div className="media">
<ErrorNotification error={error} />
</div>
);
} else if (success) {
return (
<div className="media">
<InstallSuccessNotification />
</div>
);
} else if (restart) {
return (
<div className="media">
<Notification type="warning">
{t("plugins.modal.restartNotification")}
</Notification>
</div>
);
}
return null;
};
handleRestartChange = (value: boolean) => {
this.setState({
restart: value
});
};
render() {
const { restart } = this.state;
const { plugin, onClose, classes, t } = this.props;
const body = (
<>
<div className="media">
<div className="media-content">
<p>{plugin.description}</p>
</div>
</div>
<div className="media">
<div className="media-content">
<div className="field is-horizontal">
<div
className={classNames(
classes.userLabelAlignment,
"field-label is-inline-flex"
)}
>
{t("plugins.modal.author")}:
</div>
<div
className={classNames(
classes.userFieldFlex,
"field-body is-inline-flex"
)}
>
{plugin.author}
</div>
</div>
<div className="field is-horizontal">
<div
className={classNames(
classes.userLabelAlignment,
"field-label is-inline-flex"
)}
>
{t("plugins.modal.version")}:
</div>
<div
className={classNames(
classes.userFieldFlex,
"field-body is-inline-flex"
)}
>
{plugin.version}
</div>
</div>
{this.renderDependencies()}
</div>
</div>
<div className="media">
<div className="media-content">
<Checkbox
checked={restart}
label={t("plugins.modal.restart")}
onChange={this.handleRestartChange}
disabled={false}
/>
</div>
</div>
{this.renderNotifications()}
</>
);
return (
<Modal
title={t("plugins.modal.title", {
name: plugin.displayName ? plugin.displayName : plugin.name
})}
closeFunction={() => onClose()}
body={body}
footer={this.footer()}
active={true}
/>
);
}
}
export default compose(
injectSheet(styles),
translate("admin")
)(PluginModal);

View File

@@ -0,0 +1,32 @@
// @flow
import * as React from "react";
import classNames from "classnames";
import injectSheet from "react-jss";
const styles = {
container: {
display: "flex",
justifyContent: "flex-end",
alignItems: "center"
}
};
type Props = {
children?: React.Node,
// context props
classes: any
};
class PluginTopActions extends React.Component<Props> {
render() {
const { children, classes } = this.props;
return (
<div className={classNames(classes.container, "column", "is-one-fifths", "is-mobile-action-spacing")}>
{children}
</div>
);
}
}
export default injectSheet(styles)(PluginTopActions);

View File

@@ -0,0 +1,30 @@
// @flow
import { apiClient } from "@scm-manager/ui-components";
const waitForRestart = () => {
const endTime = Number(new Date()) + 10000;
let started = false;
const executor = (resolve, reject) => {
// we need some initial delay
if (!started) {
started = true;
setTimeout(executor, 100, resolve, reject);
} else {
apiClient
.get("")
.then(resolve)
.catch(() => {
if (Number(new Date()) < endTime) {
setTimeout(executor, 500, resolve, reject);
} else {
reject(new Error("timeout reached"));
}
});
}
};
return new Promise<void>(executor);
};
export default waitForRestart;

View File

@@ -1,5 +1,5 @@
// @flow // @flow
import React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { compose } from "redux"; import { compose } from "redux";
@@ -17,11 +17,14 @@ import {
getPluginCollection, getPluginCollection,
isFetchPluginsPending isFetchPluginsPending
} from "../modules/plugins"; } from "../modules/plugins";
import PluginsList from "../components/PluginsList"; import PluginsList from "../components/PluginList";
import { import {
getAvailablePluginsLink, getAvailablePluginsLink,
getInstalledPluginsLink getInstalledPluginsLink
} from "../../../modules/indexResource"; } from "../../../modules/indexResource";
import PluginTopActions from "../components/PluginTopActions";
import PluginBottomActions from "../components/PluginBottomActions";
import InstallPendingAction from "../components/InstallPendingAction";
type Props = { type Props = {
loading: boolean, loading: boolean,
@@ -51,21 +54,62 @@ class PluginsOverview extends React.Component<Props> {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const {
installed,
} = this.props;
if (prevProps.installed !== installed) {
this.fetchPlugins();
}
}
fetchPlugins = () => {
const { const {
installed, installed,
fetchPluginsByLink, fetchPluginsByLink,
availablePluginsLink, availablePluginsLink,
installedPluginsLink installedPluginsLink
} = this.props; } = this.props;
if (prevProps.installed !== installed) { fetchPluginsByLink(
fetchPluginsByLink( installed ? installedPluginsLink : availablePluginsLink
installed ? installedPluginsLink : availablePluginsLink );
); };
renderHeader = (actions: React.Node) => {
const { installed, t } = this.props;
return (
<div className="columns">
<div className="column">
<Title title={t("plugins.title")} />
<Subtitle
subtitle={
installed
? t("plugins.installedSubtitle")
: t("plugins.availableSubtitle")
}
/>
</div>
<PluginTopActions>{actions}</PluginTopActions>
</div>
);
};
renderFooter = (actions: React.Node) => {
if (actions) {
return <PluginBottomActions>{actions}</PluginBottomActions>;
} }
} return null;
};
createActions = () => {
const { collection } = this.props;
if (collection._links.installPending) {
return <InstallPendingAction collection={collection} />;
}
return null;
};
render() { render() {
const { loading, error, collection, installed, t } = this.props; const { loading, error, collection } = this.props;
if (error) { if (error) {
return <ErrorNotification error={error} />; return <ErrorNotification error={error} />;
@@ -75,17 +119,13 @@ class PluginsOverview extends React.Component<Props> {
return <Loading />; return <Loading />;
} }
const actions = this.createActions();
return ( return (
<> <>
<Title title={t("plugins.title")} /> {this.renderHeader(actions)}
<Subtitle <hr className="header-with-actions" />
subtitle={
installed
? t("plugins.installedSubtitle")
: t("plugins.availableSubtitle")
}
/>
{this.renderPluginsList()} {this.renderPluginsList()}
{this.renderFooter(actions)}
</> </>
); );
} }
@@ -94,7 +134,7 @@ class PluginsOverview extends React.Component<Props> {
const { collection, t } = this.props; const { collection, t } = this.props;
if (collection._embedded && collection._embedded.plugins.length > 0) { if (collection._embedded && collection._embedded.plugins.length > 0) {
return <PluginsList plugins={collection._embedded.plugins} />; return <PluginsList plugins={collection._embedded.plugins} refresh={this.fetchPlugins} />;
} }
return <Notification type="info">{t("plugins.noPlugins")}</Notification>; return <Notification type="info">{t("plugins.noPlugins")}</Notification>;
} }

View File

@@ -3,24 +3,22 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.plugin.Plugin; import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.InstalledPluginDescriptor;
import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginManager;
import sonia.scm.plugin.PluginPermissions; import sonia.scm.plugin.PluginPermissions;
import sonia.scm.plugin.PluginState;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Collection; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound; import static sonia.scm.NotFoundException.notFound;
@@ -53,11 +51,8 @@ public class AvailablePluginResource {
@Produces(VndMediaType.PLUGIN_COLLECTION) @Produces(VndMediaType.PLUGIN_COLLECTION)
public Response getAvailablePlugins() { public Response getAvailablePlugins() {
PluginPermissions.read().check(); PluginPermissions.read().check();
Collection<PluginInformation> plugins = pluginManager.getAvailable() List<AvailablePlugin> available = pluginManager.getAvailable();
.stream() return Response.ok(collectionMapper.mapAvailable(available)).build();
.filter(plugin -> plugin.getState().equals(PluginState.AVAILABLE))
.collect(Collectors.toList());
return Response.ok(collectionMapper.map(plugins)).build();
} }
/** /**
@@ -66,7 +61,7 @@ public class AvailablePluginResource {
* @return available plugin. * @return available plugin.
*/ */
@GET @GET
@Path("/{name}/{version}") @Path("/{name}")
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 404, condition = "not found"), @ResponseCode(code = 404, condition = "not found"),
@@ -74,35 +69,42 @@ public class AvailablePluginResource {
}) })
@TypeHint(PluginDto.class) @TypeHint(PluginDto.class)
@Produces(VndMediaType.PLUGIN) @Produces(VndMediaType.PLUGIN)
public Response getAvailablePlugin(@PathParam("name") String name, @PathParam("version") String version) { public Response getAvailablePlugin(@PathParam("name") String name) {
PluginPermissions.read().check(); PluginPermissions.read().check();
Optional<PluginInformation> plugin = pluginManager.getAvailable() Optional<AvailablePlugin> plugin = pluginManager.getAvailable(name);
.stream()
.filter(p -> p.getId().equals(name + ":" + version))
.findFirst();
if (plugin.isPresent()) { if (plugin.isPresent()) {
return Response.ok(mapper.map(plugin.get())).build(); return Response.ok(mapper.mapAvailable(plugin.get())).build();
} else { } else {
throw notFound(entity(Plugin.class, name)); throw notFound(entity(InstalledPluginDescriptor.class, name));
} }
} }
/** /**
* Triggers plugin installation. * Triggers plugin installation.
* @param name plugin artefact name * @param name plugin name
* @param version plugin version
* @return HTTP Status. * @return HTTP Status.
*/ */
@POST @POST
@Path("/{name}/{version}/install") @Path("/{name}/install")
@Consumes(VndMediaType.PLUGIN)
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
public Response installPlugin(@PathParam("name") String name, @PathParam("version") String version) { public Response installPlugin(@PathParam("name") String name, @QueryParam("restart") boolean restartAfterInstallation) {
PluginPermissions.manage().check(); PluginPermissions.manage().check();
pluginManager.install(name + ":" + version); pluginManager.install(name, restartAfterInstallation);
return Response.ok().build();
}
@POST
@Path("/install-pending")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response installPending() {
PluginPermissions.manage().check();
pluginManager.installPendingAndRestart();
return Response.ok().build(); return Response.ok().build();
} }
} }

View File

@@ -3,11 +3,10 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.plugin.Plugin; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.InstalledPluginDescriptor;
import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginManager;
import sonia.scm.plugin.PluginPermissions; import sonia.scm.plugin.PluginPermissions;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
@@ -16,7 +15,6 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -25,17 +23,15 @@ import static sonia.scm.NotFoundException.notFound;
public class InstalledPluginResource { public class InstalledPluginResource {
private final PluginLoader pluginLoader;
private final PluginDtoCollectionMapper collectionMapper; private final PluginDtoCollectionMapper collectionMapper;
private final PluginDtoMapper mapper; private final PluginDtoMapper mapper;
private final PluginManager pluginManager; private final PluginManager pluginManager;
@Inject @Inject
public InstalledPluginResource(PluginLoader pluginLoader, PluginDtoCollectionMapper collectionMapper, PluginDtoMapper mapper, PluginManager pluginManager) { public InstalledPluginResource(PluginManager pluginManager, PluginDtoCollectionMapper collectionMapper, PluginDtoMapper mapper) {
this.pluginLoader = pluginLoader; this.pluginManager = pluginManager;
this.collectionMapper = collectionMapper; this.collectionMapper = collectionMapper;
this.mapper = mapper; this.mapper = mapper;
this.pluginManager = pluginManager;
} }
/** /**
@@ -53,8 +49,8 @@ public class InstalledPluginResource {
@Produces(VndMediaType.PLUGIN_COLLECTION) @Produces(VndMediaType.PLUGIN_COLLECTION)
public Response getInstalledPlugins() { public Response getInstalledPlugins() {
PluginPermissions.read().check(); PluginPermissions.read().check();
List<PluginWrapper> plugins = new ArrayList<>(pluginLoader.getInstalledPlugins()); List<InstalledPlugin> plugins = pluginManager.getInstalled();
return Response.ok(collectionMapper.map(plugins)).build(); return Response.ok(collectionMapper.mapInstalled(plugins)).build();
} }
/** /**
@@ -75,15 +71,11 @@ public class InstalledPluginResource {
@Produces(VndMediaType.PLUGIN) @Produces(VndMediaType.PLUGIN)
public Response getInstalledPlugin(@PathParam("name") String name) { public Response getInstalledPlugin(@PathParam("name") String name) {
PluginPermissions.read().check(); PluginPermissions.read().check();
Optional<PluginDto> pluginDto = pluginLoader.getInstalledPlugins() Optional<InstalledPlugin> pluginDto = pluginManager.getInstalled(name);
.stream()
.filter(plugin -> name.equals(plugin.getPlugin().getInformation().getName()))
.map(mapper::map)
.findFirst();
if (pluginDto.isPresent()) { if (pluginDto.isPresent()) {
return Response.ok(pluginDto.get()).build(); return Response.ok(mapper.mapInstalled(pluginDto.get())).build();
} else { } else {
throw notFound(entity(Plugin.class, name)); throw notFound(entity(InstalledPluginDescriptor.class, name));
} }
} }
} }

View File

@@ -6,9 +6,12 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.util.Set;
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@SuppressWarnings("squid:S2160") // we do not need equals for dto
public class PluginDto extends HalRepresentation { public class PluginDto extends HalRepresentation {
private String name; private String name;
@@ -18,6 +21,8 @@ public class PluginDto extends HalRepresentation {
private String author; private String author;
private String category; private String category;
private String avatarUrl; private String avatarUrl;
private boolean pending;
private Set<String> dependencies;
public PluginDto(Links links) { public PluginDto(Links links) {
add(links); add(links);

View File

@@ -3,11 +3,12 @@ package sonia.scm.api.v2.resources;
import com.google.inject.Inject; import com.google.inject.Inject;
import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Link;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginPermissions;
import java.util.Collection;
import java.util.List; import java.util.List;
import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Embedded.embeddedBuilder;
@@ -25,14 +26,14 @@ public class PluginDtoCollectionMapper {
this.mapper = mapper; this.mapper = mapper;
} }
public HalRepresentation map(List<PluginWrapper> plugins) { public HalRepresentation mapInstalled(List<InstalledPlugin> plugins) {
List<PluginDto> dtos = plugins.stream().map(mapper::map).collect(toList()); List<PluginDto> dtos = plugins.stream().map(mapper::mapInstalled).collect(toList());
return new HalRepresentation(createInstalledPluginsLinks(), embedDtos(dtos)); return new HalRepresentation(createInstalledPluginsLinks(), embedDtos(dtos));
} }
public HalRepresentation map(Collection<PluginInformation> plugins) { public HalRepresentation mapAvailable(List<AvailablePlugin> plugins) {
List<PluginDto> dtos = plugins.stream().map(mapper::map).collect(toList()); List<PluginDto> dtos = plugins.stream().map(mapper::mapAvailable).collect(toList());
return new HalRepresentation(createAvailablePluginsLinks(), embedDtos(dtos)); return new HalRepresentation(createAvailablePluginsLinks(plugins), embedDtos(dtos));
} }
private Links createInstalledPluginsLinks() { private Links createInstalledPluginsLinks() {
@@ -43,14 +44,23 @@ public class PluginDtoCollectionMapper {
return linksBuilder.build(); return linksBuilder.build();
} }
private Links createAvailablePluginsLinks() { private Links createAvailablePluginsLinks(List<AvailablePlugin> plugins) {
String baseUrl = resourceLinks.availablePluginCollection().self(); String baseUrl = resourceLinks.availablePluginCollection().self();
Links.Builder linksBuilder = linkingTo() Links.Builder linksBuilder = linkingTo()
.with(Links.linkingTo().self(baseUrl).build()); .with(Links.linkingTo().self(baseUrl).build());
if (PluginPermissions.manage().isPermitted() && containsPending(plugins)) {
linksBuilder.single(Link.link("installPending", resourceLinks.availablePluginCollection().installPending()));
}
return linksBuilder.build(); return linksBuilder.build();
} }
private boolean containsPending(List<AvailablePlugin> plugins) {
return plugins.stream().anyMatch(AvailablePlugin::isPending);
}
private Embedded embedDtos(List<PluginDto> dtos) { private Embedded embedDtos(List<PluginDto> dtos) {
return embeddedBuilder() return embeddedBuilder()
.with("plugins", dtos) .with("plugins", dtos)

View File

@@ -1,13 +1,13 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget; import org.mapstruct.MappingTarget;
import org.mapstruct.ObjectFactory; import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.Plugin;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginInformation;
import sonia.scm.plugin.PluginState; import sonia.scm.plugin.PluginPermissions;
import sonia.scm.plugin.PluginWrapper;
import javax.inject.Inject; import javax.inject.Inject;
@@ -20,35 +20,50 @@ public abstract class PluginDtoMapper {
@Inject @Inject
private ResourceLinks resourceLinks; private ResourceLinks resourceLinks;
public PluginDto map(PluginWrapper plugin) { public abstract void map(PluginInformation plugin, @MappingTarget PluginDto dto);
return map(plugin.getPlugin().getInformation());
public PluginDto mapInstalled(InstalledPlugin plugin) {
PluginDto dto = createDtoForInstalled(plugin);
map(dto, plugin);
return dto;
} }
public abstract PluginDto map(PluginInformation plugin); public PluginDto mapAvailable(AvailablePlugin plugin) {
PluginDto dto = createDtoForAvailable(plugin);
map(dto, plugin);
dto.setPending(plugin.isPending());
return dto;
}
@AfterMapping private void map(PluginDto dto, Plugin plugin) {
protected void appendCategory(@MappingTarget PluginDto dto) { dto.setDependencies(plugin.getDescriptor().getDependencies());
map(plugin.getDescriptor().getInformation(), dto);
if (dto.getCategory() == null) { if (dto.getCategory() == null) {
dto.setCategory("Miscellaneous"); dto.setCategory("Miscellaneous");
} }
} }
@ObjectFactory private PluginDto createDtoForAvailable(AvailablePlugin plugin) {
public PluginDto createDto(PluginInformation pluginInformation) { PluginInformation information = plugin.getDescriptor().getInformation();
Links.Builder linksBuilder;
if (pluginInformation.getState() != null && pluginInformation.getState().equals(PluginState.AVAILABLE)) {
linksBuilder = linkingTo()
.self(resourceLinks.availablePlugin()
.self(pluginInformation.getName(), pluginInformation.getVersion()));
linksBuilder.single(link("install", resourceLinks.availablePlugin().install(pluginInformation.getName(), pluginInformation.getVersion()))); Links.Builder links = linkingTo()
} .self(resourceLinks.availablePlugin()
else { .self(information.getName()));
linksBuilder = linkingTo()
.self(resourceLinks.installedPlugin() if (!plugin.isPending() && PluginPermissions.manage().isPermitted()) {
.self(pluginInformation.getName())); links.single(link("install", resourceLinks.availablePlugin().install(information.getName())));
} }
return new PluginDto(linksBuilder.build()); return new PluginDto(links.build());
}
private PluginDto createDtoForInstalled(InstalledPlugin plugin) {
PluginInformation information = plugin.getDescriptor().getInformation();
Links.Builder links = linkingTo()
.self(resourceLinks.installedPlugin()
.self(information.getName()));
return new PluginDto(links.build());
} }
} }

View File

@@ -6,6 +6,7 @@ import javax.inject.Inject;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@SuppressWarnings("squid:S1192") // string literals should not be duplicated
class ResourceLinks { class ResourceLinks {
private final ScmPathInfoStore scmPathInfoStore; private final ScmPathInfoStore scmPathInfoStore;
@@ -694,12 +695,12 @@ class ResourceLinks {
availablePluginLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, AvailablePluginResource.class); availablePluginLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, AvailablePluginResource.class);
} }
String self(String name, String version) { String self(String name) {
return availablePluginLinkBuilder.method("availablePlugins").parameters().method("getAvailablePlugin").parameters(name, version).href(); return availablePluginLinkBuilder.method("availablePlugins").parameters().method("getAvailablePlugin").parameters(name).href();
} }
String install(String name, String version) { String install(String name) {
return availablePluginLinkBuilder.method("availablePlugins").parameters().method("installPlugin").parameters(name, version).href(); return availablePluginLinkBuilder.method("availablePlugins").parameters().method("installPlugin").parameters(name).href();
} }
} }
@@ -714,6 +715,10 @@ class ResourceLinks {
availablePluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, AvailablePluginResource.class); availablePluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, AvailablePluginResource.class);
} }
String installPending() {
return availablePluginCollectionLinkBuilder.method("availablePlugins").parameters().method("installPending").parameters().href();
}
String self() { String self() {
return availablePluginCollectionLinkBuilder.method("availablePlugins").parameters().method("getAvailablePlugins").parameters().href(); return availablePluginCollectionLinkBuilder.method("availablePlugins").parameters().method("getAvailablePlugins").parameters().href();
} }

View File

@@ -4,7 +4,7 @@ import com.google.inject.Inject;
import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.InstalledPlugin;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -24,7 +24,7 @@ public class UIPluginDtoCollectionMapper {
this.mapper = mapper; this.mapper = mapper;
} }
public HalRepresentation map(Collection<PluginWrapper> plugins) { public HalRepresentation map(Collection<InstalledPlugin> plugins) {
List<UIPluginDto> dtos = plugins.stream().map(mapper::map).collect(toList()); List<UIPluginDto> dtos = plugins.stream().map(mapper::map).collect(toList());
return new HalRepresentation(createLinks(), embedDtos(dtos)); return new HalRepresentation(createLinks(), embedDtos(dtos));
} }

View File

@@ -2,7 +2,7 @@ package sonia.scm.api.v2.resources;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import javax.inject.Inject; import javax.inject.Inject;
@@ -25,9 +25,9 @@ public class UIPluginDtoMapper {
this.request = request; this.request = request;
} }
public UIPluginDto map(PluginWrapper plugin) { public UIPluginDto map(InstalledPlugin plugin) {
UIPluginDto dto = new UIPluginDto( UIPluginDto dto = new UIPluginDto(
plugin.getPlugin().getInformation().getName(), plugin.getDescriptor().getInformation().getName(),
getScriptResources(plugin) getScriptResources(plugin)
); );
@@ -40,8 +40,8 @@ public class UIPluginDtoMapper {
return dto; return dto;
} }
private Set<String> getScriptResources(PluginWrapper wrapper) { private Set<String> getScriptResources(InstalledPlugin wrapper) {
Set<String> scriptResources = wrapper.getPlugin().getResources().getScriptResources(); Set<String> scriptResources = wrapper.getDescriptor().getResources().getScriptResources();
if (scriptResources != null) { if (scriptResources != null) {
return scriptResources.stream() return scriptResources.stream()
.map(this::addContextPath) .map(this::addContextPath)

View File

@@ -4,7 +4,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.security.AllowAnonymousAccess; import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
@@ -46,7 +46,7 @@ public class UIPluginResource {
@TypeHint(CollectionDto.class) @TypeHint(CollectionDto.class)
@Produces(VndMediaType.UI_PLUGIN_COLLECTION) @Produces(VndMediaType.UI_PLUGIN_COLLECTION)
public Response getInstalledPlugins() { public Response getInstalledPlugins() {
List<PluginWrapper> plugins = pluginLoader.getInstalledPlugins() List<InstalledPlugin> plugins = pluginLoader.getInstalledPlugins()
.stream() .stream()
.filter(this::filter) .filter(this::filter)
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -85,8 +85,8 @@ public class UIPluginResource {
} }
} }
private boolean filter(PluginWrapper plugin) { private boolean filter(InstalledPlugin plugin) {
return plugin.getPlugin().getResources() != null; return plugin.getDescriptor().getResources() != null;
} }
} }

View File

@@ -9,11 +9,11 @@ import sonia.scm.SCMContext;
import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle; import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle;
import sonia.scm.migration.UpdateException; import sonia.scm.migration.UpdateException;
import sonia.scm.plugin.DefaultPluginLoader; import sonia.scm.plugin.DefaultPluginLoader;
import sonia.scm.plugin.Plugin; import sonia.scm.plugin.InstalledPluginDescriptor;
import sonia.scm.plugin.PluginException; import sonia.scm.plugin.PluginException;
import sonia.scm.plugin.PluginLoadException; import sonia.scm.plugin.PluginLoadException;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginsInternal; import sonia.scm.plugin.PluginsInternal;
import sonia.scm.plugin.SmpArchive; import sonia.scm.plugin.SmpArchive;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
@@ -43,7 +43,7 @@ public final class PluginBootstrap {
private final ClassLoaderLifeCycle classLoaderLifeCycle; private final ClassLoaderLifeCycle classLoaderLifeCycle;
private final ServletContext servletContext; private final ServletContext servletContext;
private final Set<PluginWrapper> plugins; private final Set<InstalledPlugin> plugins;
private final PluginLoader pluginLoader; private final PluginLoader pluginLoader;
PluginBootstrap(ServletContext servletContext, ClassLoaderLifeCycle classLoaderLifeCycle) { PluginBootstrap(ServletContext servletContext, ClassLoaderLifeCycle classLoaderLifeCycle) {
@@ -58,7 +58,7 @@ public final class PluginBootstrap {
return pluginLoader; return pluginLoader;
} }
public Set<PluginWrapper> getPlugins() { public Set<InstalledPlugin> getPlugins() {
return plugins; return plugins;
} }
@@ -66,7 +66,7 @@ public final class PluginBootstrap {
return new DefaultPluginLoader(servletContext, classLoaderLifeCycle.getBootstrapClassLoader(), plugins); return new DefaultPluginLoader(servletContext, classLoaderLifeCycle.getBootstrapClassLoader(), plugins);
} }
private Set<PluginWrapper> collectPlugins() { private Set<InstalledPlugin> collectPlugins() {
try { try {
File pluginDirectory = getPluginDirectory(); File pluginDirectory = getPluginDirectory();
@@ -105,7 +105,7 @@ public final class PluginBootstrap {
PluginIndexEntry entry) throws IOException { PluginIndexEntry entry) throws IOException {
URL url = context.getResource(PLUGIN_DIRECTORY.concat(entry.getName())); URL url = context.getResource(PLUGIN_DIRECTORY.concat(entry.getName()));
SmpArchive archive = SmpArchive.create(url); SmpArchive archive = SmpArchive.create(url);
Plugin plugin = archive.getPlugin(); InstalledPluginDescriptor plugin = archive.getPlugin();
File directory = PluginsInternal.createPluginDirectory(pluginDirectory, plugin); File directory = PluginsInternal.createPluginDirectory(pluginDirectory, plugin);
File checksumFile = PluginsInternal.getChecksumFile(directory); File checksumFile = PluginsInternal.getChecksumFile(directory);

View File

@@ -85,7 +85,7 @@ public class DefaultPluginLoader implements PluginLoader
* @param installedPlugins * @param installedPlugins
*/ */
public DefaultPluginLoader(ServletContext servletContext, ClassLoader parent, public DefaultPluginLoader(ServletContext servletContext, ClassLoader parent,
Set<PluginWrapper> installedPlugins) Set<InstalledPlugin> installedPlugins)
{ {
this.installedPlugins = installedPlugins; this.installedPlugins = installedPlugins;
this.uberClassLoader = new UberClassLoader(parent, installedPlugins); this.uberClassLoader = new UberClassLoader(parent, installedPlugins);
@@ -95,7 +95,7 @@ public class DefaultPluginLoader implements PluginLoader
try try
{ {
JAXBContext context = JAXBContext.newInstance(ScmModule.class, JAXBContext context = JAXBContext.newInstance(ScmModule.class,
Plugin.class); InstalledPluginDescriptor.class);
modules = getInstalled(parent, context, PATH_MODULECONFIG); modules = getInstalled(parent, context, PATH_MODULECONFIG);
@@ -141,7 +141,7 @@ public class DefaultPluginLoader implements PluginLoader
* @return * @return
*/ */
@Override @Override
public Collection<PluginWrapper> getInstalledPlugins() public Collection<InstalledPlugin> getInstalledPlugins()
{ {
return installedPlugins; return installedPlugins;
} }
@@ -178,7 +178,7 @@ public class DefaultPluginLoader implements PluginLoader
* *
* @return * @return
*/ */
private Iterable<Plugin> unwrap() private Iterable<InstalledPluginDescriptor> unwrap()
{ {
return PluginsInternal.unwrap(installedPlugins); return PluginsInternal.unwrap(installedPlugins);
} }
@@ -227,7 +227,7 @@ public class DefaultPluginLoader implements PluginLoader
private final ExtensionProcessor extensionProcessor; private final ExtensionProcessor extensionProcessor;
/** Field description */ /** Field description */
private final Set<PluginWrapper> installedPlugins; private final Set<InstalledPlugin> installedPlugins;
/** Field description */ /** Field description */
private final Set<ScmModule> modules; private final Set<ScmModule> modules;

View File

@@ -35,685 +35,164 @@ package sonia.scm.plugin;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.github.legman.Subscribe; import com.google.common.collect.ImmutableList;
import com.google.common.base.Predicate;
import com.google.common.io.Files;
import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.SCMContextProvider; import sonia.scm.event.ScmEventBus;
import sonia.scm.cache.Cache; import sonia.scm.lifecycle.RestartEvent;
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 ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
import javax.inject.Inject;
import java.io.File; import java.util.ArrayList;
import java.io.IOException; import java.util.List;
import java.io.InputStream; import java.util.Optional;
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 java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.xml.bind.JAXB; import static sonia.scm.ContextEntry.ContextBuilder.entity;
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 * @author Sebastian Sdorra
*/ */
@Singleton @Singleton
public class DefaultPluginManager implements PluginManager public class DefaultPluginManager implements PluginManager {
{
/** Field description */ private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginManager.class);
public static final String CACHE_NAME = "sonia.cache.plugins";
/** Field description */ private final ScmEventBus eventBus;
public static final String ENCODING = "UTF-8"; private final PluginLoader loader;
private final PluginCenter center;
private final PluginInstaller installer;
private final List<PendingPluginInstallation> pendingQueue = new ArrayList<>();
/** 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<PluginInformation> FILTER_UPDATES =
new StatePluginPredicate(PluginState.UPDATE_AVAILABLE);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
* @param context
* @param configuration
* @param pluginLoader
* @param cacheManager
* @param httpClient
*/
@Inject @Inject
public DefaultPluginManager(SCMContextProvider context, public DefaultPluginManager(ScmEventBus eventBus, PluginLoader loader, PluginCenter center, PluginInstaller installer) {
ScmConfiguration configuration, PluginLoader pluginLoader, this.eventBus = eventBus;
CacheManager cacheManager, AdvancedHttpClient httpClient) this.loader = loader;
{ this.center = center;
this.context = context; this.installer = installer;
this.configuration = configuration;
this.cache = cacheManager.getCache(CACHE_NAME);
this.httpClient = httpClient;
installedPlugins = new HashMap<>();
for (PluginWrapper wrapper : pluginLoader.getInstalledPlugins())
{
Plugin plugin = wrapper.getPlugin();
PluginInformation info = plugin.getInformation();
if ((info != null) && info.isValid())
{
installedPlugins.put(info.getId(), plugin);
}
}
} }
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*/
@Override @Override
public void clearCache() public Optional<AvailablePlugin> getAvailable(String name) {
{ PluginPermissions.read().check();
if (logger.isDebugEnabled()) return center.getAvailable()
{ .stream()
logger.debug("clear plugin cache"); .filter(filterByName(name))
} .filter(this::isNotInstalled)
.map(p -> getPending(name).orElse(p))
cache.clear(); .findFirst();
} }
/** private Optional<AvailablePlugin> getPending(String name) {
* Method description return pendingQueue
* .stream()
* .map(PendingPluginInstallation::getPlugin)
* @param config .filter(filterByName(name))
*/ .findFirst();
@Subscribe
public void configChanged(ScmConfigurationChangedEvent config)
{
clearCache();
} }
/**
* Method description
*
*
* @param id
*/
@Override @Override
public void install(String id) public Optional<InstalledPlugin> getInstalled(String name) {
{ PluginPermissions.read().check();
return loader.getInstalledPlugins()
.stream()
.filter(filterByName(name))
.findFirst();
}
@Override
public List<InstalledPlugin> getInstalled() {
PluginPermissions.read().check();
return ImmutableList.copyOf(loader.getInstalledPlugins());
}
@Override
public List<AvailablePlugin> getAvailable() {
PluginPermissions.read().check();
return center.getAvailable()
.stream()
.filter(this::isNotInstalled)
.map(p -> getPending(p.getDescriptor().getInformation().getName()).orElse(p))
.collect(Collectors.toList());
}
private <T extends Plugin> Predicate<T> filterByName(String name) {
return plugin -> name.equals(plugin.getDescriptor().getInformation().getName());
}
private boolean isNotInstalled(AvailablePlugin availablePlugin) {
return !getInstalled(availablePlugin.getDescriptor().getInformation().getName()).isPresent();
}
@Override
public void install(String name, boolean restartAfterInstallation) {
PluginPermissions.manage().check(); PluginPermissions.manage().check();
List<AvailablePlugin> plugins = collectPluginsToInstall(name);
List<PendingPluginInstallation> pendingInstallations = new ArrayList<>();
for (AvailablePlugin plugin : plugins) {
try {
PendingPluginInstallation pending = installer.install(plugin);
pendingInstallations.add(pending);
} catch (PluginInstallException ex) {
cancelPending(pendingInstallations);
throw ex;
}
}
PluginCenter center = getPluginCenter(); if (!pendingInstallations.isEmpty()) {
if (restartAfterInstallation) {
for (PluginInformation plugin : center.getPlugins()) restart("plugin installation");
{ } else {
String pluginId = plugin.getId(); pendingQueue.addAll(pendingInstallations);
if (Util.isNotEmpty(pluginId) && pluginId.equals(id))
{
plugin.setState(PluginState.INSTALLED);
// ugly workaround
Plugin newPlugin = new Plugin();
// TODO check
// newPlugin.setInformation(plugin);
installedPlugins.put(id, newPlugin);
} }
} }
} }
/**
* Method description
*
*
* @param packageStream
*
* @throws IOException
*/
@Override @Override
public void installPackage(InputStream packageStream) throws IOException public void installPendingAndRestart() {
{
PluginPermissions.manage().check(); PluginPermissions.manage().check();
if (!pendingQueue.isEmpty()) {
File tempDirectory = Files.createTempDir(); restart("install pending plugins");
try
{
new ZipUnArchiver().extractArchive(packageStream, tempDirectory);
Plugin plugin = JAXB.unmarshal(new File(tempDirectory, "plugin.xml"),
Plugin.class);
PluginCondition condition = plugin.getCondition();
if ((condition != null) &&!condition.isSupported())
{
throw new PluginConditionFailedException(condition);
}
/*
* AetherPluginHandler aph = new AetherPluginHandler(this, context,
* configuration);
* Collection<PluginRepository> 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);
} }
} }
/** private void restart(String cause) {
* Method description eventBus.post(new RestartEvent(PluginManager.class, cause));
* }
*
* @param id
*/
@Override
public void uninstall(String id)
{
PluginPermissions.manage().check();
Plugin plugin = installedPlugins.get(id); private void cancelPending(List<PendingPluginInstallation> pendingInstallations) {
pendingInstallations.forEach(PendingPluginInstallation::cancel);
}
if (plugin == null) private List<AvailablePlugin> collectPluginsToInstall(String name) {
{ List<AvailablePlugin> plugins = new ArrayList<>();
String pluginPrefix = getPluginIdPrefix(id); collectPluginsToInstall(plugins, name);
return plugins;
}
for (String nid : installedPlugins.keySet()) private boolean isInstalledOrPending(String name) {
{ return getInstalled(name).isPresent() || getPending(name).isPresent();
if (nid.startsWith(pluginPrefix)) }
{
id = nid;
plugin = installedPlugins.get(nid);
break; private void collectPluginsToInstall(List<AvailablePlugin> plugins, String name) {
if (!isInstalledOrPending(name)) {
AvailablePlugin plugin = getAvailable(name).orElseThrow(() -> NotFoundException.notFound(entity(AvailablePlugin.class, name)));
Set<String> dependencies = plugin.getDescriptor().getDependencies();
if (dependencies != null) {
for (String dependency: dependencies){
collectPluginsToInstall(plugins, dependency);
} }
} }
}
if (plugin == null) plugins.add(plugin);
{ } else {
throw new PluginNotInstalledException(id.concat(" is not install")); LOG.info("plugin {} is already installed or installation is pending, skipping installation", name);
}
/*
* 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<PluginInformation> get(Predicate<PluginInformation> predicate)
{
AssertUtil.assertIsNotNull(predicate);
PluginPermissions.read().check();
Set<PluginInformation> infoSet = new HashSet<>();
filter(infoSet, getInstalled(), predicate);
filter(infoSet, getPluginCenter().getPlugins(), predicate);
return infoSet;
}
/**
* Method description
*
*
* @return
*/
@Override
public Collection<PluginInformation> getAll()
{
PluginPermissions.read().check();
Set<PluginInformation> infoSet = getInstalled();
infoSet.addAll(getPluginCenter().getPlugins());
return infoSet;
}
/**
* Method description
*
*
* @return
*/
@Override
public Collection<PluginInformation> getAvailable()
{
PluginPermissions.read().check();
Set<PluginInformation> availablePlugins = new HashSet<>();
Set<PluginInformation> centerPlugins = getPluginCenter().getPlugins();
for (PluginInformation info : centerPlugins)
{
if (!installedPlugins.containsKey(info.getName()))
{
availablePlugins.add(info);
}
}
return availablePlugins;
}
/**
* Method description
*
*
* @return
*/
@Override
public Set<PluginInformation> getAvailableUpdates()
{
PluginPermissions.read().check();
return get(FILTER_UPDATES);
}
/**
* Method description
*
*
* @return
*/
@Override
public Set<PluginInformation> getInstalled()
{
PluginPermissions.read().check();
Set<PluginInformation> infoSet = new LinkedHashSet<>();
for (Plugin 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<PluginInformation> target,
Collection<PluginInformation> source,
Predicate<PluginInformation> 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<PluginInformation> infoSet = pc.getPlugins();
if (infoSet != null)
{
Iterator<PluginInformation> 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<PluginInformation> 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 (Plugin 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<String, PluginCenter> cache;
/** Field description */
private final AdvancedHttpClient httpClient;
/** Field description */
private final ScmConfiguration configuration;
/** Field description */
private final SCMContextProvider context;
/** Field description */
private final Map<String, Plugin> installedPlugins;
} }

View File

@@ -71,11 +71,11 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins) { public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<InstalledPlugin> plugins) {
this(servletContext, plugins, SCMContext.getContext().getStage()); this(servletContext, plugins, SCMContext.getContext().getStage());
} }
public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins, Stage stage) { public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<InstalledPlugin> plugins, Stage stage) {
this.servletContext = servletContext; this.servletContext = servletContext;
this.plugins = plugins; this.plugins = plugins;
this.cache = createCache(stage); this.cache = createCache(stage);
@@ -153,7 +153,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
resources.add(ctxResource); resources.add(ctxResource);
} }
for (PluginWrapper wrapper : plugins) for (InstalledPlugin wrapper : plugins)
{ {
URL resource = nonDirectory(wrapper.getWebResourceLoader().getResource(path)); URL resource = nonDirectory(wrapper.getWebResourceLoader().getResource(path));
@@ -205,7 +205,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
if (resource == null) if (resource == null)
{ {
for (PluginWrapper wrapper : plugins) for (InstalledPlugin wrapper : plugins)
{ {
resource = nonDirectory(wrapper.getWebResourceLoader().getResource(path)); resource = nonDirectory(wrapper.getWebResourceLoader().getResource(path));
@@ -259,7 +259,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader
private final Cache<String, URL> cache; private final Cache<String, URL> cache;
/** Field description */ /** Field description */
private final Iterable<PluginWrapper> plugins; private final Iterable<InstalledPlugin> plugins;
/** Field description */ /** Field description */
private final ServletContext servletContext; private final ServletContext servletContext;

View File

@@ -63,7 +63,7 @@ public final class ExplodedSmp implements Comparable<ExplodedSmp>
* @param path * @param path
* @param plugin * @param plugin
*/ */
ExplodedSmp(Path path, Plugin plugin) ExplodedSmp(Path path, InstalledPluginDescriptor plugin)
{ {
logger.trace("create exploded scm for plugin {} and dependencies {}", plugin.getInformation().getName(), plugin.getDependencies()); logger.trace("create exploded scm for plugin {} and dependencies {}", plugin.getInformation().getName(), plugin.getDependencies());
this.path = path; this.path = path;
@@ -163,7 +163,7 @@ public final class ExplodedSmp implements Comparable<ExplodedSmp>
* *
* @return plugin descriptor * @return plugin descriptor
*/ */
public Plugin getPlugin() public InstalledPluginDescriptor getPlugin()
{ {
return plugin; return plugin;
} }
@@ -202,5 +202,5 @@ public final class ExplodedSmp implements Comparable<ExplodedSmp>
private final Path path; private final Path path;
/** plugin object */ /** plugin object */
private final Plugin plugin; private final InstalledPluginDescriptor plugin;
} }

View File

@@ -1,64 +0,0 @@
/**
* 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;
import com.google.common.base.Predicate;
/**
*
* @author Sebastian Sdorra
*/
public class OverviewPluginPredicate implements Predicate<PluginInformation>
{
/** Field description */
public static final OverviewPluginPredicate INSTANCE =
new OverviewPluginPredicate();
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param plugin
*
* @return
*/
@Override
public boolean apply(PluginInformation plugin)
{
return plugin.getState() != PluginState.NEWER_VERSION_INSTALLED;
}
}

View File

@@ -0,0 +1,35 @@
package sonia.scm.plugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class PendingPluginInstallation {
private static final Logger LOG = LoggerFactory.getLogger(PendingPluginInstallation.class);
private final AvailablePlugin plugin;
private final Path file;
PendingPluginInstallation(AvailablePlugin plugin, Path file) {
this.plugin = plugin;
this.file = file;
}
public AvailablePlugin getPlugin() {
return plugin;
}
void cancel() {
String name = plugin.getDescriptor().getInformation().getName();
LOG.info("cancel installation of plugin {}", name);
try {
Files.delete(file);
} catch (IOException ex) {
throw new PluginFailedToCancelInstallationException("failed to cancel installation of plugin " + name, ex);
}
}
}

View File

@@ -0,0 +1,55 @@
package sonia.scm.plugin;
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.util.HttpUtil;
import sonia.scm.util.SystemUtil;
import javax.inject.Inject;
import java.util.Set;
public class PluginCenter {
private static final String CACHE_NAME = "sonia.cache.plugins";
private static final Logger LOG = LoggerFactory.getLogger(PluginCenter.class);
private final SCMContextProvider context;
private final ScmConfiguration configuration;
private final PluginCenterLoader loader;
private final Cache<String, Set<AvailablePlugin>> cache;
@Inject
public PluginCenter(SCMContextProvider context, CacheManager cacheManager, ScmConfiguration configuration, PluginCenterLoader loader) {
this.context = context;
this.configuration = configuration;
this.loader = loader;
this.cache = cacheManager.getCache(CACHE_NAME);
}
synchronized Set<AvailablePlugin> getAvailable() {
String url = buildPluginUrl(configuration.getPluginUrl());
Set<AvailablePlugin> plugins = cache.get(url);
if (plugins == null) {
LOG.debug("no cached available plugins found, start fetching");
plugins = loader.load(url);
cache.put(url, plugins);
} else {
LOG.debug("return available plugins from cache");
}
return plugins;
}
private String buildPluginUrl(String url) {
String os = HttpUtil.encode(SystemUtil.getOS());
String arch = SystemUtil.getArch();
return url.replace("{version}", context.getVersion())
.replace("{os}", os)
.replace("{arch}", arch);
}
}

View File

@@ -3,6 +3,7 @@ package sonia.scm.plugin;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
@@ -11,6 +12,7 @@ import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
@XmlRootElement @XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@@ -56,8 +58,8 @@ public final class PluginCenterDto implements Serializable {
@XmlElement(name = "conditions") @XmlElement(name = "conditions")
private Condition conditions; private Condition conditions;
@XmlElement(name = "dependecies") @XmlElement(name = "dependencies")
private Dependency dependencies; private Set<String> dependencies;
@XmlElement(name = "_links") @XmlElement(name = "_links")
private Map<String, Link> links; private Map<String, Link> links;
@@ -75,15 +77,9 @@ public final class PluginCenterDto implements Serializable {
} }
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "dependencies")
@Getter @Getter
@NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
static class Dependency {
private String name;
}
@XmlAccessorType(XmlAccessType.FIELD)
@Getter
static class Link { static class Link {
private String href; private String href;
} }

View File

@@ -1,26 +1,27 @@
package sonia.scm.plugin; package sonia.scm.plugin;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
@Mapper @Mapper
public interface PluginCenterDtoMapper { public abstract class PluginCenterDtoMapper {
@Mapping(source = "conditions", target = "condition") static final PluginCenterDtoMapper INSTANCE = Mappers.getMapper(PluginCenterDtoMapper.class);
PluginInformation map(PluginCenterDto.Plugin plugin);
PluginCondition map(PluginCenterDto.Condition condition); abstract PluginInformation map(PluginCenterDto.Plugin plugin);
abstract PluginCondition map(PluginCenterDto.Condition condition);
static Set<PluginInformation> map(List<PluginCenterDto.Plugin> dtos) { Set<AvailablePlugin> map(PluginCenterDto pluginCenterDto) {
PluginCenterDtoMapper mapper = Mappers.getMapper(PluginCenterDtoMapper.class); Set<AvailablePlugin> plugins = new HashSet<>();
Set<PluginInformation> plugins = new HashSet<>(); for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) {
for (PluginCenterDto.Plugin plugin : dtos) { String url = plugin.getLinks().get("download").getHref();
plugins.add(mapper.map(plugin)); AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(
map(plugin), map(plugin.getConditions()), plugin.getDependencies(), url, plugin.getSha256()
);
plugins.add(new AvailablePlugin(descriptor));
} }
return plugins; return plugins;
} }

View File

@@ -0,0 +1,42 @@
package sonia.scm.plugin;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.net.ahc.AdvancedHttpClient;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
class PluginCenterLoader {
private static final Logger LOG = LoggerFactory.getLogger(PluginCenterLoader.class);
private final AdvancedHttpClient client;
private final PluginCenterDtoMapper mapper;
@Inject
public PluginCenterLoader(AdvancedHttpClient client) {
this(client, PluginCenterDtoMapper.INSTANCE);
}
@VisibleForTesting
PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper) {
this.client = client;
this.mapper = mapper;
}
Set<AvailablePlugin> load(String url) {
try {
LOG.info("fetch plugins from {}", url);
PluginCenterDto pluginCenterDto = client.get(url).request().contentFromJson(PluginCenterDto.class);
return mapper.map(pluginCenterDto);
} catch (IOException ex) {
LOG.error("failed to load plugins from plugin center, returning empty list");
return Collections.emptySet();
}
}
}

View File

@@ -0,0 +1,7 @@
package sonia.scm.plugin;
public class PluginChecksumMismatchException extends PluginInstallException {
public PluginChecksumMismatchException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package sonia.scm.plugin;
public class PluginDownloadException extends PluginInstallException {
public PluginDownloadException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,7 @@
package sonia.scm.plugin;
public class PluginFailedToCancelInstallationException extends RuntimeException {
public PluginFailedToCancelInstallationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,12 @@
package sonia.scm.plugin;
public class PluginInstallException extends RuntimeException {
public PluginInstallException(String message) {
super(message);
}
public PluginInstallException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,76 @@
package sonia.scm.plugin;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.hash.HashingInputStream;
import sonia.scm.SCMContextProvider;
import sonia.scm.net.ahc.AdvancedHttpClient;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
@SuppressWarnings("UnstableApiUsage") // guava hash is marked as unstable
class PluginInstaller {
private final SCMContextProvider context;
private final AdvancedHttpClient client;
@Inject
public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client) {
this.context = context;
this.client = client;
}
@SuppressWarnings("squid:S4790") // hashing should be safe
public PendingPluginInstallation install(AvailablePlugin plugin) {
Path file = null;
try (HashingInputStream input = new HashingInputStream(Hashing.sha256(), download(plugin))) {
file = createFile(plugin);
Files.copy(input, file);
verifyChecksum(plugin, input.hash(), file);
return new PendingPluginInstallation(plugin.install(), file);
} catch (IOException ex) {
cleanup(file);
throw new PluginDownloadException("failed to download plugin", ex);
}
}
private void cleanup(Path file) {
try {
if (file != null) {
Files.deleteIfExists(file);
}
} catch (IOException e) {
throw new PluginInstallException("failed to cleanup, after broken installation");
}
}
private void verifyChecksum(AvailablePlugin plugin, HashCode hash, Path file) {
Optional<String> checksum = plugin.getDescriptor().getChecksum();
if (checksum.isPresent()) {
String calculatedChecksum = hash.toString();
if (!checksum.get().equalsIgnoreCase(calculatedChecksum)) {
cleanup(file);
throw new PluginChecksumMismatchException(
String.format("downloaded plugin checksum %s does not match expected %s", calculatedChecksum, checksum.get())
);
}
}
}
private InputStream download(AvailablePlugin plugin) throws IOException {
return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream();
}
private Path createFile(AvailablePlugin plugin) throws IOException {
Path directory = context.resolve(Paths.get("plugins"));
Files.createDirectories(directory);
return directory.resolve(plugin.getDescriptor().getInformation().getName() + ".smp");
}
}

View File

@@ -157,7 +157,7 @@ public final class PluginNode
* *
* @return * @return
*/ */
public PluginWrapper getWrapper() public InstalledPlugin getWrapper()
{ {
return wrapper; return wrapper;
} }
@@ -170,7 +170,7 @@ public final class PluginNode
* *
* @param wrapper * @param wrapper
*/ */
public void setWrapper(PluginWrapper wrapper) public void setWrapper(InstalledPlugin wrapper)
{ {
this.wrapper = wrapper; this.wrapper = wrapper;
} }
@@ -192,5 +192,5 @@ public final class PluginNode
private final ExplodedSmp plugin; private final ExplodedSmp plugin;
/** Field description */ /** Field description */
private PluginWrapper wrapper; private InstalledPlugin wrapper;
} }

View File

@@ -123,7 +123,7 @@ public final class PluginProcessor
try try
{ {
this.context = JAXBContext.newInstance(Plugin.class); this.context = JAXBContext.newInstance(InstalledPluginDescriptor.class);
} }
catch (JAXBException ex) catch (JAXBException ex)
{ {
@@ -160,7 +160,7 @@ public final class PluginProcessor
* *
* @throws IOException * @throws IOException
*/ */
public Set<PluginWrapper> collectPlugins(ClassLoader classLoader) public Set<InstalledPlugin> collectPlugins(ClassLoader classLoader)
throws IOException throws IOException
{ {
logger.info("collect plugins"); logger.info("collect plugins");
@@ -187,7 +187,7 @@ public final class PluginProcessor
logger.trace("create plugin wrappers and build classloaders"); logger.trace("create plugin wrappers and build classloaders");
Set<PluginWrapper> wrappers = createPluginWrappers(classLoader, rootNodes); Set<InstalledPlugin> wrappers = createPluginWrappers(classLoader, rootNodes);
logger.debug("collected {} plugins", wrappers.size()); logger.debug("collected {} plugins", wrappers.size());
@@ -204,7 +204,7 @@ public final class PluginProcessor
* *
* @throws IOException * @throws IOException
*/ */
private void appendPluginWrapper(Set<PluginWrapper> plugins, private void appendPluginWrapper(Set<InstalledPlugin> plugins,
ClassLoader classLoader, PluginNode node) ClassLoader classLoader, PluginNode node)
throws IOException throws IOException
{ {
@@ -217,7 +217,7 @@ public final class PluginProcessor
for (PluginNode parent : node.getParents()) for (PluginNode parent : node.getParents())
{ {
PluginWrapper wrapper = parent.getWrapper(); InstalledPlugin wrapper = parent.getWrapper();
if (wrapper != null) if (wrapper != null)
{ {
@@ -236,8 +236,8 @@ public final class PluginProcessor
} }
PluginWrapper plugin = InstalledPlugin plugin =
createPluginWrapper(createParentPluginClassLoader(classLoader, parents), createPlugin(createParentPluginClassLoader(classLoader, parents),
smp); smp);
if (plugin != null) if (plugin != null)
@@ -257,7 +257,7 @@ public final class PluginProcessor
* *
* @throws IOException * @throws IOException
*/ */
private void appendPluginWrappers(Set<PluginWrapper> plugins, private void appendPluginWrappers(Set<InstalledPlugin> plugins,
ClassLoader classLoader, List<PluginNode> nodes) ClassLoader classLoader, List<PluginNode> nodes)
throws IOException throws IOException
{ {
@@ -371,7 +371,7 @@ public final class PluginProcessor
ClassLoader classLoader; ClassLoader classLoader;
URL[] urlArray = urls.toArray(new URL[urls.size()]); URL[] urlArray = urls.toArray(new URL[urls.size()]);
Plugin plugin = smp.getPlugin(); InstalledPluginDescriptor plugin = smp.getPlugin();
String id = plugin.getInformation().getName(false); String id = plugin.getInformation().getName(false);
@@ -431,73 +431,36 @@ public final class PluginProcessor
return result; return result;
} }
/** private InstalledPluginDescriptor createDescriptor(ClassLoader classLoader, Path descriptor) {
* Method description
*
*
*
* @param classLoader
* @param descriptor
*
* @return
*/
private Plugin createPlugin(ClassLoader classLoader, Path descriptor)
{
ClassLoader ctxcl = Thread.currentThread().getContextClassLoader(); ClassLoader ctxcl = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(classLoader); Thread.currentThread().setContextClassLoader(classLoader);
try {
try return (InstalledPluginDescriptor) context.createUnmarshaller().unmarshal(descriptor.toFile());
{ } catch (JAXBException ex) {
return (Plugin) context.createUnmarshaller().unmarshal( throw new PluginLoadException("could not load plugin desriptor ".concat(descriptor.toString()), ex);
descriptor.toFile()); } finally {
}
catch (JAXBException ex)
{
throw new PluginLoadException(
"could not load plugin desriptor ".concat(descriptor.toString()), ex);
}
finally
{
Thread.currentThread().setContextClassLoader(ctxcl); Thread.currentThread().setContextClassLoader(ctxcl);
} }
} }
/** private InstalledPlugin createPlugin(ClassLoader classLoader, ExplodedSmp smp) throws IOException {
* Method description InstalledPlugin plugin = null;
*
*
* @param classLoader
* @param smp
*
* @return
*
* @throws IOException
*/
private PluginWrapper createPluginWrapper(ClassLoader classLoader,
ExplodedSmp smp)
throws IOException
{
PluginWrapper wrapper = null;
Path directory = smp.getPath(); Path directory = smp.getPath();
Path descriptor = directory.resolve(PluginConstants.FILE_DESCRIPTOR); Path descriptorPath = directory.resolve(PluginConstants.FILE_DESCRIPTOR);
if (Files.exists(descriptor)) if (Files.exists(descriptorPath)) {
{
ClassLoader cl = createClassLoader(classLoader, smp); ClassLoader cl = createClassLoader(classLoader, smp);
Plugin plugin = createPlugin(cl, descriptor); InstalledPluginDescriptor descriptor = createDescriptor(cl, descriptorPath);
WebResourceLoader resourceLoader = createWebResourceLoader(directory); WebResourceLoader resourceLoader = createWebResourceLoader(directory);
wrapper = new PluginWrapper(plugin, cl, resourceLoader, directory); plugin = new InstalledPlugin(descriptor, cl, resourceLoader, directory);
} } else {
else
{
logger.warn("found plugin directory without plugin descriptor"); logger.warn("found plugin directory without plugin descriptor");
} }
return wrapper; return plugin;
} }
/** /**
@@ -512,11 +475,11 @@ public final class PluginProcessor
* *
* @throws IOException * @throws IOException
*/ */
private Set<PluginWrapper> createPluginWrappers(ClassLoader classLoader, private Set<InstalledPlugin> createPluginWrappers(ClassLoader classLoader,
List<PluginNode> rootNodes) List<PluginNode> rootNodes)
throws IOException throws IOException
{ {
Set<PluginWrapper> plugins = Sets.newHashSet(); Set<InstalledPlugin> plugins = Sets.newHashSet();
appendPluginWrappers(plugins, classLoader, rootNodes); appendPluginWrappers(plugins, classLoader, rootNodes);

View File

@@ -86,7 +86,7 @@ public final class PluginTree
for (ExplodedSmp smp : smpOrdered) for (ExplodedSmp smp : smpOrdered)
{ {
Plugin plugin = smp.getPlugin(); InstalledPluginDescriptor plugin = smp.getPlugin();
if (plugin.getScmVersion() != SCM_VERSION) if (plugin.getScmVersion() != SCM_VERSION)
{ {

View File

@@ -87,8 +87,8 @@ public final class PluginsInternal
* *
* @throws IOException * @throws IOException
*/ */
public static Set<PluginWrapper> collectPlugins(ClassLoaderLifeCycle classLoaderLifeCycle, public static Set<InstalledPlugin> collectPlugins(ClassLoaderLifeCycle classLoaderLifeCycle,
Path directory) Path directory)
throws IOException throws IOException
{ {
PluginProcessor processor = new PluginProcessor(classLoaderLifeCycle, directory); PluginProcessor processor = new PluginProcessor(classLoaderLifeCycle, directory);
@@ -105,7 +105,7 @@ public final class PluginsInternal
* *
* @return * @return
*/ */
public static File createPluginDirectory(File parent, Plugin plugin) public static File createPluginDirectory(File parent, InstalledPluginDescriptor plugin)
{ {
PluginInformation info = plugin.getInformation(); PluginInformation info = plugin.getInformation();
@@ -159,7 +159,7 @@ public final class PluginsInternal
* *
* @return * @return
*/ */
public static Iterable<Plugin> unwrap(Iterable<PluginWrapper> wrapped) public static Iterable<InstalledPluginDescriptor> unwrap(Iterable<InstalledPlugin> wrapped)
{ {
return Iterables.transform(wrapped, new Unwrap()); return Iterables.transform(wrapped, new Unwrap());
} }
@@ -188,7 +188,7 @@ public final class PluginsInternal
* @version Enter version here..., 14/06/05 * @version Enter version here..., 14/06/05
* @author Enter your name here... * @author Enter your name here...
*/ */
private static class Unwrap implements Function<PluginWrapper, Plugin> private static class Unwrap implements Function<InstalledPlugin, InstalledPluginDescriptor>
{ {
/** /**
@@ -200,9 +200,9 @@ public final class PluginsInternal
* @return * @return
*/ */
@Override @Override
public Plugin apply(PluginWrapper wrapper) public InstalledPluginDescriptor apply(InstalledPlugin wrapper)
{ {
return wrapper.getPlugin(); return wrapper.getDescriptor();
} }
} }
} }

View File

@@ -65,7 +65,7 @@ public final class UberClassLoader extends ClassLoader
* @param parent * @param parent
* @param plugins * @param plugins
*/ */
public UberClassLoader(ClassLoader parent, Iterable<PluginWrapper> plugins) public UberClassLoader(ClassLoader parent, Iterable<InstalledPlugin> plugins)
{ {
super(parent); super(parent);
this.plugins = plugins; this.plugins = plugins;
@@ -87,7 +87,7 @@ public final class UberClassLoader extends ClassLoader
} }
private Class<?> findClassInPlugins(String name) throws ClassNotFoundException { private Class<?> findClassInPlugins(String name) throws ClassNotFoundException {
for (PluginWrapper plugin : plugins) { for (InstalledPlugin plugin : plugins) {
Class<?> clazz = findClass(plugin.getClassLoader(), name); Class<?> clazz = findClass(plugin.getClassLoader(), name);
if (clazz != null) { if (clazz != null) {
return clazz; return clazz;
@@ -119,7 +119,7 @@ public final class UberClassLoader extends ClassLoader
{ {
URL url = null; URL url = null;
for (PluginWrapper plugin : plugins) for (InstalledPlugin plugin : plugins)
{ {
ClassLoader cl = plugin.getClassLoader(); ClassLoader cl = plugin.getClassLoader();
@@ -149,7 +149,7 @@ public final class UberClassLoader extends ClassLoader
{ {
List<URL> urls = Lists.newArrayList(); List<URL> urls = Lists.newArrayList();
for (PluginWrapper plugin : plugins) for (InstalledPlugin plugin : plugins)
{ {
ClassLoader cl = plugin.getClassLoader(); ClassLoader cl = plugin.getClassLoader();
@@ -194,5 +194,5 @@ public final class UberClassLoader extends ClassLoader
Maps.newConcurrentMap(); Maps.newConcurrentMap();
/** Field description */ /** Field description */
private final Iterable<PluginWrapper> plugins; private final Iterable<InstalledPlugin> plugins;
} }

View File

@@ -16,9 +16,11 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.AvailablePluginDescriptor;
import sonia.scm.plugin.PluginCondition;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginInformation;
import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginManager;
import sonia.scm.plugin.PluginState;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Provider; import javax.inject.Provider;
@@ -27,6 +29,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Collections; import java.util.Collections;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -87,10 +90,10 @@ class AvailablePluginResourceTest {
@Test @Test
void getAvailablePlugins() throws URISyntaxException, UnsupportedEncodingException { void getAvailablePlugins() throws URISyntaxException, UnsupportedEncodingException {
PluginInformation pluginInformation = new PluginInformation(); AvailablePlugin plugin = createPlugin();
pluginInformation.setState(PluginState.AVAILABLE);
when(pluginManager.getAvailable()).thenReturn(Collections.singletonList(pluginInformation)); when(pluginManager.getAvailable()).thenReturn(Collections.singletonList(plugin));
when(collectionMapper.map(Collections.singletonList(pluginInformation))).thenReturn(new MockedResultDto()); when(collectionMapper.mapAvailable(Collections.singletonList(plugin))).thenReturn(new MockedResultDto());
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available");
request.accept(VndMediaType.PLUGIN_COLLECTION); request.accept(VndMediaType.PLUGIN_COLLECTION);
@@ -105,16 +108,18 @@ class AvailablePluginResourceTest {
@Test @Test
void getAvailablePlugin() throws UnsupportedEncodingException, URISyntaxException { void getAvailablePlugin() throws UnsupportedEncodingException, URISyntaxException {
PluginInformation pluginInformation = new PluginInformation(); PluginInformation pluginInformation = new PluginInformation();
pluginInformation.setState(PluginState.AVAILABLE);
pluginInformation.setName("pluginName"); pluginInformation.setName("pluginName");
pluginInformation.setVersion("2.0.0"); pluginInformation.setVersion("2.0.0");
when(pluginManager.getAvailable()).thenReturn(Collections.singletonList(pluginInformation));
AvailablePlugin plugin = createPlugin(pluginInformation);
when(pluginManager.getAvailable("pluginName")).thenReturn(Optional.of(plugin));
PluginDto pluginDto = new PluginDto(); PluginDto pluginDto = new PluginDto();
pluginDto.setName("pluginName"); pluginDto.setName("pluginName");
when(mapper.map(pluginInformation)).thenReturn(pluginDto); when(mapper.mapAvailable(plugin)).thenReturn(pluginDto);
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available/pluginName/2.0.0"); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available/pluginName");
request.accept(VndMediaType.PLUGIN); request.accept(VndMediaType.PLUGIN);
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
@@ -126,15 +131,36 @@ class AvailablePluginResourceTest {
@Test @Test
void installPlugin() throws URISyntaxException { void installPlugin() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/pluginName/2.0.0/install"); MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/pluginName/install");
request.accept(VndMediaType.PLUGIN);
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
verify(pluginManager).install("pluginName:2.0.0"); verify(pluginManager).install("pluginName", false);
assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus());
} }
@Test
void installPendingPlugin() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/install-pending");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
verify(pluginManager).installPendingAndRestart();
assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus());
}
}
private AvailablePlugin createPlugin() {
return createPlugin(new PluginInformation());
}
private AvailablePlugin createPlugin(PluginInformation pluginInformation) {
AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(
pluginInformation, new PluginCondition(), Collections.emptySet(), "https://download.hitchhiker.com", null
);
return new AvailablePlugin(descriptor);
} }
@Nested @Nested
@@ -156,7 +182,7 @@ class AvailablePluginResourceTest {
@Test @Test
void shouldNotGetAvailablePluginIfMissingPermission() throws URISyntaxException { void shouldNotGetAvailablePluginIfMissingPermission() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available/pluginName/2.0.0"); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available/pluginName");
request.accept(VndMediaType.PLUGIN); request.accept(VndMediaType.PLUGIN);
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
@@ -166,7 +192,7 @@ class AvailablePluginResourceTest {
@Test @Test
void shouldNotInstallPluginIfMissingPermission() throws URISyntaxException { void shouldNotInstallPluginIfMissingPermission() throws URISyntaxException {
ThreadContext.unbindSubject(); ThreadContext.unbindSubject();
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/pluginName/2.0.0/install"); MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/pluginName/install");
request.accept(VndMediaType.PLUGIN); request.accept(VndMediaType.PLUGIN);
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();

View File

@@ -16,11 +16,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.plugin.Plugin; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.InstalledPluginDescriptor;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginInformation;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginManager;
import sonia.scm.plugin.PluginState;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Provider; import javax.inject.Provider;
@@ -28,12 +27,12 @@ import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Collections; import java.util.Collections;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class InstalledPluginResourceTest { class InstalledPluginResourceTest {
@@ -46,15 +45,15 @@ class InstalledPluginResourceTest {
@Mock @Mock
Provider<AvailablePluginResource> availablePluginResourceProvider; Provider<AvailablePluginResource> availablePluginResourceProvider;
@Mock
private PluginLoader pluginLoader;
@Mock @Mock
private PluginDtoCollectionMapper collectionMapper; private PluginDtoCollectionMapper collectionMapper;
@Mock @Mock
private PluginDtoMapper mapper; private PluginDtoMapper mapper;
@Mock
private PluginManager pluginManager;
@InjectMocks @InjectMocks
InstalledPluginResource installedPluginResource; InstalledPluginResource installedPluginResource;
@@ -86,9 +85,9 @@ class InstalledPluginResourceTest {
@Test @Test
void getInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException { void getInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException {
PluginWrapper pluginWrapper = new PluginWrapper(null, null, null, null); InstalledPlugin installedPlugin = createPlugin();
when(pluginLoader.getInstalledPlugins()).thenReturn(Collections.singletonList(pluginWrapper)); when(pluginManager.getInstalled()).thenReturn(Collections.singletonList(installedPlugin));
when(collectionMapper.map(Collections.singletonList(pluginWrapper))).thenReturn(new MockedResultDto()); when(collectionMapper.mapInstalled(Collections.singletonList(installedPlugin))).thenReturn(new MockedResultDto());
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed"); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed");
request.accept(VndMediaType.PLUGIN_COLLECTION); request.accept(VndMediaType.PLUGIN_COLLECTION);
@@ -105,14 +104,13 @@ class InstalledPluginResourceTest {
PluginInformation pluginInformation = new PluginInformation(); PluginInformation pluginInformation = new PluginInformation();
pluginInformation.setVersion("2.0.0"); pluginInformation.setVersion("2.0.0");
pluginInformation.setName("pluginName"); pluginInformation.setName("pluginName");
pluginInformation.setState(PluginState.INSTALLED); InstalledPlugin installedPlugin = createPlugin(pluginInformation);
Plugin plugin = new Plugin(2, pluginInformation, null, null, false, null);
PluginWrapper pluginWrapper = new PluginWrapper(plugin, null, null, null); when(pluginManager.getInstalled("pluginName")).thenReturn(Optional.of(installedPlugin));
when(pluginLoader.getInstalledPlugins()).thenReturn(Collections.singletonList(pluginWrapper));
PluginDto pluginDto = new PluginDto(); PluginDto pluginDto = new PluginDto();
pluginDto.setName("pluginName"); pluginDto.setName("pluginName");
when(mapper.map(pluginWrapper)).thenReturn(pluginDto); when(mapper.mapInstalled(installedPlugin)).thenReturn(pluginDto);
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed/pluginName"); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed/pluginName");
request.accept(VndMediaType.PLUGIN); request.accept(VndMediaType.PLUGIN);
@@ -125,6 +123,18 @@ class InstalledPluginResourceTest {
} }
} }
private InstalledPlugin createPlugin() {
return createPlugin(new PluginInformation());
}
private InstalledPlugin createPlugin(PluginInformation information) {
InstalledPlugin plugin = mock(InstalledPlugin.class);
InstalledPluginDescriptor descriptor = mock(InstalledPluginDescriptor.class);
lenient().when(descriptor.getInformation()).thenReturn(information);
lenient().when(plugin.getDescriptor()).thenReturn(descriptor);
return plugin;
}
@Nested @Nested
class WithoutAuthorization { class WithoutAuthorization {

View File

@@ -1,16 +1,26 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import com.google.common.collect.ImmutableSet;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.AvailablePluginDescriptor;
import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginInformation;
import sonia.scm.plugin.PluginState;
import java.net.URI; import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class PluginDtoMapperTest { class PluginDtoMapperTest {
@@ -21,11 +31,25 @@ class PluginDtoMapperTest {
@InjectMocks @InjectMocks
private PluginDtoMapperImpl mapper; private PluginDtoMapperImpl mapper;
@Mock
private Subject subject;
@BeforeEach
void bindSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void unbindSubject() {
ThreadContext.unbindSubject();
}
@Test @Test
void shouldMapInformation() { void shouldMapInformation() {
PluginInformation information = createPluginInformation(); PluginInformation information = createPluginInformation();
PluginDto dto = mapper.map(information); PluginDto dto = new PluginDto();
mapper.map(information, dto);
assertThat(dto.getName()).isEqualTo("scm-cas-plugin"); assertThat(dto.getName()).isEqualTo("scm-cas-plugin");
assertThat(dto.getVersion()).isEqualTo("1.0.0"); assertThat(dto.getVersion()).isEqualTo("1.0.0");
@@ -48,41 +72,76 @@ class PluginDtoMapperTest {
@Test @Test
void shouldAppendInstalledSelfLink() { void shouldAppendInstalledSelfLink() {
PluginInformation information = createPluginInformation(); InstalledPlugin plugin = createInstalled();
information.setState(PluginState.INSTALLED);
PluginDto dto = mapper.map(information); PluginDto dto = mapper.mapInstalled(plugin);
assertThat(dto.getLinks().getLinkBy("self").get().getHref()) assertThat(dto.getLinks().getLinkBy("self").get().getHref())
.isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin"); .isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin");
} }
private InstalledPlugin createInstalled(PluginInformation information) {
InstalledPlugin plugin = mock(InstalledPlugin.class, Answers.RETURNS_DEEP_STUBS);
when(plugin.getDescriptor().getInformation()).thenReturn(information);
return plugin;
}
@Test @Test
void shouldAppendAvailableSelfLink() { void shouldAppendAvailableSelfLink() {
PluginInformation information = createPluginInformation(); AvailablePlugin plugin = createAvailable();
information.setState(PluginState.AVAILABLE);
PluginDto dto = mapper.map(information); PluginDto dto = mapper.mapAvailable(plugin);
assertThat(dto.getLinks().getLinkBy("self").get().getHref()) assertThat(dto.getLinks().getLinkBy("self").get().getHref())
.isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/1.0.0"); .isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin");
}
@Test
void shouldNotAppendInstallLinkWithoutPermissions() {
AvailablePlugin plugin = createAvailable();
PluginDto dto = mapper.mapAvailable(plugin);
assertThat(dto.getLinks().getLinkBy("install")).isEmpty();
} }
@Test @Test
void shouldAppendInstallLink() { void shouldAppendInstallLink() {
PluginInformation information = createPluginInformation(); when(subject.isPermitted("plugin:manage")).thenReturn(true);
information.setState(PluginState.AVAILABLE); AvailablePlugin plugin = createAvailable();
PluginDto dto = mapper.map(information); PluginDto dto = mapper.mapAvailable(plugin);
assertThat(dto.getLinks().getLinkBy("install").get().getHref()) assertThat(dto.getLinks().getLinkBy("install").get().getHref())
.isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/1.0.0/install"); .isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install");
} }
@Test @Test
void shouldReturnMiscellaneousIfCategoryIsNull() { void shouldReturnMiscellaneousIfCategoryIsNull() {
PluginInformation information = createPluginInformation(); PluginInformation information = createPluginInformation();
information.setCategory(null); information.setCategory(null);
AvailablePlugin plugin = createAvailable(information);
PluginDto dto = mapper.map(information); PluginDto dto = mapper.mapAvailable(plugin);
assertThat(dto.getCategory()).isEqualTo("Miscellaneous"); assertThat(dto.getCategory()).isEqualTo("Miscellaneous");
} }
@Test
void shouldAppendDependencies() {
AvailablePlugin plugin = createAvailable();
when(plugin.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("one", "two"));
PluginDto dto = mapper.mapAvailable(plugin);
assertThat(dto.getDependencies()).containsOnly("one", "two");
}
private InstalledPlugin createInstalled() {
return createInstalled(createPluginInformation());
}
private AvailablePlugin createAvailable() {
return createAvailable(createPluginInformation());
}
private AvailablePlugin createAvailable(PluginInformation information) {
AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class);
when(descriptor.getInformation()).thenReturn(information);
return new AvailablePlugin(descriptor);
}
} }

View File

@@ -170,7 +170,7 @@ public class UIRootResourceTest {
assertTrue(response.getContentAsString().contains("/scm/my/bundle.js")); assertTrue(response.getContentAsString().contains("/scm/my/bundle.js"));
} }
private void mockPlugins(PluginWrapper... plugins) { private void mockPlugins(InstalledPlugin... plugins) {
when(pluginLoader.getInstalledPlugins()).thenReturn(Lists.newArrayList(plugins)); when(pluginLoader.getInstalledPlugins()).thenReturn(Lists.newArrayList(plugins));
} }
@@ -180,16 +180,16 @@ public class UIRootResourceTest {
return new PluginResources(scripts, styles); return new PluginResources(scripts, styles);
} }
private PluginWrapper mockPlugin(String id) { private InstalledPlugin mockPlugin(String id) {
return mockPlugin(id, id, null); return mockPlugin(id, id, null);
} }
private PluginWrapper mockPlugin(String id, String name, PluginResources pluginResources) { private InstalledPlugin mockPlugin(String id, String name, PluginResources pluginResources) {
PluginWrapper wrapper = mock(PluginWrapper.class); InstalledPlugin wrapper = mock(InstalledPlugin.class);
when(wrapper.getId()).thenReturn(id); when(wrapper.getId()).thenReturn(id);
Plugin plugin = mock(Plugin.class); InstalledPluginDescriptor plugin = mock(InstalledPluginDescriptor.class);
when(wrapper.getPlugin()).thenReturn(plugin); when(wrapper.getDescriptor()).thenReturn(plugin);
when(plugin.getResources()).thenReturn(pluginResources); when(plugin.getResources()).thenReturn(pluginResources);
PluginInformation information = mock(PluginInformation.class); PluginInformation information = mock(PluginInformation.class);

View File

@@ -0,0 +1,386 @@
package sonia.scm.plugin;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.NotFoundException;
import sonia.scm.event.ScmEventBus;
import sonia.scm.lifecycle.RestartEvent;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.in;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DefaultPluginManagerTest {
@Mock
private ScmEventBus eventBus;
@Mock
private PluginLoader loader;
@Mock
private PluginCenter center;
@Mock
private PluginInstaller installer;
@InjectMocks
private DefaultPluginManager manager;
@Mock
private Subject subject;
@BeforeEach
void mockInstaller() {
lenient().when(installer.install(any())).then(ic -> {
AvailablePlugin plugin = ic.getArgument(0);
return new PendingPluginInstallation(plugin.install(), null);
});
}
@Nested
class WithAdminPermissions {
@BeforeEach
void setUpSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void clearThreadContext() {
ThreadContext.unbindSubject();
}
@Test
void shouldReturnInstalledPlugins() {
InstalledPlugin review = createInstalled("scm-review-plugin");
InstalledPlugin git = createInstalled("scm-git-plugin");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(review, git));
List<InstalledPlugin> installed = manager.getInstalled();
assertThat(installed).containsOnly(review, git);
}
@Test
void shouldReturnReviewPlugin() {
InstalledPlugin review = createInstalled("scm-review-plugin");
InstalledPlugin git = createInstalled("scm-git-plugin");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(review, git));
Optional<InstalledPlugin> plugin = manager.getInstalled("scm-review-plugin");
assertThat(plugin).contains(review);
}
@Test
void shouldReturnEmptyForNonInstalledPlugin() {
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of());
Optional<InstalledPlugin> plugin = manager.getInstalled("scm-review-plugin");
assertThat(plugin).isEmpty();
}
@Test
void shouldReturnAvailablePlugins() {
AvailablePlugin review = createAvailable("scm-review-plugin");
AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git));
List<AvailablePlugin> available = manager.getAvailable();
assertThat(available).containsOnly(review, git);
}
@Test
void shouldFilterOutAllInstalled() {
InstalledPlugin installedGit = createInstalled("scm-git-plugin");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit));
AvailablePlugin review = createAvailable("scm-review-plugin");
AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git));
List<AvailablePlugin> available = manager.getAvailable();
assertThat(available).containsOnly(review);
}
@Test
void shouldReturnAvailable() {
AvailablePlugin review = createAvailable("scm-review-plugin");
AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git));
Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin");
assertThat(available).contains(git);
}
@Test
void shouldReturnEmptyForNonExistingAvailable() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin");
assertThat(available).isEmpty();
}
@Test
void shouldReturnEmptyForInstalledPlugin() {
InstalledPlugin installedGit = createInstalled("scm-git-plugin");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit));
AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(git));
Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin");
assertThat(available).isEmpty();
}
@Test
void shouldInstallThePlugin() {
AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(git));
manager.install("scm-git-plugin", false);
verify(installer).install(git);
verify(eventBus, never()).post(any());
}
@Test
void shouldInstallDependingPlugins() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail));
manager.install("scm-review-plugin", false);
verify(installer).install(mail);
verify(installer).install(review);
}
@Test
void shouldNotInstallAlreadyInstalledDependencies() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail));
InstalledPlugin installedMail = createInstalled("scm-mail-plugin");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail));
manager.install("scm-review-plugin", false);
ArgumentCaptor<AvailablePlugin> captor = ArgumentCaptor.forClass(AvailablePlugin.class);
verify(installer).install(captor.capture());
assertThat(captor.getValue().getDescriptor().getInformation().getName()).isEqualTo("scm-review-plugin");
}
@Test
void shouldRollbackOnFailedInstallation() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin");
when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin"));
AvailablePlugin notification = createAvailable("scm-notification-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail, notification));
PendingPluginInstallation pendingNotification = mock(PendingPluginInstallation.class);
doReturn(pendingNotification).when(installer).install(notification);
PendingPluginInstallation pendingMail = mock(PendingPluginInstallation.class);
doReturn(pendingMail).when(installer).install(mail);
doThrow(new PluginChecksumMismatchException("checksum does not match")).when(installer).install(review);
assertThrows(PluginInstallException.class, () -> manager.install("scm-review-plugin", false));
verify(pendingNotification).cancel();
verify(pendingMail).cancel();
}
@Test
void shouldInstallNothingIfOneOfTheDependenciesIsNotAvailable() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin");
when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin"));
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail));
assertThrows(NotFoundException.class, () -> manager.install("scm-review-plugin", false));
verify(installer, never()).install(any());
}
@Test
void shouldSendRestartEventAfterInstallation() {
AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(git));
manager.install("scm-git-plugin", true);
verify(installer).install(git);
verify(eventBus).post(any(RestartEvent.class));
}
@Test
void shouldNotSendRestartEventIfNoPluginWasInstalled() {
InstalledPlugin gitInstalled = createInstalled("scm-git-plugin");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(gitInstalled));
manager.install("scm-git-plugin", true);
verify(eventBus, never()).post(any());
}
@Test
void shouldNotInstallAlreadyPendingPlugins() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false);
manager.install("scm-review-plugin", false);
// only one interaction
verify(installer).install(any());
}
@Test
void shouldSendRestartEvent() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false);
manager.installPendingAndRestart();
verify(eventBus).post(any(RestartEvent.class));
}
@Test
void shouldNotSendRestartEventWithoutPendingPlugins() {
manager.installPendingAndRestart();
verify(eventBus, never()).post(any());
}
@Test
void shouldReturnSingleAvailableAsPending() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false);
Optional<AvailablePlugin> available = manager.getAvailable("scm-review-plugin");
assertThat(available.get().isPending()).isTrue();
}
@Test
void shouldReturnAvailableAsPending() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false);
List<AvailablePlugin> available = manager.getAvailable();
assertThat(available.get(0).isPending()).isTrue();
}
}
@Nested
class WithoutReadPermissions {
@BeforeEach
void setUpSubject() {
ThreadContext.bind(subject);
doThrow(AuthorizationException.class).when(subject).checkPermission("plugin:read");
}
@AfterEach
void clearThreadContext() {
ThreadContext.unbindSubject();
}
@Test
void shouldThrowAuthorizationExceptionsForReadMethods() {
assertThrows(AuthorizationException.class, () -> manager.getInstalled());
assertThrows(AuthorizationException.class, () -> manager.getInstalled("test"));
assertThrows(AuthorizationException.class, () -> manager.getAvailable());
assertThrows(AuthorizationException.class, () -> manager.getAvailable("test"));
}
}
@Nested
class WithoutManagePermissions {
@BeforeEach
void setUpSubject() {
ThreadContext.bind(subject);
doThrow(AuthorizationException.class).when(subject).checkPermission("plugin:manage");
}
@AfterEach
void clearThreadContext() {
ThreadContext.unbindSubject();
}
@Test
void shouldThrowAuthorizationExceptionsForInstallMethod() {
assertThrows(AuthorizationException.class, () -> manager.install("test", false));
}
@Test
void shouldThrowAuthorizationExceptionsForInstallPendingAndRestart() {
assertThrows(AuthorizationException.class, () -> manager.installPendingAndRestart());
}
}
private AvailablePlugin createAvailable(String name) {
PluginInformation information = new PluginInformation();
information.setName(name);
return createAvailable(information);
}
private InstalledPlugin createInstalled(String name) {
PluginInformation information = new PluginInformation();
information.setName(name);
return createInstalled(information);
}
private InstalledPlugin createInstalled(PluginInformation information) {
InstalledPlugin plugin = mock(InstalledPlugin.class, Answers.RETURNS_DEEP_STUBS);
returnInformation(plugin, information);
return plugin;
}
private AvailablePlugin createAvailable(PluginInformation information) {
AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class);
lenient().when(descriptor.getInformation()).thenReturn(information);
return new AvailablePlugin(descriptor);
}
private void returnInformation(Plugin mockedPlugin, PluginInformation information) {
when(mockedPlugin.getDescriptor().getInformation()).thenReturn(information);
}
}

View File

@@ -102,7 +102,7 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase
public void testGetResourceFromCache() { public void testGetResourceFromCache() {
DefaultUberWebResourceLoader resourceLoader = DefaultUberWebResourceLoader resourceLoader =
new DefaultUberWebResourceLoader(servletContext, new DefaultUberWebResourceLoader(servletContext,
new ArrayList<PluginWrapper>(), Stage.PRODUCTION); new ArrayList<InstalledPlugin>(), Stage.PRODUCTION);
resourceLoader.getCache().put("/myresource", GITHUB); resourceLoader.getCache().put("/myresource", GITHUB);
@@ -131,8 +131,8 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase
{ {
File directory = temp.newFolder(); File directory = temp.newFolder();
File file = file(directory, "myresource"); File file = file(directory, "myresource");
PluginWrapper wrapper = createPluginWrapper(directory); InstalledPlugin wrapper = createPluginWrapper(directory);
List<PluginWrapper> plugins = Lists.newArrayList(wrapper); List<InstalledPlugin> plugins = Lists.newArrayList(wrapper);
WebResourceLoader resourceLoader = WebResourceLoader resourceLoader =
new DefaultUberWebResourceLoader(servletContext, plugins); new DefaultUberWebResourceLoader(servletContext, plugins);
@@ -170,8 +170,8 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase
File directory = temp.newFolder(); File directory = temp.newFolder();
File file = file(directory, "myresource"); File file = file(directory, "myresource");
PluginWrapper wrapper = createPluginWrapper(directory); InstalledPlugin wrapper = createPluginWrapper(directory);
List<PluginWrapper> plugins = Lists.newArrayList(wrapper); List<InstalledPlugin> plugins = Lists.newArrayList(wrapper);
UberWebResourceLoader resourceLoader = UberWebResourceLoader resourceLoader =
new DefaultUberWebResourceLoader(servletContext, plugins); new DefaultUberWebResourceLoader(servletContext, plugins);
@@ -197,11 +197,11 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase
WebResourceLoader loader = mock(WebResourceLoader.class); WebResourceLoader loader = mock(WebResourceLoader.class);
when(loader.getResource("/myresource")).thenReturn(url); when(loader.getResource("/myresource")).thenReturn(url);
PluginWrapper pluginWrapper = mock(PluginWrapper.class); InstalledPlugin installedPlugin = mock(InstalledPlugin.class);
when(pluginWrapper.getWebResourceLoader()).thenReturn(loader); when(installedPlugin.getWebResourceLoader()).thenReturn(loader);
WebResourceLoader resourceLoader = WebResourceLoader resourceLoader =
new DefaultUberWebResourceLoader(servletContext, Lists.newArrayList(pluginWrapper)); new DefaultUberWebResourceLoader(servletContext, Lists.newArrayList(installedPlugin));
assertNull(resourceLoader.getResource("/myresource")); assertNull(resourceLoader.getResource("/myresource"));
} }
@@ -214,11 +214,11 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase
WebResourceLoader loader = mock(WebResourceLoader.class); WebResourceLoader loader = mock(WebResourceLoader.class);
when(loader.getResource("/myresource")).thenReturn(url); when(loader.getResource("/myresource")).thenReturn(url);
PluginWrapper pluginWrapper = mock(PluginWrapper.class); InstalledPlugin installedPlugin = mock(InstalledPlugin.class);
when(pluginWrapper.getWebResourceLoader()).thenReturn(loader); when(installedPlugin.getWebResourceLoader()).thenReturn(loader);
UberWebResourceLoader resourceLoader = UberWebResourceLoader resourceLoader =
new DefaultUberWebResourceLoader(servletContext, Lists.newArrayList(pluginWrapper)); new DefaultUberWebResourceLoader(servletContext, Lists.newArrayList(installedPlugin));
List<URL> resources = resourceLoader.getResources("/myresource"); List<URL> resources = resourceLoader.getResources("/myresource");
Assertions.assertThat(resources).isEmpty(); Assertions.assertThat(resources).isEmpty();
@@ -232,7 +232,7 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase
* *
* @return * @return
*/ */
private PluginWrapper createPluginWrapper(File directory) private InstalledPlugin createPluginWrapper(File directory)
{ {
return createPluginWrapper(directory.toPath()); return createPluginWrapper(directory.toPath());
} }
@@ -245,9 +245,9 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase
* *
* @return * @return
*/ */
private PluginWrapper createPluginWrapper(Path directory) private InstalledPlugin createPluginWrapper(Path directory)
{ {
return new PluginWrapper(null, null, new PathWebResourceLoader(directory), return new InstalledPlugin(null, null, new PathWebResourceLoader(directory),
directory); directory);
} }

View File

@@ -133,7 +133,7 @@ public class ExplodedSmpTest
info.setName(name); info.setName(name);
info.setVersion(version); info.setVersion(version);
Plugin plugin = new Plugin(2, info, null, null, false, InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(2, info, null, null, false,
Sets.newSet(dependencies)); Sets.newSet(dependencies));
return new ExplodedSmp(null, plugin); return new ExplodedSmp(null, plugin);

View File

@@ -0,0 +1,46 @@
package sonia.scm.plugin;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@ExtendWith({MockitoExtension.class, TempDirectory.class})
class PendingPluginInstallationTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private AvailablePlugin plugin;
@Test
void shouldDeleteFileOnCancel(@TempDirectory.TempDir Path directory) throws IOException {
Path file = directory.resolve("file");
Files.write(file, "42".getBytes());
when(plugin.getDescriptor().getInformation().getName()).thenReturn("scm-awesome-plugin");
PendingPluginInstallation installation = new PendingPluginInstallation(plugin, file);
installation.cancel();
assertThat(file).doesNotExist();
}
@Test
void shouldThrowExceptionIfCancelFailed(@TempDirectory.TempDir Path directory) {
Path file = directory.resolve("file");
when(plugin.getDescriptor().getInformation().getName()).thenReturn("scm-awesome-plugin");
PendingPluginInstallation installation = new PendingPluginInstallation(plugin, file);
assertThrows(PluginFailedToCancelInstallationException.class, installation::cancel);
}
}

View File

@@ -1,6 +1,13 @@
package sonia.scm.plugin; package sonia.scm.plugin;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -10,11 +17,19 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static sonia.scm.plugin.PluginCenterDto.Plugin; import static sonia.scm.plugin.PluginCenterDto.Plugin;
import static sonia.scm.plugin.PluginCenterDto.*; import static sonia.scm.plugin.PluginCenterDto.*;
@ExtendWith(MockitoExtension.class)
class PluginCenterDtoMapperTest { class PluginCenterDtoMapperTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private PluginCenterDto dto;
@InjectMocks
private PluginCenterDtoMapperImpl mapper;
@Test @Test
void shouldMapSinglePlugin() { void shouldMapSinglePlugin() {
Plugin plugin = new Plugin( Plugin plugin = new Plugin(
@@ -27,19 +42,26 @@ class PluginCenterDtoMapperTest {
"http://avatar.url", "http://avatar.url",
"555000444", "555000444",
new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), new Condition(Collections.singletonList("linux"), "amd64","2.0.0"),
new Dependency("scm-review-plugin"), ImmutableSet.of("scm-review-plugin"),
new HashMap<>()); ImmutableMap.of("download", new Link("http://download.hitchhiker.com"))
);
PluginInformation result = PluginCenterDtoMapper.map(Collections.singletonList(plugin)).iterator().next(); when(dto.getEmbedded().getPlugins()).thenReturn(Collections.singletonList(plugin));
AvailablePluginDescriptor descriptor = mapper.map(dto).iterator().next().getDescriptor();
PluginInformation information = descriptor.getInformation();
PluginCondition condition = descriptor.getCondition();
assertThat(result.getAuthor()).isEqualTo(plugin.getAuthor()); assertThat(descriptor.getUrl()).isEqualTo("http://download.hitchhiker.com");
assertThat(result.getCategory()).isEqualTo(plugin.getCategory()); assertThat(descriptor.getChecksum()).contains("555000444");
assertThat(result.getVersion()).isEqualTo(plugin.getVersion());
assertThat(result.getCondition().getArch()).isEqualTo(plugin.getConditions().getArch()); assertThat(information.getAuthor()).isEqualTo(plugin.getAuthor());
assertThat(result.getCondition().getMinVersion()).isEqualTo(plugin.getConditions().getMinVersion()); assertThat(information.getCategory()).isEqualTo(plugin.getCategory());
assertThat(result.getCondition().getOs().iterator().next()).isEqualTo(plugin.getConditions().getOs().iterator().next()); assertThat(information.getVersion()).isEqualTo(plugin.getVersion());
assertThat(result.getDescription()).isEqualTo(plugin.getDescription()); assertThat(condition.getArch()).isEqualTo(plugin.getConditions().getArch());
assertThat(result.getName()).isEqualTo(plugin.getName()); assertThat(condition.getMinVersion()).isEqualTo(plugin.getConditions().getMinVersion());
assertThat(condition.getOs().iterator().next()).isEqualTo(plugin.getConditions().getOs().iterator().next());
assertThat(information.getDescription()).isEqualTo(plugin.getDescription());
assertThat(information.getName()).isEqualTo(plugin.getName());
} }
@Test @Test
@@ -54,8 +76,9 @@ class PluginCenterDtoMapperTest {
"https://avatar.url", "https://avatar.url",
"12345678aa", "12345678aa",
new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), new Condition(Collections.singletonList("linux"), "amd64","2.0.0"),
new Dependency("scm-review-plugin"), ImmutableSet.of("scm-review-plugin"),
new HashMap<>()); ImmutableMap.of("download", new Link("http://download.hitchhiker.com/review"))
);
Plugin plugin2 = new Plugin( Plugin plugin2 = new Plugin(
"scm-hitchhiker-plugin", "scm-hitchhiker-plugin",
@@ -67,15 +90,16 @@ class PluginCenterDtoMapperTest {
"http://avatar.url", "http://avatar.url",
"555000444", "555000444",
new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), new Condition(Collections.singletonList("linux"), "amd64","2.0.0"),
new Dependency("scm-review-plugin"), ImmutableSet.of("scm-review-plugin"),
new HashMap<>()); ImmutableMap.of("download", new Link("http://download.hitchhiker.com/hitchhiker"))
);
Set<PluginInformation> resultSet = PluginCenterDtoMapper.map(Arrays.asList(plugin1, plugin2)); when(dto.getEmbedded().getPlugins()).thenReturn(Arrays.asList(plugin1, plugin2));
List<PluginInformation> pluginsList = new ArrayList<>(resultSet); Set<AvailablePlugin> resultSet = mapper.map(dto);
PluginInformation pluginInformation1 = pluginsList.get(1); PluginInformation pluginInformation1 = findPlugin(resultSet, plugin1.getName());
PluginInformation pluginInformation2 = pluginsList.get(0); PluginInformation pluginInformation2 = findPlugin(resultSet, plugin2.getName());
assertThat(pluginInformation1.getAuthor()).isEqualTo(plugin1.getAuthor()); assertThat(pluginInformation1.getAuthor()).isEqualTo(plugin1.getAuthor());
assertThat(pluginInformation1.getVersion()).isEqualTo(plugin1.getVersion()); assertThat(pluginInformation1.getVersion()).isEqualTo(plugin1.getVersion());
@@ -83,4 +107,14 @@ class PluginCenterDtoMapperTest {
assertThat(pluginInformation2.getVersion()).isEqualTo(plugin2.getVersion()); assertThat(pluginInformation2.getVersion()).isEqualTo(plugin2.getVersion());
assertThat(resultSet.size()).isEqualTo(2); assertThat(resultSet.size()).isEqualTo(2);
} }
private PluginInformation findPlugin(Set<AvailablePlugin> resultSet, String name) {
return resultSet
.stream()
.filter(p -> name.equals(p.getDescriptor().getInformation().getName()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("could not find plugin " + name))
.getDescriptor()
.getInformation();
}
} }

View File

@@ -0,0 +1,50 @@
package sonia.scm.plugin;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.net.ahc.AdvancedHttpClient;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PluginCenterLoaderTest {
private static final String PLUGIN_URL = "https://plugins.hitchhiker.com";
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private AdvancedHttpClient client;
@Mock
private PluginCenterDtoMapper mapper;
@InjectMocks
private PluginCenterLoader loader;
@Test
void shouldFetch() throws IOException {
Set<AvailablePlugin> plugins = Collections.emptySet();
PluginCenterDto dto = new PluginCenterDto();
when(client.get(PLUGIN_URL).request().contentFromJson(PluginCenterDto.class)).thenReturn(dto);
when(mapper.map(dto)).thenReturn(plugins);
Set<AvailablePlugin> fetched = loader.load(PLUGIN_URL);
assertThat(fetched).isSameAs(plugins);
}
@Test
void shouldReturnEmptySetIfPluginCenterNotBeReached() throws IOException {
when(client.get(PLUGIN_URL).request()).thenThrow(new IOException("failed to fetch"));
Set<AvailablePlugin> fetch = loader.load(PLUGIN_URL);
assertThat(fetch).isEmpty();
}
}

View File

@@ -0,0 +1,73 @@
package sonia.scm.plugin;
import com.google.common.collect.ImmutableSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.cache.CacheManager;
import sonia.scm.cache.MapCacheManager;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.util.SystemUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PluginCenterTest {
private static final String PLUGIN_URL_BASE = "https://plugins.hitchhiker.com/";
private static final String PLUGIN_URL = PLUGIN_URL_BASE + "{version}";
@Mock
private PluginCenterLoader loader;
@Mock
private SCMContextProvider contextProvider;
private ScmConfiguration configuration;
private CacheManager cacheManager;
private PluginCenter pluginCenter;
@BeforeEach
void setUpPluginCenter() {
when(contextProvider.getVersion()).thenReturn("2.0.0");
cacheManager = new MapCacheManager();
configuration = new ScmConfiguration();
configuration.setPluginUrl(PLUGIN_URL);
pluginCenter = new PluginCenter(contextProvider, cacheManager, configuration, loader);
}
@Test
void shouldFetchPlugins() {
Set<AvailablePlugin> plugins = new HashSet<>();
when(loader.load(PLUGIN_URL_BASE + "2.0.0")).thenReturn(plugins);
assertThat(pluginCenter.getAvailable()).isSameAs(plugins);
}
@Test
void shouldCache() {
Set<AvailablePlugin> first = new HashSet<>();
when(loader.load(anyString())).thenReturn(first, new HashSet<>());
assertThat(pluginCenter.getAvailable()).isSameAs(first);
assertThat(pluginCenter.getAvailable()).isSameAs(first);
}
}

View File

@@ -0,0 +1,117 @@
package sonia.scm.plugin;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.net.ahc.AdvancedHttpClient;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith({MockitoExtension.class, TempDirectory.class})
class PluginInstallerTest {
@Mock
private SCMContextProvider context;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private AdvancedHttpClient client;
@InjectMocks
private PluginInstaller installer;
private Path directory;
@BeforeEach
void setUpContext(@TempDirectory.TempDir Path directory) {
this.directory = directory;
lenient().when(context.resolve(any())).then(ic -> {
Path arg = ic.getArgument(0);
return directory.resolve(arg);
});
}
@Test
void shouldDownloadPlugin() throws IOException {
mockContent("42");
installer.install(createGitPlugin());
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).hasContent("42");
}
@Test
void shouldReturnPendingPluginInstallation() throws IOException {
mockContent("42");
AvailablePlugin gitPlugin = createGitPlugin();
PendingPluginInstallation pending = installer.install(gitPlugin);
assertThat(pending).isNotNull();
assertThat(pending.getPlugin().getDescriptor()).isEqualTo(gitPlugin.getDescriptor());
assertThat(pending.getPlugin().isPending()).isTrue();
}
private void mockContent(String content) throws IOException {
when(client.get("https://download.hitchhiker.com").request().contentAsStream())
.thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
}
private AvailablePlugin createGitPlugin() {
return createPlugin(
"scm-git-plugin",
"https://download.hitchhiker.com",
"73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049" // 42
);
}
@Test
void shouldThrowPluginDownloadException() throws IOException {
when(client.get("https://download.hitchhiker.com").request()).thenThrow(new IOException("failed to download"));
assertThrows(PluginDownloadException.class, () -> installer.install(createGitPlugin()));
}
@Test
void shouldThrowPluginChecksumMismatchException() throws IOException {
mockContent("21");
assertThrows(PluginChecksumMismatchException.class, () -> installer.install(createGitPlugin()));
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
}
@Test
void shouldThrowPluginDownloadExceptionAndCleanup() throws IOException {
InputStream stream = mock(InputStream.class);
when(stream.read(any(), anyInt(), anyInt())).thenThrow(new IOException("failed to read"));
when(client.get("https://download.hitchhiker.com").request().contentAsStream()).thenReturn(stream);
assertThrows(PluginDownloadException.class, () -> installer.install(createGitPlugin()));
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
}
private AvailablePlugin createPlugin(String name, String url, String checksum) {
PluginInformation information = new PluginInformation();
information.setName(name);
AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(
information, null, Collections.emptySet(), url, checksum
);
return new AvailablePlugin(descriptor);
}
}

View File

@@ -129,7 +129,7 @@ public class PluginProcessorTest
{ {
copySmp(PLUGIN_A); copySmp(PLUGIN_A);
PluginWrapper plugin = collectAndGetFirst(); InstalledPlugin plugin = collectAndGetFirst();
assertThat(plugin.getId(), is(PLUGIN_A.id)); assertThat(plugin.getId(), is(PLUGIN_A.id));
} }
@@ -145,15 +145,15 @@ public class PluginProcessorTest
{ {
copySmps(PLUGIN_A, PLUGIN_B); copySmps(PLUGIN_A, PLUGIN_B);
Set<PluginWrapper> plugins = collectPlugins(); Set<InstalledPlugin> plugins = collectPlugins();
assertThat(plugins, hasSize(2)); assertThat(plugins, hasSize(2));
PluginWrapper a = findPlugin(plugins, PLUGIN_A.id); InstalledPlugin a = findPlugin(plugins, PLUGIN_A.id);
assertNotNull(a); assertNotNull(a);
PluginWrapper b = findPlugin(plugins, PLUGIN_B.id); InstalledPlugin b = findPlugin(plugins, PLUGIN_B.id);
assertNotNull(b); assertNotNull(b);
} }
@@ -178,7 +178,7 @@ public class PluginProcessorTest
{ {
copySmp(PLUGIN_A); copySmp(PLUGIN_A);
PluginWrapper plugin = collectAndGetFirst(); InstalledPlugin plugin = collectAndGetFirst();
ClassLoader cl = plugin.getClassLoader(); ClassLoader cl = plugin.getClassLoader();
// load parent class // load parent class
@@ -216,9 +216,9 @@ public class PluginProcessorTest
{ {
copySmps(PLUGIN_A, PLUGIN_B); copySmps(PLUGIN_A, PLUGIN_B);
Set<PluginWrapper> plugins = collectPlugins(); Set<InstalledPlugin> plugins = collectPlugins();
PluginWrapper plugin = findPlugin(plugins, PLUGIN_B.id); InstalledPlugin plugin = findPlugin(plugins, PLUGIN_B.id);
ClassLoader cl = plugin.getClassLoader(); ClassLoader cl = plugin.getClassLoader();
// load parent class // load parent class
@@ -247,7 +247,7 @@ public class PluginProcessorTest
{ {
copySmp(PLUGIN_A); copySmp(PLUGIN_A);
PluginWrapper plugin = collectAndGetFirst(); InstalledPlugin plugin = collectAndGetFirst();
WebResourceLoader wrl = plugin.getWebResourceLoader(); WebResourceLoader wrl = plugin.getWebResourceLoader();
assertNotNull(wrl); assertNotNull(wrl);
@@ -269,7 +269,7 @@ public class PluginProcessorTest
{ {
copySmp(PLUGIN_F_1_0_0); copySmp(PLUGIN_F_1_0_0);
PluginWrapper plugin = collectAndGetFirst(); InstalledPlugin plugin = collectAndGetFirst();
assertThat(plugin.getId(), is(PLUGIN_F_1_0_0.id)); assertThat(plugin.getId(), is(PLUGIN_F_1_0_0.id));
copySmp(PLUGIN_F_1_0_1); copySmp(PLUGIN_F_1_0_1);
@@ -302,9 +302,9 @@ public class PluginProcessorTest
* *
* @throws IOException * @throws IOException
*/ */
private PluginWrapper collectAndGetFirst() throws IOException private InstalledPlugin collectAndGetFirst() throws IOException
{ {
Set<PluginWrapper> plugins = collectPlugins(); Set<InstalledPlugin> plugins = collectPlugins();
assertThat(plugins, hasSize(1)); assertThat(plugins, hasSize(1));
@@ -319,7 +319,7 @@ public class PluginProcessorTest
* *
* @throws IOException * @throws IOException
*/ */
private Set<PluginWrapper> collectPlugins() throws IOException private Set<InstalledPlugin> collectPlugins() throws IOException
{ {
return processor.collectPlugins(PluginProcessorTest.class.getClassLoader()); return processor.collectPlugins(PluginProcessorTest.class.getClassLoader());
} }
@@ -368,14 +368,14 @@ public class PluginProcessorTest
* *
* @return * @return
*/ */
private PluginWrapper findPlugin(Iterable<PluginWrapper> plugin, private InstalledPlugin findPlugin(Iterable<InstalledPlugin> plugin,
final String id) final String id)
{ {
return Iterables.find(plugin, new Predicate<PluginWrapper>() return Iterables.find(plugin, new Predicate<InstalledPlugin>()
{ {
@Override @Override
public boolean apply(PluginWrapper input) public boolean apply(InstalledPlugin input)
{ {
return id.equals(input.getId()); return id.equals(input.getId());
} }

View File

@@ -71,7 +71,7 @@ public class PluginTreeTest
{ {
PluginCondition condition = new PluginCondition("999", PluginCondition condition = new PluginCondition("999",
new ArrayList<String>(), "hit"); new ArrayList<String>(), "hit");
Plugin plugin = new Plugin(2, createInfo("a", "1"), null, condition, InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(2, createInfo("a", "1"), null, condition,
false, null); false, null);
ExplodedSmp smp = createSmp(plugin); ExplodedSmp smp = createSmp(plugin);
@@ -114,7 +114,7 @@ public class PluginTreeTest
@Test(expected = PluginException.class) @Test(expected = PluginException.class)
public void testScmVersion() throws IOException public void testScmVersion() throws IOException
{ {
Plugin plugin = new Plugin(1, createInfo("a", "1"), null, null, false, InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(1, createInfo("a", "1"), null, null, false,
null); null);
ExplodedSmp smp = createSmp(plugin); ExplodedSmp smp = createSmp(plugin);
@@ -182,7 +182,7 @@ public class PluginTreeTest
* *
* @throws IOException * @throws IOException
*/ */
private ExplodedSmp createSmp(Plugin plugin) throws IOException private ExplodedSmp createSmp(InstalledPluginDescriptor plugin) throws IOException
{ {
return new ExplodedSmp(tempFolder.newFile().toPath(), plugin); return new ExplodedSmp(tempFolder.newFile().toPath(), plugin);
} }
@@ -199,7 +199,7 @@ public class PluginTreeTest
*/ */
private ExplodedSmp createSmp(String name) throws IOException private ExplodedSmp createSmp(String name) throws IOException
{ {
return createSmp(new Plugin(2, createInfo(name, "1.0.0"), null, null, return createSmp(new InstalledPluginDescriptor(2, createInfo(name, "1.0.0"), null, null,
false, null)); false, null));
} }
@@ -225,7 +225,7 @@ public class PluginTreeTest
dependencySet.add(d); dependencySet.add(d);
} }
Plugin plugin = new Plugin(2, createInfo(name, "1"), null, null, InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(2, createInfo(name, "1"), null, null,
false, dependencySet); false, dependencySet);
return createSmp(plugin); return createSmp(plugin);