mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 06:55:47 +01:00
Merged in feature/install_plugins (pull request #299)
Feature/install plugins
This commit is contained in:
32
scm-core/src/main/java/sonia/scm/plugin/AvailablePlugin.java
Normal file
32
scm-core/src/main/java/sonia/scm/plugin/AvailablePlugin.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public interface PluginDescriptor {
|
||||||
|
|
||||||
|
PluginInformation getInformation();
|
||||||
|
|
||||||
|
PluginCondition getCondition();
|
||||||
|
|
||||||
|
Set<String> getDependencies();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
68
scm-ui/src/admin/plugins/components/InstallPendingAction.js
Normal file
68
scm-ui/src/admin/plugins/components/InstallPendingAction.js
Normal 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);
|
||||||
134
scm-ui/src/admin/plugins/components/InstallPendingModal.js
Normal file
134
scm-ui/src/admin/plugins/components/InstallPendingModal.js
Normal 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);
|
||||||
@@ -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);
|
||||||
30
scm-ui/src/admin/plugins/components/PluginBottomActions.js
Normal file
30
scm-ui/src/admin/plugins/components/PluginBottomActions.js
Normal 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);
|
||||||
@@ -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}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
270
scm-ui/src/admin/plugins/components/PluginModal.js
Normal file
270
scm-ui/src/admin/plugins/components/PluginModal.js
Normal 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);
|
||||||
32
scm-ui/src/admin/plugins/components/PluginTopActions.js
Normal file
32
scm-ui/src/admin/plugins/components/PluginTopActions.js
Normal 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);
|
||||||
30
scm-ui/src/admin/plugins/components/waitForRestart.js
Normal file
30
scm-ui/src/admin/plugins/components/waitForRestart.js
Normal 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;
|
||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java
Normal file
55
scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
public class PluginChecksumMismatchException extends PluginInstallException {
|
||||||
|
public PluginChecksumMismatchException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
public class PluginDownloadException extends PluginInstallException {
|
||||||
|
public PluginDownloadException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
public class PluginFailedToCancelInstallationException extends RuntimeException {
|
||||||
|
public PluginFailedToCancelInstallationException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user