Merge with develop branch

This commit is contained in:
Sebastian Sdorra
2020-08-12 12:32:16 +02:00
40 changed files with 1823 additions and 462 deletions

View File

@@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Introduced merge detection for receive hooks ([#1278](https://github.com/scm-manager/scm-manager/pull/1278)) - Introduced merge detection for receive hooks ([#1278](https://github.com/scm-manager/scm-manager/pull/1278))
- Anonymous mode for the web ui ([#1284](https://github.com/scm-manager/scm-manager/pull/1284)) - Anonymous mode for the web ui ([#1284](https://github.com/scm-manager/scm-manager/pull/1284))
- Check versions of plugin dependencies on plugin installation ([#1283](https://github.com/scm-manager/scm-manager/pull/1283))
### Fixed ### Fixed
- Repository names may not end with ".git" ([#1277](https://github.com/scm-manager/scm-manager/pull/1277)) - Repository names may not end with ".git" ([#1277](https://github.com/scm-manager/scm-manager/pull/1277))
- Add preselected value to options in dropdown component if missing ([#1287](https://github.com/scm-manager/scm-manager/pull/1287))
## [2.3.1] - 2020-08-04 ## [2.3.1] - 2020-08-04
### Added ### Added

View File

@@ -893,7 +893,7 @@
<servlet.version>3.1.0</servlet.version> <servlet.version>3.1.0</servlet.version>
<jaxrs.version>2.1.1</jaxrs.version> <jaxrs.version>2.1.1</jaxrs.version>
<resteasy.version>4.5.5.Final</resteasy.version> <resteasy.version>4.5.6.Final</resteasy.version>
<jersey-client.version>1.19.4</jersey-client.version> <jersey-client.version>1.19.4</jersey-client.version>
<jackson.version>2.11.1</jackson.version> <jackson.version>2.11.1</jackson.version>
<guice.version>4.2.3</guice.version> <guice.version>4.2.3</guice.version>

View File

@@ -37,6 +37,7 @@ import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -67,7 +68,12 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin
* @param condition * @param condition
* @param childFirstClassLoader * @param childFirstClassLoader
* @param dependencies * @param dependencies
*
* @deprecated this constructor uses dependencies with plain strings,
* which is deprecated because the version information is missing.
* This class should not instantiated manually, it is designed to be loaded by jaxb.
*/ */
@Deprecated
public InstalledPluginDescriptor(int scmVersion, PluginInformation information, public InstalledPluginDescriptor(int scmVersion, PluginInformation information,
PluginResources resources, PluginCondition condition, PluginResources resources, PluginCondition condition,
boolean childFirstClassLoader, Set<String> dependencies, Set<String> optionalDependencies) boolean childFirstClassLoader, Set<String> dependencies, Set<String> optionalDependencies)
@@ -77,8 +83,17 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin
this.resources = resources; this.resources = resources;
this.condition = condition; this.condition = condition;
this.childFirstClassLoader = childFirstClassLoader; this.childFirstClassLoader = childFirstClassLoader;
this.dependencies = dependencies; this.dependencies = mapToNameAndVersionSet(dependencies);
this.optionalDependencies = optionalDependencies; this.optionalDependencies = mapToNameAndVersionSet(optionalDependencies);
}
private static Set<NameAndVersion> mapToNameAndVersionSet(Set<String> dependencies) {
if (dependencies == null){
return ImmutableSet.of();
}
return dependencies.stream()
.map(d -> new NameAndVersion(d, null))
.collect(Collectors.toSet());
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -173,13 +188,19 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin
* @since 2.0.0 * @since 2.0.0
*/ */
@Override @Override
public Set<String> getDependencies() public Set<String> getDependencies() {
{ return mapToStringSet(getDependenciesWithVersion());
if (dependencies == null)
{
dependencies = ImmutableSet.of();
} }
/**
* Returns name and versions of the plugins which are this plugin depends on.
* @return dependencies with their versions
* @since 2.4.0
*/
public Set<NameAndVersion> getDependenciesWithVersion() {
if (dependencies == null) {
dependencies = ImmutableSet.of();
}
return dependencies; return dependencies;
} }
@@ -193,11 +214,18 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin
*/ */
@Override @Override
public Set<String> getOptionalDependencies() { public Set<String> getOptionalDependencies() {
if (optionalDependencies == null) return mapToStringSet(getOptionalDependenciesWithVersion());
{
optionalDependencies = ImmutableSet.of();
} }
/**
* Returns name and versions of the plugins which are this plugin optional depends on.
* @return optional dependencies with their versions
* @since 2.4.0
*/
public Set<NameAndVersion> getOptionalDependenciesWithVersion() {
if (optionalDependencies == null) {
optionalDependencies = ImmutableSet.of();
}
return optionalDependencies; return optionalDependencies;
} }
@@ -205,6 +233,12 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin
return ImmutableSet.copyOf(Iterables.concat(getDependencies(), getOptionalDependencies())); return ImmutableSet.copyOf(Iterables.concat(getDependencies(), getOptionalDependencies()));
} }
private Set<String> mapToStringSet(Set<NameAndVersion> dependencies) {
return dependencies.stream()
.map(NameAndVersion::getName)
.collect(Collectors.toSet());
}
/** /**
* Method description * Method description
* *
@@ -263,12 +297,12 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin
/** Field description */ /** Field description */
@XmlElement(name = "dependency") @XmlElement(name = "dependency")
@XmlElementWrapper(name = "dependencies") @XmlElementWrapper(name = "dependencies")
private Set<String> dependencies; private Set<NameAndVersion> dependencies;
/** Field description */ /** Field description */
@XmlElement(name = "dependency") @XmlElement(name = "dependency")
@XmlElementWrapper(name = "optional-dependencies") @XmlElementWrapper(name = "optional-dependencies")
private Set<String> optionalDependencies; private Set<NameAndVersion> optionalDependencies;
/** Field description */ /** Field description */
@XmlElement(name = "information") @XmlElement(name = "information")

View File

@@ -0,0 +1,104 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import com.google.common.base.Strings;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import sonia.scm.version.Version;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlValue;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.Optional;
/**
* @since 2.4.0
*/
@Getter
@EqualsAndHashCode
@XmlAccessorType(XmlAccessType.FIELD)
public class NameAndVersion {
@XmlValue
private String name;
@XmlAttribute(name = "version")
@XmlJavaTypeAdapter(VersionXmlAdapter.class)
private Version version;
NameAndVersion() {
// required for jaxb
}
public NameAndVersion(String name) {
this(name, null);
}
public NameAndVersion(String name, String version) {
this.name = name;
if (!Strings.isNullOrEmpty(version)) {
this.version = Version.parse(version);
}
}
public Optional<Version> getVersion() {
return Optional.ofNullable(version);
}
public Version mustGetVersion() {
if (version == null) {
throw new IllegalStateException("version is not set");
}
return version;
}
@Override
public String toString() {
return name + (version != null ? ":" + version.getParsedVersion() : "");
}
static class VersionXmlAdapter extends XmlAdapter<String, Version> {
@Override
public Version unmarshal(String v) {
if (Strings.isNullOrEmpty(v)) {
return null;
}
return Version.parse(v);
}
@Override
public String marshal(Version v) {
if (v != null) {
return v.getUnparsedVersion();
}
return null;
}
}
}

View File

@@ -47,7 +47,7 @@ public final class ValidationUtil
public static final String REGEX_NAME = public static final String REGEX_NAME =
"^[A-Za-z0-9\\.\\-_][A-Za-z0-9\\.\\-_@]*$"; "^[A-Za-z0-9\\.\\-_][A-Za-z0-9\\.\\-_@]*$";
public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$"; public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])(?!.*[.]git$)^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$";
/** Field description */ /** Field description */
private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME); private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME);

View File

@@ -268,11 +268,28 @@ public final class Version implements Comparable<Version>
* *
* @return true if newer * @return true if newer
*/ */
public boolean isNewer(String versionString) public boolean isNewer(String versionString) {
{ return isNewer(Version.parse(versionString));
Version o = Version.parse(versionString); }
return (o != null) && isNewer(o); /**
* Returns true if the given version is newer or equal.
* @param versionString other version
* @return true if newer
* @since 2.4.0
*/
public boolean isNewerOrEqual(String versionString) {
return isNewerOrEqual(Version.parse(versionString));
}
/**
* Returns true if the given version is newer or equal.
* @param o other version
* @return {@code true} if newer or equal
* @since 2.4.0
*/
public boolean isNewerOrEqual(Version o) {
return compareTo(o) <= 0;
} }
/** /**
@@ -296,13 +313,31 @@ public final class Version implements Comparable<Version>
* *
* @return true if older * @return true if older
*/ */
public boolean isOlder(String versionString) public boolean isOlder(String versionString) {
{ return isOlder(Version.parse(versionString));
Version o = Version.parse(versionString);
return (o != null) && isOlder(o);
} }
/**
* Returns true if the given version is older or equal.
* @param versionString other version
* @return {@code true} if older or equal
* @since 2.4.0
*/
public boolean isOlderOrEqual(String versionString) {
return isOlderOrEqual(Version.parse(versionString));
}
/**
* Returns true if the given version is older or equal.
* @param o other version
* @return {@code true} if older or equal
* @since 2.4.0
*/
public boolean isOlderOrEqual(Version o) {
return compareTo(o) >= 0;
}
/** /**
* Returns true if the version is a snapshot. * Returns true if the version is a snapshot.
* *

View File

@@ -0,0 +1,63 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import javax.xml.bind.JAXB;
import java.net.URL;
import static org.assertj.core.api.Assertions.assertThat;
class InstalledPluginDescriptorTest {
private static InstalledPluginDescriptor descriptor;
@BeforeAll
@SuppressWarnings("UnstableApiUsage")
static void unmarshal() {
URL resource = Resources.getResource("sonia/scm/plugin/review-plugin.xml");
descriptor = JAXB.unmarshal(resource, InstalledPluginDescriptor.class);
}
@Test
void shouldUnmarshallDependencies() {
assertThat(descriptor.getDependencies()).containsOnly("scm-mail-plugin");
assertThat(descriptor.getOptionalDependencies()).containsOnly("scm-editor-plugin", "scm-landingpage-plugin");
assertThat(descriptor.getDependenciesInclusiveOptionals()).containsOnly("scm-mail-plugin", "scm-editor-plugin", "scm-landingpage-plugin");
}
@Test
void shouldUnmarshallDependenciesWithVersion() {
assertThat(descriptor.getDependenciesWithVersion()).containsOnly(new NameAndVersion("scm-mail-plugin", "2.1.0"));
assertThat(descriptor.getOptionalDependenciesWithVersion()).containsOnly(
new NameAndVersion("scm-landingpage-plugin", "1.0.0"),
new NameAndVersion("scm-editor-plugin")
);
}
}

View File

@@ -202,7 +202,9 @@ public class ValidationUtilTest
"scm/main", "scm/main",
"scm/plugins/git-plugin", "scm/plugins/git-plugin",
"_scm", "_scm",
"-scm" "-scm",
"scm.git",
"scm.git.git"
}; };
for (String path : validPaths) { for (String path : validPaths) {

View File

@@ -24,93 +24,74 @@
package sonia.scm.version; package sonia.scm.version;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test; import org.junit.jupiter.api.Test;
import static org.junit.Assert.*;
//~--- JDK imports ------------------------------------------------------------
import java.util.Arrays; import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
/** /**
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class VersionTest class VersionTest {
{
/**
* Method description
*
*/
@Test @Test
public void parseSimpleVersion() void parseSimpleVersion() {
{
Version v = Version.parse("1.0"); Version v = Version.parse("1.0");
assertTrue(v.getMajor() == 1); assertThat(v.getMajor()).isOne();
assertTrue(v.getMinor() == 0); assertThat(v.getMinor()).isZero();
assertTrue(v.getPatch() == 0); assertThat(v.getPatch()).isZero();
assertFalse(v.isSnapshot()); assertThat(v.isSnapshot()).isFalse();
assertTrue(v.getType() == VersionType.RELEASE); assertThat(v.getType()).isSameAs(VersionType.RELEASE);
assertEquals(v.getParsedVersion(), "1.0.0"); assertThat(v.getParsedVersion()).isEqualTo("1.0.0");
// test with snapshot // test with snapshot
v = Version.parse("1.1-SNAPSHOT"); v = Version.parse("1.1-SNAPSHOT");
assertTrue(v.getMajor() == 1); assertThat(v.getMajor()).isOne();
assertTrue(v.getMinor() == 1); assertThat(v.getMinor()).isOne();
assertTrue(v.getPatch() == 0); assertThat(v.getPatch()).isZero();
assertTrue(v.isSnapshot()); assertThat(v.isSnapshot()).isTrue();
assertTrue(v.getType() == VersionType.RELEASE); assertThat(v.getType()).isSameAs(VersionType.RELEASE);
assertEquals(v.getParsedVersion(), "1.1.0-SNAPSHOT"); assertThat(v.getParsedVersion()).isEqualTo("1.1.0-SNAPSHOT");
// test with maintenance // test with maintenance
v = Version.parse("2.3.14"); v = Version.parse("2.3.14");
assertTrue(v.getMajor() == 2); assertThat(v.getMajor()).isEqualTo(2);
assertTrue(v.getMinor() == 3); assertThat(v.getMinor()).isEqualTo(3);
assertTrue(v.getPatch() == 14); assertThat(v.getPatch()).isEqualTo(14);
assertFalse(v.isSnapshot()); assertThat(v.isSnapshot()).isFalse();
assertTrue(v.getType() == VersionType.RELEASE); assertThat(v.getType()).isSameAs(VersionType.RELEASE);
assertEquals(v.getParsedVersion(), "2.3.14"); assertThat(v.getParsedVersion()).isEqualTo("2.3.14");
} }
/**
* Method description
*
*/
@Test @Test
public void parseTypeVersions() void parseTypeVersions() {
{
Version v = Version.parse("1.0-alpha"); Version v = Version.parse("1.0-alpha");
assertTrue(v.getMajor() == 1); assertThat(v.getMajor()).isOne();
assertTrue(v.getMinor() == 0); assertThat(v.getMinor()).isZero();
assertTrue(v.getPatch() == 0); assertThat(v.getPatch()).isZero();
assertFalse(v.isSnapshot()); assertThat(v.isSnapshot()).isFalse();
assertTrue(v.getType() == VersionType.ALPHA); assertThat(v.getType()).isSameAs(VersionType.ALPHA);
assertTrue(v.getTypeVersion() == 1); assertThat(v.getTypeVersion()).isOne();
assertEquals(v.getParsedVersion(), "1.0.0-alpha1"); assertThat(v.getParsedVersion()).isEqualTo("1.0.0-alpha1");
// Test release candidate // Test release candidate
v = Version.parse("2.1.2-RC3"); v = Version.parse("2.1.2-RC3");
assertTrue(v.getMajor() == 2); assertThat(v.getMajor()).isEqualTo(2);
assertTrue(v.getMinor() == 1); assertThat(v.getMinor()).isEqualTo(1);
assertTrue(v.getPatch() == 2); assertThat(v.getPatch()).isEqualTo(2);
assertFalse(v.isSnapshot()); assertThat(v.isSnapshot()).isFalse();
assertTrue(v.getType() == VersionType.RELEASE_CANDIDAT); assertThat(v.getType()).isSameAs(VersionType.RELEASE_CANDIDAT);
assertTrue(v.getTypeVersion() == 3); assertThat(v.getTypeVersion()).isEqualTo(3);
assertEquals(v.getParsedVersion(), "2.1.2-RC3"); assertThat(v.getParsedVersion()).isEqualTo("2.1.2-RC3");
} }
/**
* Method description
*
*/
@Test @Test
public void testCompareTo() void testCompareTo() {
{
Version[] versions = new Version[9]; Version[] versions = new Version[9];
versions[0] = Version.parse("2.3.1-SNAPSHOT"); versions[0] = Version.parse("2.3.1-SNAPSHOT");
@@ -123,48 +104,45 @@ public class VersionTest
versions[7] = Version.parse("2.3"); versions[7] = Version.parse("2.3");
versions[8] = Version.parse("2.4.6"); versions[8] = Version.parse("2.4.6");
Arrays.sort(versions); Arrays.sort(versions);
assertEquals(versions[0].getParsedVersion(), "2.4.6"); assertThat(versions[0].getParsedVersion()).isEqualTo("2.4.6");
assertEquals(versions[1].getParsedVersion(), "2.3.1"); assertThat(versions[1].getParsedVersion()).isEqualTo("2.3.1");
assertEquals(versions[2].getParsedVersion(), "2.3.1-SNAPSHOT"); assertThat(versions[2].getParsedVersion()).isEqualTo("2.3.1-SNAPSHOT");
assertEquals(versions[3].getParsedVersion(), "2.3.1-RC1"); assertThat(versions[3].getParsedVersion()).isEqualTo("2.3.1-RC1");
assertEquals(versions[4].getParsedVersion(), "2.3.1-beta2"); assertThat(versions[4].getParsedVersion()).isEqualTo("2.3.1-beta2");
assertEquals(versions[5].getParsedVersion(), "2.3.1-beta1"); assertThat(versions[5].getParsedVersion()).isEqualTo("2.3.1-beta1");
assertEquals(versions[6].getParsedVersion(), "2.3.1-alpha2"); assertThat(versions[6].getParsedVersion()).isEqualTo("2.3.1-alpha2");
assertEquals(versions[7].getParsedVersion(), "2.3.1-M1"); assertThat(versions[7].getParsedVersion()).isEqualTo("2.3.1-M1");
assertEquals(versions[8].getParsedVersion(), "2.3.0"); assertThat(versions[8].getParsedVersion()).isEqualTo("2.3.0");
} }
/**
* Method description
*
*/
@Test @Test
public void testIsNewer() void testIsNewer() {
{ assertThat(Version.parse("1.0").isNewer("1.0.1")).isFalse();
assertFalse(Version.parse("1.0").isNewer("1.0.1")); assertThat(Version.parse("1.1").isNewer("1.1-alpha1")).isTrue();
assertTrue(Version.parse("1.1").isNewer("1.1-alpha1")); assertThat(Version.parse("1.1").isNewer("1.1-RC5")).isTrue();
assertTrue(Version.parse("1.1").isNewer("1.1-RC5"));
} }
/**
* Method description
*
*/
@Test @Test
public void testIsOlder() void testIsOlder() {
{ assertThat(Version.parse("1.0.1").isOlder("1.0")).isFalse();
assertFalse(Version.parse("1.0.1").isOlder("1.0")); assertThat(Version.parse("1.1-alpha1").isOlder("1.1")).isTrue();
assertTrue(Version.parse("1.1-alpha1").isOlder("1.1")); assertThat(Version.parse("1.1-RC5").isOlder("1.1")).isTrue();
assertTrue(Version.parse("1.1-RC5").isOlder("1.1"));
} }
/** @Test
* Method description void testIsOlderOrEqual() {
* assertThat(Version.parse("1.0.0").isOlderOrEqual("1.0.1")).isTrue();
*/ assertThat(Version.parse("1.0.1").isOlderOrEqual("1.0.1")).isTrue();
@Test(expected = VersionParseException.class) }
public void testUnparseable()
{ @Test
Version.parse("aaaa"); void testINewerOrEqual() {
assertThat(Version.parse("1.0.1").isNewerOrEqual("1.0.0")).isTrue();
assertThat(Version.parse("1.0.1").isOlderOrEqual("1.0.1")).isTrue();
}
@Test
void testUnparseable() {
assertThrows(VersionParseException.class, () -> Version.parse("aaaa"));
} }
} }

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<plugin>
<scm-version>2</scm-version>
<information>
<displayName>Review</displayName>
<author>Cloudogu GmbH</author>
<category>Workflow</category>
<name>scm-review-plugin</name>
<version>2.3.0-SNAPSHOT</version>
<description>Depict a review process with pull requests</description>
</information>
<conditions>
<min-version>2.4.0-SNAPSHOT</min-version>
</conditions>
<resources>
<script>assets/scm-review-plugin.bundle.js</script>
</resources>
<subscriber>
<class>com.cloudogu.scm.review.emailnotification.EmailNotificationHook</class>
<event>com.cloudogu.scm.review.pullrequest.service.PullRequestRejectedEvent</event>
</subscriber>
<extension-point>
<description>Each {@link Rule} class implementation defines a type of workflow rule.&lt;br&gt;
&lt;br&gt;
Rules applied to your repositories are represented by {@link AppliedRule}s&lt;br&gt;
to support multiple {@link Rule}s of the same type with distinct configuration.
</description>
<autoBind>true</autoBind>
<multi>true</multi>
<class>com.cloudogu.scm.review.workflow.Rule</class>
</extension-point>
<rest-resource>
<value>v2/pull-requests</value>
<class>com.cloudogu.scm.review.config.api.RepositoryConfigResource</class>
</rest-resource>
<event>
<class>com.cloudogu.scm.review.pullrequest.service.PullRequestEvent</class>
</event>
<extension>
<class>com.cloudogu.scm.review.ProcessChangedFilesHook</class>
</extension>
<dependencies>
<dependency version="2.1.0">scm-mail-plugin</dependency>
</dependencies>
<optional-dependencies>
<dependency>scm-editor-plugin</dependency>
<dependency version="1.0.0">scm-landingpage-plugin</dependency>
</optional-dependencies>
</plugin>

View File

@@ -38608,6 +38608,42 @@ exports[`Storyshots Forms|Checkbox With HelpText 1`] = `
</div> </div>
`; `;
exports[`Storyshots Forms|DropDown Add preselect if missing in options 1`] = `
<div
className="select"
>
<select
onChange={[Function]}
value="D"
>
<option
selected={false}
value="alpha"
>
A
</option>
<option
selected={false}
value="beta"
>
B
</option>
<option
selected={false}
value="gamma"
>
C
</option>
<option
selected={true}
value="D"
>
D
</option>
</select>
</div>
`;
exports[`Storyshots Forms|DropDown Default 1`] = ` exports[`Storyshots Forms|DropDown Default 1`] = `
<div <div
className="select" className="select"
@@ -38664,6 +38700,12 @@ exports[`Storyshots Forms|DropDown With Translation 1`] = `
> >
The Meaning Of Liff The Meaning Of Liff
</option> </option>
<option
selected={true}
value="dirk"
>
dirk
</option>
</select> </select>
</div> </div>
`; `;

View File

@@ -48,4 +48,14 @@ storiesOf("Forms|DropDown", module)
// nothing to do // nothing to do
}} }}
/> />
))
.add("Add preselect if missing in options", () => (
<DropDown
optionValues={["alpha", "beta", "gamma"]}
options={["A", "B", "C"]}
preselectedOption={"D"}
optionSelected={selection => {
// nothing to do
}}
/>
)); ));

View File

@@ -36,6 +36,11 @@ type Props = {
class DropDown extends React.Component<Props> { class DropDown extends React.Component<Props> {
render() { render() {
const { options, optionValues, preselectedOption, className, disabled } = this.props; const { options, optionValues, preselectedOption, className, disabled } = this.props;
if (preselectedOption && options.filter(o => o === preselectedOption).length === 0) {
options.push(preselectedOption);
}
return ( return (
<div className={classNames(className, "select", disabled ? "disabled" : "")}> <div className={classNames(className, "select", disabled ? "disabled" : "")}>
<select value={preselectedOption ? preselectedOption : ""} onChange={this.change} disabled={disabled}> <select value={preselectedOption ? preselectedOption : ""} onChange={this.change} disabled={disabled}>

View File

@@ -42,6 +42,7 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -68,17 +69,30 @@ public class DefaultPluginManager implements PluginManager {
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>(); private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
private final Function<List<AvailablePlugin>, PluginInstallationContext> contextFactory;
@Inject @Inject
public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus) { public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus) {
this(loader, center, installer, restarter, eventBus, null);
}
DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus, Function<List<AvailablePlugin>, PluginInstallationContext> contextFactory) {
this.loader = loader; this.loader = loader;
this.center = center; this.center = center;
this.installer = installer; this.installer = installer;
this.restarter = restarter; this.restarter = restarter;
this.eventBus = eventBus; this.eventBus = eventBus;
if (contextFactory != null) {
this.contextFactory = contextFactory;
} else {
this.contextFactory = (availablePlugins -> PluginInstallationContext.from(getInstalled(), availablePlugins));
}
this.computeInstallationDependencies(); this.computeInstallationDependencies();
} }
@VisibleForTesting @VisibleForTesting
synchronized void computeInstallationDependencies() { synchronized void computeInstallationDependencies() {
loader.getInstalledPlugins() loader.getInstalledPlugins()
@@ -167,9 +181,10 @@ public class DefaultPluginManager implements PluginManager {
List<AvailablePlugin> plugins = collectPluginsToInstall(name); List<AvailablePlugin> plugins = collectPluginsToInstall(name);
List<PendingPluginInstallation> pendingInstallations = new ArrayList<>(); List<PendingPluginInstallation> pendingInstallations = new ArrayList<>();
for (AvailablePlugin plugin : plugins) { for (AvailablePlugin plugin : plugins) {
try { try {
PendingPluginInstallation pending = installer.install(plugin); PendingPluginInstallation pending = installer.install(contextFactory.apply(plugins), plugin);
dependencyTracker.addInstalled(plugin.getDescriptor()); dependencyTracker.addInstalled(plugin.getDescriptor());
pendingInstallations.add(pending); pendingInstallations.add(pending);
eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLED, plugin)); eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLED, plugin));

View File

@@ -0,0 +1,62 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class DependencyNotFoundException extends PluginInstallException {
private final String plugin;
private final String missingDependency;
public DependencyNotFoundException(String plugin, String missingDependency) {
super(
entity("Dependency", missingDependency)
.in("Plugin", plugin)
.build(),
String.format(
"missing dependency %s of plugin %s",
missingDependency,
plugin
)
);
this.plugin = plugin;
this.missingDependency = missingDependency;
}
public String getPlugin() {
return plugin;
}
public String getMissingDependency() {
return missingDependency;
}
@Override
public String getCode() {
return "5GS6lwvWF1";
}
}

View File

@@ -0,0 +1,60 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import lombok.Getter;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@Getter
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class DependencyVersionMismatchException extends PluginInstallException {
private final String plugin;
private final String dependency;
private final String minVersion;
private final String currentVersion;
public DependencyVersionMismatchException(String plugin, String dependency, String minVersion, String currentVersion) {
super(
entity("Dependency", dependency)
.in("Plugin", plugin)
.build(),
String.format(
"%s requires dependency %s at least in version %s, but it is installed in version %s",
plugin, dependency, minVersion, currentVersion
)
);
this.plugin = plugin;
this.dependency = dependency;
this.minVersion = minVersion;
this.currentVersion = currentVersion;
}
@Override
public String getCode() {
return "E5S6niWwi1";
}
}

View File

@@ -30,11 +30,10 @@ import com.google.common.base.Function;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
//~--- JDK imports ------------------------------------------------------------
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects;
import java.util.Set; //~--- JDK imports ------------------------------------------------------------
/** /**
* The ExplodedSmp object represents an extracted SCM-Manager plugin. The object * The ExplodedSmp object represents an extracted SCM-Manager plugin. The object
@@ -107,6 +106,25 @@ public final class ExplodedSmp
return plugin; return plugin;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExplodedSmp that = (ExplodedSmp) o;
return path.equals(that.path);
}
@Override
public int hashCode() {
return Objects.hash(path);
}
@Override
public String toString() {
PluginInformation information = plugin.getInformation();
return information.getName() + "@" + information.getVersion() + " (" + path + ")";
}
//~--- inner classes -------------------------------------------------------- //~--- inner classes --------------------------------------------------------
/** /**

View File

@@ -26,6 +26,7 @@ package sonia.scm.plugin;
import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.ContextEntry.ContextBuilder.entity;
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class PluginChecksumMismatchException extends PluginInstallException { public class PluginChecksumMismatchException extends PluginInstallException {
public PluginChecksumMismatchException(AvailablePlugin plugin, String calculatedChecksum, String expectedChecksum) { public PluginChecksumMismatchException(AvailablePlugin plugin, String calculatedChecksum, String expectedChecksum) {
super( super(

View File

@@ -0,0 +1,51 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import lombok.Getter;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@Getter
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class PluginInformationMismatchException extends PluginInstallException {
private final PluginInformation api;
private final PluginInformation downloaded;
public PluginInformationMismatchException(PluginInformation api, PluginInformation downloaded, String message) {
super(
entity("Plugin", api.getName()).build(),
message
);
this.api = api;
this.downloaded = downloaded;
}
@Override
public String getCode() {
return "4RS6niPRX1";
}
}

View File

@@ -0,0 +1,73 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public final class PluginInstallationContext {
private final Map<String, NameAndVersion> dependencies;
private PluginInstallationContext(Map<String, NameAndVersion> dependencies) {
this.dependencies = dependencies;
}
public static PluginInstallationContext empty() {
return new PluginInstallationContext(Collections.emptyMap());
}
public static PluginInstallationContext fromDescriptors(Iterable<? extends PluginDescriptor> installed, Iterable<? extends PluginDescriptor> pending) {
Map<String, NameAndVersion> dependencies = new HashMap<>();
appendDescriptors(dependencies, installed);
appendDescriptors(dependencies, pending);
return new PluginInstallationContext(dependencies);
}
public static PluginInstallationContext from(Iterable<? extends Plugin> installed, Iterable<? extends Plugin> pending) {
Map<String, NameAndVersion> dependencies = new HashMap<>();
appendPlugins(dependencies, installed);
appendPlugins(dependencies, pending);
return new PluginInstallationContext(dependencies);
}
private static <P extends PluginDescriptor> void appendDescriptors(Map<String, NameAndVersion> dependencies, Iterable<P> descriptors) {
descriptors.forEach(desc -> appendPlugins(dependencies, desc.getInformation()));
}
private static <P extends Plugin> void appendPlugins(Map<String, NameAndVersion> dependencies, Iterable<P> plugins) {
plugins.forEach(plugin -> appendPlugins(dependencies, plugin.getDescriptor().getInformation()));
}
private static void appendPlugins(Map<String, NameAndVersion> dependencies, PluginInformation information) {
dependencies.put(information.getName(), new NameAndVersion(information.getName(), information.getVersion()));
}
public Optional<NameAndVersion> find(String name) {
return Optional.ofNullable(dependencies.get(name));
}
}

View File

@@ -0,0 +1,104 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import sonia.scm.version.Version;
import java.util.Optional;
import java.util.Set;
public final class PluginInstallationVerifier {
private final PluginInstallationContext context;
private final InstalledPluginDescriptor descriptor;
private PluginInstallationVerifier(PluginInstallationContext context, InstalledPluginDescriptor descriptor) {
this.context = context;
this.descriptor = descriptor;
}
public static void verify(PluginInstallationContext context, InstalledPlugin plugin) {
verify(context, plugin.getDescriptor());
}
public static void verify(PluginInstallationContext context, InstalledPluginDescriptor descriptor) {
new PluginInstallationVerifier(context, descriptor).doVerification();
}
private void doVerification() {
verifyConditions();
verifyDependencies();
verifyOptionalDependencies();
}
private void verifyConditions() {
// TODO we should provide more details here, which condition has failed
if (!descriptor.getCondition().isSupported()) {
throw new PluginConditionFailedException(
descriptor.getCondition(),
String.format(
"could not load plugin %s, the plugin condition does not match",
descriptor.getInformation().getName()
)
);
}
}
private void verifyDependencies() {
Set<NameAndVersion> dependencies = descriptor.getDependenciesWithVersion();
for (NameAndVersion dependency : dependencies) {
NameAndVersion installed = context.find(dependency.getName())
.orElseThrow(
() -> new DependencyNotFoundException(descriptor.getInformation().getName(), dependency.getName())
);
dependency.getVersion().ifPresent(requiredVersion -> verifyDependencyVersion(dependency, installed));
}
}
private void verifyOptionalDependencies() {
Set<NameAndVersion> dependencies = descriptor.getOptionalDependenciesWithVersion();
for (NameAndVersion dependency : dependencies) {
Optional<Version> version = dependency.getVersion();
if (version.isPresent()) {
Optional<NameAndVersion> installed = context.find(dependency.getName());
installed.ifPresent(nameAndVersion -> verifyDependencyVersion(dependency, nameAndVersion));
}
}
}
private void verifyDependencyVersion(NameAndVersion required, NameAndVersion installed) {
Version requiredVersion = required.mustGetVersion();
Version installedVersion = installed.mustGetVersion();
if (installedVersion.isOlder(requiredVersion)) {
throw new DependencyVersionMismatchException(
descriptor.getInformation().getName(),
required.getName(),
requiredVersion.getUnparsedVersion(),
installedVersion.getUnparsedVersion()
);
}
}
}

View File

@@ -38,36 +38,72 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Optional; import java.util.Optional;
@SuppressWarnings("UnstableApiUsage") // guava hash is marked as unstable @SuppressWarnings("UnstableApiUsage")
// guava hash is marked as unstable
class PluginInstaller { class PluginInstaller {
private final SCMContextProvider context; private final SCMContextProvider scmContext;
private final AdvancedHttpClient client; private final AdvancedHttpClient client;
private final SmpDescriptorExtractor smpDescriptorExtractor; private final SmpDescriptorExtractor smpDescriptorExtractor;
@Inject @Inject
public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client, SmpDescriptorExtractor smpDescriptorExtractor) { public PluginInstaller(SCMContextProvider scmContext, AdvancedHttpClient client, SmpDescriptorExtractor smpDescriptorExtractor) {
this.context = context; this.scmContext = scmContext;
this.client = client; this.client = client;
this.smpDescriptorExtractor = smpDescriptorExtractor; this.smpDescriptorExtractor = smpDescriptorExtractor;
} }
@SuppressWarnings("squid:S4790") // hashing should be safe @SuppressWarnings("squid:S4790") // hashing should be safe
public PendingPluginInstallation install(AvailablePlugin plugin) { public PendingPluginInstallation install(PluginInstallationContext context, AvailablePlugin plugin) {
Path file = null; Path file = null;
try (HashingInputStream input = new HashingInputStream(Hashing.sha256(), download(plugin))) { try (HashingInputStream input = new HashingInputStream(Hashing.sha256(), download(plugin))) {
file = createFile(plugin); file = createFile(plugin);
Files.copy(input, file); Files.copy(input, file);
verifyChecksum(plugin, input.hash(), file); verifyChecksum(plugin, input.hash(), file);
verifyConditions(plugin, file);
InstalledPluginDescriptor descriptor = smpDescriptorExtractor.extractPluginDescriptor(file);
verifyInformation(plugin.getDescriptor(), descriptor);
PluginInstallationVerifier.verify(context, descriptor);
return new PendingPluginInstallation(plugin.install(), file); return new PendingPluginInstallation(plugin.install(), file);
} catch (PluginException ex) {
cleanup(file);
throw ex;
} catch (IOException ex) { } catch (IOException ex) {
cleanup(file); cleanup(file);
throw new PluginDownloadException(plugin, ex); throw new PluginDownloadException(plugin, ex);
} }
} }
private void verifyInformation(AvailablePluginDescriptor descriptorFromPluginCenter, InstalledPluginDescriptor downloadedDescriptor) {
verifyInformation(descriptorFromPluginCenter.getInformation(), downloadedDescriptor.getInformation());
}
private void verifyInformation(PluginInformation informationFromPluginCenter, PluginInformation downloadedInformation) {
if (!informationFromPluginCenter.getName().equals(downloadedInformation.getName())) {
throw new PluginInformationMismatchException(
informationFromPluginCenter, downloadedInformation,
String.format(
"downloaded plugin name \"%s\" does not match the expected name \"%s\" from plugin-center",
downloadedInformation.getName(),
informationFromPluginCenter.getName()
)
);
}
if (!informationFromPluginCenter.getVersion().equals(downloadedInformation.getVersion())) {
throw new PluginInformationMismatchException(
informationFromPluginCenter, downloadedInformation,
String.format(
"downloaded plugin version \"%s\" does not match the expected version \"%s\" from plugin-center",
downloadedInformation.getVersion(),
informationFromPluginCenter.getVersion()
)
);
}
}
private void cleanup(Path file) { private void cleanup(Path file) {
try { try {
if (file != null) { if (file != null) {
@@ -89,26 +125,12 @@ class PluginInstaller {
} }
} }
private void verifyConditions(AvailablePlugin plugin, Path file) throws IOException {
InstalledPluginDescriptor pluginDescriptor = smpDescriptorExtractor.extractPluginDescriptor(file);
if (!pluginDescriptor.getCondition().isSupported()) {
cleanup(file);
throw new PluginConditionFailedException(
pluginDescriptor.getCondition(),
String.format(
"could not load plugin %s, the plugin condition does not match",
plugin.getDescriptor().getInformation().getName()
)
);
}
}
private InputStream download(AvailablePlugin plugin) throws IOException { private InputStream download(AvailablePlugin plugin) throws IOException {
return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream(); return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream();
} }
private Path createFile(AvailablePlugin plugin) throws IOException { private Path createFile(AvailablePlugin plugin) throws IOException {
Path directory = context.resolve(Paths.get("plugins")); Path directory = scmContext.resolve(Paths.get("plugins"));
Files.createDirectories(directory); Files.createDirectories(directory);
return directory.resolve(plugin.getDescriptor().getInformation().getName() + ".smp"); return directory.resolve(plugin.getDescriptor().getInformation().getName() + ".smp");
} }

View File

@@ -35,7 +35,6 @@ import com.google.common.hash.Hashing;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle; import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle;
import sonia.scm.plugin.ExplodedSmp.PathTransformer;
import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException; import javax.xml.bind.JAXBException;
@@ -50,11 +49,14 @@ import java.nio.file.Path;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -103,6 +105,8 @@ public final class PluginProcessor
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
private final SmpDescriptorExtractor extractor = new SmpDescriptorExtractor();
private ClassLoaderLifeCycle classLoaderLifeCycle; private ClassLoaderLifeCycle classLoaderLifeCycle;
/** /**
@@ -162,25 +166,16 @@ public final class PluginProcessor
{ {
logger.info("collect plugins"); logger.info("collect plugins");
Set<Path> archives = collect(pluginDirectory, new PluginArchiveFilter()); Set<ExplodedSmp> installedPlugins = findInstalledPlugins();
logger.debug("found {} installed plugins", installedPlugins.size());
logger.debug("extract {} archives", archives.size()); Set<ExplodedSmp> newlyInstalledPlugins = installPending(installedPlugins);
logger.debug("finished installation of {} plugins", newlyInstalledPlugins.size());
extract(archives); Set<ExplodedSmp> plugins = concat(installedPlugins, newlyInstalledPlugins);
List<Path> dirs =
collectPluginDirectories(pluginDirectory)
.stream()
.filter(isPluginDirectory())
.collect(toList());
logger.debug("process {} directories: {}", dirs.size(), dirs);
List<ExplodedSmp> smps = Lists.transform(dirs, new PathTransformer());
logger.trace("start building plugin tree"); logger.trace("start building plugin tree");
PluginTree pluginTree = new PluginTree(plugins);
PluginTree pluginTree = new PluginTree(smps);
logger.info("install plugin tree:\n{}", pluginTree); logger.info("install plugin tree:\n{}", pluginTree);
@@ -195,6 +190,52 @@ public final class PluginProcessor
return ImmutableSet.copyOf(wrappers); return ImmutableSet.copyOf(wrappers);
} }
private Set<ExplodedSmp> concat(Set<ExplodedSmp> installedPlugins, Set<ExplodedSmp> newlyInstalledPlugins) {
// We first add all newly installed plugins,
// after that we add the missing plugins, which are already installed.
// ExplodedSmp is equal by its path, so duplicates (updates) are not in the result.
return ImmutableSet.<ExplodedSmp>builder()
.addAll(newlyInstalledPlugins)
.addAll(installedPlugins)
.build();
}
private Set<ExplodedSmp> installPending(Set<ExplodedSmp> installedPlugins) throws IOException {
Set<Path> archives = collect(pluginDirectory, new PluginArchiveFilter());
logger.debug("start installation of {} pending archives", archives.size());
Map<Path, InstalledPluginDescriptor> pending = new HashMap<>();
for (Path archive : archives) {
pending.put(archive, extractor.extractPluginDescriptor(archive));
}
PluginInstallationContext installationContext = PluginInstallationContext.fromDescriptors(
installedPlugins.stream().map(ExplodedSmp::getPlugin).collect(toSet()),
pending.values()
);
for (Map.Entry<Path, InstalledPluginDescriptor> entry : pending.entrySet()) {
try {
PluginInstallationVerifier.verify(installationContext, entry.getValue());
} catch (PluginException ex) {
Path path = entry.getKey();
logger.error("failed to install smp {}, because it could not be verified", path);
logger.error("to restore scm-manager functionality remove the smp file {} from the plugin directory", path);
throw ex;
}
}
return extract(archives);
}
private Set<ExplodedSmp> findInstalledPlugins() throws IOException {
return collectPluginDirectories(pluginDirectory)
.stream()
.filter(isPluginDirectory())
.map(ExplodedSmp::create)
.collect(Collectors.toSet());
}
private Predicate<Path> isPluginDirectory() { private Predicate<Path> isPluginDirectory() {
return dir -> Files.exists(dir.resolve(DIRECTORY_METAINF).resolve("scm").resolve("plugin.xml")); return dir -> Files.exists(dir.resolve(DIRECTORY_METAINF).resolve("scm").resolve("plugin.xml"));
} }
@@ -505,10 +546,12 @@ public final class PluginProcessor
* *
* @throws IOException * @throws IOException
*/ */
private void extract(Iterable<Path> archives) throws IOException private Set<ExplodedSmp> extract(Iterable<Path> archives) throws IOException
{ {
logger.debug("extract archives"); logger.debug("extract archives");
ImmutableSet.Builder<ExplodedSmp> extracted = ImmutableSet.builder();
for (Path archive : archives) for (Path archive : archives)
{ {
File archiveFile = archive.toFile(); File archiveFile = archive.toFile();
@@ -519,17 +562,18 @@ public final class PluginProcessor
logger.debug("extract plugin {}", smp.getPlugin()); logger.debug("extract plugin {}", smp.getPlugin());
File directory = File directory = PluginsInternal.createPluginDirectory(pluginDirectory.toFile(), smp.getPlugin());
PluginsInternal.createPluginDirectory(pluginDirectory.toFile(),
smp.getPlugin());
String checksum = com.google.common.io.Files.hash(archiveFile, String checksum = com.google.common.io.Files.hash(archiveFile, Hashing.sha256()).toString();
Hashing.sha256()).toString();
File checksumFile = PluginsInternal.getChecksumFile(directory); File checksumFile = PluginsInternal.getChecksumFile(directory);
PluginsInternal.extract(smp, checksum, directory, checksumFile, false); PluginsInternal.extract(smp, checksum, directory, checksumFile, false);
moveArchive(archive); moveArchive(archive);
extracted.add(ExplodedSmp.create(directory.toPath()));
} }
return extracted.build();
} }
/** /**

View File

@@ -30,6 +30,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -71,7 +72,7 @@ public final class PluginTree
* *
* @param smps * @param smps
*/ */
public PluginTree(List<ExplodedSmp> smps) public PluginTree(Collection<ExplodedSmp> smps)
{ {
smps.forEach(s -> { smps.forEach(s -> {
@@ -155,7 +156,8 @@ public final class PluginTree
} }
private void append(StringBuilder buffer, String indent, PluginNode node) { private void append(StringBuilder buffer, String indent, PluginNode node) {
buffer.append(indent).append("+- ").append(node.getId()).append("\n"); PluginInformation information = node.getPlugin().getPlugin().getInformation();
buffer.append(indent).append("+- ").append(node.getId()).append("@").append(information.getVersion()).append("\n");
for (PluginNode child : node.getChildren()) { for (PluginNode child : node.getChildren()) {
append(buffer, indent + " ", child); append(buffer, indent + " ", child);
} }

View File

@@ -241,6 +241,18 @@
"displayName": "Fehler beim Löschen falscher Downloads", "displayName": "Fehler beim Löschen falscher Downloads",
"description": "Ein fehlerhaft heruntergeladenes Plugin konnte nicht gelöscht werden. Bitte prüfen Sie die Server Logs und löschen die Datei manuell." "description": "Ein fehlerhaft heruntergeladenes Plugin konnte nicht gelöscht werden. Bitte prüfen Sie die Server Logs und löschen die Datei manuell."
}, },
"5GS6lwvWF1": {
"displayName": "Abhänigkeit konnte nicht gefunden werden",
"description": "Eine der Abhänigkeiten des Plugins konnte nicht gefunden werden. Bitte prüfen Sie die Logs für weitere Informationen."
},
"E5S6niWwi1": {
"displayName": "Version einer Abhänigkeit zu niedrig",
"description": "Die Version einer Abhänigkeit des Plugin ist zu niedrig. Bitte prüfen Sie die Logs für weitere Informationen."
},
"4RS6niPRX1": {
"displayName": "Plugin information stimmen nicht überein",
"description": "Die Informationen des heruntergeladenen Plugins stimmen nicht mit den Informationen des Plugin Centers überein. Bitte prüfen Sie die Logs für weitere Informationen."
},
"2qRyyaVcJ1": { "2qRyyaVcJ1": {
"displayName": "Ungültig formatiertes Element", "displayName": "Ungültig formatiertes Element",
"description": "Die Eingabe beinhaltete unfültige Formate. Bitte prüfen Sie die Server Logs für genauere Informationen." "description": "Die Eingabe beinhaltete unfültige Formate. Bitte prüfen Sie die Server Logs für genauere Informationen."

View File

@@ -241,6 +241,18 @@
"displayName": "Error while cleaning up failed plugin", "displayName": "Error while cleaning up failed plugin",
"description": "A failed plugin download could not be removed correctly. Please check the server log and remove the plugin manually." "description": "A failed plugin download could not be removed correctly. Please check the server log and remove the plugin manually."
}, },
"5GS6lwvWF1": {
"displayName": "Dependency not found",
"description": "One of the plugin dependencies could not be found. Please check the server logs for more details."
},
"E5S6niWwi1": {
"displayName": "Dependency version mismatch",
"description": "The plugin depends on a newer version of an already installed plugin. Please check the server logs for more details."
},
"4RS6niPRX1": {
"displayName": "Plugin information mismatch",
"description": "The downloaded plugin does not match the information provided by the plugin center. Please check the server logs for more details."
},
"2qRyyaVcJ1": { "2qRyyaVcJ1": {
"displayName": "Invalid format in element", "displayName": "Invalid format in element",
"description": "The input had some invalid formats. Please check the server log for further information." "description": "The input had some invalid formats. Please check the server log for further information."

View File

@@ -90,20 +90,28 @@ class DefaultPluginManagerTest {
@Captor @Captor
private ArgumentCaptor<PluginEvent> eventCaptor; private ArgumentCaptor<PluginEvent> eventCaptor;
@InjectMocks
private DefaultPluginManager manager; private DefaultPluginManager manager;
@Mock @Mock
private Subject subject; private Subject subject;
private final PluginInstallationContext context = PluginInstallationContext.empty();
@BeforeEach @BeforeEach
void mockInstaller() { void mockInstaller() {
lenient().when(installer.install(any())).then(ic -> { lenient().when(installer.install(any(), any())).then(ic -> {
AvailablePlugin plugin = ic.getArgument(0); AvailablePlugin plugin = ic.getArgument(1);
return new PendingPluginInstallation(plugin.install(), null); return new PendingPluginInstallation(plugin.install(), null);
}); });
} }
@BeforeEach
void setUpObjectUnderTest() {
manager = new DefaultPluginManager(
loader, center, installer, restarter, eventBus, plugins -> context
);
}
@Nested @Nested
class WithAdminPermissions { class WithAdminPermissions {
@@ -209,7 +217,7 @@ class DefaultPluginManagerTest {
manager.install("scm-git-plugin", false); manager.install("scm-git-plugin", false);
verify(installer).install(git); verify(installer).install(context, git);
verify(restarter, never()).restart(any(), any()); verify(restarter, never()).restart(any(), any());
} }
@@ -222,8 +230,8 @@ class DefaultPluginManagerTest {
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
verify(installer).install(mail); verify(installer).install(context, mail);
verify(installer).install(review); verify(installer).install(context, review);
} }
@Test @Test
@@ -239,7 +247,7 @@ class DefaultPluginManagerTest {
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
ArgumentCaptor<AvailablePlugin> captor = ArgumentCaptor.forClass(AvailablePlugin.class); ArgumentCaptor<AvailablePlugin> captor = ArgumentCaptor.forClass(AvailablePlugin.class);
verify(installer).install(captor.capture()); verify(installer).install(any(), captor.capture());
assertThat(captor.getValue().getDescriptor().getInformation().getName()).isEqualTo("scm-review-plugin"); assertThat(captor.getValue().getDescriptor().getInformation().getName()).isEqualTo("scm-review-plugin");
} }
@@ -256,8 +264,8 @@ class DefaultPluginManagerTest {
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
verify(installer).install(mail); verify(installer).install(context, mail);
verify(installer).install(review); verify(installer).install(context, review);
} }
@Test @Test
@@ -272,8 +280,8 @@ class DefaultPluginManagerTest {
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
verify(installer).install(mail); verify(installer).install(context, mail);
verify(installer).install(review); verify(installer).install(context, review);
} }
@Test @Test
@@ -285,8 +293,8 @@ class DefaultPluginManagerTest {
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
verify(installer, never()).install(mail); verify(installer, never()).install(context, mail);
verify(installer).install(review); verify(installer).install(context, review);
} }
@Test @Test
@@ -299,12 +307,12 @@ class DefaultPluginManagerTest {
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail, notification)); when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail, notification));
PendingPluginInstallation pendingNotification = mock(PendingPluginInstallation.class); PendingPluginInstallation pendingNotification = mock(PendingPluginInstallation.class);
doReturn(pendingNotification).when(installer).install(notification); doReturn(pendingNotification).when(installer).install(context, notification);
PendingPluginInstallation pendingMail = mock(PendingPluginInstallation.class); PendingPluginInstallation pendingMail = mock(PendingPluginInstallation.class);
doReturn(pendingMail).when(installer).install(mail); doReturn(pendingMail).when(installer).install(context, mail);
doThrow(new PluginChecksumMismatchException(mail, "1", "2")).when(installer).install(review); doThrow(new PluginChecksumMismatchException(mail, "1", "2")).when(installer).install(context, review);
assertThrows(PluginInstallException.class, () -> manager.install("scm-review-plugin", false)); assertThrows(PluginInstallException.class, () -> manager.install("scm-review-plugin", false));
@@ -322,7 +330,7 @@ class DefaultPluginManagerTest {
assertThrows(NotFoundException.class, () -> manager.install("scm-review-plugin", false)); assertThrows(NotFoundException.class, () -> manager.install("scm-review-plugin", false));
verify(installer, never()).install(any()); verify(installer, never()).install(any(), any());
} }
@Test @Test
@@ -332,7 +340,7 @@ class DefaultPluginManagerTest {
manager.install("scm-git-plugin", true); manager.install("scm-git-plugin", true);
verify(installer).install(git); verify(installer).install(context, git);
verify(restarter).restart(any(), any()); verify(restarter).restart(any(), any());
} }
@@ -353,7 +361,7 @@ class DefaultPluginManagerTest {
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
// only one interaction // only one interaction
verify(installer).install(any()); verify(installer).install(any(), any());
} }
@Test @Test
@@ -538,7 +546,7 @@ class DefaultPluginManagerTest {
AvailablePlugin git = createAvailable("scm-git-plugin"); AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); when(center.getAvailable()).thenReturn(ImmutableSet.of(git));
PendingPluginInstallation gitPendingPluginInformation = mock(PendingPluginInstallation.class); PendingPluginInstallation gitPendingPluginInformation = mock(PendingPluginInstallation.class);
when(installer.install(git)).thenReturn(gitPendingPluginInformation); when(installer.install(context, git)).thenReturn(gitPendingPluginInformation);
manager.install("scm-git-plugin", false); manager.install("scm-git-plugin", false);
manager.uninstall("scm-ssh-plugin", false); manager.uninstall("scm-ssh-plugin", false);
@@ -571,8 +579,8 @@ class DefaultPluginManagerTest {
manager.updateAll(); manager.updateAll();
verify(installer).install(newMailPlugin); verify(installer).install(context, newMailPlugin);
verify(installer).install(newReviewPlugin); verify(installer).install(context, newReviewPlugin);
} }
@@ -587,7 +595,7 @@ class DefaultPluginManagerTest {
manager.updateAll(); manager.updateAll();
verify(installer, never()).install(oldScriptPlugin); verify(installer, never()).install(context, oldScriptPlugin);
} }
@Test @Test
@@ -607,7 +615,7 @@ class DefaultPluginManagerTest {
void shouldFirePluginEventOnFailedInstallation() { void shouldFirePluginEventOnFailedInstallation() {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
doThrow(new PluginDownloadException(review, new IOException())).when(installer).install(review); doThrow(new PluginDownloadException(review, new IOException())).when(installer).install(context, review);
assertThrows(PluginDownloadException.class, () -> manager.install("scm-review-plugin", false)); assertThrows(PluginDownloadException.class, () -> manager.install("scm-review-plugin", false));

View File

@@ -0,0 +1,115 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PluginInstallationContextTest {
@Test
void shouldReturnInstalledPlugin() {
Set<InstalledPlugin> installed = installed("scm-git-plugin", "1.0.0");
Set<AvailablePlugin> pending = Collections.emptySet();
PluginInstallationContext context = PluginInstallationContext.from(installed, pending);
Optional<NameAndVersion> plugin = context.find("scm-git-plugin");
assertThat(plugin).contains(new NameAndVersion("scm-git-plugin", "1.0.0"));
}
@Test
void shouldReturnPendingPlugin() {
Set<InstalledPlugin> installed = Collections.emptySet();
Set<AvailablePlugin> pending = pending("scm-hg-plugin", "1.0.0");
PluginInstallationContext context = PluginInstallationContext.from(installed, pending);
Optional<NameAndVersion> plugin = context.find("scm-hg-plugin");
assertThat(plugin).contains(new NameAndVersion("scm-hg-plugin", "1.0.0"));
}
@Test
void shouldReturnPendingEvenWithInstalled() {
Set<InstalledPlugin> installed = installed("scm-svn-plugin", "1.1.0");
Set<AvailablePlugin> pending = pending("scm-svn-plugin", "1.2.0");
PluginInstallationContext context = PluginInstallationContext.from(installed, pending);
Optional<NameAndVersion> plugin = context.find("scm-svn-plugin");
assertThat(plugin).contains(new NameAndVersion("scm-svn-plugin", "1.2.0"));
}
@Test
void shouldReturnEmpty() {
Set<InstalledPlugin> installed = Collections.emptySet();
Set<AvailablePlugin> pending = Collections.emptySet();
PluginInstallationContext context = PluginInstallationContext.from(installed, pending);
Optional<NameAndVersion> plugin = context.find("scm-legacy-plugin");
assertThat(plugin).isEmpty();
}
@Test
void shouldCreateContextFromDescriptor() {
Set<InstalledPluginDescriptor> installed = mockDescriptor(InstalledPluginDescriptor.class, "scm-svn-plugin", "1.1.0");
Set<AvailablePluginDescriptor> pending = mockDescriptor(AvailablePluginDescriptor.class, "scm-svn-plugin", "1.2.0");
PluginInstallationContext context = PluginInstallationContext.fromDescriptors(installed, pending);
Optional<NameAndVersion> plugin = context.find("scm-svn-plugin");
assertThat(plugin).contains(new NameAndVersion("scm-svn-plugin", "1.2.0"));
}
private Set<InstalledPlugin> installed(String name, String version) {
return mockPlugin(InstalledPlugin.class, name, version);
}
private Set<AvailablePlugin> pending(String name, String version) {
return mockPlugin(AvailablePlugin.class, name, version);
}
private <P extends Plugin> Set<P> mockPlugin(Class<P> pluginClass, String name, String version) {
P plugin = mock(pluginClass, Answers.RETURNS_DEEP_STUBS);
when(plugin.getDescriptor().getInformation().getName()).thenReturn(name);
when(plugin.getDescriptor().getInformation().getVersion()).thenReturn(version);
return Collections.singleton(plugin);
}
private <D extends PluginDescriptor> Set<D> mockDescriptor(Class<D> descriptorClass, String name, String version) {
D desc = mock(descriptorClass, Answers.RETURNS_DEEP_STUBS);
when(desc.getInformation().getName()).thenReturn(name);
when(desc.getInformation().getVersion()).thenReturn(version);
return Collections.singleton(desc);
}
}

View File

@@ -0,0 +1,174 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
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.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.plugin.PluginInstallationContext.empty;
@ExtendWith(MockitoExtension.class)
class PluginInstallationVerifierTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private InstalledPluginDescriptor descriptor;
// hog stands for "Heart of Gold"
private static final String HOG_PLUGIN = "scm-hog-plugin";
// iid stands for "Infinite Improbability Drive"
private static final String IID_PLUGIN = "scm-iid-plugin";
@BeforeEach
void setUpDescriptor() {
PluginInformation information = new PluginInformation();
information.setName(HOG_PLUGIN);
information.setVersion("1.0.0");
when(descriptor.getInformation()).thenReturn(information);
}
@Test
void shouldFailOnCondition() {
PluginInstallationContext context = empty();
assertThrows(PluginConditionFailedException.class, () -> PluginInstallationVerifier.verify(context, descriptor));
}
@Test
void shouldFailOnMissingDependency() {
matchConditions();
when(descriptor.getDependenciesWithVersion()).thenReturn(Collections.singleton(new NameAndVersion(IID_PLUGIN)));
PluginInstallationContext context = empty();
DependencyNotFoundException exception = assertThrows(
DependencyNotFoundException.class, () -> PluginInstallationVerifier.verify(context, descriptor)
);
assertThat(exception.getPlugin()).isEqualTo(HOG_PLUGIN);
assertThat(exception.getMissingDependency()).isEqualTo(IID_PLUGIN);
}
private void matchConditions() {
when(descriptor.getCondition().isSupported()).thenReturn(true);
}
@Test
void shouldFailOnDependencyVersionMismatch() {
matchConditions();
// mock installation of iid 1.0.0
PluginInstallationContext context = mockInstallationOf(IID_PLUGIN, "1.0.0");
// mock dependency of iid 1.1.0
mockDependingOf(IID_PLUGIN, "1.1.0");
DependencyVersionMismatchException exception = assertThrows(
DependencyVersionMismatchException.class, () -> PluginInstallationVerifier.verify(context, descriptor)
);
assertThat(exception.getPlugin()).isEqualTo(HOG_PLUGIN);
assertThat(exception.getDependency()).isEqualTo(IID_PLUGIN);
assertThat(exception.getMinVersion()).isEqualTo("1.1.0");
assertThat(exception.getCurrentVersion()).isEqualTo("1.0.0");
}
@Test
void shouldFailOnOptionalDependencyVersionMismatch() {
matchConditions();
// mock installation of iid 1.0.0
PluginInstallationContext context = mockInstallationOf(IID_PLUGIN, "1.0.0");
// mock dependency of iid 1.1.0
mockOptionalDependingOf(IID_PLUGIN, "1.1.0");
DependencyVersionMismatchException exception = assertThrows(
DependencyVersionMismatchException.class, () -> PluginInstallationVerifier.verify(context, descriptor)
);
assertThat(exception.getPlugin()).isEqualTo(HOG_PLUGIN);
assertThat(exception.getDependency()).isEqualTo(IID_PLUGIN);
assertThat(exception.getMinVersion()).isEqualTo("1.1.0");
assertThat(exception.getCurrentVersion()).isEqualTo("1.0.0");
}
@Test
@SuppressWarnings("squid:S2699") // we are happy if no exception is thrown
void shouldVerifyPlugin() {
matchConditions();
PluginInstallationContext context = empty();
PluginInstallationVerifier.verify(context, descriptor);
}
@Test
@SuppressWarnings("squid:S2699") // we are happy if no exception is thrown
void shouldVerifyPluginWithDependencies() {
matchConditions();
// mock installation of iid 1.1.0
PluginInstallationContext context = mockInstallationOf(IID_PLUGIN, "1.1.0");
// mock dependency of iid 1.1.0
mockDependingOf(IID_PLUGIN, "1.1.0");
PluginInstallationVerifier.verify(context, descriptor);
}
@Test
@SuppressWarnings("squid:S2699") // we are happy if no exception is thrown
void shouldVerifyPluginWithOptionalDependency() {
matchConditions();
PluginInstallationContext context = PluginInstallationContext.empty();
// mock dependency of iid 1.1.0
mockOptionalDependingOf(IID_PLUGIN, "1.1.0");
PluginInstallationVerifier.verify(context, descriptor);
}
private void mockOptionalDependingOf(String plugin, String version) {
when(descriptor.getOptionalDependenciesWithVersion()).thenReturn(Collections.singleton(new NameAndVersion(plugin, version)));
}
private void mockDependingOf(String plugin, String version) {
when(descriptor.getDependenciesWithVersion()).thenReturn(Collections.singleton(new NameAndVersion(plugin, version)));
}
private PluginInstallationContext mockInstallationOf(String plugin, String version) {
PluginInstallationContext context = mock(PluginInstallationContext.class);
when(context.find(IID_PLUGIN)).thenReturn(Optional.of(new NameAndVersion(plugin, version)));
return context;
}
}

View File

@@ -83,7 +83,7 @@ class PluginInstallerTest {
void shouldDownloadPlugin() throws IOException { void shouldDownloadPlugin() throws IOException {
mockContent("42"); mockContent("42");
installer.install(createGitPlugin()); installer.install(PluginInstallationContext.empty(), createGitPlugin());
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).hasContent("42"); assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).hasContent("42");
} }
@@ -93,7 +93,7 @@ class PluginInstallerTest {
mockContent("42"); mockContent("42");
AvailablePlugin gitPlugin = createGitPlugin(); AvailablePlugin gitPlugin = createGitPlugin();
PendingPluginInstallation pending = installer.install(gitPlugin); PendingPluginInstallation pending = installer.install(PluginInstallationContext.empty(), gitPlugin);
assertThat(pending).isNotNull(); assertThat(pending).isNotNull();
assertThat(pending.getPlugin().getDescriptor()).isEqualTo(gitPlugin.getDescriptor()); assertThat(pending.getPlugin().getDescriptor()).isEqualTo(gitPlugin.getDescriptor());
@@ -117,14 +117,18 @@ class PluginInstallerTest {
void shouldThrowPluginDownloadException() throws IOException { void shouldThrowPluginDownloadException() throws IOException {
when(client.get("https://download.hitchhiker.com").request()).thenThrow(new IOException("failed to download")); when(client.get("https://download.hitchhiker.com").request()).thenThrow(new IOException("failed to download"));
assertThrows(PluginDownloadException.class, () -> installer.install(createGitPlugin())); PluginInstallationContext context = PluginInstallationContext.empty();
AvailablePlugin gitPlugin = createGitPlugin();
assertThrows(PluginDownloadException.class, () -> installer.install(context, gitPlugin));
} }
@Test @Test
void shouldThrowPluginChecksumMismatchException() throws IOException { void shouldThrowPluginChecksumMismatchException() throws IOException {
mockContent("21"); mockContent("21");
assertThrows(PluginChecksumMismatchException.class, () -> installer.install(createGitPlugin())); PluginInstallationContext context = PluginInstallationContext.empty();
AvailablePlugin gitPlugin = createGitPlugin();
assertThrows(PluginChecksumMismatchException.class, () -> installer.install(context, gitPlugin));
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
} }
@@ -134,7 +138,9 @@ class PluginInstallerTest {
when(stream.read(any(), anyInt(), anyInt())).thenThrow(new IOException("failed to read")); when(stream.read(any(), anyInt(), anyInt())).thenThrow(new IOException("failed to read"));
when(client.get("https://download.hitchhiker.com").request().contentAsStream()).thenReturn(stream); when(client.get("https://download.hitchhiker.com").request().contentAsStream()).thenReturn(stream);
assertThrows(PluginDownloadException.class, () -> installer.install(createGitPlugin())); PluginInstallationContext context = PluginInstallationContext.empty();
AvailablePlugin gitPlugin = createGitPlugin();
assertThrows(PluginDownloadException.class, () -> installer.install(context, gitPlugin));
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
} }
@@ -144,13 +150,44 @@ class PluginInstallerTest {
InstalledPluginDescriptor supportedPlugin = createPluginDescriptor(false); InstalledPluginDescriptor supportedPlugin = createPluginDescriptor(false);
when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin); when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin);
assertThrows(PluginConditionFailedException.class, () -> installer.install(createGitPlugin())); PluginInstallationContext context = PluginInstallationContext.empty();
AvailablePlugin gitPlugin = createGitPlugin();
assertThrows(PluginConditionFailedException.class, () -> installer.install(context, gitPlugin));
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
} }
@Test
void shouldFailForNameMismatch() throws IOException {
mockContent("42");
InstalledPluginDescriptor supportedPlugin = createPluginDescriptor("scm-svn-plugin", "1.0.0", true);
when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin);
PluginInstallationContext context = PluginInstallationContext.empty();
AvailablePlugin gitPlugin = createGitPlugin();
PluginInformationMismatchException exception = assertThrows(PluginInformationMismatchException.class, () -> installer.install(context, gitPlugin));
assertThat(exception.getApi().getName()).isEqualTo("scm-git-plugin");
assertThat(exception.getDownloaded().getName()).isEqualTo("scm-svn-plugin");
}
@Test
void shouldFailForVersionMismatch() throws IOException {
mockContent("42");
InstalledPluginDescriptor supportedPlugin = createPluginDescriptor("scm-git-plugin", "1.1.0", true);
when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin);
PluginInstallationContext context = PluginInstallationContext.empty();
AvailablePlugin gitPlugin = createGitPlugin();
PluginInformationMismatchException exception = assertThrows(PluginInformationMismatchException.class, () -> installer.install(context, gitPlugin));
assertThat(exception.getApi().getVersion()).isEqualTo("1.0.0");
assertThat(exception.getDownloaded().getVersion()).isEqualTo("1.1.0");
}
private AvailablePlugin createPlugin(String name, String url, String checksum) { private AvailablePlugin createPlugin(String name, String url, String checksum) {
PluginInformation information = new PluginInformation(); PluginInformation information = new PluginInformation();
information.setName(name); information.setName(name);
information.setVersion("1.0.0");
AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor( AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(
information, null, Collections.emptySet(), url, checksum information, null, Collections.emptySet(), url, checksum
); );
@@ -158,9 +195,15 @@ class PluginInstallerTest {
} }
private InstalledPluginDescriptor createPluginDescriptor(boolean supported) { private InstalledPluginDescriptor createPluginDescriptor(boolean supported) {
return createPluginDescriptor("scm-git-plugin", "1.0.0", supported);
}
private InstalledPluginDescriptor createPluginDescriptor(String name, String version, boolean supported) {
InstalledPluginDescriptor installedPluginDescriptor = mock(InstalledPluginDescriptor.class, RETURNS_DEEP_STUBS); InstalledPluginDescriptor installedPluginDescriptor = mock(InstalledPluginDescriptor.class, RETURNS_DEEP_STUBS);
lenient().when(installedPluginDescriptor.getInformation().getId()).thenReturn(name);
lenient().when(installedPluginDescriptor.getInformation().getName()).thenReturn(name);
lenient().when(installedPluginDescriptor.getInformation().getVersion()).thenReturn(version);
lenient().when(installedPluginDescriptor.getCondition().isSupported()).thenReturn(supported); lenient().when(installedPluginDescriptor.getCondition().isSupported()).thenReturn(supported);
lenient().when(installedPluginDescriptor.getInformation().getId()).thenReturn("scm-git-plugin");
return installedPluginDescriptor; return installedPluginDescriptor;
} }
} }

View File

@@ -27,158 +27,176 @@ package sonia.scm.plugin;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeEach;
import org.junit.Before; import org.junit.jupiter.api.Test;
import org.junit.Rule; import org.junit.jupiter.api.io.TempDir;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle; import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle;
import static org.hamcrest.Matchers.*; import javax.xml.bind.JAXB;
import static org.junit.Assert.*;
//~--- JDK imports ------------------------------------------------------------
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL; import java.net.URL;
import java.nio.file.Path;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class PluginProcessorTest class PluginProcessorTest {
{
/** Field description */
private static final PluginResource PLUGIN_A = private static final PluginResource PLUGIN_A =
new PluginResource("sonia/scm/plugin/scm-a-plugin.smp", "scm-a-plugin.smp", new PluginResource("sonia/scm/plugin/scm-a-plugin.smp", "scm-a-plugin.smp",
"scm-a-plugin:1.0.0-SNAPSHOT"); "scm-a-plugin:1.0.0-SNAPSHOT");
/** Field description */
private static final PluginResource PLUGIN_B = private static final PluginResource PLUGIN_B =
new PluginResource("sonia/scm/plugin/scm-b-plugin.smp", "scm-b-plugin.smp", new PluginResource("sonia/scm/plugin/scm-b-plugin.smp", "scm-b-plugin.smp",
"scm-b-plugin:1.0.0-SNAPSHOT"); "scm-b-plugin:1.0.0-SNAPSHOT");
/** Field description */
private static final PluginResource PLUGIN_C = private static final PluginResource PLUGIN_C =
new PluginResource("sonia/scm/plugin/scm-c-plugin.smp", "scm-c-plugin.smp", new PluginResource("sonia/scm/plugin/scm-c-plugin.smp", "scm-c-plugin.smp",
"scm-c-plugin:1.0.0-SNAPSHOT"); "scm-c-plugin:1.0.0-SNAPSHOT");
/** Field description */
private static final PluginResource PLUGIN_D = private static final PluginResource PLUGIN_D =
new PluginResource("sonia/scm/plugin/scm-d-plugin.smp", "scm-d-plugin.smp", new PluginResource("sonia/scm/plugin/scm-d-plugin.smp", "scm-d-plugin.smp",
"scm-d-plugin:1.0.0-SNAPSHOT"); "scm-d-plugin:1.0.0-SNAPSHOT");
/** Field description */
private static final PluginResource PLUGIN_E = private static final PluginResource PLUGIN_E =
new PluginResource("sonia/scm/plugin/scm-e-plugin.smp", "scm-e-plugin.smp", new PluginResource("sonia/scm/plugin/scm-e-plugin.smp", "scm-e-plugin.smp",
"scm-e-plugin:1.0.0-SNAPSHOT"); "scm-e-plugin:1.0.0-SNAPSHOT");
/** Field description */
private static final PluginResource PLUGIN_F_1_0_0 = private static final PluginResource PLUGIN_F_1_0_0 =
new PluginResource("sonia/scm/plugin/scm-f-plugin-1.0.0.smp", new PluginResource("sonia/scm/plugin/scm-f-plugin-1.0.0.smp",
"scm-f-plugin.smp", "scm-f-plugin:1.0.0"); "scm-f-plugin.smp", "scm-f-plugin:1.0.0");
/** Field description */
private static final PluginResource PLUGIN_F_1_0_1 = private static final PluginResource PLUGIN_F_1_0_1 =
new PluginResource("sonia/scm/plugin/scm-f-plugin-1.0.1.smp", new PluginResource("sonia/scm/plugin/scm-f-plugin-1.0.1.smp",
"scm-f-plugin.smp", "scm-f-plugin:1.0.1"); "scm-f-plugin.smp", "scm-f-plugin:1.0.1");
//~--- methods -------------------------------------------------------------- private static final String PLUGIN_G = "scm-g-plugin";
private static final String PLUGIN_H = "scm-h-plugin";
private static final String PLUGIN_I = "scm-i-plugin";
/** private File pluginDirectory;
* Method description private PluginProcessor processor;
*
* @BeforeEach
* @throws IOException void setUp(@TempDir Path tempDirectoryPath) {
*/ pluginDirectory = tempDirectoryPath.toFile();
@Test(expected = PluginCircularDependencyException.class) processor = new PluginProcessor(ClassLoaderLifeCycle.create(), tempDirectoryPath);
public void testCircularDependencies() throws IOException }
{
@Test
void shouldFailOnPluginCondition() throws IOException {
createPendingPluginInstallation(PLUGIN_G);
assertThrows(PluginConditionFailedException.class, this::collectPlugins);
}
@Test
void shouldFailOnWrongDependencyVersion() throws IOException {
createPendingPluginInstallation(PLUGIN_H);
createPendingPluginInstallation(PLUGIN_I);
assertThrows(DependencyVersionMismatchException.class, this::collectPlugins);
}
@Test
void shouldNotContainDuplicatesOnUpdate() throws IOException {
createInstalledPlugin("scm-mail-plugin-2-0-0");
createInstalledPlugin("scm-review-plugin-2-0-0");
createPendingPluginInstallation("scm-mail-plugin-2-1-0");
createPendingPluginInstallation("scm-review-plugin-2-1-0");
Set<String> plugins = collectPlugins().stream()
.map(p -> p.getDescriptor().getInformation().getName(true))
.collect(Collectors.toSet());
assertThat(plugins).containsOnly("scm-mail-plugin:2.1.0", "scm-review-plugin:2.1.0");
}
@SuppressWarnings("UnstableApiUsage")
private void createPendingPluginInstallation(String descriptorResource) throws IOException {
URL resource = resource(descriptorResource);
InstalledPluginDescriptor descriptor = JAXB.unmarshal(resource, InstalledPluginDescriptor.class);
File file = new File(pluginDirectory, descriptor.getInformation().getName() + ".smp");
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(file))) {
zip.putNextEntry(new ZipEntry("META-INF/scm/plugin.xml"));
Resources.copy(resource, zip);
}
}
@SuppressWarnings("UnstableApiUsage")
private void createInstalledPlugin(String descriptorResource) throws IOException {
URL resource = resource(descriptorResource);
InstalledPluginDescriptor descriptor = JAXB.unmarshal(resource, InstalledPluginDescriptor.class);
File directory = new File(pluginDirectory, descriptor.getInformation().getName());
File scmDirectory = new File(directory, "META-INF" + File.separator + "scm");
assertThat(scmDirectory.mkdirs()).isTrue();
try (OutputStream output = new FileOutputStream(new File(scmDirectory, "plugin.xml"))) {
Resources.copy(resource, output);
}
}
@SuppressWarnings("UnstableApiUsage")
private URL resource(String descriptorResource) {
return Resources.getResource("sonia/scm/plugin/" + descriptorResource + ".xml");
}
@Test
void shouldFailOnCircularDependencies() throws IOException {
copySmps(PLUGIN_C, PLUGIN_D, PLUGIN_E); copySmps(PLUGIN_C, PLUGIN_D, PLUGIN_E);
collectPlugins(); assertThrows(PluginCircularDependencyException.class, this::collectPlugins);
} }
/**
* Method description
*
*
* @throws IOException
*/
@Test @Test
public void testCollectPlugins() throws IOException void shouldCollectPlugins() throws IOException {
{
copySmp(PLUGIN_A); copySmp(PLUGIN_A);
InstalledPlugin plugin = collectAndGetFirst(); InstalledPlugin plugin = collectAndGetFirst();
assertThat(plugin.getId()).isEqualTo(PLUGIN_A.id);
assertThat(plugin.getId(), is(PLUGIN_A.id));
} }
@Test @Test
public void shouldCollectPluginsAndDoNotFailOnNonPluginDirectories() throws IOException { void shouldCollectPluginsAndDoNotFailOnNonPluginDirectories() throws IOException {
new File(pluginDirectory, "some-directory").mkdirs(); assertThat(new File(pluginDirectory, "some-directory").mkdirs()).isTrue();
copySmp(PLUGIN_A); copySmp(PLUGIN_A);
InstalledPlugin plugin = collectAndGetFirst(); InstalledPlugin plugin = collectAndGetFirst();
assertThat(plugin.getId(), is(PLUGIN_A.id)); assertThat(plugin.getId()).isEqualTo(PLUGIN_A.id);
} }
/**
* Method description
*
*
* @throws IOException
*/
@Test @Test
public void testCollectPluginsWithDependencies() throws IOException void shouldCollectPluginsWithDependencies() throws IOException {
{
copySmps(PLUGIN_A, PLUGIN_B); copySmps(PLUGIN_A, PLUGIN_B);
Set<InstalledPlugin> plugins = collectPlugins(); Set<InstalledPlugin> plugins = collectPlugins();
assertThat(plugins).hasSize(2);
assertThat(plugins, hasSize(2));
InstalledPlugin a = findPlugin(plugins, PLUGIN_A.id); InstalledPlugin a = findPlugin(plugins, PLUGIN_A.id);
assertThat(a).isNotNull();
assertNotNull(a);
InstalledPlugin b = findPlugin(plugins, PLUGIN_B.id); InstalledPlugin b = findPlugin(plugins, PLUGIN_B.id);
assertThat(b).isNotNull();
assertNotNull(b);
} }
/**
* Method description
*
*
* @throws ClassNotFoundException
* @throws IOException
* @throws IllegalAccessException
* @throws IllegalArgumentException
* @throws InstantiationException
* @throws InvocationTargetException
* @throws NoSuchMethodException
*/
@Test @Test
public void testPluginClassLoader() void shouldCreateWorkingPluginClassLoader() throws Exception {
throws IOException, ClassNotFoundException, InstantiationException,
IllegalAccessException, NoSuchMethodException, IllegalArgumentException,
InvocationTargetException
{
copySmp(PLUGIN_A); copySmp(PLUGIN_A);
InstalledPlugin plugin = collectAndGetFirst(); InstalledPlugin plugin = collectAndGetFirst();
@@ -187,36 +205,20 @@ public class PluginProcessorTest
// load parent class // load parent class
Class<?> clazz = cl.loadClass(PluginResource.class.getName()); Class<?> clazz = cl.loadClass(PluginResource.class.getName());
assertSame(PluginResource.class, clazz); assertThat(PluginResource.class).isSameAs(clazz);
// load packaged class // load packaged class
clazz = cl.loadClass("sonia.scm.plugins.HelloService"); clazz = cl.loadClass("sonia.scm.plugins.HelloService");
assertNotNull(clazz); assertThat(clazz).isNotNull();
Object instance = clazz.newInstance(); Object instance = clazz.newInstance();
Object result = clazz.getMethod("sayHello").invoke(instance); Object result = clazz.getMethod("sayHello").invoke(instance);
assertEquals("hello", result); assertThat(result).isEqualTo("hello");
} }
/**
* Method description
*
*
* @throws ClassNotFoundException
* @throws IOException
* @throws IllegalAccessException
* @throws IllegalArgumentException
* @throws InstantiationException
* @throws InvocationTargetException
* @throws NoSuchMethodException
*/
@Test @Test
public void testPluginClassLoaderWithDependencies() void shouldCreateWorkingPluginClassLoaderWithDependencies() throws Exception {
throws IOException, ClassNotFoundException, InstantiationException,
IllegalAccessException, NoSuchMethodException, IllegalArgumentException,
InvocationTargetException
{
copySmps(PLUGIN_A, PLUGIN_B); copySmps(PLUGIN_A, PLUGIN_B);
Set<InstalledPlugin> plugins = collectPlugins(); Set<InstalledPlugin> plugins = collectPlugins();
@@ -227,213 +229,88 @@ public class PluginProcessorTest
// load parent class // load parent class
Class<?> clazz = cl.loadClass(PluginResource.class.getName()); Class<?> clazz = cl.loadClass(PluginResource.class.getName());
assertSame(PluginResource.class, clazz); assertThat(PluginResource.class).isSameAs(clazz);
// load packaged class // load packaged class
clazz = cl.loadClass("sonia.scm.plugins.HelloAgainService"); clazz = cl.loadClass("sonia.scm.plugins.HelloAgainService");
assertNotNull(clazz); assertThat(clazz).isNotNull();
Object instance = clazz.newInstance(); Object instance = clazz.newInstance();
Object result = clazz.getMethod("sayHelloAgain").invoke(instance); Object result = clazz.getMethod("sayHelloAgain").invoke(instance);
assertEquals("hello again", result); assertThat(result).isEqualTo("hello again");
} }
/**
* Method description
*
*
* @throws IOException
*/
@Test @Test
public void testPluginWebResourceLoader() throws IOException @SuppressWarnings("UnstableApiUsage")
{ void shouldCreatePluginWebResourceLoader() throws IOException {
copySmp(PLUGIN_A); copySmp(PLUGIN_A);
InstalledPlugin plugin = collectAndGetFirst(); InstalledPlugin plugin = collectAndGetFirst();
WebResourceLoader wrl = plugin.getWebResourceLoader(); WebResourceLoader wrl = plugin.getWebResourceLoader();
assertThat(wrl).isNotNull();
assertNotNull(wrl);
URL url = wrl.getResource("hello"); URL url = wrl.getResource("hello");
assertThat(url).isNotNull();
assertNotNull(url); assertThat(Resources.toString(url, Charsets.UTF_8)).isEqualTo("hello");
assertThat(Resources.toString(url, Charsets.UTF_8), is("hello"));
} }
/**
* Method description
*
*
* @throws IOException
*/
@Test @Test
public void testUpdate() throws IOException void shouldDoPluginUpdate() throws IOException {
{
copySmp(PLUGIN_F_1_0_0); copySmp(PLUGIN_F_1_0_0);
InstalledPlugin plugin = collectAndGetFirst(); InstalledPlugin plugin = collectAndGetFirst();
assertThat(plugin.getId()).isEqualTo(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);
plugin = collectAndGetFirst(); plugin = collectAndGetFirst();
assertThat(plugin.getId(), is(PLUGIN_F_1_0_1.id)); assertThat(plugin.getId()).isEqualTo(PLUGIN_F_1_0_1.id);
} }
//~--- set methods ---------------------------------------------------------- private InstalledPlugin collectAndGetFirst() throws IOException {
/**
* Method description
*
*
* @throws IOException
*/
@Before
public void setUp() throws IOException
{
pluginDirectory = temp.newFolder();
processor = new PluginProcessor(ClassLoaderLifeCycle.create(), pluginDirectory.toPath());
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @return
*
* @throws IOException
*/
private InstalledPlugin collectAndGetFirst() throws IOException
{
Set<InstalledPlugin> plugins = collectPlugins(); Set<InstalledPlugin> plugins = collectPlugins();
assertThat(plugins, hasSize(1)); assertThat(plugins).hasSize(1);
return Iterables.get(plugins, 0); return Iterables.get(plugins, 0);
} }
/** private Set<InstalledPlugin> collectPlugins() throws IOException {
* Method description
*
*
* @return
*
* @throws IOException
*/
private Set<InstalledPlugin> collectPlugins() throws IOException
{
return processor.collectPlugins(PluginProcessorTest.class.getClassLoader()); return processor.collectPlugins(PluginProcessorTest.class.getClassLoader());
} }
/** @SuppressWarnings("UnstableApiUsage")
* Method description private void copySmp(PluginResource plugin) throws IOException {
*
*
* @param plugin
*
* @throws IOException
*/
private void copySmp(PluginResource plugin) throws IOException
{
URL resource = Resources.getResource(plugin.path); URL resource = Resources.getResource(plugin.path);
File file = new File(pluginDirectory, plugin.name); File file = new File(pluginDirectory, plugin.name);
try (OutputStream out = new FileOutputStream(file)) try (OutputStream out = new FileOutputStream(file)) {
{
Resources.copy(resource, out); Resources.copy(resource, out);
} }
} }
/** private void copySmps(PluginResource... plugins) throws IOException {
* Method description for (PluginResource plugin : plugins) {
*
*
* @param plugins
*
* @throws IOException
*/
private void copySmps(PluginResource... plugins) throws IOException
{
for (PluginResource plugin : plugins)
{
copySmp(plugin); copySmp(plugin);
} }
} }
/** private InstalledPlugin findPlugin(Iterable<InstalledPlugin> plugin, final String id) {
* Method description return Iterables.find(plugin, input -> id.equals(input.getId()));
*
*
* @param plugin
* @param id
*
* @return
*/
private InstalledPlugin findPlugin(Iterable<InstalledPlugin> plugin,
final String id)
{
return Iterables.find(plugin, new Predicate<InstalledPlugin>()
{
@Override
public boolean apply(InstalledPlugin input)
{
return id.equals(input.getId());
}
});
} }
private static class PluginResource {
//~--- inner classes -------------------------------------------------------- private final String path;
private final String name;
private final String id;
/**
* Class description
*
*
* @version Enter version here..., 14/12/06
* @author Enter your name here...
*/
private static class PluginResource
{
/** public PluginResource(String path, String name, String id) {
* Constructs ...
*
*
* @param path
* @param name
* @param id
*/
public PluginResource(String path, String name, String id)
{
this.path = path; this.path = path;
this.name = name; this.name = name;
this.id = id; this.id = id;
} }
//~--- fields -------------------------------------------------------------
/** Field description */
private final String id;
/** Field description */
private final String name;
/** Field description */
private final String path;
} }
//~--- fields ---------------------------------------------------------------
/** Field description */
@Rule
public TemporaryFolder temp = new TemporaryFolder();
/** Field description */
private File pluginDirectory;
/** Field description */
private PluginProcessor processor;
} }

View File

@@ -63,11 +63,13 @@ class SmpDescriptorExtractorTest {
"\n" + "\n" +
"</plugin>\n"; "</plugin>\n";
private final SmpDescriptorExtractor extractor = new SmpDescriptorExtractor();
@Test @Test
void shouldExtractPluginXml(@TempDir Path tempDir) throws IOException { void shouldExtractPluginXml(@TempDir Path tempDir) throws IOException {
Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", PLUGIN_XML); Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", PLUGIN_XML);
InstalledPluginDescriptor installedPluginDescriptor = new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile); InstalledPluginDescriptor installedPluginDescriptor = extractor.extractPluginDescriptor(pluginFile);
Assertions.assertThat(installedPluginDescriptor.getInformation().getName()).isEqualTo("scm-test-plugin"); Assertions.assertThat(installedPluginDescriptor.getInformation().getName()).isEqualTo("scm-test-plugin");
} }
@@ -76,14 +78,14 @@ class SmpDescriptorExtractorTest {
void shouldFailWithoutPluginXml(@TempDir Path tempDir) throws IOException { void shouldFailWithoutPluginXml(@TempDir Path tempDir) throws IOException {
Path pluginFile = createZipFile(tempDir, "META-INF/wrong/plugin.xml", PLUGIN_XML); Path pluginFile = createZipFile(tempDir, "META-INF/wrong/plugin.xml", PLUGIN_XML);
assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile)); assertThrows(IOException.class, () -> extractor.extractPluginDescriptor(pluginFile));
} }
@Test @Test
void shouldFailWithIllegalPluginXml(@TempDir Path tempDir) throws IOException { void shouldFailWithIllegalPluginXml(@TempDir Path tempDir) throws IOException {
Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", "<not><parsable>content</parsable></not>"); Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", "<not><parsable>content</parsable></not>");
assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile)); assertThrows(IOException.class, () -> extractor.extractPluginDescriptor(pluginFile));
} }
Path createZipFile(Path tempDir, String internalFileName, String content) throws IOException { Path createZipFile(Path tempDir, String internalFileName, String content) throws IOException {

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<plugin>
<scm-version>2</scm-version>
<information>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-g-plugin</artifactId>
<version>1.0.0</version>
<name>scm-g-plugin</name>
<description>Plugin g has a min version over 9000</description>
</information>
<conditions>
<min-version>9000.0.0</min-version>
</conditions>
<packages>
<package>sonia.scm.plugins</package>
</packages>
</plugin>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<plugin>
<scm-version>2</scm-version>
<information>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-h-plugin</artifactId>
<version>1.0.0</version>
<name>scm-h-plugin</name>
<description>Plugin h is nothing special</description>
</information>
<packages>
<package>sonia.scm.plugins</package>
</packages>
</plugin>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<plugin>
<scm-version>2</scm-version>
<information>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-i-plugin</artifactId>
<version>1.0.0</version>
<name>scm-i-plugin</name>
<description>Plugin i depends on h in version 1.2.0</description>
</information>
<packages>
<package>sonia.scm.plugins</package>
</packages>
<dependencies>
<dependency version="1.2.0">scm-h-plugin</dependency>
</dependencies>
</plugin>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<plugin>
<scm-version>2</scm-version>
<information>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-mail-plugin</artifactId>
<version>2.0.0</version>
<name>scm-mail-plugin</name>
<description>The awesome mail plugin</description>
</information>
<packages>
<package>sonia.scm.plugins</package>
</packages>
</plugin>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<plugin>
<scm-version>2</scm-version>
<information>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-mail-plugin</artifactId>
<version>2.1.0</version>
<name>scm-mail-plugin</name>
<description>The awesome mail plugin</description>
</information>
<packages>
<package>sonia.scm.plugins</package>
</packages>
</plugin>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<plugin>
<scm-version>2</scm-version>
<information>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-review-plugin</artifactId>
<version>2.0.0</version>
<name>scm-review-plugin</name>
<description>The awesome review plugin</description>
</information>
<packages>
<package>sonia.scm.plugins</package>
</packages>
<dependencies>
<dependency version="2.0.0">scm-mail-plugin</dependency>
</dependencies>
</plugin>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<plugin>
<scm-version>2</scm-version>
<information>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-review-plugin</artifactId>
<version>2.1.0</version>
<name>scm-review-plugin</name>
<description>The awesome review plugin</description>
</information>
<packages>
<package>sonia.scm.plugins</package>
</packages>
<dependencies>
<dependency version="2.1.0">scm-mail-plugin</dependency>
</dependencies>
</plugin>