diff --git a/Jenkinsfile b/Jenkinsfile index 0f586bf346..36135cf4a4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -7,12 +7,15 @@ import com.cloudogu.ces.cesbuildlib.* node('docker') { // Change this as when we go back to default - necessary for proper SonarQube analysis - mainBranch = "2.0.0-m3" + mainBranch = '2.0.0-m3' properties([ // Keep only the last 10 build to preserve space buildDiscarder(logRotator(numToKeepStr: '10')), - disableConcurrentBuilds() + disableConcurrentBuilds(), + parameters([ + string(name: 'dockerTag', trim: true, defaultValue: 'latest', description: 'Extra Docker Tag for cloudogu/scm-manager image') + ]) ]) timeout(activity: true, time: 30, unit: 'MINUTES') { @@ -51,9 +54,9 @@ node('docker') { if (isMainBranch()) { -// stage('Lifecycle') { -// nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build' -// } + stage('Lifecycle') { + nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build' + } stage('Archive') { archiveArtifacts 'scm-webapp/target/scm-webapp.war' @@ -66,6 +69,13 @@ node('docker') { docker.withRegistry('', 'hub.docker.com-cesmarvin') { image.push(dockerImageTag) image.push('latest') + if (!'latest'.equals(params.dockerTag)) { + image.push(params.dockerTag) + + def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}" + currentBuild.description = newDockerTag + image.push(newDockerTag) + } } } @@ -92,7 +102,7 @@ String mainBranch Maven setupMavenBuild() { // Keep this version number in sync with .mvn/maven-wrapper.properties - Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8") + Maven mvn = new MavenInDocker(this, '3.5.2-jdk-8') if (isMainBranch()) { // Release starts javadoc, which takes very long, so do only for certain branches diff --git a/docs/dtd/plugin/2.0.0-01.dtd b/docs/dtd/plugin/2.0.0-01.dtd index eec149f3e8..954a2c2219 100644 --- a/docs/dtd/plugin/2.0.0-01.dtd +++ b/docs/dtd/plugin/2.0.0-01.dtd @@ -29,46 +29,28 @@ - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + @@ -121,4 +103,4 @@ - \ No newline at end of file + diff --git a/docs/logo/favicon_16x16px.ico b/docs/logo/favicon_16x16px.ico new file mode 100644 index 0000000000..3436795fdf Binary files /dev/null and b/docs/logo/favicon_16x16px.ico differ diff --git a/docs/logo/favicon_16x16px_transparent.ico b/docs/logo/favicon_16x16px_transparent.ico new file mode 100644 index 0000000000..e5803f340d Binary files /dev/null and b/docs/logo/favicon_16x16px_transparent.ico differ diff --git a/docs/logo/scm-manager_logo.ai b/docs/logo/scm-manager_logo.ai new file mode 100644 index 0000000000..6fe98a15e0 --- /dev/null +++ b/docs/logo/scm-manager_logo.ai @@ -0,0 +1 @@ +%!PS-Adobe-2.0 %%Creator: Adobe Photoshop(TM) Pen Path Export 7.0 %%Title: (scm-manager_logo.ai) %%DocumentNeededResources: procset Adobe_packedarray 2.0 0 %%+ procset Adobe_IllustratorA_AI3 1.0 1 %%ColorUsage: Black&White %%BoundingBox: 0 0 800 275 %%HiResBoundingBox: 0 0 800 275 %AI3_Cropmarks: 0 0 800 275 %%DocumentPreview: None %%EndComments %%BeginProlog %%IncludeResource: procset Adobe_packedarray 2.0 0 Adobe_packedarray /initialize get exec %%IncludeResource: procset Adobe_IllustratorA_AI3 1.0 1 %%EndProlog %%BeginSetup Adobe_IllustratorA_AI3 /initialize get exec n %%EndSetup 0.0 0.0 0.0 1.0 k 0 i 0 J 0 j 1 w 4 M []0 d %%Note: %%Trailer %%EOF \ No newline at end of file diff --git a/docs/logo/scm-manager_logo.jpg b/docs/logo/scm-manager_logo.jpg new file mode 100644 index 0000000000..f518e3f2b2 Binary files /dev/null and b/docs/logo/scm-manager_logo.jpg differ diff --git a/docs/logo/scm-manager_logo.png b/docs/logo/scm-manager_logo.png new file mode 100644 index 0000000000..90a17c7ee4 Binary files /dev/null and b/docs/logo/scm-manager_logo.png differ diff --git a/docs/logo/scm-manager_logo_img.jpg b/docs/logo/scm-manager_logo_img.jpg new file mode 100644 index 0000000000..f2d3b35b66 Binary files /dev/null and b/docs/logo/scm-manager_logo_img.jpg differ diff --git a/docs/logo/scm-manager_logo_img.png b/docs/logo/scm-manager_logo_img.png new file mode 100644 index 0000000000..f349fc5d22 Binary files /dev/null and b/docs/logo/scm-manager_logo_img.png differ diff --git a/docs/logo/scm-manager_logo_img_neg.jpg b/docs/logo/scm-manager_logo_img_neg.jpg new file mode 100644 index 0000000000..1b56b5e627 Binary files /dev/null and b/docs/logo/scm-manager_logo_img_neg.jpg differ diff --git a/docs/logo/scm-manager_logo_img_neg.png b/docs/logo/scm-manager_logo_img_neg.png new file mode 100644 index 0000000000..796a02882c Binary files /dev/null and b/docs/logo/scm-manager_logo_img_neg.png differ diff --git a/docs/logo/scm-manager_logo_neg.jpg b/docs/logo/scm-manager_logo_neg.jpg new file mode 100644 index 0000000000..0d6704d0e3 Binary files /dev/null and b/docs/logo/scm-manager_logo_neg.jpg differ diff --git a/docs/logo/scm-manager_logo_neg.png b/docs/logo/scm-manager_logo_neg.png new file mode 100644 index 0000000000..3eb75aa3ee Binary files /dev/null and b/docs/logo/scm-manager_logo_neg.png differ diff --git a/docs/logo/scm-manager_logo_neg1.jpg b/docs/logo/scm-manager_logo_neg1.jpg new file mode 100644 index 0000000000..d776c6b6e6 Binary files /dev/null and b/docs/logo/scm-manager_logo_neg1.jpg differ diff --git a/docs/logo/scm-manager_logo_neg1.png b/docs/logo/scm-manager_logo_neg1.png new file mode 100644 index 0000000000..817785710b Binary files /dev/null and b/docs/logo/scm-manager_logo_neg1.png differ diff --git a/docs/logo/scm-manager_logo_pos1.jpg b/docs/logo/scm-manager_logo_pos1.jpg new file mode 100644 index 0000000000..fd2533c198 Binary files /dev/null and b/docs/logo/scm-manager_logo_pos1.jpg differ diff --git a/docs/logo/scm-manager_logo_pos1.png b/docs/logo/scm-manager_logo_pos1.png new file mode 100644 index 0000000000..b911417358 Binary files /dev/null and b/docs/logo/scm-manager_logo_pos1.png differ diff --git a/pom.xml b/pom.xml index ba06282720..3ab3cb82a3 100644 --- a/pom.xml +++ b/pom.xml @@ -437,8 +437,15 @@ sonia.scm.maven smp-maven-plugin - 1.0.0-alpha-4 + 1.0.0-alpha-6 + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + @@ -633,7 +640,6 @@ org.apache.maven.plugins maven-deploy-plugin - 2.7 diff --git a/scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java b/scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java new file mode 100644 index 0000000000..63b9d0d4fc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java @@ -0,0 +1,21 @@ +package sonia.scm; + +import java.util.Collections; + +public class IllegalIdentifierChangeException extends BadRequestException { + + private static final String CODE = "thbsUFokjk"; + + public IllegalIdentifierChangeException(ContextEntry.ContextBuilder context, String message) { + super(context.build(), message); + } + + public IllegalIdentifierChangeException(String message) { + super(Collections.emptyList(), message); + } + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java index 6afb542646..b313f68af8 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java @@ -2,6 +2,8 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; +import java.util.List; + /** * The {@link HalAppender} can be used within an {@link HalEnricher} to append hateoas links to a json response. * @@ -34,6 +36,14 @@ public interface HalAppender { */ void appendEmbedded(String rel, HalRepresentation embeddedItem); + /** + * Appends a list of embedded objects to the json response. + * + * @param rel name of relation + * @param embeddedItems embedded objects + */ + void appendEmbedded(String rel, List embeddedItems); + /** * Builder for link arrays. */ diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 8d3db8b348..4afbb6a895 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -73,7 +73,12 @@ public class ScmConfiguration implements Configuration { * Default plugin url */ public static final String DEFAULT_PLUGINURL = - "http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false"; + "http://download.scm-manager.org/api/v2/plugins.json?os={os}&arch={arch}&snapshot=false&version={version}"; + + /** + * Default url for login information (plugin and feature tips on the login page). + */ + public static final String DEFAULT_LOGIN_INFO_URL = "https://login-info.scm-manager.org/api/v1/login-info"; /** * Default plugin url from version 1.0 @@ -156,7 +161,6 @@ public class ScmConfiguration implements Configuration { * Authentication realm for basic authentication. */ private String realmDescription = HttpUtil.AUTHENTICATION_REALM; - private boolean enableRepositoryArchive = false; private boolean disableGroupingGrid = false; /** * JavaScript date format from moment.js @@ -177,6 +181,9 @@ public class ScmConfiguration implements Configuration { @XmlElement(name = "namespace-strategy") private String namespaceStrategy = "UsernameNamespaceStrategy"; + @XmlElement(name = "login-info-url") + private String loginInfoUrl = DEFAULT_LOGIN_INFO_URL; + /** * Calls the {@link sonia.scm.ConfigChangedListener#configChanged(Object)} @@ -210,12 +217,12 @@ public class ScmConfiguration implements Configuration { this.forceBaseUrl = other.forceBaseUrl; this.baseUrl = other.baseUrl; this.disableGroupingGrid = other.disableGroupingGrid; - this.enableRepositoryArchive = other.enableRepositoryArchive; this.skipFailedAuthenticators = other.skipFailedAuthenticators; this.loginAttemptLimit = other.loginAttemptLimit; this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout; this.enabledXsrfProtection = other.enabledXsrfProtection; this.namespaceStrategy = other.namespaceStrategy; + this.loginInfoUrl = other.loginInfoUrl; } /** @@ -334,10 +341,6 @@ public class ScmConfiguration implements Configuration { return enableProxy; } - public boolean isEnableRepositoryArchive() { - return enableRepositoryArchive; - } - public boolean isForceBaseUrl() { return forceBaseUrl; } @@ -350,6 +353,9 @@ public class ScmConfiguration implements Configuration { return namespaceStrategy; } + public String getLoginInfoUrl() { + return loginInfoUrl; + } /** * Returns true if failed authenticators are skipped. @@ -381,16 +387,6 @@ public class ScmConfiguration implements Configuration { this.enableProxy = enableProxy; } - /** - * Enable or disable the repository archive. Default is disabled. - * - * @param enableRepositoryArchive true to disable the repository archive - * @since 1.14 - */ - public void setEnableRepositoryArchive(boolean enableRepositoryArchive) { - this.enableRepositoryArchive = enableRepositoryArchive; - } - public void setForceBaseUrl(boolean forceBaseUrl) { this.forceBaseUrl = forceBaseUrl; } @@ -477,6 +473,10 @@ public class ScmConfiguration implements Configuration { this.namespaceStrategy = namespaceStrategy; } + public void setLoginInfoUrl(String loginInfoUrl) { + this.loginInfoUrl = loginInfoUrl; + } + @Override // Only for permission checks, don't serialize to XML @XmlTransient diff --git a/scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java b/scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java deleted file mode 100644 index 179e488236..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java +++ /dev/null @@ -1,22 +0,0 @@ -package sonia.scm.group; - -import java.util.Collection; - -/** - * This class represents all associated groups which are provided by external systems for a certain user. - * - * @author Sebastian Sdorra - * @since 2.0.0 - */ -public class ExternalGroupNames extends GroupNames { - public ExternalGroupNames() { - } - - public ExternalGroupNames(String groupName, String... groupNames) { - super(groupName, groupNames); - } - - public ExternalGroupNames(Collection collection) { - super(collection); - } -} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupCollector.java b/scm-core/src/main/java/sonia/scm/group/GroupCollector.java new file mode 100644 index 0000000000..4546db1bc4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupCollector.java @@ -0,0 +1,10 @@ +package sonia.scm.group; + +import java.util.Set; + +public interface GroupCollector { + + String AUTHENTICATED = "_authenticated"; + + Set collect(String principal); +} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupNames.java b/scm-core/src/main/java/sonia/scm/group/GroupNames.java deleted file mode 100644 index c28f9f5ef1..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/GroupNames.java +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.group; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Joiner; -import com.google.common.base.Objects; -import com.google.common.collect.Lists; - -import java.io.Serializable; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * This class represents all associated groups for a user. - * - * @author Sebastian Sdorra - * @since 1.21 - */ -public class GroupNames implements Serializable, Iterable -{ - - /** - * Group for all authenticated users - * @since 1.31 - */ - public static final String AUTHENTICATED = "_authenticated"; - - /** Field description */ - private static final long serialVersionUID = 8615685985213897947L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public GroupNames() - { - this(Collections.emptyList()); - } - - /** - * Constructs ... - * - * - * @param groupName - * @param groupNames - */ - public GroupNames(String groupName, String... groupNames) - { - this(Lists.asList(groupName, groupNames)); - } - - /** - * Constructs ... - * - * - * @param collection - */ - public GroupNames(Collection collection) - { - this.collection = Collections.unmodifiableCollection(collection); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param groupName - * - * @return - */ - public boolean contains(String groupName) - { - return collection.contains(groupName); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final GroupNames other = (GroupNames) obj; - - return Objects.equal(collection, other.collection); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() - { - return Objects.hashCode(collection); - } - - /** - * Method description - * - * - * @return - */ - @Override - public Iterator iterator() - { - return collection.iterator(); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - return Joiner.on(", ").join(collection); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Collection getCollection() - { - return collection; - } - - - //~--- fields --------------------------------------------------------------- - /** Field description */ - private final Collection collection; -} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupResolver.java b/scm-core/src/main/java/sonia/scm/group/GroupResolver.java new file mode 100644 index 0000000000..5aba63c93b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupResolver.java @@ -0,0 +1,10 @@ +package sonia.scm.group; + +import sonia.scm.plugin.ExtensionPoint; + +import java.util.Set; + +@ExtensionPoint +public interface GroupResolver { + Set resolve(String principal); +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/AvailablePlugin.java b/scm-core/src/main/java/sonia/scm/plugin/AvailablePlugin.java new file mode 100644 index 0000000000..db8d96ca15 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/AvailablePlugin.java @@ -0,0 +1,32 @@ +package sonia.scm.plugin; + +import com.google.common.base.Preconditions; + +public class AvailablePlugin implements Plugin { + + private final AvailablePluginDescriptor pluginDescriptor; + private final boolean pending; + + public AvailablePlugin(AvailablePluginDescriptor pluginDescriptor) { + this(pluginDescriptor, false); + } + + private AvailablePlugin(AvailablePluginDescriptor pluginDescriptor, boolean pending) { + this.pluginDescriptor = pluginDescriptor; + this.pending = pending; + } + + @Override + public AvailablePluginDescriptor getDescriptor() { + return pluginDescriptor; + } + + public boolean isPending() { + return pending; + } + + public AvailablePlugin install() { + Preconditions.checkState(!pending, "installation is already pending"); + return new AvailablePlugin(pluginDescriptor, true); + } +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java new file mode 100644 index 0000000000..1c164a0d81 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java @@ -0,0 +1,47 @@ +package sonia.scm.plugin; + +import java.util.Optional; +import java.util.Set; + +/** + * @since 2.0.0 + */ +public class AvailablePluginDescriptor implements PluginDescriptor { + + private final PluginInformation information; + private final PluginCondition condition; + private final Set dependencies; + private final String url; + private final String checksum; + + public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set dependencies, String url, String checksum) { + this.information = information; + this.condition = condition; + this.dependencies = dependencies; + this.url = url; + this.checksum = checksum; + } + + public String getUrl() { + return url; + } + + public Optional getChecksum() { + return Optional.ofNullable(checksum); + } + + @Override + public PluginInformation getInformation() { + return information; + } + + @Override + public PluginCondition getCondition() { + return condition; + } + + @Override + public Set getDependencies() { + return dependencies; + } +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginWrapper.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java similarity index 83% rename from scm-core/src/main/java/sonia/scm/plugin/PluginWrapper.java rename to scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java index 46c3a4a980..2021d4d00f 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginWrapper.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java @@ -36,27 +36,27 @@ package sonia.scm.plugin; import java.nio.file.Path; /** - * Wrapper for a {@link Plugin}. The wrapper holds the directory, + * Wrapper for a {@link InstalledPluginDescriptor}. The wrapper holds the directory, * {@link ClassLoader} and {@link WebResourceLoader} of a plugin. * * @author Sebastian Sdorra * @since 2.0.0 */ -public final class PluginWrapper +public final class InstalledPlugin implements Plugin { /** * Constructs a new plugin wrapper. * - * @param plugin wrapped plugin + * @param descriptor wrapped plugin * @param classLoader plugin class loader * @param webResourceLoader web resource loader * @param directory plugin directory */ - public PluginWrapper(Plugin plugin, ClassLoader classLoader, - WebResourceLoader webResourceLoader, Path directory) + public InstalledPlugin(InstalledPluginDescriptor descriptor, ClassLoader classLoader, + WebResourceLoader webResourceLoader, Path directory) { - this.plugin = plugin; + this.descriptor = descriptor; this.classLoader = classLoader; this.webResourceLoader = webResourceLoader; this.directory = directory; @@ -94,18 +94,19 @@ public final class PluginWrapper */ public String getId() { - return plugin.getInformation().getId(); + return descriptor.getInformation().getId(); } /** - * Returns the plugin. + * Returns the plugin descriptor. * * - * @return plugin + * @return plugin descriptor */ - public Plugin getPlugin() + @Override + public InstalledPluginDescriptor getDescriptor() { - return plugin; + return descriptor; } /** @@ -128,7 +129,7 @@ public final class PluginWrapper private final Path directory; /** plugin */ - private final Plugin plugin; + private final InstalledPluginDescriptor descriptor; /** plugin web resource loader */ private final WebResourceLoader webResourceLoader; diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java new file mode 100644 index 0000000000..88ae4c5dff --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.plugin; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Set; + +//~--- JDK imports ------------------------------------------------------------ + +/** + * + * @author Sebastian Sdorra + */ +@XmlRootElement(name = "plugin") +@XmlAccessorType(XmlAccessType.FIELD) +public final class InstalledPluginDescriptor extends ScmModule implements PluginDescriptor +{ + + /** + * Constructs ... + * + */ + InstalledPluginDescriptor() {} + + /** + * Constructs ... + * + * + * @param scmVersion + * @param information + * @param resources + * @param condition + * @param childFirstClassLoader + * @param dependencies + */ + public InstalledPluginDescriptor(int scmVersion, PluginInformation information, + PluginResources resources, PluginCondition condition, + boolean childFirstClassLoader, Set dependencies) + { + this.scmVersion = scmVersion; + this.information = information; + this.resources = resources; + this.condition = condition; + this.childFirstClassLoader = childFirstClassLoader; + this.dependencies = dependencies; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param obj + * + * @return + */ + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + final InstalledPluginDescriptor other = (InstalledPluginDescriptor) obj; + + return Objects.equal(scmVersion, other.scmVersion) + && Objects.equal(condition, other.condition) + && Objects.equal(information, other.information) + && Objects.equal(resources, other.resources) + && Objects.equal(childFirstClassLoader, other.childFirstClassLoader) + && Objects.equal(dependencies, other.dependencies); + } + + /** + * Method description + * + * + * @return + */ + @Override + public int hashCode() + { + return Objects.hashCode(scmVersion, condition, information, resources, + childFirstClassLoader, dependencies); + } + + /** + * Method description + * + * + * @return + */ + @Override + public String toString() + { + //J- + return MoreObjects.toStringHelper(this) + .add("scmVersion", scmVersion) + .add("condition", condition) + .add("information", information) + .add("resources", resources) + .add("childFirstClassLoader", childFirstClassLoader) + .add("dependencies", dependencies) + .toString(); + //J+ + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @return + */ + @Override + public PluginCondition getCondition() + { + return condition; + } + + /** + * Method description + * + * + * @return + * + * @since 2.0.0 + */ + @Override + public Set getDependencies() + { + if (dependencies == null) + { + dependencies = ImmutableSet.of(); + } + + return dependencies; + } + + /** + * Method description + * + * + * @return + */ + @Override + public PluginInformation getInformation() + { + return information; + } + + /** + * Method description + * + * + * @return + */ + public PluginResources getResources() + { + return resources; + } + + /** + * Method description + * + * + * @return + */ + public int getScmVersion() + { + return scmVersion; + } + + /** + * Method description + * + * + * @return + */ + public boolean isChildFirstClassLoader() + { + return childFirstClassLoader; + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + @XmlElement(name = "child-first-classloader") + private boolean childFirstClassLoader; + + /** Field description */ + @XmlElement(name = "conditions") + private PluginCondition condition; + + /** Field description */ + @XmlElement(name = "dependency") + @XmlElementWrapper(name = "dependencies") + private Set dependencies; + + /** Field description */ + @XmlElement(name = "information") + private PluginInformation information; + + /** Field description */ + private PluginResources resources; + + /** Field description */ + @XmlElement(name = "scm-version") + private int scmVersion = 1; +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/Plugin.java b/scm-core/src/main/java/sonia/scm/plugin/Plugin.java index e8fd166e78..8b440f8ab9 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/Plugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/Plugin.java @@ -1,255 +1,6 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - package sonia.scm.plugin; -//~--- non-JDK imports -------------------------------------------------------- +public interface Plugin { -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; -import com.google.common.collect.ImmutableSet; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; -import java.util.Set; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement -@XmlAccessorType(XmlAccessType.FIELD) -public final class Plugin extends ScmModule -{ - - /** - * Constructs ... - * - */ - Plugin() {} - - /** - * Constructs ... - * - * - * @param scmVersion - * @param information - * @param resources - * @param condition - * @param childFirstClassLoader - * @param dependencies - */ - public Plugin(int scmVersion, PluginInformation information, - PluginResources resources, PluginCondition condition, - boolean childFirstClassLoader, Set dependencies) - { - this.scmVersion = scmVersion; - this.information = information; - this.resources = resources; - this.condition = condition; - this.childFirstClassLoader = childFirstClassLoader; - this.dependencies = dependencies; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param obj - * - * @return - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final Plugin other = (Plugin) obj; - - return Objects.equal(scmVersion, other.scmVersion) - && Objects.equal(condition, other.condition) - && Objects.equal(information, other.information) - && Objects.equal(resources, other.resources) - && Objects.equal(childFirstClassLoader, other.childFirstClassLoader) - && Objects.equal(dependencies, other.dependencies); - } - - /** - * Method description - * - * - * @return - */ - @Override - public int hashCode() - { - return Objects.hashCode(scmVersion, condition, information, resources, - childFirstClassLoader, dependencies); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - //J- - return MoreObjects.toStringHelper(this) - .add("scmVersion", scmVersion) - .add("condition", condition) - .add("information", information) - .add("resources", resources) - .add("childFirstClassLoader", childFirstClassLoader) - .add("dependencies", dependencies) - .toString(); - //J+ - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public PluginCondition getCondition() - { - return condition; - } - - /** - * Method description - * - * - * @return - * - * @since 2.0.0 - */ - public Set getDependencies() - { - if (dependencies == null) - { - dependencies = ImmutableSet.of(); - } - - return dependencies; - } - - /** - * Method description - * - * - * @return - */ - public PluginInformation getInformation() - { - return information; - } - - /** - * Method description - * - * - * @return - */ - public PluginResources getResources() - { - return resources; - } - - /** - * Method description - * - * - * @return - */ - public int getScmVersion() - { - return scmVersion; - } - - /** - * Method description - * - * - * @return - */ - public boolean isChildFirstClassLoader() - { - return childFirstClassLoader; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "child-first-classloader") - private boolean childFirstClassLoader; - - /** Field description */ - @XmlElement(name = "conditions") - private PluginCondition condition; - - /** Field description */ - @XmlElement(name = "dependency") - @XmlElementWrapper(name = "dependencies") - private Set dependencies; - - /** Field description */ - private PluginInformation information; - - /** Field description */ - private PluginResources resources; - - /** Field description */ - @XmlElement(name = "scm-version") - private int scmVersion = 1; + PluginDescriptor getDescriptor(); } diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginCenter.java b/scm-core/src/main/java/sonia/scm/plugin/PluginCenter.java deleted file mode 100644 index e1598e0490..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginCenter.java +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -import java.util.HashSet; -import java.util.Set; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "plugin-center") -@XmlAccessorType(XmlAccessType.FIELD) -public class PluginCenter implements Serializable -{ - - /** Field description */ - private static final long serialVersionUID = -6414175308610267397L; - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Set getPlugins() - { - return plugins; - } - - /** - * Method description - * - * - * @return - */ - public Set getRepositories() - { - return repositories; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param plugins - */ - public void setPlugins(Set plugins) - { - this.plugins = plugins; - } - - /** - * Method description - * - * - * @param repositories - */ - public void setRepositories(Set repositories) - { - this.repositories = repositories; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "plugin") - @XmlElementWrapper(name = "plugins") - private Set plugins = new HashSet(); - - /** Field description */ - @XmlElement(name = "repository") - @XmlElementWrapper(name = "repositories") - private Set repositories = new HashSet(); -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/PluginDescriptor.java new file mode 100644 index 0000000000..6e800faff0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginDescriptor.java @@ -0,0 +1,13 @@ +package sonia.scm.plugin; + +import java.util.Set; + +public interface PluginDescriptor { + + PluginInformation getInformation(); + + PluginCondition getCondition(); + + Set getDependencies(); + +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java b/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java index 6de52c3cca..b669cb63fa 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

* Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + *

* 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + *

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,558 +24,82 @@ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + *

* http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- import com.github.sdorra.ssp.PermissionObject; import com.github.sdorra.ssp.StaticPermissions; -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; +import lombok.Data; import sonia.scm.Validateable; import sonia.scm.util.Util; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; //~--- JDK imports ------------------------------------------------------------ /** - * * @author Sebastian Sdorra */ +@Data @StaticPermissions( - value = "plugin", - generatedClass = "PluginPermissions", + value = "plugin", + generatedClass = "PluginPermissions", permissions = {}, - globalPermissions = { "read", "manage" }, + globalPermissions = {"read", "manage"}, custom = true, customGlobal = true ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "plugin-information") -public class PluginInformation - implements PermissionObject, Validateable, Cloneable, Serializable -{ +public class PluginInformation implements PermissionObject, Validateable, Cloneable, Serializable { - /** Field description */ private static final long serialVersionUID = 461382048865977206L; - //~--- methods -------------------------------------------------------------- + private String name; + private String version; + private String displayName; + private String description; + private String author; + private String category; + private String avatarUrl; - /** - * Method description - * - * - * @return - * - * @since 1.11 - */ @Override - public PluginInformation clone() - { + public PluginInformation clone() { PluginInformation clone = new PluginInformation(); - - clone.setArtifactId(artifactId); + clone.setName(name); + clone.setVersion(version); + clone.setDisplayName(displayName); + clone.setDescription(description); clone.setAuthor(author); clone.setCategory(category); - clone.setTags(tags); - - if (condition != null) - { - clone.setCondition(condition.clone()); - } - - clone.setDescription(description); - clone.setGroupId(groupId); - clone.setName(name); - - if (Util.isNotEmpty(screenshots)) - { - clone.setScreenshots(new ArrayList(screenshots)); - } - - clone.setState(state); - clone.setUrl(url); - clone.setVersion(version); - clone.setWiki(wiki); - + clone.setAvatarUrl(avatarUrl); return clone; } - /** - * Method description - * - * - * @param obj - * - * @return - */ @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final PluginInformation other = (PluginInformation) obj; - - //J- - return Objects.equal(artifactId, other.artifactId) - && Objects.equal(author, other.author) - && Objects.equal(category, other.category) - && Objects.equal(tags, other.tags) - && Objects.equal(condition, other.condition) - && Objects.equal(description, other.description) - && Objects.equal(groupId, other.groupId) - && Objects.equal(name, other.name) - && Objects.equal(screenshots, other.screenshots) - && Objects.equal(state, other.state) - && Objects.equal(url, other.url) - && Objects.equal(version, other.version) - && Objects.equal(wiki, other.wiki); - //J+ + public String getId() { + return getName(true); } - /** - * Method description - * - * - * @return - */ - @Override - public int hashCode() - { - return Objects.hashCode(artifactId, author, category, tags, condition, - description, groupId, name, screenshots, state, url, version, wiki); - } + public String getName(boolean withVersion) { + StringBuilder id = new StringBuilder(name); - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - //J- - return MoreObjects.toStringHelper(this) - .add("artifactId", artifactId) - .add("author", author) - .add("category", category) - .add("tags", tags) - .add("condition", condition) - .add("description", description) - .add("groupId", groupId) - .add("name", name) - .add("screenshots", screenshots) - .add("state", state) - .add("url", url) - .add("version", version) - .add("wiki", wiki) - .toString(); - //J+ - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getArtifactId() - { - return artifactId; - } - - /** - * Method description - * - * - * @return - */ - public String getAuthor() - { - return author; - } - - /** - * Method description - * - * - * @return - */ - public String getCategory() - { - return category; - } - - /** - * Method description - * - * - * @return - */ - public PluginCondition getCondition() - { - return condition; - } - - /** - * Method description - * - * - * @return - */ - public String getDescription() - { - return description; - } - - /** - * Method description - * - * - * @return - */ - public String getGroupId() - { - return groupId; - } - - /** - * Method description - * - * - * @return - */ - @Override - public String getId() - { - return getId(true); - } - - /** - * Method description - * - * - * @param withVersion - * - * @return - * @since 1.21 - */ - public String getId(boolean withVersion) - { - StringBuilder id = new StringBuilder(groupId); - - id.append(":").append(artifactId); - - if (withVersion) - { + if (withVersion) { id.append(":").append(version); } - return id.toString(); } - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public List getScreenshots() - { - return screenshots; - } - - /** - * Method description - * - * - * @return - */ - public PluginState getState() - { - return state; - } - - /** - * Method description - * - * - * @return - */ - public List getTags() - { - return tags; - } - - /** - * Method description - * - * - * @return - */ - public String getUrl() - { - return url; - } - - /** - * Method description - * - * - * @return - */ - public String getVersion() - { - return version; - } - - /** - * Method description - * - * - * @return - */ - public String getWiki() - { - return wiki; - } - - /** - * Method description - * - * - * @return - */ @Override - public boolean isValid() - { - return Util.isNotEmpty(groupId) && Util.isNotEmpty(artifactId) - && Util.isNotEmpty(name) && Util.isNotEmpty(version); + public boolean isValid() { + return Util.isNotEmpty(name) && Util.isNotEmpty(version); } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param artifactId - */ - public void setArtifactId(String artifactId) - { - this.artifactId = artifactId; - } - - /** - * Method description - * - * - * @param author - */ - public void setAuthor(String author) - { - this.author = author; - } - - /** - * Method description - * - * - * @param category - */ - public void setCategory(String category) - { - this.category = category; - } - - /** - * Method description - * - * - * @param condition - */ - public void setCondition(PluginCondition condition) - { - this.condition = condition; - } - - /** - * Method description - * - * - * @param description - */ - public void setDescription(String description) - { - this.description = description; - } - - /** - * Method description - * - * - * @param groupId - */ - public void setGroupId(String groupId) - { - this.groupId = groupId; - } - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Method description - * - * - * @param screenshots - */ - public void setScreenshots(List screenshots) - { - this.screenshots = screenshots; - } - - /** - * Method description - * - * - * @param state - */ - public void setState(PluginState state) - { - this.state = state; - } - - /** - * Method description - * - * - * @param tags - */ - public void setTags(List tags) - { - this.tags = tags; - } - - /** - * Method description - * - * - * @param url - */ - public void setUrl(String url) - { - this.url = url; - } - - /** - * Method description - * - * - * @param version - */ - public void setVersion(String version) - { - this.version = version; - } - - /** - * Method description - * - * - * @param wiki - */ - public void setWiki(String wiki) - { - this.wiki = wiki; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String artifactId; - - /** Field description */ - private String author; - - /** Field description */ - private String category; - - /** Field description */ - private PluginCondition condition; - - /** Field description */ - private String description; - - /** Field description */ - private String groupId; - - /** Field description */ - private String name; - - /** Field description */ - @XmlElement(name = "screenshot") - @XmlElementWrapper(name = "screenshots") - private List screenshots; - - /** Field description */ - private PluginState state; - - /** Field description */ - @XmlElement(name = "tag") - @XmlElementWrapper(name = "tags") - private List tags; - - /** Field description */ - private String url; - - /** Field description */ - private String version; - - /** Field description */ - private String wiki; } diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginInformationComparator.java b/scm-core/src/main/java/sonia/scm/plugin/PluginInformationComparator.java deleted file mode 100644 index f44de35e8a..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginInformationComparator.java +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -import java.util.Comparator; - -/** - * - * @author Sebastian Sdorra - * @since 1.6 - */ -public class PluginInformationComparator - implements Comparator, Serializable -{ - - /** Field description */ - public static final PluginInformationComparator INSTANCE = - new PluginInformationComparator(); - - /** Field description */ - private static final long serialVersionUID = -8339752498853225668L; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param plugin - * @param other - * - * @return - */ - @Override - public int compare(PluginInformation plugin, PluginInformation other) - { - int result = 0; - - result = Util.compare(plugin.getGroupId(), other.getGroupId()); - - if (result == 0) - { - result = Util.compare(plugin.getArtifactId(), other.getArtifactId()); - - if (result == 0) - { - PluginState state = plugin.getState(); - PluginState otherState = other.getState(); - - if ((state != null) && (otherState != null)) - { - result = state.getCompareValue() - otherState.getCompareValue(); - } - else if ((state == null) && (otherState != null)) - { - result = 1; - } - else if ((state != null) && (otherState == null)) - { - result = -1; - } - } - } - - return result; - } -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java b/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java index 2d65d1cc98..e82d945024 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java @@ -68,7 +68,7 @@ public interface PluginLoader * * @return */ - public Collection getInstalledPlugins(); + public Collection getInstalledPlugins(); /** * Returns a {@link ClassLoader} which is able to load classes and resources diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java index b1ec502fc4..b7b8f69519 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java @@ -33,113 +33,56 @@ package sonia.scm.plugin; -//~--- JDK imports ------------------------------------------------------------ - -import com.google.common.base.Predicate; -import java.io.IOException; -import java.io.InputStream; - -import java.util.Collection; +import java.util.List; +import java.util.Optional; /** + * The plugin manager is responsible for plugin related tasks, such as install, uninstall or updating. * * @author Sebastian Sdorra */ -public interface PluginManager -{ +public interface PluginManager { /** - * Method description - * + * Returns the available plugin with the given name. + * @param name of plugin + * @return optional available plugin. */ - public void clearCache(); + Optional getAvailable(String name); /** - * Method description - * - * - * @param id + * Returns the installed plugin with the given name. + * @param name of plugin + * @return optional installed plugin. */ - public void install(String id); + Optional getInstalled(String name); + /** - * Installs a plugin package from a inputstream. + * Returns all installed plugins. * - * - * @param packageStream package input stream - * - * @throws IOException - * @since 1.21 + * @return a list of installed plugins. */ - public void installPackage(InputStream packageStream) throws IOException; + List getInstalled(); /** - * Method description + * Returns all available plugins. The list contains the plugins which are loaded from the plugin center, but without + * the installed plugins. * - * - * @param id + * @return a list of available plugins. */ - public void uninstall(String id); + List getAvailable(); /** - * Method description + * Installs the plugin with the given name from the list of available plugins. * - * - * @param id + * @param name plugin name + * @param restartAfterInstallation restart context after plugin installation */ - public void update(String id); - - //~--- get methods ---------------------------------------------------------- + void install(String name, boolean restartAfterInstallation); /** - * Method description - * - * - * @param id - * - * @return + * Install all pending plugins and restart the scm context. */ - public PluginInformation get(String id); - - /** - * Method description - * - * - * @param filter - * - * @return - */ - public Collection get(Predicate filter); - - /** - * Method description - * - * - * @return - */ - public Collection getAll(); - - /** - * Method description - * - * - * @return - */ - public Collection getAvailable(); - - /** - * Method description - * - * - * @return - */ - public Collection getAvailableUpdates(); - - /** - * Method description - * - * - * @return - */ - public Collection getInstalled(); + void installPendingAndRestart(); } diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginRepository.java b/scm-core/src/main/java/sonia/scm/plugin/PluginRepository.java deleted file mode 100644 index 1d4cc07338..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginRepository.java +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; - -import java.io.Serializable; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -public class PluginRepository implements Serializable -{ - - /** Field description */ - private static final long serialVersionUID = -9504354306304731L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - PluginRepository() {} - - /** - * Constructs ... - * - * - * @param id - * @param url - */ - public PluginRepository(String id, String url) - { - this.id = id; - this.url = url; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param obj - * - * @return - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final PluginRepository other = (PluginRepository) obj; - - return Objects.equal(id, other.id) && Objects.equal(url, other.url); - } - - /** - * Method description - * - * - * @return - */ - @Override - public int hashCode() - { - return Objects.hashCode(id, url); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - return MoreObjects.toStringHelper(this).add("id", id).add("url", - url).toString(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return id; - } - - /** - * Method description - * - * - * @return - */ - public String getUrl() - { - return url; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String id; - - /** Field description */ - private String url; -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginState.java b/scm-core/src/main/java/sonia/scm/plugin/PluginState.java deleted file mode 100644 index 39803d3455..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginState.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -/** - * - * @author Sebastian Sdorra - */ -public enum PluginState -{ - CORE(100), AVAILABLE(60), INSTALLED(80), NEWER_VERSION_INSTALLED(20), - UPDATE_AVAILABLE(40); - - /** - * Constructs ... - * - * - * @param compareValue - */ - private PluginState(int compareValue) - { - this.compareValue = compareValue; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @since 1.6 - * @return - */ - public int getCompareValue() - { - return compareValue; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final int compareValue; -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/Plugins.java b/scm-core/src/main/java/sonia/scm/plugin/Plugins.java index 6359850712..f33a254581 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/Plugins.java +++ b/scm-core/src/main/java/sonia/scm/plugin/Plugins.java @@ -65,7 +65,7 @@ public final class Plugins { try { - context = JAXBContext.newInstance(Plugin.class, ScmModule.class); + context = JAXBContext.newInstance(InstalledPluginDescriptor.class, ScmModule.class); } catch (JAXBException ex) { @@ -91,7 +91,7 @@ public final class Plugins * * @return */ - public static Plugin parsePluginDescriptor(Path path) + public static InstalledPluginDescriptor parsePluginDescriptor(Path path) { return parsePluginDescriptor(Files.asByteSource(path.toFile())); } @@ -104,15 +104,15 @@ public final class Plugins * * @return */ - public static Plugin parsePluginDescriptor(ByteSource data) + public static InstalledPluginDescriptor parsePluginDescriptor(ByteSource data) { Preconditions.checkNotNull(data, "data parameter is required"); - Plugin plugin; + InstalledPluginDescriptor plugin; try (InputStream stream = data.openStream()) { - plugin = (Plugin) context.createUnmarshaller().unmarshal(stream); + plugin = (InstalledPluginDescriptor) context.createUnmarshaller().unmarshal(stream); } catch (JAXBException ex) { diff --git a/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java b/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java index 63d5e8fb8f..e1ea622bdf 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java +++ b/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java @@ -206,7 +206,7 @@ public final class SmpArchive * * @throws IOException */ - public Plugin getPlugin() throws IOException + public InstalledPluginDescriptor getPlugin() throws IOException { if (plugin == null) { @@ -219,16 +219,10 @@ public final class SmpArchive throw new PluginException("could not find information section"); } - if (Strings.isNullOrEmpty(info.getGroupId())) + if (Strings.isNullOrEmpty(info.getName())) { throw new PluginException( - "could not find groupId in plugin descriptor"); - } - - if (Strings.isNullOrEmpty(info.getArtifactId())) - { - throw new PluginException( - "could not find artifactId in plugin descriptor"); + "could not find name in plugin descriptor"); } if (Strings.isNullOrEmpty(info.getVersion())) @@ -251,9 +245,9 @@ public final class SmpArchive * * @throws IOException */ - private Plugin createPlugin() throws IOException + private InstalledPluginDescriptor createPlugin() throws IOException { - Plugin p = null; + InstalledPluginDescriptor p = null; NonClosingZipInputStream zis = null; try @@ -418,5 +412,5 @@ public final class SmpArchive private final ByteSource archive; /** Field description */ - private Plugin plugin; + private InstalledPluginDescriptor plugin; } diff --git a/scm-core/src/main/java/sonia/scm/plugin/StatePluginPredicate.java b/scm-core/src/main/java/sonia/scm/plugin/StatePluginPredicate.java deleted file mode 100644 index ef7836f74a..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/StatePluginPredicate.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Predicate; - -/** - * - * @author Sebastian Sdorra - */ -public class StatePluginPredicate implements Predicate -{ - - /** - * Constructs ... - * - * - * @param state - */ - public StatePluginPredicate(PluginState state) - { - this.state = state; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param plugin - * - * @return - */ - @Override - public boolean apply(PluginInformation plugin) - { - return state == plugin.getState(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final PluginState state; -} diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 5bb50db06f..463085a7ea 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -82,10 +82,9 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per private String namespace; private String name; @XmlElement(name = "permission") - private final Set permissions = new HashSet<>(); + private Set permissions = new HashSet<>(); @XmlElement(name = "public") private boolean publicReadable = false; - private boolean archived = false; private String type; @@ -216,16 +215,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per return type; } - /** - * Returns true if the repository is archived. - * - * @return true if the repository is archived - * @since 1.14 - */ - public boolean isArchived() { - return archived; - } - /** * Returns {@code true} if the repository is healthy. * @@ -264,16 +253,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per && ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact)); } - /** - * Archive or un archive this repository. - * - * @param archived true to enable archive - * @since 1.14 - */ - public void setArchived(boolean archived) { - this.archived = archived; - } - public void setContact(String contact) { this.contact = contact; } @@ -331,6 +310,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per try { repository = (Repository) super.clone(); + // fix permission reference on clone + repository.permissions = new HashSet<>(permissions); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } @@ -352,7 +333,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per repository.setDescription(description); repository.setPermissions(permissions); repository.setPublicReadable(publicReadable); - repository.setArchived(archived); // do not copy health check results } @@ -381,7 +361,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per && Objects.equal(contact, other.contact) && Objects.equal(description, other.description) && Objects.equal(publicReadable, other.publicReadable) - && Objects.equal(archived, other.archived) && Objects.equal(permissions, other.permissions) && Objects.equal(type, other.type) && Objects.equal(creationDate, other.creationDate) @@ -393,7 +372,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per @Override public int hashCode() { return Objects.hashCode(id, namespace, name, contact, description, publicReadable, - archived, permissions, type, creationDate, lastModified, properties, + permissions, type, creationDate, lastModified, properties, healthCheckFailures); } @@ -406,7 +385,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per .add("contact", contact) .add("description", description) .add("publicReadable", publicReadable) - .add("archived", archived) .add("permissions", permissions) .add("type", type) .add("lastModified", lastModified) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java deleted file mode 100644 index a427050633..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm.repository; - -/** - * - * @author Sebastian Sdorra - * - * @since 1.14 - */ -public class RepositoryIsNotArchivedException extends RuntimeException { - - private static final long serialVersionUID = 7728748133123987511L; - - public RepositoryIsNotArchivedException() { - super("Repository could not be deleted, because it is not archived."); - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java new file mode 100644 index 0000000000..b5b2f2a08b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java @@ -0,0 +1,68 @@ +package sonia.scm.repository.api; + +import sonia.scm.FeatureNotSupportedException; +import sonia.scm.repository.Feature; +import sonia.scm.repository.spi.DiffCommandRequest; + +import java.util.Set; + +abstract class AbstractDiffCommandBuilder { + + + /** request for the diff command implementation */ + final DiffCommandRequest request = new DiffCommandRequest(); + + private final Set supportedFeatures; + + AbstractDiffCommandBuilder(Set supportedFeatures) { + this.supportedFeatures = supportedFeatures; + } + + /** + * Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given + * here. In other words: What changes would be new to the ancestor changeset given here when the branch would + * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! + * + * @return {@code this} + */ + public T setAncestorChangeset(String revision) + { + if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { + throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name()); + } + request.setAncestorChangeset(revision); + + return self(); + } + + /** + * Show the difference only for the given path. + * + * + * @param path path for difference + * + * @return {@code this} + */ + public T setPath(String path) + { + request.setPath(path); + return self(); + } + + /** + * Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this + * and another revision. + * + * + * @param revision revision for difference + * + * @return {@code this} + */ + public T setRevision(String revision) + { + request.setRevision(revision); + return self(); + } + + abstract T self(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java index e380727769..3249e54ec3 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java @@ -53,11 +53,6 @@ public enum Command */ BRANCHES, - /** - * @since 2.0 - */ - BRANCH, - /** * @since 1.31 */ @@ -71,10 +66,5 @@ public enum Command /** * @since 2.0 */ - MODIFICATIONS, - - /** - * @since 2.0 - */ - MERGE + MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java index 9e7094d5bf..18d4e11a7f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java @@ -38,10 +38,8 @@ package sonia.scm.repository.api; import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.FeatureNotSupportedException; import sonia.scm.repository.Feature; import sonia.scm.repository.spi.DiffCommand; -import sonia.scm.repository.spi.DiffCommandRequest; import sonia.scm.util.IOUtil; import java.io.ByteArrayOutputStream; @@ -72,7 +70,7 @@ import java.util.Set; * @author Sebastian Sdorra * @since 1.17 */ -public final class DiffCommandBuilder +public final class DiffCommandBuilder extends AbstractDiffCommandBuilder { /** @@ -81,6 +79,9 @@ public final class DiffCommandBuilder private static final Logger logger = LoggerFactory.getLogger(DiffCommandBuilder.class); + /** implementation of the diff command */ + private final DiffCommand diffCommand; + //~--- constructors --------------------------------------------------------- /** @@ -92,8 +93,8 @@ public final class DiffCommandBuilder */ DiffCommandBuilder(DiffCommand diffCommand, Set supportedFeatures) { + super(supportedFeatures); this.diffCommand = diffCommand; - this.supportedFeatures = supportedFeatures; } //~--- methods -------------------------------------------------------------- @@ -162,54 +163,6 @@ public final class DiffCommandBuilder return this; } - - /** - * Show the difference only for the given path. - * - * - * @param path path for difference - * - * @return {@code this} - */ - public DiffCommandBuilder setPath(String path) - { - request.setPath(path); - - return this; - } - - /** - * Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this - * and another revision. - * - * - * @param revision revision for difference - * - * @return {@code this} - */ - public DiffCommandBuilder setRevision(String revision) - { - request.setRevision(revision); - - return this; - } - /** - * Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given - * here. In other words: What changes would be new to the ancestor changeset given here when the branch would - * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! - * - * @return {@code this} - */ - public DiffCommandBuilder setAncestorChangeset(String revision) - { - if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { - throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name()); - } - request.setAncestorChangeset(revision); - - return this; - } - //~--- get methods ---------------------------------------------------------- /** @@ -233,12 +186,8 @@ public final class DiffCommandBuilder diffCommand.getDiffResult(request, outputStream); } - //~--- fields --------------------------------------------------------------- - - /** implementation of the diff command */ - private final DiffCommand diffCommand; - private Set supportedFeatures; - - /** request for the diff command implementation */ - private final DiffCommandRequest request = new DiffCommandRequest(); + @Override + DiffCommandBuilder self() { + return this; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java new file mode 100644 index 0000000000..a3b1bafe0b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +public interface DiffFile extends Iterable { + + String getOldRevision(); + + String getNewRevision(); + + String getOldPath(); + + String getNewPath(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java new file mode 100644 index 0000000000..193e5e75d5 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +import java.util.OptionalInt; + +public interface DiffLine { + + OptionalInt getOldLineNumber(); + + OptionalInt getNewLineNumber(); + + String getContent(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java new file mode 100644 index 0000000000..b662db4e2d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java @@ -0,0 +1,8 @@ +package sonia.scm.repository.api; + +public interface DiffResult extends Iterable { + + String getOldRevision(); + + String getNewRevision(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java new file mode 100644 index 0000000000..7e152f3d0f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java @@ -0,0 +1,41 @@ +package sonia.scm.repository.api; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Feature; +import sonia.scm.repository.spi.DiffResultCommand; + +import java.io.IOException; +import java.util.Set; + +public class DiffResultCommandBuilder extends AbstractDiffCommandBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(DiffResultCommandBuilder.class); + + private final DiffResultCommand diffResultCommand; + + DiffResultCommandBuilder(DiffResultCommand diffResultCommand, Set supportedFeatures) { + super(supportedFeatures); + this.diffResultCommand = diffResultCommand; + } + + /** + * Returns the content of the difference as parsed objects. + * + * @return content of the difference + */ + public DiffResult getDiffResult() throws IOException { + Preconditions.checkArgument(request.isValid(), + "path and/or revision is required"); + + LOG.debug("create diff result for {}", request); + + return diffResultCommand.getDiffResult(request); + } + + @Override + DiffResultCommandBuilder self() { + return this; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java new file mode 100644 index 0000000000..c8a3e1ebca --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java @@ -0,0 +1,24 @@ +package sonia.scm.repository.api; + +public interface Hunk extends Iterable { + + default String getRawHeader() { + return String.format("@@ -%s +%s @@", getLineMarker(getOldStart(), getOldLineCount()), getLineMarker(getNewStart(), getNewLineCount())); + } + + default String getLineMarker(int start, int lineCount) { + if (lineCount == 1) { + return Integer.toString(start); + } else { + return String.format("%s,%s", start, lineCount); + } + } + + int getOldStart(); + + int getOldLineCount(); + + int getNewStart(); + + int getNewLineCount(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index 90978d75ea..5807ffa998 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -31,7 +31,6 @@ package sonia.scm.repository.api; -import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.cache.CacheManager; @@ -239,6 +238,21 @@ public final class RepositoryService implements Closeable { return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures()); } + /** + * The diff command shows differences between revisions for a specified file + * or the entire revision. + * + * @return instance of {@link DiffResultCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + */ + public DiffResultCommandBuilder getDiffResultCommand() { + LOG.debug("create diff result command for repository {}", + repository.getNamespaceAndName()); + + return new DiffResultCommandBuilder(provider.getDiffResultCommand(), provider.getSupportedFeatures()); + } + /** * The incoming command shows new {@link Changeset}s found in a different * repository location. diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java new file mode 100644 index 0000000000..ee50178d76 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java @@ -0,0 +1,9 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffResult; + +import java.io.IOException; + +public interface DiffResultCommand { + DiffResult getDiffResult(DiffCommandRequest request) throws IOException; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java index a82eb7c30a..bf9cdf6a25 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java @@ -158,6 +158,11 @@ public abstract class RepositoryServiceProvider implements Closeable throw new CommandNotSupportedException(Command.DIFF); } + public DiffResultCommand getDiffResultCommand() + { + throw new CommandNotSupportedException(Command.DIFF_RESULT); + } + /** * Method description * diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java index 0924716bd8..afe81ac27f 100644 --- a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java @@ -99,15 +99,6 @@ public interface AccessTokenBuilder { */ AccessTokenBuilder scope(Scope scope); - /** - * Define the logged in user as member of the given groups. - * - * @param groups group names - * - * @return {@code this} - */ - AccessTokenBuilder groups(String... groups); - /** * Creates a new {@link AccessToken} with the provided settings. * diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java index ea3e7ce9f5..6ec64a67de 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java @@ -45,7 +45,6 @@ import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.subject.SimplePrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.group.GroupDAO; import sonia.scm.user.User; import sonia.scm.user.UserDAO; @@ -71,8 +70,6 @@ public final class DAORealmHelper { private final UserDAO userDAO; - private final GroupCollector groupCollector; - private final String realm; //~--- constructors --------------------------------------------------------- @@ -83,14 +80,12 @@ public final class DAORealmHelper { * * @param loginAttemptHandler login attempt handler for wrapping credentials matcher * @param userDAO user dao - * @param groupCollector collect groups for a principal * @param realm name of realm */ - public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupCollector groupCollector, String realm) { + public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, String realm) { this.loginAttemptHandler = loginAttemptHandler; this.realm = realm; this.userDAO = userDAO; - this.groupCollector = groupCollector; } //~--- get methods ---------------------------------------------------------- @@ -120,7 +115,7 @@ public final class DAORealmHelper { UsernamePasswordToken upt = (UsernamePasswordToken) token; String principal = upt.getUsername(); - return getAuthenticationInfo(principal, null, null, Collections.emptySet()); + return getAuthenticationInfo(principal, null, null); } /** @@ -135,7 +130,7 @@ public final class DAORealmHelper { } - private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope, Iterable groups) { + private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) { checkArgument(!Strings.isNullOrEmpty(principal), "username is required"); LOG.debug("try to authenticate {}", principal); @@ -153,7 +148,6 @@ public final class DAORealmHelper { collection.add(principal, realm); collection.add(user, realm); - collection.add(groupCollector.collect(principal, groups), realm); collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm); String creds = credentials; @@ -207,17 +201,17 @@ public final class DAORealmHelper { return this; } - /** - * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info. - * - * @param groups extra groups - * - * @return {@code this} - */ - public AuthenticationInfoBuilder withGroups(Iterable groups) { - this.groups = groups; - return this; - } +// /** +// * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info. +// * +// * @param groups extra groups +// * +// * @return {@code this} +// */ +// public AuthenticationInfoBuilder withGroups(Iterable groups) { +// this.groups = groups; +// return this; +// } /** * Build creates the authentication info from the given information. @@ -225,7 +219,7 @@ public final class DAORealmHelper { * @return authentication info */ public AuthenticationInfo build() { - return getAuthenticationInfo(principal, credentials, scope, groups); + return getAuthenticationInfo(principal, credentials, scope); } } diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java index ee2bf11e21..dd59de2ac8 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java @@ -30,7 +30,7 @@ */ package sonia.scm.security; -import sonia.scm.group.GroupDAO; +import sonia.scm.cache.CacheManager; import sonia.scm.user.UserDAO; import javax.inject.Inject; @@ -45,20 +45,19 @@ public final class DAORealmHelperFactory { private final LoginAttemptHandler loginAttemptHandler; private final UserDAO userDAO; - private final GroupCollector groupCollector; + private final CacheManager cacheManager; /** * Constructs a new instance. - * * @param loginAttemptHandler login attempt handler * @param userDAO user dao - * @param groupDAO group dao + * @param cacheManager */ @Inject - public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO) { + public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, CacheManager cacheManager) { this.loginAttemptHandler = loginAttemptHandler; this.userDAO = userDAO; - this.groupCollector = new GroupCollector(groupDAO); + this.cacheManager = cacheManager; } /** @@ -69,7 +68,7 @@ public final class DAORealmHelperFactory { * @return new {@link DAORealmHelper} instance. */ public DAORealmHelper create(String realm) { - return new DAORealmHelper(loginAttemptHandler, userDAO, groupCollector, realm); + return new DAORealmHelper(loginAttemptHandler, userDAO, realm); } } diff --git a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java b/scm-core/src/main/java/sonia/scm/security/GroupCollector.java deleted file mode 100644 index 56687af7ef..0000000000 --- a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java +++ /dev/null @@ -1,43 +0,0 @@ -package sonia.scm.security; - -import com.google.common.collect.ImmutableSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; - -/** - * Collect groups for a certain principal. - * Warning: The class is only for internal use and should never used directly. - */ -class GroupCollector { - - private static final Logger LOG = LoggerFactory.getLogger(GroupCollector.class); - - private final GroupDAO groupDAO; - - GroupCollector(GroupDAO groupDAO) { - this.groupDAO = groupDAO; - } - - GroupNames collect(String principal, Iterable groupNames) { - ImmutableSet.Builder builder = ImmutableSet.builder(); - - builder.add(GroupNames.AUTHENTICATED); - - for (String group : groupNames) { - builder.add(group); - } - - for (Group group : groupDAO.getAll()) { - if (group.isMember(principal)) { - builder.add(group.getName()); - } - } - - GroupNames groups = new GroupNames(builder.build()); - LOG.debug("collected following groups for principal {}: {}", principal, groups); - return groups; - } -} diff --git a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java index d421d33f45..b2175f304a 100644 --- a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java @@ -32,24 +32,15 @@ import com.google.inject.Inject; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.subject.SimplePrincipalCollection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import sonia.scm.AlreadyExistsException; import sonia.scm.NotFoundException; -import sonia.scm.group.ExternalGroupNames; import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupManager; import sonia.scm.plugin.Extension; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; -import java.util.Collection; -import java.util.Collections; - -import static java.util.Arrays.asList; - /** * Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated * users with the local database. @@ -60,12 +51,9 @@ import static java.util.Arrays.asList; @Extension public final class SyncingRealmHelper { - private static final Logger LOG = LoggerFactory.getLogger(SyncingRealmHelper.class); - private final AdministrationContext ctx; private final UserManager userManager; private final GroupManager groupManager; - private final GroupCollector groupCollector; /** * Constructs a new SyncingRealmHelper. @@ -73,134 +61,28 @@ public final class SyncingRealmHelper { * @param ctx administration context * @param userManager user manager * @param groupManager group manager - * @param groupDAO group dao */ @Inject - public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager, GroupDAO groupDAO) { + public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager) { this.ctx = ctx; this.userManager = userManager; this.groupManager = groupManager; - this.groupCollector = new GroupCollector(groupDAO); } - /** - * Create {@link AuthenticationInfo} from user and groups. - */ - public AuthenticationInfoBuilder.ForRealm authenticationInfo() { - return new AuthenticationInfoBuilder().new ForRealm(); - } - - public class AuthenticationInfoBuilder { - private String realm; - private User user; - private Collection groups = Collections.emptySet(); - private Collection externalGroups = Collections.emptySet(); - - private AuthenticationInfo build() { - return SyncingRealmHelper.this.createAuthenticationInfo(realm, user, groups, externalGroups); - } - - public class ForRealm { - private ForRealm() { - } - - /** - * Sets the realm. - * @param realm name of the realm - */ - public ForUser forRealm(String realm) { - AuthenticationInfoBuilder.this.realm = realm; - return AuthenticationInfoBuilder.this.new ForUser(); - } - } - - public class ForUser { - private ForUser() { - } - - /** - * Sets the user. - * @param user authenticated user - */ - public AuthenticationInfoBuilder.WithGroups andUser(User user) { - AuthenticationInfoBuilder.this.user = user; - return AuthenticationInfoBuilder.this.new WithGroups(); - } - } - - public class WithGroups { - private WithGroups() { - } - - /** - * Set the internal groups for the user. - * @param groups groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withGroups(String... groups) { - return withGroups(asList(groups)); - } - - /** - * Set the internal groups for the user. - * @param groups groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withGroups(Collection groups) { - AuthenticationInfoBuilder.this.groups = groups; - return this; - } - - /** - * Set the external groups for the user. - * @param externalGroups external groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withExternalGroups(String... externalGroups) { - return withExternalGroups(asList(externalGroups)); - } - - /** - * Set the external groups for the user. - * @param externalGroups external groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withExternalGroups(Collection externalGroups) { - AuthenticationInfoBuilder.this.externalGroups = externalGroups; - return this; - } - - /** - * Builds the {@link AuthenticationInfo} from the given options. - * - * @return complete autentication info - */ - public AuthenticationInfo build() { - return AuthenticationInfoBuilder.this.build(); - } - } - } - - //~--- methods -------------------------------------------------------------- - /** * Create {@link AuthenticationInfo} from user and groups. * * * @param realm name of the realm * @param user authenticated user - * @param groups groups of the authenticated user * * @return authentication info */ - private AuthenticationInfo createAuthenticationInfo(String realm, User user, - Collection groups, Collection externalGroups) { + public AuthenticationInfo createAuthenticationInfo(String realm, User user) { SimplePrincipalCollection collection = new SimplePrincipalCollection(); collection.add(user.getId(), realm); collection.add(user, realm); - collection.add(groupCollector.collect(user.getId(), groups), realm); - collection.add(new ExternalGroupNames(externalGroups), realm); return new SimpleAuthenticationInfo(collection, user.getPassword()); } diff --git a/scm-core/src/main/java/sonia/scm/util/IOUtil.java b/scm-core/src/main/java/sonia/scm/util/IOUtil.java index 16f84d1031..4058c089f4 100644 --- a/scm-core/src/main/java/sonia/scm/util/IOUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/IOUtil.java @@ -37,14 +37,11 @@ package sonia.scm.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.io.Command; import sonia.scm.io.CommandResult; import sonia.scm.io.SimpleCommand; import sonia.scm.io.ZipUnArchiver; -//~--- JDK imports ------------------------------------------------------------ - import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -55,12 +52,13 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra diff --git a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java similarity index 74% rename from scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java rename to scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java index 973b3af4cb..8b1868309b 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java @@ -1,16 +1,11 @@ package sonia.scm.web.filter; -import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; import sonia.scm.util.HttpUtil; import sonia.scm.web.UserAgent; import sonia.scm.web.UserAgentParser; import sonia.scm.web.WebTokenGenerator; -import sonia.scm.web.protocol.HttpProtocolServlet; -import javax.inject.Inject; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -18,14 +13,11 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Set; -@Priority(Filters.PRIORITY_AUTHENTICATION) -@WebElement(value = HttpProtocolServlet.PATTERN) -public class HttpProtocolServletAuthenticationFilter extends AuthenticationFilter { +public class HttpProtocolServletAuthenticationFilterBase extends AuthenticationFilter { private final UserAgentParser userAgentParser; - @Inject - public HttpProtocolServletAuthenticationFilter( + protected HttpProtocolServletAuthenticationFilterBase( ScmConfiguration configuration, Set tokenGenerators, UserAgentParser userAgentParser) { diff --git a/scm-core/src/test/java/sonia/scm/plugin/AvailablePluginTest.java b/scm-core/src/test/java/sonia/scm/plugin/AvailablePluginTest.java new file mode 100644 index 0000000000..bfdf74fdb1 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/plugin/AvailablePluginTest.java @@ -0,0 +1,32 @@ +package sonia.scm.plugin; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +class AvailablePluginTest { + + @Mock + private AvailablePluginDescriptor descriptor; + + @Test + void shouldReturnNewPendingPluginOnInstall() { + AvailablePlugin plugin = new AvailablePlugin(descriptor); + assertThat(plugin.isPending()).isFalse(); + + AvailablePlugin installed = plugin.install(); + assertThat(installed.isPending()).isTrue(); + } + + @Test + void shouldThrowIllegalStateExceptionIfAlreadyPending() { + AvailablePlugin plugin = new AvailablePlugin(descriptor).install(); + assertThrows(IllegalStateException.class, () -> plugin.install()); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java b/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java index 95addf388f..07e182216f 100644 --- a/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java +++ b/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java @@ -85,7 +85,7 @@ public class SmpArchiveTest public void testExtract() throws IOException, ParserConfigurationException, SAXException { - File archive = createArchive("sonia.sample", "sample", "1.0"); + File archive = createArchive("sonia.sample", "1.0"); File target = tempFolder.newFolder(); IOUtil.mkdirs(target); @@ -112,8 +112,8 @@ public class SmpArchiveTest @Test public void testGetPlugin() throws IOException { - File archive = createArchive("sonia.sample", "sample", "1.0"); - Plugin plugin = SmpArchive.create(archive).getPlugin(); + File archive = createArchive("sonia.sample", "1.0"); + InstalledPluginDescriptor plugin = SmpArchive.create(archive).getPlugin(); assertNotNull(plugin); @@ -121,8 +121,7 @@ public class SmpArchiveTest assertNotNull(info); - assertEquals("sonia.sample", info.getGroupId()); - assertEquals("sample", info.getArtifactId()); + assertEquals("sonia.sample", info.getName()); assertEquals("1.0", info.getVersion()); } @@ -132,22 +131,9 @@ public class SmpArchiveTest * @throws IOException */ @Test(expected = PluginException.class) - public void testWithMissingArtifactId() throws IOException + public void testWithMissingName() throws IOException { - File archive = createArchive("sonia.sample", null, "1.0"); - - SmpArchive.create(archive).getPlugin(); - } - - /** - * Method description - * - * @throws IOException - */ - @Test(expected = PluginException.class) - public void testWithMissingGroupId() throws IOException - { - File archive = createArchive(null, "sample", "1.0"); + File archive = createArchive( null, "1.0"); SmpArchive.create(archive).getPlugin(); } @@ -160,7 +146,7 @@ public class SmpArchiveTest @Test(expected = PluginException.class) public void testWithMissingVersion() throws IOException { - File archive = createArchive("sonia.sample", "sample", null); + File archive = createArchive("sonia.sample", null); SmpArchive.create(archive).getPlugin(); } @@ -169,13 +155,12 @@ public class SmpArchiveTest * Method description * * - * @param groupId - * @param artifactId + * @param name * @param version * * @return */ - private File createArchive(String groupId, String artifactId, String version) + private File createArchive(String name, String version) { File archiveFile; @@ -183,7 +168,7 @@ public class SmpArchiveTest { File descriptor = tempFolder.newFile(); - writeDescriptor(descriptor, groupId, artifactId, version); + writeDescriptor(descriptor, name, version); archiveFile = tempFolder.newFile(); try (ZipOutputStream zos = @@ -229,14 +214,13 @@ public class SmpArchiveTest * * * @param descriptor - * @param groupId - * @param artifactId + * @param name * @param version * * @throws IOException */ - private void writeDescriptor(File descriptor, String groupId, - String artifactId, String version) + private void writeDescriptor(File descriptor, String name, + String version) throws IOException { try @@ -252,8 +236,7 @@ public class SmpArchiveTest writer.writeStartDocument(); writer.writeStartElement("plugin"); writer.writeStartElement("information"); - writeElement(writer, "groupId", groupId); - writeElement(writer, "artifactId", artifactId); + writeElement(writer, "name", name); writeElement(writer, "version", version); writer.writeEndElement(); diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java new file mode 100644 index 0000000000..6053e10ad5 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java @@ -0,0 +1,22 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +class RepositoryTest { + + @Test + void shouldCreateNewPermissionOnClone() { + Repository repository = new Repository(); + repository.setPermissions(Arrays.asList(new RepositoryPermission("one", "role", false))); + + Repository cloned = repository.clone(); + cloned.setPermissions(Arrays.asList(new RepositoryPermission("two", "role", false))); + + assertThat(repository.getPermissions()).extracting(r -> r.getName()).containsOnly("one"); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java b/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java new file mode 100644 index 0000000000..086df81741 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java @@ -0,0 +1,53 @@ +package sonia.scm.repository.api; + +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +class HunkTest { + + @Test + void shouldGetComplexHeader() { + String rawHeader = createHunk(2, 3, 4, 5).getRawHeader(); + + assertThat(rawHeader).isEqualTo("@@ -2,3 +4,5 @@"); + } + + @Test + void shouldReturnSingleNumberForOne() { + String rawHeader = createHunk(42, 1, 5, 1).getRawHeader(); + + assertThat(rawHeader).isEqualTo("@@ -42 +5 @@"); + } + + private Hunk createHunk(int oldStart, int oldLineCount, int newStart, int newLineCount) { + return new Hunk() { + @Override + public int getOldStart() { + return oldStart; + } + + @Override + public int getOldLineCount() { + return oldLineCount; + } + + @Override + public int getNewStart() { + return newStart; + } + + @Override + public int getNewLineCount() { + return newLineCount; + } + + @Override + public Iterator iterator() { + return null; + } + }; + } +} diff --git a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java index 78dbd4fdd2..0fbcc20ac0 100644 --- a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java @@ -1,20 +1,16 @@ package sonia.scm.security; -import com.google.common.collect.ImmutableList; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.PrincipalCollection; -import org.junit.Ignore; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.group.Group; import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; import sonia.scm.user.User; import sonia.scm.user.UserDAO; @@ -38,7 +34,7 @@ class DAORealmHelperTest { @BeforeEach void setUpObjectUnderTest() { - helper = new DAORealmHelper(loginAttemptHandler, userDAO, new GroupCollector(groupDAO), "hitchhiker"); + helper = new DAORealmHelper(loginAttemptHandler, userDAO, "hitchhiker"); } @Test @@ -73,29 +69,9 @@ class DAORealmHelperTest { AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build(); PrincipalCollection principals = authenticationInfo.getPrincipals(); assertThat(principals.oneByType(User.class)).isSameAs(user); - assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated"); assertThat(principals.oneByType(Scope.class)).isEmpty(); } - @Test - @Ignore - void shouldReturnAuthenticationInfoWithGroups() { - User user = new User("trillian"); - when(userDAO.get("trillian")).thenReturn(user); - - Group one = new Group("xml", "one", "trillian"); - Group two = new Group("xml", "two", "trillian"); - Group six = new Group("xml", "six", "dent"); - when(groupDAO.getAll()).thenReturn(ImmutableList.of(one, two, six)); - - AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian") - .withGroups(ImmutableList.of("three")) - .build(); - - PrincipalCollection principals = authenticationInfo.getPrincipals(); - assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated", "one", "two", "three"); - } - @Test void shouldReturnAuthenticationInfoWithScope() { User user = new User("trillian"); @@ -148,7 +124,6 @@ class DAORealmHelperTest { PrincipalCollection principals = authenticationInfo.getPrincipals(); assertThat(principals.oneByType(User.class)).isSameAs(user); - assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated"); assertThat(principals.oneByType(Scope.class)).isEmpty(); assertThat(authenticationInfo.getCredentials()).isNull(); diff --git a/scm-core/src/test/java/sonia/scm/security/GroupCollectorTest.java b/scm-core/src/test/java/sonia/scm/security/GroupCollectorTest.java deleted file mode 100644 index 3fb59d1614..0000000000 --- a/scm-core/src/test/java/sonia/scm/security/GroupCollectorTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package sonia.scm.security; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; - -import java.util.Collections; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class GroupCollectorTest { - - @Mock - private GroupDAO groupDAO; - - @InjectMocks - private GroupCollector collector; - - @Test - void shouldAlwaysReturnAuthenticatedGroup() { - GroupNames groupNames = collector.collect("trillian", Collections.emptySet()); - assertThat(groupNames).containsOnly("_authenticated"); - } - - @Nested - class WithGroupsFromDao { - - @BeforeEach - void setUpGroupsDao() { - List groups = Lists.newArrayList( - new Group("xml", "heartOfGold", "trillian"), - new Group("xml", "g42", "dent", "prefect"), - new Group("xml", "fjordsOfAfrican", "dent", "trillian") - ); - when(groupDAO.getAll()).thenReturn(groups); - } - - @Test - void shouldReturnGroupsFromDao() { - GroupNames groupNames = collector.collect("trillian", Collections.emptySet()); - assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican"); - } - - @Test - void shouldCombineGivenWithDao() { - GroupNames groupNames = collector.collect("trillian", ImmutableList.of("awesome", "incredible")); - assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible"); - } - - } - -} diff --git a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java index 20d1010b57..ca7c2efdc6 100644 --- a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java @@ -36,31 +36,30 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Throwables; -import com.google.common.collect.Lists; import org.apache.shiro.authc.AuthenticationInfo; -import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.AlreadyExistsException; -import sonia.scm.group.ExternalGroupNames; import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupManager; -import sonia.scm.group.GroupNames; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; import sonia.scm.web.security.PrivilegedAction; import java.io.IOException; -import java.util.List; import static org.hamcrest.Matchers.hasItem; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ @@ -78,9 +77,6 @@ public class SyncingRealmHelperTest { @Mock private UserManager userManager; - @Mock - private GroupDAO groupDAO; - private SyncingRealmHelper helper; /** @@ -106,7 +102,7 @@ public class SyncingRealmHelperTest { } }; - helper = new SyncingRealmHelper(ctx, userManager, groupManager, groupDAO); + helper = new SyncingRealmHelper(ctx, userManager, groupManager); } /** @@ -183,67 +179,15 @@ public class SyncingRealmHelperTest { verify(userManager, times(1)).modify(user); } - @Test - public void builderShouldSetInternalGroups() { - AuthenticationInfo authenticationInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(new User("ziltoid")) - .withGroups("internal") - .build(); - - GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class); - Assertions.assertThat(groupNames.getCollection()).contains("_authenticated", "internal"); - } - - @Test - public void builderShouldSetExternalGroups() { - AuthenticationInfo authenticationInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(new User("ziltoid")) - .withExternalGroups("external") - .build(); - - ExternalGroupNames groupNames = authenticationInfo.getPrincipals().oneByType(ExternalGroupNames.class); - Assertions.assertThat(groupNames.getCollection()).containsOnly("external"); - } @Test public void builderShouldSetValues() { User user = new User("ziltoid"); - AuthenticationInfo authInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(user) - .build(); + AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test", user); assertNotNull(authInfo); assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal()); assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test")); assertEquals(user, authInfo.getPrincipals().oneByType(User.class)); } - - @Test - public void shouldReturnCombinedGroupNames() { - User user = new User("tricia"); - - List groups = Lists.newArrayList(new Group("xml", "heartOfGold", "tricia")); - when(groupDAO.getAll()).thenReturn(groups); - - AuthenticationInfo authInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(user) - .withGroups("fjordsOfAfrican") - .withExternalGroups("g42") - .build(); - - - GroupNames groupNames = authInfo.getPrincipals().oneByType(GroupNames.class); - Assertions.assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican"); - - ExternalGroupNames externalGroupNames = authInfo.getPrincipals().oneByType(ExternalGroupNames.class); - Assertions.assertThat(externalGroupNames).contains("g42"); - } } diff --git a/scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java similarity index 91% rename from scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java rename to scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java index ff493e2b84..1f9b4fad07 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java +++ b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java @@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class HttpProtocolServletAuthenticationFilterTest { +class HttpProtocolServletAuthenticationFilterBaseTest { private ScmConfiguration configuration = new ScmConfiguration(); @@ -32,7 +32,7 @@ class HttpProtocolServletAuthenticationFilterTest { @Mock private UserAgentParser userAgentParser; - private HttpProtocolServletAuthenticationFilter authenticationFilter; + private HttpProtocolServletAuthenticationFilterBase authenticationFilter; @Mock private HttpServletRequest request; @@ -48,7 +48,7 @@ class HttpProtocolServletAuthenticationFilterTest { @BeforeEach void setUpObjectUnderTest() { - authenticationFilter = new HttpProtocolServletAuthenticationFilter(configuration, tokenGenerators, userAgentParser); + authenticationFilter = new HttpProtocolServletAuthenticationFilterBase(configuration, tokenGenerators, userAgentParser); } @Test diff --git a/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml b/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml index 87aa3775ea..a9e84994dc 100644 --- a/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml +++ b/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml @@ -5,6 +5,5 @@ space existing false - false xml diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index 20de47ffa4..c7d97a6891 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -123,23 +123,6 @@ public class TestData { ; } - public static void createUserPermission(String username, String roleName, String repositoryType) { - String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); - LOG.info("create permission with name {} and role {} using the endpoint: {}", username, roleName, defaultPermissionUrl); - given(VndMediaType.REPOSITORY_PERMISSION) - .when() - .content("{\n" + - "\t\"role\": " + roleName + ",\n" + - "\t\"name\": \"" + username + "\",\n" + - "\t\"groupPermission\": false\n" + - "\t\n" + - "}") - .post(defaultPermissionUrl) - .then() - .statusCode(HttpStatus.SC_CREATED) - ; - } - public static List getUserPermissions(String username, String password, String repositoryType) { return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK) .extract() @@ -245,7 +228,6 @@ public class TestData { .add("contact", "zaphod.beeblebrox@hitchhiker.com") .add("description", "Heart of Gold") .add("name", getDefaultRepoName(repositoryType)) - .add("archived", false) .add("type", repositoryType) .build().toString(); } diff --git a/scm-plugins/scm-git-plugin/pom.xml b/scm-plugins/scm-git-plugin/pom.xml index a838e2f146..ac1f007394 100644 --- a/scm-plugins/scm-git-plugin/pom.xml +++ b/scm-plugins/scm-git-plugin/pom.xml @@ -10,7 +10,6 @@ scm-git-plugin - scm-git-plugin smp https://bitbucket.org/sdorra/scm-manager Plugin for the version control system Git diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index 7aacdb256a..7175d3b646 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -43,6 +43,7 @@ import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -441,7 +442,7 @@ public final class GitUtil * * @return */ - public static String getId(ObjectId objectId) + public static String getId(AnyObjectId objectId) { String id = Util.EMPTY_STRING; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java new file mode 100644 index 0000000000..ca417550f4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java @@ -0,0 +1,118 @@ +package sonia.scm.repository.spi; + +import com.google.common.base.Strings; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import sonia.scm.repository.GitUtil; +import sonia.scm.util.Util; + +import java.io.IOException; +import java.util.List; + +final class Differ implements AutoCloseable { + + private final RevWalk walk; + private final TreeWalk treeWalk; + private final RevCommit commit; + + private Differ(RevCommit commit, RevWalk walk, TreeWalk treeWalk) { + this.commit = commit; + this.walk = walk; + this.treeWalk = treeWalk; + } + + static Diff diff(Repository repository, DiffCommandRequest request) throws IOException { + try (Differ differ = create(repository, request)) { + return differ.diff(); + } + } + + private static Differ create(Repository repository, DiffCommandRequest request) throws IOException { + RevWalk walk = new RevWalk(repository); + + ObjectId revision = repository.resolve(request.getRevision()); + RevCommit commit = walk.parseCommit(revision); + + walk.markStart(commit); + commit = walk.next(); + TreeWalk treeWalk = new TreeWalk(repository); + treeWalk.reset(); + treeWalk.setRecursive(true); + + if (Util.isNotEmpty(request.getPath())) + { + treeWalk.setFilter(PathFilter.create(request.getPath())); + } + + + if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) + { + ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); + ObjectId ancestorId = computeCommonAncestor(repository, revision, otherRevision); + RevTree tree = walk.parseCommit(ancestorId).getTree(); + treeWalk.addTree(tree); + } + else if (commit.getParentCount() > 0) + { + RevTree tree = commit.getParent(0).getTree(); + + if (tree != null) + { + treeWalk.addTree(tree); + } + else + { + treeWalk.addTree(new EmptyTreeIterator()); + } + } + else + { + treeWalk.addTree(new EmptyTreeIterator()); + } + + treeWalk.addTree(commit.getTree()); + + return new Differ(commit, walk, treeWalk); + } + + private static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { + return GitUtil.computeCommonAncestor(repository, revision1, revision2); + } + + private Diff diff() throws IOException { + List entries = DiffEntry.scan(treeWalk); + return new Diff(commit, entries); + } + + @Override + public void close() { + GitUtil.release(walk); + GitUtil.release(treeWalk); + } + + public static class Diff { + + private final RevCommit commit; + private final List entries; + + private Diff(RevCommit commit, List entries) { + this.commit = commit; + this.entries = entries; + } + + public RevCommit getCommit() { + return commit; + } + + public List getEntries() { + return entries; + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java new file mode 100644 index 0000000000..8d445e1c44 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java @@ -0,0 +1,20 @@ +package sonia.scm.repository.spi; + +public class FileRange { + + private final int start; + private final int lineCount; + + public FileRange(int start, int lineCount) { + this.start = start; + this.lineCount = lineCount; + } + + public int getStart() { + return start; + } + + public int getLineCount() { + return lineCount; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index 2d56c8e786..5d5f27806b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

* Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + *

* 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + *

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,9 +24,8 @@ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + *

* http://bitbucket.org/sdorra/scm-manager - * */ @@ -34,148 +33,41 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Strings; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevTree; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.treewalk.EmptyTreeIterator; -import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.treewalk.filter.PathFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; -import sonia.scm.util.Util; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.util.List; /** * * @author Sebastian Sdorra */ -public class GitDiffCommand extends AbstractGitCommand implements DiffCommand -{ +public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { - /** - * the logger for GitDiffCommand - */ - private static final Logger logger = - LoggerFactory.getLogger(GitDiffCommand.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param context - * @param repository - */ - public GitDiffCommand(GitContext context, Repository repository) - { + GitDiffCommand(GitContext context, Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param output - */ @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) - { - RevWalk walk = null; - TreeWalk treeWalk = null; - DiffFormatter formatter = null; + public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException { + @SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService + org.eclipse.jgit.lib.Repository repository = open(); + try (DiffFormatter formatter = new DiffFormatter(new BufferedOutputStream(output))) { + formatter.setRepository(repository); - try - { - org.eclipse.jgit.lib.Repository gr = open(); + Differ.Diff diff = Differ.diff(repository, request); - walk = new RevWalk(gr); - - ObjectId revision = gr.resolve(request.getRevision()); - RevCommit commit = walk.parseCommit(revision); - - walk.markStart(commit); - commit = walk.next(); - treeWalk = new TreeWalk(gr); - treeWalk.reset(); - treeWalk.setRecursive(true); - - if (Util.isNotEmpty(request.getPath())) - { - treeWalk.setFilter(PathFilter.create(request.getPath())); - } - - - if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) - { - ObjectId otherRevision = gr.resolve(request.getAncestorChangeset()); - ObjectId ancestorId = computeCommonAncestor(gr, revision, otherRevision); - RevTree tree = walk.parseCommit(ancestorId).getTree(); - treeWalk.addTree(tree); - } - else if (commit.getParentCount() > 0) - { - RevTree tree = commit.getParent(0).getTree(); - - if (tree != null) - { - treeWalk.addTree(tree); - } - else - { - treeWalk.addTree(new EmptyTreeIterator()); - } - } - else - { - treeWalk.addTree(new EmptyTreeIterator()); - } - - treeWalk.addTree(commit.getTree()); - formatter = new DiffFormatter(new BufferedOutputStream(output)); - formatter.setRepository(gr); - - List entries = DiffEntry.scan(treeWalk); - - for (DiffEntry e : entries) - { - if (!e.getOldId().equals(e.getNewId())) - { + for (DiffEntry e : diff.getEntries()) { + if (!e.getOldId().equals(e.getNewId())) { formatter.format(e); } } formatter.flush(); } - catch (Exception ex) - { - // TODO throw exception - logger.error("could not create diff", ex); - } - finally - { - GitUtil.release(walk); - GitUtil.release(treeWalk); - GitUtil.release(formatter); - } - } - - private ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { - return GitUtil.computeCommonAncestor(repository, revision1, revision2); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java new file mode 100644 index 0000000000..e7f26d6885 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java @@ -0,0 +1,107 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffFormatter; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.DiffFile; +import sonia.scm.repository.api.DiffResult; +import sonia.scm.repository.api.Hunk; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Iterator; +import java.util.stream.Collectors; + +public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand { + + GitDiffResultCommand(GitContext context, Repository repository) { + super(context, repository); + } + + public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException { + org.eclipse.jgit.lib.Repository repository = open(); + return new GitDiffResult(repository, Differ.diff(repository, diffCommandRequest)); + } + + private class GitDiffResult implements DiffResult { + + private final org.eclipse.jgit.lib.Repository repository; + private final Differ.Diff diff; + + private GitDiffResult(org.eclipse.jgit.lib.Repository repository, Differ.Diff diff) { + this.repository = repository; + this.diff = diff; + } + + @Override + public String getOldRevision() { + return GitUtil.getId(diff.getCommit().getParent(0).getId()); + } + + @Override + public String getNewRevision() { + return GitUtil.getId(diff.getCommit().getId()); + } + + @Override + public Iterator iterator() { + return diff.getEntries() + .stream() + .map(diffEntry -> new GitDiffFile(repository, diffEntry)) + .collect(Collectors.toList()) + .iterator(); + } + } + + private class GitDiffFile implements DiffFile { + + private final org.eclipse.jgit.lib.Repository repository; + private final DiffEntry diffEntry; + + private GitDiffFile(org.eclipse.jgit.lib.Repository repository, DiffEntry diffEntry) { + this.repository = repository; + this.diffEntry = diffEntry; + } + + @Override + public String getOldRevision() { + return GitUtil.getId(diffEntry.getOldId().toObjectId()); + } + + @Override + public String getNewRevision() { + return GitUtil.getId(diffEntry.getNewId().toObjectId()); + } + + @Override + public String getOldPath() { + return diffEntry.getOldPath(); + } + + @Override + public String getNewPath() { + return diffEntry.getNewPath(); + } + + @Override + public Iterator iterator() { + String content = format(repository, diffEntry); + GitHunkParser parser = new GitHunkParser(); + return parser.parse(content).iterator(); + } + + private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DiffFormatter formatter = new DiffFormatter(baos)) { + formatter.setRepository(repository); + formatter.format(entry); + return baos.toString(); + } catch (IOException ex) { + throw new InternalRepositoryException(GitDiffResultCommand.this.repository, "failed to format diff entry", ex); + } + } + + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java new file mode 100644 index 0000000000..9a272d7b10 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java @@ -0,0 +1,48 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.Iterator; +import java.util.List; + +public class GitHunk implements Hunk { + + private final FileRange oldFileRange; + private final FileRange newFileRange; + private List lines; + + public GitHunk(FileRange oldFileRange, FileRange newFileRange) { + this.oldFileRange = oldFileRange; + this.newFileRange = newFileRange; + } + + @Override + public int getOldStart() { + return oldFileRange.getStart(); + } + + @Override + public int getOldLineCount() { + return oldFileRange.getLineCount(); + } + + @Override + public int getNewStart() { + return newFileRange.getStart(); + } + + @Override + public int getNewLineCount() { + return newFileRange.getLineCount(); + } + + @Override + public Iterator iterator() { + return lines.iterator(); + } + + void setLines(List lines) { + this.lines = lines; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java new file mode 100644 index 0000000000..b7594bb5d6 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java @@ -0,0 +1,176 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.Scanner; + +import static java.util.OptionalInt.of; + +final class GitHunkParser { + private static final int HEADER_PREFIX_LENGTH = "@@ -".length(); + private static final int HEADER_SUFFIX_LENGTH = " @@".length(); + + private GitHunk currentGitHunk = null; + private List collectedLines = null; + private int oldLineCounter = 0; + private int newLineCounter = 0; + + GitHunkParser() { + } + + public List parse(String content) { + List hunks = new ArrayList<>(); + + try (Scanner scanner = new Scanner(content)) { + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.startsWith("@@")) { + parseHeader(hunks, line); + } else if (currentGitHunk != null) { + parseDiffLine(line); + } + } + } + if (currentGitHunk != null) { + currentGitHunk.setLines(collectedLines); + } + + return hunks; + } + + private void parseHeader(List hunks, String line) { + if (currentGitHunk != null) { + currentGitHunk.setLines(collectedLines); + } + String hunkHeader = line.substring(HEADER_PREFIX_LENGTH, line.length() - HEADER_SUFFIX_LENGTH); + String[] split = hunkHeader.split("\\s"); + + FileRange oldFileRange = createFileRange(split[0]); + // TODO merge contains two two block which starts with "-" e.g. -1,3 -2,4 +3,6 + // check if it is relevant for our use case + FileRange newFileRange = createFileRange(split[1]); + + currentGitHunk = new GitHunk(oldFileRange, newFileRange); + hunks.add(currentGitHunk); + + collectedLines = new ArrayList<>(); + oldLineCounter = currentGitHunk.getOldStart(); + newLineCounter = currentGitHunk.getNewStart(); + } + + private void parseDiffLine(String line) { + String content = line.substring(1); + switch (line.charAt(0)) { + case ' ': + collectedLines.add(new UnchangedGitDiffLine(newLineCounter, oldLineCounter, content)); + ++newLineCounter; + ++oldLineCounter; + break; + case '+': + collectedLines.add(new AddedGitDiffLine(newLineCounter, content)); + ++newLineCounter; + break; + case '-': + collectedLines.add(new RemovedGitDiffLine(oldLineCounter, content)); + ++oldLineCounter; + break; + default: + throw new IllegalStateException("cannot handle diff line: " + line); + } + } + + private static class AddedGitDiffLine implements DiffLine { + private final int newLineNumber; + private final String content; + + private AddedGitDiffLine(int newLineNumber, String content) { + this.newLineNumber = newLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return OptionalInt.empty(); + } + + @Override + public OptionalInt getNewLineNumber() { + return of(newLineNumber); + } + + @Override + public String getContent() { + return content; + } + } + + private static class RemovedGitDiffLine implements DiffLine { + private final int oldLineNumber; + private final String content; + + private RemovedGitDiffLine(int oldLineNumber, String content) { + this.oldLineNumber = oldLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return of(oldLineNumber); + } + + @Override + public OptionalInt getNewLineNumber() { + return OptionalInt.empty(); + } + + @Override + public String getContent() { + return content; + } + } + + private static class UnchangedGitDiffLine implements DiffLine { + private final int newLineNumber; + private final int oldLineNumber; + private final String content; + + private UnchangedGitDiffLine(int newLineNumber, int oldLineNumber, String content) { + this.newLineNumber = newLineNumber; + this.oldLineNumber = oldLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return of(oldLineNumber); + } + + @Override + public OptionalInt getNewLineNumber() { + return of(newLineNumber); + } + + @Override + public String getContent() { + return content; + } + } + + private static FileRange createFileRange(String fileRangeString) { + int start; + int lineCount = 1; + int commaIndex = fileRangeString.indexOf(','); + if (commaIndex > 0) { + start = Integer.parseInt(fileRangeString.substring(0, commaIndex)); + lineCount = Integer.parseInt(fileRangeString.substring(commaIndex + 1)); + } else { + start = Integer.parseInt(fileRangeString); + } + + return new FileRange(start, lineCount); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index f4b19d1e85..6870c85dea 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -59,7 +59,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider Command.BLAME, Command.BROWSE, Command.CAT, - Command.DIFF, + Command.DIFF, + Command.DIFF_RESULT, Command.LOG, Command.TAGS, Command.BRANCHES, @@ -168,6 +169,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitDiffCommand(context, repository); } + @Override + public DiffResultCommand getDiffResultCommand() { + return new GitDiffResultCommand(context, repository); + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/plugin.xml b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/plugin.xml index ff699441a8..ba1d625fb4 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/plugin.xml +++ b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/plugin.xml @@ -46,14 +46,10 @@ 2 - Sebastian Sdorra - Git - - git - scm - vcs - dvcs - + Git + Cloudogu GmbH + Source Code Management + /images/git-logo.png diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java index f6e462f968..fd9c45be5c 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -3,6 +3,7 @@ package sonia.scm.repository.spi; import org.junit.Test; import java.io.ByteArrayOutputStream; +import java.io.IOException; import static org.junit.Assert.assertEquals; @@ -38,7 +39,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { "+f\n"; @Test - public void diffForOneRevisionShouldCreateDiff() { + public void diffForOneRevisionShouldCreateDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); @@ -48,7 +49,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffForOneBranchShouldCreateDiff() { + public void diffForOneBranchShouldCreateDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); @@ -58,7 +59,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffForPathShouldCreateLimitedDiff() { + public void diffForPathShouldCreateLimitedDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); @@ -69,7 +70,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffBetweenTwoBranchesShouldCreateDiff() { + public void diffBetweenTwoBranchesShouldCreateDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("master"); @@ -80,7 +81,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() { + public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("master"); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java new file mode 100644 index 0000000000..f359ae987c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java @@ -0,0 +1,89 @@ +package sonia.scm.repository.spi; + +import org.junit.Test; +import sonia.scm.repository.api.DiffFile; +import sonia.scm.repository.api.DiffResult; +import sonia.scm.repository.api.Hunk; + +import java.io.IOException; +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GitDiffResultCommandTest extends AbstractGitCommandTestBase { + + @Test + public void shouldReturnOldAndNewRevision() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + + assertThat(diffResult.getNewRevision()).isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + assertThat(diffResult.getOldRevision()).isEqualTo("592d797cd36432e591416e8b2b98154f4f163411"); + } + + @Test + public void shouldReturnFilePaths() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator iterator = diffResult.iterator(); + DiffFile a = iterator.next(); + assertThat(a.getNewPath()).isEqualTo("a.txt"); + assertThat(a.getOldPath()).isEqualTo("a.txt"); + + DiffFile b = iterator.next(); + assertThat(b.getOldPath()).isEqualTo("b.txt"); + assertThat(b.getNewPath()).isEqualTo("/dev/null"); + } + + @Test + public void shouldReturnFileRevisions() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + assertThat(a.getOldRevision()).isEqualTo("78981922613b2afb6025042ff6bd878ac1994e85"); + assertThat(a.getNewRevision()).isEqualTo("1dc60c7504f4326bc83b9b628c384ec8d7e57096"); + + DiffFile b = iterator.next(); + assertThat(b.getOldRevision()).isEqualTo("61780798228d17af2d34fce4cfbdf35556832472"); + assertThat(b.getNewRevision()).isEqualTo("0000000000000000000000000000000000000000"); + } + + @Test + public void shouldReturnFileHunks() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + Iterator hunks = a.iterator(); + + Hunk hunk = hunks.next(); + assertThat(hunk.getOldStart()).isEqualTo(1); + assertThat(hunk.getOldLineCount()).isEqualTo(1); + + assertThat(hunk.getNewStart()).isEqualTo(1); + assertThat(hunk.getNewLineCount()).isEqualTo(1); + } + + @Test + public void shouldReturnFileHunksWithFullFileRange() throws IOException { + DiffResult diffResult = createDiffResult("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); + Iterator iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + Iterator hunks = a.iterator(); + + Hunk hunk = hunks.next(); + assertThat(hunk.getOldStart()).isEqualTo(1); + assertThat(hunk.getOldLineCount()).isEqualTo(1); + + assertThat(hunk.getNewStart()).isEqualTo(1); + assertThat(hunk.getNewLineCount()).isEqualTo(2); + } + + private DiffResult createDiffResult(String s) throws IOException { + GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision(s); + + return gitDiffResultCommand.getDiffResult(diffCommandRequest); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java new file mode 100644 index 0000000000..a58fae644d --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java @@ -0,0 +1,138 @@ +package sonia.scm.repository.spi; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.Iterator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class GitHunkParserTest { + + private static final String DIFF_001 = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1 +1,2 @@\n" + + " a\n" + + "+added line\n"; + + private static final String DIFF_002 = "diff --git a/file b/file\n" + + "index 5e89957..e8823e1 100644\n" + + "--- a/file\n" + + "+++ b/file\n" + + "@@ -2,6 +2,9 @@\n" + + " 2\n" + + " 3\n" + + " 4\n" + + "+5\n" + + "+6\n" + + "+7\n" + + " 8\n" + + " 9\n" + + " 10\n" + + "@@ -15,14 +18,13 @@\n" + + " 18\n" + + " 19\n" + + " 20\n" + + "+21\n" + + "+22\n" + + " 23\n" + + " 24\n" + + " 25\n" + + " 26\n" + + " 27\n" + + "-a\n" + + "-b\n" + + "-c\n" + + " 28\n" + + " 29\n" + + " 30"; + + private static final String DIFF_003 = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1,2 +1 @@\n" + + " a\n" + + "-removed line\n"; + + private static final String ILLEGAL_DIFF = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1,2 +1 @@\n" + + " a\n" + + "~illegal line\n"; + + @Test + void shouldParseHunks() { + List hunks = new GitHunkParser().parse(DIFF_001); + assertThat(hunks).hasSize(1); + assertHunk(hunks.get(0), 1, 1, 1, 2); + } + + @Test + void shouldParseMultipleHunks() { + List hunks = new GitHunkParser().parse(DIFF_002); + + assertThat(hunks).hasSize(2); + assertHunk(hunks.get(0), 2, 6, 2, 9); + assertHunk(hunks.get(1), 15, 14, 18, 13); + } + + @Test + void shouldParseAddedHunkLines() { + List hunks = new GitHunkParser().parse(DIFF_001); + + Hunk hunk = hunks.get(0); + + Iterator lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(1); + assertThat(line1.getNewLineNumber()).hasValue(1); + assertThat(line1.getContent()).isEqualTo("a"); + + DiffLine line2 = lines.next(); + assertThat(line2.getOldLineNumber()).isEmpty(); + assertThat(line2.getNewLineNumber()).hasValue(2); + assertThat(line2.getContent()).isEqualTo("added line"); + } + + @Test + void shouldParseRemovedHunkLines() { + List hunks = new GitHunkParser().parse(DIFF_003); + + Hunk hunk = hunks.get(0); + + Iterator lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(1); + assertThat(line1.getNewLineNumber()).hasValue(1); + assertThat(line1.getContent()).isEqualTo("a"); + + DiffLine line2 = lines.next(); + assertThat(line2.getOldLineNumber()).hasValue(2); + assertThat(line2.getNewLineNumber()).isEmpty(); + assertThat(line2.getContent()).isEqualTo("removed line"); + } + + @Test + void shouldFailForIllegalLine() { + assertThrows(IllegalStateException.class, () -> new GitHunkParser().parse(ILLEGAL_DIFF)); + } + + private void assertHunk(Hunk hunk, int oldStart, int oldLineCount, int newStart, int newLineCount) { + assertThat(hunk.getOldStart()).isEqualTo(oldStart); + assertThat(hunk.getOldLineCount()).isEqualTo(oldLineCount); + + assertThat(hunk.getNewStart()).isEqualTo(newStart); + assertThat(hunk.getNewLineCount()).isEqualTo(newLineCount); + } + +} diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index 025f79add3..e5decb0567 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -10,7 +10,6 @@ scm-hg-plugin - scm-hg-plugin smp https://bitbucket.org/sdorra/scm-manager Plugin for the version control system Mercurial diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java index b8883a0d92..1d227fb54e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java @@ -40,11 +40,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.web.HgUtil; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.util.Map; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/plugin.xml b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/plugin.xml index 1d0b05c4a8..352192121f 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/plugin.xml +++ b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/plugin.xml @@ -46,15 +46,10 @@ jo 2 - Sebastian Sdorra - Mercurial - - mercurial - hg - scm - vcs - dvcs - + Mercurial + Cloudogu GmbH + Source Code Management + /images/hg-logo.png diff --git a/scm-plugins/scm-legacy-plugin/pom.xml b/scm-plugins/scm-legacy-plugin/pom.xml index 6cfa74ea61..1a12234014 100644 --- a/scm-plugins/scm-legacy-plugin/pom.xml +++ b/scm-plugins/scm-legacy-plugin/pom.xml @@ -6,8 +6,9 @@ scm-plugins 2.0.0-SNAPSHOT - sonia.scm.plugins + scm-legacy-plugin + Support migrated repository urls and v1 passwords 2.0.0-SNAPSHOT smp @@ -21,6 +22,7 @@ ${servlet.version} provided + javax.ws.rs jsr311-api diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java new file mode 100644 index 0000000000..ca6dea0898 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java @@ -0,0 +1,22 @@ +package sonia.scm.legacy; + +import sonia.scm.Priority; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.filter.Filters; +import sonia.scm.filter.WebElement; +import sonia.scm.web.UserAgentParser; +import sonia.scm.web.WebTokenGenerator; +import sonia.scm.web.filter.HttpProtocolServletAuthenticationFilterBase; + +import javax.inject.Inject; +import java.util.Set; + +@Priority(Filters.PRIORITY_AUTHENTICATION) +@WebElement(value = "/git/*", morePatterns = {"/hg/*", "/svn/*"}) +public class LegacyProtocolServletAuthenticationFilter extends HttpProtocolServletAuthenticationFilterBase { + + @Inject + public LegacyProtocolServletAuthenticationFilter(ScmConfiguration configuration, Set tokenGenerators, UserAgentParser userAgentParser) { + super(configuration, tokenGenerators, userAgentParser); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js index 396558f852..6728a93c78 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js +++ b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js @@ -1,6 +1,6 @@ //@flow import React from "react"; -import { withRouter } from "react-router-dom"; +import {withRouter} from "react-router-dom"; class DummyComponent extends React.Component { render() { diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/index.js b/scm-plugins/scm-legacy-plugin/src/main/js/index.js index 97c3eb7e32..7be0386359 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/js/index.js +++ b/scm-plugins/scm-legacy-plugin/src/main/js/index.js @@ -1,14 +1,9 @@ // @flow import React from "react"; -import { withRouter } from "react-router-dom"; -import { binder } from "@scm-manager/ui-extensions"; -import { - ProtectedRoute, - apiClient, - ErrorNotification, - ErrorBoundary -} from "@scm-manager/ui-components"; +import {withRouter} from "react-router-dom"; +import {binder} from "@scm-manager/ui-extensions"; +import {apiClient, ErrorBoundary, ErrorNotification, ProtectedRoute} from "@scm-manager/ui-components"; import DummyComponent from "./DummyComponent"; import type {Links} from "@scm-manager/ui-types"; diff --git a/scm-plugins/scm-legacy-plugin/src/main/resources/META-INF/scm/plugin.xml b/scm-plugins/scm-legacy-plugin/src/main/resources/META-INF/scm/plugin.xml index f8a3c8c7b4..2a6b553cdf 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/resources/META-INF/scm/plugin.xml +++ b/scm-plugins/scm-legacy-plugin/src/main/resources/META-INF/scm/plugin.xml @@ -46,7 +46,9 @@ 2 - Sebastian Sdorra + Legacy + Cloudogu GmbH + Legacy Support diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java index 169c80eae2..a28f87dbf8 100644 --- a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java @@ -10,7 +10,7 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) diff --git a/scm-plugins/scm-svn-plugin/pom.xml b/scm-plugins/scm-svn-plugin/pom.xml index 4386efde5b..83da627eb9 100644 --- a/scm-plugins/scm-svn-plugin/pom.xml +++ b/scm-plugins/scm-svn-plugin/pom.xml @@ -10,7 +10,6 @@ scm-svn-plugin - scm-svn-plugin smp https://bitbucket.org/sdorra/scm-manager Plugin for the version control system Subversion diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/plugin.xml b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/plugin.xml index 302abd2b10..5e941e98e1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/plugin.xml +++ b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/plugin.xml @@ -46,14 +46,10 @@ 2 - Sebastian Sdorra - Subversion - - subversion - scm - vcs - svn - + Subversion + Cloudogu GmbH + Source Code Management + /images/svn-logo.gif diff --git a/scm-ui-components/packages/ui-components/src/Autocomplete.js b/scm-ui-components/packages/ui-components/src/Autocomplete.js index 22107e75b6..116df6562a 100644 --- a/scm-ui-components/packages/ui-components/src/Autocomplete.js +++ b/scm-ui-components/packages/ui-components/src/Autocomplete.js @@ -1,7 +1,7 @@ // @flow import React from "react"; -import { AsyncCreatable, Async } from "react-select"; -import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; +import {Async, AsyncCreatable} from "react-select"; +import type {AutocompleteObject, SelectValue} from "@scm-manager/ui-types"; import LabelWithHelpIcon from "./forms/LabelWithHelpIcon"; type Props = { diff --git a/scm-ui-components/packages/ui-components/src/CardColumn.js b/scm-ui-components/packages/ui-components/src/CardColumn.js index e1eb65255a..713e1bced3 100644 --- a/scm-ui-components/packages/ui-components/src/CardColumn.js +++ b/scm-ui-components/packages/ui-components/src/CardColumn.js @@ -25,12 +25,17 @@ const styles = { }, content: { display: "flex", - flexGrow: 1 + flexGrow: 1, + alignItems: "center", + justifyContent: "space-between" }, footer: { display: "flex", marginTop: "auto", paddingBottom: "1.5rem" + }, + noBottomMargin: { + marginBottom: "0 !important" } }; @@ -38,24 +43,37 @@ type Props = { title: string, description: string, avatar: React.Node, + contentRight?: React.Node, footerLeft: React.Node, footerRight: React.Node, - link: string, + link?: string, + action?: () => void, + // context props classes: any }; class CardColumn extends React.Component { createLink = () => { - const { link } = this.props; + const { link, action } = this.props; if (link) { return ; + } else if (action) { + return {e.preventDefault(); action();}} href="#" />; } return null; }; render() { - const { avatar, title, description, footerLeft, footerRight, classes } = this.props; + const { + avatar, + title, + description, + contentRight, + footerLeft, + footerRight, + classes + } = this.props; const link = this.createLink(); return ( <> @@ -64,16 +82,29 @@ class CardColumn extends React.Component {

{avatar}
-
+
-
+

{title}

{description}

+ {contentRight && contentRight}
-
+
{footerLeft}
{footerRight}
diff --git a/scm-ui-components/packages/ui-components/src/ErrorNotification.js b/scm-ui-components/packages/ui-components/src/ErrorNotification.js index 72173679dd..0c6367a474 100644 --- a/scm-ui-components/packages/ui-components/src/ErrorNotification.js +++ b/scm-ui-components/packages/ui-components/src/ErrorNotification.js @@ -1,7 +1,7 @@ //@flow import React from "react"; -import { translate } from "react-i18next"; -import { BackendError, ForbiddenError, UnauthorizedError } from "./errors"; +import {translate} from "react-i18next"; +import {BackendError, ForbiddenError, UnauthorizedError} from "./errors"; import Notification from "./Notification"; import BackendErrorNotification from "./BackendErrorNotification"; diff --git a/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js index e39130f1d7..8aeeedc3e4 100644 --- a/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js @@ -1,6 +1,6 @@ // @flow import React from "react"; -import { translate } from "react-i18next"; +import {translate} from "react-i18next"; import type AutocompleteProps from "./UserGroupAutocomplete"; import UserGroupAutocomplete from "./UserGroupAutocomplete"; diff --git a/scm-ui-components/packages/ui-components/src/Notification.js b/scm-ui-components/packages/ui-components/src/Notification.js index b48ae16851..46b6b8edcf 100644 --- a/scm-ui-components/packages/ui-components/src/Notification.js +++ b/scm-ui-components/packages/ui-components/src/Notification.js @@ -2,11 +2,18 @@ import * as React from "react"; import classNames from "classnames"; -type NotificationType = "primary" | "info" | "success" | "warning" | "danger"; +type NotificationType = + | "primary" + | "info" + | "success" + | "warning" + | "danger" + | "inherit"; type Props = { type: NotificationType, onClose?: () => void, + className?: string, children?: React.Node }; @@ -24,9 +31,12 @@ class Notification extends React.Component { } render() { - const { type, children } = this.props; + const { type, className, children } = this.props; + + const color = type !== "inherit" ? "is-" + type : ""; + return ( -
+
{this.renderCloseButton()} {children}
diff --git a/scm-ui-components/packages/ui-components/src/UserAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js index 9ef7aaa7a7..308835d8db 100644 --- a/scm-ui-components/packages/ui-components/src/UserAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js @@ -1,6 +1,6 @@ // @flow import React from "react"; -import { translate } from "react-i18next"; +import {translate} from "react-i18next"; import type AutocompleteProps from "./UserGroupAutocomplete"; import UserGroupAutocomplete from "./UserGroupAutocomplete"; diff --git a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js index 0d6e3ec46e..d038e21221 100644 --- a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js @@ -1,6 +1,6 @@ // @flow import React from "react"; -import type { SelectValue } from "@scm-manager/ui-types"; +import type {SelectValue} from "@scm-manager/ui-types"; import Autocomplete from "./Autocomplete"; export type AutocompleteProps = { diff --git a/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js b/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js index 2dcb56047c..b73688ebbf 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js +++ b/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js @@ -14,7 +14,7 @@ class ButtonGroup extends React.Component { const childWrapper = []; React.Children.forEach(children, child => { if (child) { - childWrapper.push(

{child}

); + childWrapper.push(

{child}

); } }); diff --git a/scm-ui-components/packages/ui-components/src/repos/Diff.js b/scm-ui-components/packages/ui-components/src/repos/Diff.js index 369e812344..7a5bcbf4a2 100644 --- a/scm-ui-components/packages/ui-components/src/repos/Diff.js +++ b/scm-ui-components/packages/ui-components/src/repos/Diff.js @@ -1,10 +1,10 @@ //@flow import React from "react"; import DiffFile from "./DiffFile"; -import type { DiffObjectProps } from "./DiffTypes"; +import type {DiffObjectProps, File} from "./DiffTypes"; type Props = DiffObjectProps & { - diff: any + diff: File[] }; class Diff extends React.Component { diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js index a921a4a0a4..ab020f723d 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js @@ -1,17 +1,10 @@ //@flow import React from "react"; -import { - Hunk, - Diff as DiffComponent, - getChangeKey, - Change, - DiffObjectProps, - File -} from "react-diff-view"; +import {Change, Diff as DiffComponent, DiffObjectProps, File, getChangeKey, Hunk} from "react-diff-view"; import injectSheets from "react-jss"; import classNames from "classnames"; -import { translate } from "react-i18next"; -import { ButtonGroup, Button } from "../buttons"; +import {translate} from "react-i18next"; +import {Button, ButtonGroup} from "../buttons"; const styles = { panel: { @@ -46,6 +39,7 @@ const styles = { type Props = DiffObjectProps & { file: File, + collapsible: true, // context props classes: any, t: string => string @@ -66,9 +60,11 @@ class DiffFile extends React.Component { } toggleCollapse = () => { - this.setState(state => ({ - collapsed: !state.collapsed - })); + if (this.props.collapsable) { + this.setState(state => ({ + collapsed: !state.collapsed + })); + } }; toggleSideBySide = () => { @@ -173,6 +169,9 @@ class DiffFile extends React.Component { renderChangeTag = (file: any) => { const { t, classes } = this.props; + if (!file.type) { + return; + } const key = "diff.changes." + file.type; let value = t(key); if (key === value) { @@ -205,6 +204,7 @@ class DiffFile extends React.Component { file, fileControlFactory, fileAnnotationFactory, + collapsible, classes, t } = this.props; @@ -227,6 +227,7 @@ class DiffFile extends React.Component {
); } + const collapseIcon = collapsible? : null; const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) @@ -240,7 +241,7 @@ class DiffFile extends React.Component { onClick={this.toggleCollapse} title={this.hoverFileTitle(file)} > - + {collapseIcon} diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js b/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js index 74803e0a4e..dcd23bc9a8 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js @@ -27,12 +27,17 @@ export type Hunk = { content: string }; +export type ChangeType = "insert" | "delete" | "normal"; + export type Change = { content: string, - isNormal: boolean, - newLineNumber: number, - oldLineNumber: number, - type: string + isNormal?: boolean, + isInsert?: boolean, + isDelete?: boolean, + lineNumber?: number, + newLineNumber?: number, + oldLineNumber?: number, + type: ChangeType }; export type BaseContext = { diff --git a/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js b/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js index ce1a074b41..876b757f44 100644 --- a/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js +++ b/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js @@ -1,19 +1,19 @@ //@flow import React from "react"; -import { apiClient } from "../apiclient"; +import {apiClient} from "../apiclient"; import ErrorNotification from "../ErrorNotification"; import parser from "gitdiff-parser"; import Loading from "../Loading"; import Diff from "./Diff"; -import type {DiffObjectProps} from "./DiffTypes"; +import type {DiffObjectProps, File} from "./DiffTypes"; type Props = DiffObjectProps & { url: string }; type State = { - diff?: any, + diff?: File[], loading: boolean, error?: Error }; @@ -47,7 +47,8 @@ class LoadingDiff extends React.Component { .get(url) .then(response => response.text()) .then(parser.parse) - .then(diff => { + // $FlowFixMe + .then((diff: File[]) => { this.setState({ loading: false, diff: diff diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js index 72ce9a2b38..57166ece9d 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js @@ -21,7 +21,7 @@ class ChangesetButtonGroup extends React.Component { const sourcesLink = createSourcesLink(repository, changeset); return ( - +
+
+ +
{ />
+
+
+ +
+
); } + handleLoginInfoUrlChange = (value: string) => { + this.props.onChange(true, value, "loginInfoUrl"); + }; handleRealmDescriptionChange = (value: string) => { this.props.onChange(true, value, "realmDescription"); }; @@ -80,6 +105,9 @@ class GeneralSettings extends React.Component { handleNamespaceStrategyChange = (value: string) => { this.props.onChange(true, value, "namespaceStrategy"); }; + handlePluginCenterUrlChange = (value: string) => { + this.props.onChange(true, value, "pluginUrl"); + }; } export default translate("config")(GeneralSettings); diff --git a/scm-ui/src/admin/containers/Admin.js b/scm-ui/src/admin/containers/Admin.js index 58e37a0a7c..22c6184548 100644 --- a/scm-ui/src/admin/containers/Admin.js +++ b/scm-ui/src/admin/containers/Admin.js @@ -1,14 +1,24 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import {Redirect, Route, Switch} from "react-router-dom"; +import { Redirect, Route, Switch } from "react-router-dom"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import type { History } from "history"; import { connect } from "react-redux"; import { compose } from "redux"; import type { Links } from "@scm-manager/ui-types"; -import { Page, Navigation, NavLink, Section, SubNavigation } from "@scm-manager/ui-components"; -import { getLinks } from "../../modules/indexResource"; +import { + Page, + Navigation, + NavLink, + Section, + SubNavigation +} from "@scm-manager/ui-components"; +import { + getLinks, + getAvailablePluginsLink, + getInstalledPluginsLink +} from "../../modules/indexResource"; import AdminDetails from "./AdminDetails"; import PluginsOverview from "../plugins/containers/PluginsOverview"; import GlobalConfig from "./GlobalConfig"; @@ -18,6 +28,8 @@ import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole"; type Props = { links: Links, + availablePluginsLink: string, + installedPluginsLink: string, // context objects t: string => string, @@ -28,7 +40,7 @@ type Props = { class Admin extends React.Component { stripEndingSlash = (url: string) => { if (url.endsWith("/")) { - if(url.includes("role")) { + if (url.includes("role")) { return url.substring(0, url.length - 2); } return url.substring(0, url.length - 1); @@ -47,7 +59,7 @@ class Admin extends React.Component { }; render() { - const { links, t } = this.props; + const { links, availablePluginsLink, installedPluginsLink, t } = this.props; const url = this.matchedUrl(); const extensionProps = { @@ -62,34 +74,54 @@ class Admin extends React.Component { - - + + ( - + )} /> ( - + )} /> ( - + )} /> ( - + )} /> { ( - + )} /> ( - - )} + render={() => } /> { icon="fas fa-info-circle" label={t("admin.menu.informationNavLink")} /> - { - links.plugins && - - - {/* Activate this again after available plugins page is created */} - {/**/} - - } + {(availablePluginsLink || installedPluginsLink) && ( + + {installedPluginsLink && ( + + )} + {availablePluginsLink && ( + + )} + + )} { const mapStateToProps = (state: any) => { const links = getLinks(state); + const availablePluginsLink = getAvailablePluginsLink(state); + const installedPluginsLink = getInstalledPluginsLink(state); return { - links + links, + availablePluginsLink, + installedPluginsLink }; }; diff --git a/scm-ui/src/admin/containers/AdminDetails.js b/scm-ui/src/admin/containers/AdminDetails.js index a7a135aa97..803524abed 100644 --- a/scm-ui/src/admin/containers/AdminDetails.js +++ b/scm-ui/src/admin/containers/AdminDetails.js @@ -1,9 +1,11 @@ // @flow import React from "react"; -import { connect } from "react-redux"; -import { translate } from "react-i18next"; -import { Loading, Title, Subtitle } from "@scm-manager/ui-components"; -import { getAppVersion } from "../../modules/indexResource"; +import {connect} from "react-redux"; +import injectSheet from "react-jss"; +import {translate} from "react-i18next"; +import classNames from "classnames"; +import {Image, Loading, Subtitle, Title} from "@scm-manager/ui-components"; +import {getAppVersion} from "../../modules/indexResource"; type Props = { loading: boolean, @@ -11,22 +13,71 @@ type Props = { version: string, - // context objects + // context props + classes: any, t: string => string }; +const styles = { + boxShadow: { + boxShadow: "0 2px 3px rgba(40, 177, 232, 0.1), 0 0 0 2px rgb(40, 177, 232, 0.2)" + }, + boxTitle: { + fontWeight: "500 !important" + }, + imagePadding: { + padding: "0.2rem 0.4rem" + } +}; + class AdminDetails extends React.Component { render() { - const { t, loading } = this.props; + const {loading, classes, t} = this.props; if (loading) { - return ; + return ; } return ( <> - - <Subtitle subtitle={this.props.version} /> + <Title title={t("admin.info.currentAppVersion")}/> + <Subtitle subtitle={this.props.version}/> + <div className={classNames("box", classes.boxShadow)}> + <article className="media"> + <div className={classNames("media-left", classes.imagePadding)}> + <Image + src="/images/iconCommunitySupport.png" + alt={t("admin.info.communityIconAlt")} + /> + </div> + <div className="media-content"> + <div className="content"> + <h3 className={classes.boxTitle}>{t("admin.info.communityTitle")}</h3> + <p>{t("admin.info.communityInfo")}</p> + <a className="button is-info is-pulled-right" target="_blank" + href="https://scm-manager.org/support/">{t("admin.info.communityButton")}</a> + </div> + </div> + </article> + </div> + <div className={classNames("box", classes.boxShadow)}> + <article className="media"> + <div className={classNames("media-left", classes.imagePadding)}> + <Image + src="/images/iconEnterpriseSupport.png" + alt={t("admin.info.enterpriseIconAlt")} + /> + </div> + <div className="media-content"> + <div className="content"> + <h3 className={classes.boxTitle}>{t("admin.info.enterpriseTitle")}</h3> + <p>{t("admin.info.enterpriseInfo")}<br/><strong>{t("admin.info.enterprisePartner")}</strong></p> + <a className="button is-info is-pulled-right is-normal" target="_blank" + href={t("admin.info.enterpriseLink")}>{t("admin.info.enterpriseButton")}</a> + </div> + </div> + </article> + </div> </> ); } @@ -39,4 +90,4 @@ const mapStateToProps = (state: any) => { }; }; -export default connect(mapStateToProps)(translate("admin")(AdminDetails)); +export default connect(mapStateToProps)(injectSheet(styles)(translate("admin")(AdminDetails))); diff --git a/scm-ui/src/admin/modules/config.test.js b/scm-ui/src/admin/modules/config.test.js index 23f58c61ac..b580256a94 100644 --- a/scm-ui/src/admin/modules/config.test.js +++ b/scm-ui/src/admin/modules/config.test.js @@ -35,7 +35,6 @@ const config = { proxyUser: null, enableProxy: false, realmDescription: "SONIA :: SCM Manager", - enableRepositoryArchive: false, disableGroupingGrid: false, dateFormat: "YYYY-MM-DD HH:mm:ss", anonymousAccessEnabled: false, @@ -64,7 +63,6 @@ const configWithNullValues = { proxyUser: null, enableProxy: false, realmDescription: "SONIA :: SCM Manager", - enableRepositoryArchive: false, disableGroupingGrid: false, dateFormat: "YYYY-MM-DD HH:mm:ss", anonymousAccessEnabled: false, diff --git a/scm-ui/src/admin/plugins/components/InstallPendingAction.js b/scm-ui/src/admin/plugins/components/InstallPendingAction.js new file mode 100644 index 0000000000..49a444de11 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/InstallPendingAction.js @@ -0,0 +1,68 @@ +// @flow +import React from "react"; +import { Button } from "@scm-manager/ui-components"; +import type { PluginCollection } from "@scm-manager/ui-types"; +import { translate } from "react-i18next"; +import InstallPendingModal from "./InstallPendingModal"; + +type Props = { + collection: PluginCollection, + + // context props + t: string => string +}; + +type State = { + showModal: boolean +}; + +class InstallPendingAction extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + showModal: false + }; + } + + openModal = () => { + this.setState({ + showModal: true + }); + }; + + closeModal = () => { + this.setState({ + showModal: false + }); + }; + + renderModal = () => { + const { showModal } = this.state; + const { collection } = this.props; + if (showModal) { + return ( + <InstallPendingModal + collection={collection} + onClose={this.closeModal} + /> + ); + } + return null; + }; + + render() { + const { t } = this.props; + return ( + <> + {this.renderModal()} + <Button + color="primary" + label={t("plugins.installPending")} + action={this.openModal} + /> + </> + ); + } +} + +export default translate("admin")(InstallPendingAction); diff --git a/scm-ui/src/admin/plugins/components/InstallPendingModal.js b/scm-ui/src/admin/plugins/components/InstallPendingModal.js new file mode 100644 index 0000000000..e79d815b3d --- /dev/null +++ b/scm-ui/src/admin/plugins/components/InstallPendingModal.js @@ -0,0 +1,134 @@ +// @flow +import React from "react"; +import { + apiClient, + Button, + ButtonGroup, + ErrorNotification, + Modal, + Notification +} from "@scm-manager/ui-components"; +import type { PluginCollection } from "@scm-manager/ui-types"; +import { translate } from "react-i18next"; +import waitForRestart from "./waitForRestart"; +import InstallSuccessNotification from "./InstallSuccessNotification"; + +type Props = { + onClose: () => void, + collection: PluginCollection, + + // context props + t: string => string +}; + +type State = { + loading: boolean, + success: boolean, + error?: Error +}; + +class InstallPendingModal extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + loading: false, + success: false + }; + } + + renderNotifications = () => { + const { t } = this.props; + const { error, success } = this.state; + if (error) { + return <ErrorNotification error={error} />; + } else if (success) { + return <InstallSuccessNotification />; + } else { + return ( + <Notification type="warning"> + {t("plugins.modal.restartNotification")} + </Notification> + ); + } + }; + + installAndRestart = () => { + const { collection } = this.props; + this.setState({ + loading: true + }); + + apiClient + .post(collection._links.installPending.href) + .then(waitForRestart) + .then(() => { + this.setState({ + success: true, + loading: false, + error: undefined + }); + }) + .catch(error => { + this.setState({ + success: false, + loading: false, + error: error + }); + }); + }; + + renderBody = () => { + const { collection, t } = this.props; + return ( + <> + <div className="media"> + <div className="content"> + <p>{t("plugins.modal.installPending")}</p> + <ul> + {collection._embedded.plugins + .filter(plugin => plugin.pending) + .map(plugin => ( + <li key={plugin.name} className="has-text-weight-bold"> + {plugin.name} + </li> + ))} + </ul> + </div> + </div> + <div className="media">{this.renderNotifications()}</div> + </> + ); + }; + + renderFooter = () => { + const { onClose, t } = this.props; + const { loading, error, success } = this.state; + return ( + <ButtonGroup> + <Button + color="warning" + label={t("plugins.modal.installAndRestart")} + loading={loading} + action={this.installAndRestart} + disabled={error || success} + /> + <Button label={t("plugins.modal.abort")} action={onClose} /> + </ButtonGroup> + ); + }; + + render() { + const { onClose, t } = this.props; + return ( + <Modal + title={t("plugins.modal.installAndRestart")} + closeFunction={onClose} + body={this.renderBody()} + footer={this.renderFooter()} + active={true} + /> + ); + } +} + +export default translate("admin")(InstallPendingModal); diff --git a/scm-ui/src/admin/plugins/components/InstallSuccessNotification.js b/scm-ui/src/admin/plugins/components/InstallSuccessNotification.js new file mode 100644 index 0000000000..daebb8a8d0 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/InstallSuccessNotification.js @@ -0,0 +1,25 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Notification } from "@scm-manager/ui-components"; + +type Props = { + // context props + t: string => string +}; + +class InstallSuccessNotification extends React.Component<Props> { + render() { + const { t } = this.props; + return ( + <Notification type="success"> + {t("plugins.modal.successNotification")}{" "} + <a onClick={e => window.location.reload(true)}> + {t("plugins.modal.reload")} + </a> + </Notification> + ); + } +} + +export default translate("admin")(InstallSuccessNotification); diff --git a/scm-ui/src/admin/plugins/components/PluginAvatar.js b/scm-ui/src/admin/plugins/components/PluginAvatar.js index 10408f14bd..42a1fd732b 100644 --- a/scm-ui/src/admin/plugins/components/PluginAvatar.js +++ b/scm-ui/src/admin/plugins/components/PluginAvatar.js @@ -1,8 +1,8 @@ //@flow import React from "react"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; -import type { Plugin } from "@scm-manager/ui-types"; -import { Image } from "@scm-manager/ui-components"; +import {ExtensionPoint} from "@scm-manager/ui-extensions"; +import type {Plugin} from "@scm-manager/ui-types"; +import {Image} from "@scm-manager/ui-components"; type Props = { plugin: Plugin @@ -14,7 +14,7 @@ export default class PluginAvatar extends React.Component<Props> { return ( <p className="image is-64x64"> <ExtensionPoint name="plugins.plugin-avatar" props={{ plugin }}> - <Image src="/images/blib.jpg" alt="Logo" /> + <Image src={plugin.avatarUrl ? plugin.avatarUrl : "/images/blib.jpg"} alt="Logo" /> </ExtensionPoint> </p> ); diff --git a/scm-ui/src/admin/plugins/components/PluginBottomActions.js b/scm-ui/src/admin/plugins/components/PluginBottomActions.js new file mode 100644 index 0000000000..668fc0d285 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/PluginBottomActions.js @@ -0,0 +1,30 @@ +// @flow +import * as React from "react"; +import classNames from "classnames"; +import injectSheet from "react-jss"; + +const styles = { + container: { + border: "2px solid #e9f7fd", + padding: "1em 1em", + marginTop: "2em", + display: "flex", + justifyContent: "center" + } +}; + +type Props = { + children?: React.Node, + + // context props + classes: any +}; + +class PluginBottomActions extends React.Component<Props> { + render() { + const { children, classes } = this.props; + return <div className={classNames(classes.container)}>{children}</div>; + } +} + +export default injectSheet(styles)(PluginBottomActions); diff --git a/scm-ui/src/admin/plugins/components/PluginEntry.js b/scm-ui/src/admin/plugins/components/PluginEntry.js index a8cdaad915..7a8bf6cf6c 100644 --- a/scm-ui/src/admin/plugins/components/PluginEntry.js +++ b/scm-ui/src/admin/plugins/components/PluginEntry.js @@ -1,44 +1,120 @@ //@flow import React from "react"; +import injectSheet from "react-jss"; import type { Plugin } from "@scm-manager/ui-types"; import { CardColumn } from "@scm-manager/ui-components"; import PluginAvatar from "./PluginAvatar"; +import PluginModal from "./PluginModal"; +import classNames from "classnames"; type Props = { - plugin: Plugin + plugin: Plugin, + refresh: () => void, + + // context props + classes: any }; -class PluginEntry extends React.Component<Props> { +type State = { + showModal: boolean +}; + +const styles = { + link: { + cursor: "pointer", + pointerEvents: "all" + }, + spinner: { + position: "absolute", + right: 0, + top: 0 + } +}; + +class PluginEntry extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + showModal: false + }; + } + createAvatar = (plugin: Plugin) => { return <PluginAvatar plugin={plugin} />; }; - createFooterLeft = (plugin: Plugin) => { - return <small className="level-item">{plugin.author}</small>; + toggleModal = () => { + this.setState(prevState => ({ + showModal: !prevState.showModal + })); }; createFooterRight = (plugin: Plugin) => { - return <p className="level-item">{plugin.version}</p>; + return <small className="level-item">{plugin.author}</small>; + }; + + isInstallable = () => { + const { plugin } = this.props; + return plugin._links && plugin._links.install && plugin._links.install.href; + }; + + createFooterLeft = () => { + const { classes } = this.props; + if (this.isInstallable()) { + return ( + <span + className={classNames(classes.link, "level-item")} + onClick={this.toggleModal} + > + <i className="fas fa-download has-text-info" /> + </span> + ); + } + }; + + createPendingSpinner = () => { + const { plugin, classes } = this.props; + if (plugin.pending) { + return ( + <span className={classes.spinner}> + <i className="fas fa-spinner fa-spin has-text-info" /> + </span> + ); + } + return null; }; render() { - const { plugin } = this.props; + const { plugin, refresh } = this.props; + const { showModal } = this.state; const avatar = this.createAvatar(plugin); - const footerLeft = this.createFooterLeft(plugin); + const footerLeft = this.createFooterLeft(); const footerRight = this.createFooterRight(plugin); - // TODO: Add link to plugin page below - return ( - <CardColumn - link="#" - avatar={avatar} - title={plugin.name} - description={plugin.description} - footerLeft={footerLeft} - footerRight={footerRight} + const modal = showModal ? ( + <PluginModal + plugin={plugin} + refresh={refresh} + onClose={this.toggleModal} /> + ) : null; + + return ( + <> + <CardColumn + action={this.isInstallable() ? this.toggleModal : null} + avatar={avatar} + title={plugin.displayName ? plugin.displayName : plugin.name} + description={plugin.description} + contentRight={this.createPendingSpinner()} + footerLeft={footerLeft} + footerRight={footerRight} + /> + {modal} + </> ); } } -export default PluginEntry; +export default injectSheet(styles)(PluginEntry); diff --git a/scm-ui/src/admin/plugins/components/PluginGroupEntry.js b/scm-ui/src/admin/plugins/components/PluginGroupEntry.js index 44046eb6ab..4255606742 100644 --- a/scm-ui/src/admin/plugins/components/PluginGroupEntry.js +++ b/scm-ui/src/admin/plugins/components/PluginGroupEntry.js @@ -5,14 +5,15 @@ import type { PluginGroup } from "@scm-manager/ui-types"; import PluginEntry from "./PluginEntry"; type Props = { - group: PluginGroup + group: PluginGroup, + refresh: () => void }; class PluginGroupEntry extends React.Component<Props> { render() { - const { group } = this.props; - const entries = group.plugins.map((plugin, index) => { - return <PluginEntry plugin={plugin} key={index} />; + const { group, refresh } = this.props; + const entries = group.plugins.map(plugin => { + return <PluginEntry plugin={plugin} key={plugin.name} refresh={refresh} />; }); return <CardColumnGroup name={group.name} elements={entries} />; } diff --git a/scm-ui/src/admin/plugins/components/PluginsList.js b/scm-ui/src/admin/plugins/components/PluginList.js similarity index 83% rename from scm-ui/src/admin/plugins/components/PluginsList.js rename to scm-ui/src/admin/plugins/components/PluginList.js index e04d78d46e..bc8cfe7197 100644 --- a/scm-ui/src/admin/plugins/components/PluginsList.js +++ b/scm-ui/src/admin/plugins/components/PluginList.js @@ -5,18 +5,19 @@ import PluginGroupEntry from "../components/PluginGroupEntry"; import groupByCategory from "./groupByCategory"; type Props = { - plugins: Plugin[] + plugins: Plugin[], + refresh: () => void }; class PluginList extends React.Component<Props> { render() { - const { plugins } = this.props; + const { plugins, refresh } = this.props; const groups = groupByCategory(plugins); return ( <div className="content is-plugin-page"> {groups.map(group => { - return <PluginGroupEntry group={group} key={group.name} />; + return <PluginGroupEntry group={group} key={group.name} refresh={refresh} />; })} </div> ); diff --git a/scm-ui/src/admin/plugins/components/PluginModal.js b/scm-ui/src/admin/plugins/components/PluginModal.js new file mode 100644 index 0000000000..4440bf0ef3 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/PluginModal.js @@ -0,0 +1,270 @@ +//@flow +import React from "react"; +import { compose } from "redux"; +import { translate } from "react-i18next"; +import injectSheet from "react-jss"; +import type { Plugin } from "@scm-manager/ui-types"; +import { + apiClient, + Button, + ButtonGroup, + Checkbox, + ErrorNotification, + Modal, + Notification +} from "@scm-manager/ui-components"; +import classNames from "classnames"; +import waitForRestart from "./waitForRestart"; +import InstallSuccessNotification from "./InstallSuccessNotification"; + +type Props = { + plugin: Plugin, + refresh: () => void, + onClose: () => void, + + // context props + classes: any, + t: (key: string, params?: Object) => string +}; + +type State = { + success: boolean, + restart: boolean, + loading: boolean, + error?: Error +}; + +const styles = { + userLabelAlignment: { + textAlign: "left", + marginRight: 0, + minWidth: "5.5em" + }, + userFieldFlex: { + flexGrow: 4 + } +}; + +class PluginModal extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + loading: false, + restart: false, + success: false + }; + } + + onInstallSuccess = () => { + const { restart } = this.state; + const { refresh, onClose } = this.props; + + const newState = { + loading: false, + error: undefined + }; + + if (restart) { + waitForRestart() + .then(() => { + this.setState({ + ...newState, + success: true + }); + }) + .catch(error => { + this.setState({ + loading: false, + success: false, + error + }); + }); + } else { + this.setState(newState, () => { + refresh(); + onClose(); + }); + } + }; + + install = (e: Event) => { + const { restart } = this.state; + const { plugin } = this.props; + this.setState({ + loading: true + }); + e.preventDefault(); + apiClient + .post(plugin._links.install.href + "?restart=" + restart.toString()) + .then(this.onInstallSuccess) + .catch(error => { + this.setState({ + loading: false, + error: error + }); + }); + }; + + footer = () => { + const { onClose, t } = this.props; + const { loading, error, restart, success } = this.state; + + let color = "primary"; + let label = "plugins.modal.install"; + if (restart) { + color = "warning"; + label = "plugins.modal.installAndRestart"; + } + return ( + <ButtonGroup> + <Button + label={t(label)} + color={color} + action={this.install} + loading={loading} + disabled={!!error || success} + /> + <Button label={t("plugins.modal.abort")} action={onClose} /> + </ButtonGroup> + ); + }; + + renderDependencies() { + const { plugin, classes, t } = this.props; + + let dependencies = null; + if (plugin.dependencies && plugin.dependencies.length > 0) { + dependencies = ( + <div className="media"> + <Notification type="warning"> + <strong>{t("plugins.modal.dependencyNotification")}</strong> + <ul className={classes.listSpacing}> + {plugin.dependencies.map((dependency, index) => { + return <li key={index}>{dependency}</li>; + })} + </ul> + </Notification> + </div> + ); + } + return dependencies; + } + + renderNotifications = () => { + const { t } = this.props; + const { restart, error, success } = this.state; + if (error) { + return ( + <div className="media"> + <ErrorNotification error={error} /> + </div> + ); + } else if (success) { + return ( + <div className="media"> + <InstallSuccessNotification /> + </div> + ); + } else if (restart) { + return ( + <div className="media"> + <Notification type="warning"> + {t("plugins.modal.restartNotification")} + </Notification> + </div> + ); + } + return null; + }; + + handleRestartChange = (value: boolean) => { + this.setState({ + restart: value + }); + }; + + render() { + const { restart } = this.state; + const { plugin, onClose, classes, t } = this.props; + + const body = ( + <> + <div className="media"> + <div className="media-content"> + <p>{plugin.description}</p> + </div> + </div> + <div className="media"> + <div className="media-content"> + <div className="field is-horizontal"> + <div + className={classNames( + classes.userLabelAlignment, + "field-label is-inline-flex" + )} + > + {t("plugins.modal.author")}: + </div> + <div + className={classNames( + classes.userFieldFlex, + "field-body is-inline-flex" + )} + > + {plugin.author} + </div> + </div> + <div className="field is-horizontal"> + <div + className={classNames( + classes.userLabelAlignment, + "field-label is-inline-flex" + )} + > + {t("plugins.modal.version")}: + </div> + <div + className={classNames( + classes.userFieldFlex, + "field-body is-inline-flex" + )} + > + {plugin.version} + </div> + </div> + + {this.renderDependencies()} + </div> + </div> + <div className="media"> + <div className="media-content"> + <Checkbox + checked={restart} + label={t("plugins.modal.restart")} + onChange={this.handleRestartChange} + disabled={false} + /> + </div> + </div> + {this.renderNotifications()} + </> + ); + + return ( + <Modal + title={t("plugins.modal.title", { + name: plugin.displayName ? plugin.displayName : plugin.name + })} + closeFunction={() => onClose()} + body={body} + footer={this.footer()} + active={true} + /> + ); + } +} + +export default compose( + injectSheet(styles), + translate("admin") +)(PluginModal); diff --git a/scm-ui/src/admin/plugins/components/PluginTopActions.js b/scm-ui/src/admin/plugins/components/PluginTopActions.js new file mode 100644 index 0000000000..d30c755eb2 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/PluginTopActions.js @@ -0,0 +1,32 @@ +// @flow +import * as React from "react"; +import classNames from "classnames"; +import injectSheet from "react-jss"; + +const styles = { + container: { + display: "flex", + justifyContent: "flex-end", + alignItems: "center" + } +}; + +type Props = { + children?: React.Node, + + // context props + classes: any +}; + +class PluginTopActions extends React.Component<Props> { + render() { + const { children, classes } = this.props; + return ( + <div className={classNames(classes.container, "column", "is-one-fifths", "is-mobile-action-spacing")}> + {children} + </div> + ); + } +} + +export default injectSheet(styles)(PluginTopActions); diff --git a/scm-ui/src/admin/plugins/components/groupByCategory.js b/scm-ui/src/admin/plugins/components/groupByCategory.js index 1c542d45e3..49b6590d9a 100644 --- a/scm-ui/src/admin/plugins/components/groupByCategory.js +++ b/scm-ui/src/admin/plugins/components/groupByCategory.js @@ -6,7 +6,7 @@ export default function groupByCategory( ): PluginGroup[] { let groups = {}; for (let plugin of plugins) { - const groupName = plugin.type; + const groupName = plugin.category; let group = groups[groupName]; if (!group) { diff --git a/scm-ui/src/admin/plugins/components/waitForRestart.js b/scm-ui/src/admin/plugins/components/waitForRestart.js new file mode 100644 index 0000000000..a2f800a013 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/waitForRestart.js @@ -0,0 +1,30 @@ +// @flow +import { apiClient } from "@scm-manager/ui-components"; + +const waitForRestart = () => { + const endTime = Number(new Date()) + 10000; + let started = false; + + const executor = (resolve, reject) => { + // we need some initial delay + if (!started) { + started = true; + setTimeout(executor, 1000, resolve, reject); + } else { + apiClient + .get("") + .then(resolve) + .catch(() => { + if (Number(new Date()) < endTime) { + setTimeout(executor, 500, resolve, reject); + } else { + reject(new Error("timeout reached")); + } + }); + } + }; + + return new Promise<void>(executor); +}; + +export default waitForRestart; diff --git a/scm-ui/src/admin/plugins/containers/PluginsOverview.js b/scm-ui/src/admin/plugins/containers/PluginsOverview.js index 7a3fc7ec4a..ce4324cd3d 100644 --- a/scm-ui/src/admin/plugins/containers/PluginsOverview.js +++ b/scm-ui/src/admin/plugins/containers/PluginsOverview.js @@ -1,5 +1,5 @@ // @flow -import React from "react"; +import * as React from "react"; import { connect } from "react-redux"; import { translate } from "react-i18next"; import { compose } from "redux"; @@ -17,8 +17,14 @@ import { getPluginCollection, isFetchPluginsPending } from "../modules/plugins"; -import PluginsList from "../components/PluginsList"; -import { getPluginsLink } from "../../../modules/indexResource"; +import PluginsList from "../components/PluginList"; +import { + getAvailablePluginsLink, + getInstalledPluginsLink +} from "../../../modules/indexResource"; +import PluginTopActions from "../components/PluginTopActions"; +import PluginBottomActions from "../components/PluginBottomActions"; +import InstallPendingAction from "../components/InstallPendingAction"; type Props = { loading: boolean, @@ -26,7 +32,8 @@ type Props = { collection: PluginCollection, baseUrl: string, installed: boolean, - pluginsLink: string, + availablePluginsLink: string, + installedPluginsLink: string, // context objects t: string => string, @@ -37,12 +44,72 @@ type Props = { class PluginsOverview extends React.Component<Props> { componentDidMount() { - const { fetchPluginsByLink, pluginsLink } = this.props; - fetchPluginsByLink(pluginsLink); + const { + installed, + fetchPluginsByLink, + availablePluginsLink, + installedPluginsLink + } = this.props; + fetchPluginsByLink(installed ? installedPluginsLink : availablePluginsLink); } + componentDidUpdate(prevProps) { + const { + installed, + } = this.props; + if (prevProps.installed !== installed) { + this.fetchPlugins(); + } + } + + fetchPlugins = () => { + const { + installed, + fetchPluginsByLink, + availablePluginsLink, + installedPluginsLink + } = this.props; + fetchPluginsByLink( + installed ? installedPluginsLink : availablePluginsLink + ); + }; + + renderHeader = (actions: React.Node) => { + const { installed, t } = this.props; + return ( + <div className="columns"> + <div className="column"> + <Title title={t("plugins.title")} /> + <Subtitle + subtitle={ + installed + ? t("plugins.installedSubtitle") + : t("plugins.availableSubtitle") + } + /> + </div> + <PluginTopActions>{actions}</PluginTopActions> + </div> + ); + }; + + renderFooter = (actions: React.Node) => { + if (actions) { + return <PluginBottomActions>{actions}</PluginBottomActions>; + } + return null; + }; + + createActions = () => { + const { collection } = this.props; + if (collection._links.installPending) { + return <InstallPendingAction collection={collection} />; + } + return null; + }; + render() { - const { loading, error, collection, installed, t } = this.props; + const { loading, error, collection } = this.props; if (error) { return <ErrorNotification error={error} />; @@ -52,17 +119,13 @@ class PluginsOverview extends React.Component<Props> { return <Loading />; } + const actions = this.createActions(); return ( <> - <Title title={t("plugins.title")} /> - <Subtitle - subtitle={ - installed - ? t("plugins.installedSubtitle") - : t("plugins.availableSubtitle") - } - /> + {this.renderHeader(actions)} + <hr className="header-with-actions" /> {this.renderPluginsList()} + {this.renderFooter(actions)} </> ); } @@ -71,7 +134,7 @@ class PluginsOverview extends React.Component<Props> { const { collection, t } = this.props; if (collection._embedded && collection._embedded.plugins.length > 0) { - return <PluginsList plugins={collection._embedded.plugins} />; + return <PluginsList plugins={collection._embedded.plugins} refresh={this.fetchPlugins} />; } return <Notification type="info">{t("plugins.noPlugins")}</Notification>; } @@ -81,13 +144,15 @@ const mapStateToProps = state => { const collection = getPluginCollection(state); const loading = isFetchPluginsPending(state); const error = getFetchPluginsFailure(state); - const pluginsLink = getPluginsLink(state); + const availablePluginsLink = getAvailablePluginsLink(state); + const installedPluginsLink = getInstalledPluginsLink(state); return { collection, loading, error, - pluginsLink + availablePluginsLink, + installedPluginsLink }; }; diff --git a/scm-ui/src/components/InfoBox.js b/scm-ui/src/components/InfoBox.js new file mode 100644 index 0000000000..f6fc170826 --- /dev/null +++ b/scm-ui/src/components/InfoBox.js @@ -0,0 +1,83 @@ +//@flow +import * as React from "react"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { translate } from "react-i18next"; +import type { InfoItem } from "./InfoItem"; + +const styles = { + image: { + display: "flex", + alignItems: "center", + justifyContent: "center", + flexDirection: "column", + width: 160, + height: 160 + }, + icon: { + color: "#bff1e6" + }, + label: { + marginTop: "0.5em" + }, + content: { + marginLeft: "1.5em" + }, + link: { + display: "block", + marginBottom: "1.5rem" + } +}; + +type Props = { + type: "plugin" | "feature", + item: InfoItem, + + // context props + classes: any, + t: string => string +}; + +class InfoBox extends React.Component<Props> { + + renderBody = () => { + const { item, t } = this.props; + + const bodyClasses = classNames("media-content", "content", this.props.classes.content); + const title = item ? item.title : t("login.loading"); + const summary = item ? item.summary : t("login.loading"); + + return ( + <div className={bodyClasses}> + <h4 className="has-text-link">{title}</h4> + <p>{summary}</p> + </div> + ); + + }; + + render() { + const { item, type, classes, t } = this.props; + const icon = type === "plugin" ? "puzzle-piece" : "star"; + return ( + <a href={item._links.self.href} className={classes.link}> + <div className="box media"> + <figure className="media-left"> + <div + className={classNames("image", "box", "has-background-info", "has-text-white", "has-text-weight-bold", classes.image)}> + <i className={classNames("fas", "fa-" + icon, "fa-2x", classes.icon)}/> + <div className={classNames("is-size-4", classes.label)}>{t("login." + type)}</div> + <div className={classNames("is-size-4")}>{t("login.tip")}</div> + </div> + </figure> + {this.renderBody()} + </div> + </a> + ); + } + +} + +export default injectSheet(styles)(translate("commons")(InfoBox)); + + diff --git a/scm-ui/src/components/InfoItem.js b/scm-ui/src/components/InfoItem.js new file mode 100644 index 0000000000..b947bd3fce --- /dev/null +++ b/scm-ui/src/components/InfoItem.js @@ -0,0 +1,8 @@ +// @flow +import type { Link } from "@scm-manager/ui-types"; + +export type InfoItem = { + title: string, + summary: string, + _links: {[string]: Link} +}; diff --git a/scm-ui/src/components/LoginForm.js b/scm-ui/src/components/LoginForm.js new file mode 100644 index 0000000000..95d4c0b7d3 --- /dev/null +++ b/scm-ui/src/components/LoginForm.js @@ -0,0 +1,120 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import { Image, ErrorNotification, InputField, SubmitButton, UnauthorizedError } from "@scm-manager/ui-components"; +import classNames from "classnames"; +import injectSheet from "react-jss"; + +const styles = { + avatar: { + marginTop: "-70px", + paddingBottom: "20px" + }, + avatarImage: { + border: "1px solid lightgray", + padding: "5px", + background: "#fff", + borderRadius: "50%", + width: "128px", + height: "128px" + }, + avatarSpacing: { + marginTop: "5rem" + } +}; + +type Props = { + error?: Error, + loading: boolean, + loginHandler: (username: string, password: string) => void, + + // context props + t: string => string, + classes: any +}; + +type State = { + username: string, + password: string +}; + +class LoginForm extends React.Component<Props, State> { + + constructor(props: Props) { + super(props); + this.state = { username: "", password: "" }; + } + + handleSubmit = (event: Event) => { + event.preventDefault(); + if (this.isValid()) { + this.props.loginHandler( + this.state.username, + this.state.password + ); + } + }; + + handleUsernameChange = (value: string) => { + this.setState({ username: value }); + }; + + handlePasswordChange = (value: string) => { + this.setState({ password: value }); + }; + + isValid() { + return this.state.username && this.state.password; + } + + areCredentialsInvalid() { + const { t, error } = this.props; + if (error instanceof UnauthorizedError) { + return new Error(t("errorNotification.wrongLoginCredentials")); + } else { + return error; + } + } + + render() { + const { loading, classes, t } = this.props; + return ( + <div className="column is-4 box has-text-centered has-background-white-ter"> + <h3 className="title">{t("login.title")}</h3> + <p className="subtitle">{t("login.subtitle")}</p> + <div className={classNames("box", classes.avatarSpacing)}> + <figure className={classes.avatar}> + <Image + className={classes.avatarImage} + src="/images/blib.jpg" + alt={t("login.logo-alt")} + /> + </figure> + <ErrorNotification error={this.areCredentialsInvalid()}/> + <form onSubmit={this.handleSubmit}> + <InputField + placeholder={t("login.username-placeholder")} + autofocus={true} + onChange={this.handleUsernameChange} + /> + <InputField + placeholder={t("login.password-placeholder")} + type="password" + onChange={this.handlePasswordChange} + /> + <SubmitButton + label={t("login.submit")} + fullWidth={true} + loading={loading} + /> + </form> + </div> + </div> + ); + } + +} + +export default injectSheet(styles)(translate("commons")(LoginForm)); + + diff --git a/scm-ui/src/components/LoginInfo.js b/scm-ui/src/components/LoginInfo.js new file mode 100644 index 0000000000..3f7ea5ede3 --- /dev/null +++ b/scm-ui/src/components/LoginInfo.js @@ -0,0 +1,97 @@ +//@flow +import React from "react"; +import InfoBox from "./InfoBox"; +import type { InfoItem } from "./InfoItem"; +import LoginForm from "./LoginForm"; +import { Loading } from "@scm-manager/ui-components"; + +type Props = { + loginInfoLink?: string, + loading?: boolean, + error?: Error, + loginHandler: (username: string, password: string) => void, +}; + +type LoginInfoResponse = { + plugin?: InfoItem, + feature?: InfoItem +}; + +type State = { + info?: LoginInfoResponse, + loading?: boolean, +}; + +class LoginInfo extends React.Component<Props, State> { + + constructor(props: Props) { + super(props); + this.state = { + loading: !!props.loginInfoLink + }; + } + + fetchLoginInfo = (url: string) => { + return fetch(url) + .then(response => response.json()) + .then(info => { + this.setState({ + info, + loading: false + }); + }); + }; + + timeout = (ms: number, promise: Promise<any>) => { + return new Promise<LoginInfoResponse>((resolve, reject) => { + setTimeout(() => { + reject(new Error("timeout during fetch of login info")); + }, ms); + promise.then(resolve, reject); + }); + }; + + componentDidMount() { + const { loginInfoLink } = this.props; + if (!loginInfoLink) { + return; + } + this.timeout(1000, this.fetchLoginInfo(loginInfoLink)) + .catch(() => { + this.setState({ + loading: false + }); + }); + } + + createInfoPanel = (info: LoginInfoResponse) => ( + <div className="column is-7 is-offset-1 is-paddingless"> + <InfoBox item={info.feature} type="feature" /> + <InfoBox item={info.plugin} type="plugin" /> + </div> + ); + + render() { + const { info, loading } = this.state; + if (loading) { + return <Loading/>; + } + + let infoPanel; + if (info) { + infoPanel = this.createInfoPanel(info); + } + + return ( + <> + <LoginForm {...this.props} /> + {infoPanel} + </> + ); + } + +} + +export default LoginInfo; + + diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index d14d9f5896..f8246ab88b 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -1,8 +1,6 @@ //@flow import React from "react"; import { Redirect, withRouter } from "react-router-dom"; -import injectSheet from "react-jss"; -import { translate } from "react-i18next"; import { login, isAuthenticated, @@ -10,148 +8,64 @@ import { getLoginFailure } from "../modules/auth"; import { connect } from "react-redux"; - -import { - InputField, - SubmitButton, - ErrorNotification, - Image, UnauthorizedError -} from "@scm-manager/ui-components"; +import { getLoginLink, getLoginInfoLink } from "../modules/indexResource"; +import LoginInfo from "../components/LoginInfo"; import classNames from "classnames"; -import { getLoginLink } from "../modules/indexResource"; +import injectSheet from "react-jss"; const styles = { - avatar: { - marginTop: "-70px", - paddingBottom: "20px" - }, - avatarImage: { - border: "1px solid lightgray", - padding: "5px", - background: "#fff", - borderRadius: "50%", - width: "128px", - height: "128px" - }, - avatarSpacing: { - marginTop: "5rem" + section: { + paddingTop: "2em" } }; type Props = { authenticated: boolean, loading: boolean, - error: Error, + error?: Error, link: string, + loginInfoLink?: string, // dispatcher props login: (link: string, username: string, password: string) => void, // context props - t: string => string, classes: any, + t: string => string, from: any, location: any }; -type State = { - username: string, - password: string -}; +class Login extends React.Component<Props> { -class Login extends React.Component<Props, State> { - constructor(props: Props) { - super(props); - this.state = { username: "", password: "" }; - } - - handleUsernameChange = (value: string) => { - this.setState({ username: value }); + handleLogin = (username: string, password: string): void => { + const { link, login } = this.props; + login(link, username, password); }; - handlePasswordChange = (value: string) => { - this.setState({ password: value }); - }; - - handleSubmit = (event: Event) => { - event.preventDefault(); - if (this.isValid()) { - this.props.login( - this.props.link, - this.state.username, - this.state.password - ); - } - }; - - isValid() { - return this.state.username && this.state.password; - } - - isInValid() { - return !this.isValid(); - } - - areCredentialsInvalid() { - const { t, error } = this.props; - if (error instanceof UnauthorizedError) { - return new Error(t("errorNotification.wrongLoginCredentials")); - } else { - return error; - } - } - renderRedirect = () => { const { from } = this.props.location.state || { from: { pathname: "/" } }; - return <Redirect to={from} />; + return <Redirect to={from}/>; }; render() { - const { authenticated, loading, t, classes } = this.props; + const { authenticated, classes, ...restProps } = this.props; if (authenticated) { return this.renderRedirect(); } return ( - <section className="hero"> + <section className={classNames("hero", classes.section )}> <div className="hero-body"> - <div className="container has-text-centered"> - <div className="column is-4 is-offset-4"> - <h3 className="title">{t("login.title")}</h3> - <p className="subtitle">{t("login.subtitle")}</p> - <div className={classNames("box", classes.avatarSpacing)}> - <figure className={classes.avatar}> - <Image - className={classes.avatarImage} - src="/images/blib.jpg" - alt={t("login.logo-alt")} - /> - </figure> - <ErrorNotification error={this.areCredentialsInvalid()} /> - <form onSubmit={this.handleSubmit}> - <InputField - placeholder={t("login.username-placeholder")} - autofocus={true} - onChange={this.handleUsernameChange} - /> - <InputField - placeholder={t("login.password-placeholder")} - type="password" - onChange={this.handlePasswordChange} - /> - <SubmitButton - label={t("login.submit")} - fullWidth={true} - loading={loading} - /> - </form> - </div> + <div className="container"> + <div className="columns is-centered"> + <LoginInfo loginHandler={this.handleLogin} {...restProps} /> </div> </div> </div> </section> - ); + ); } } @@ -160,11 +74,13 @@ const mapStateToProps = state => { const loading = isLoginPending(state); const error = getLoginFailure(state); const link = getLoginLink(state); + const loginInfoLink = getLoginInfoLink(state); return { authenticated, loading, error, - link + link, + loginInfoLink }; }; @@ -179,6 +95,6 @@ const StyledLogin = injectSheet(styles)( connect( mapStateToProps, mapDispatchToProps - )(translate("commons")(Login)) + )(Login) ); export default withRouter(StyledLogin); diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 1d358ecc87..5e982652e8 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -34,7 +34,7 @@ class Main extends React.Component<Props> { render() { const { authenticated, links } = this.props; const redirectUrlFactory = binder.getExtension("main.redirect", this.props); - let url = "/repos"; + let url = "/repos/"; if (redirectUrlFactory) { url = redirectUrlFactory(this.props); } @@ -44,9 +44,10 @@ class Main extends React.Component<Props> { <Redirect exact from="/" to={url} /> <Route exact path="/login" component={Login} /> <Route path="/logout" component={Logout} /> + <Redirect exact strict from="/repos" to="/repos/" /> <ProtectedRoute exact - path="/repos" + path="/repos/" component={Overview} authenticated={authenticated} /> @@ -67,9 +68,10 @@ class Main extends React.Component<Props> { component={RepositoryRoot} authenticated={authenticated} /> + <Redirect exact strict from="/users" to="/users/" /> <ProtectedRoute exact - path="/users" + path="/users/" component={Users} authenticated={authenticated} /> @@ -89,10 +91,10 @@ class Main extends React.Component<Props> { path="/user/:name" component={SingleUser} /> - + <Redirect exact strict from="/groups" to="/groups/" /> <ProtectedRoute exact - path="/groups" + path="/groups/" component={Groups} authenticated={authenticated} /> diff --git a/scm-ui/src/modules/indexResource.js b/scm-ui/src/modules/indexResource.js index 9bfa620674..d62e6b8b5d 100644 --- a/scm-ui/src/modules/indexResource.js +++ b/scm-ui/src/modules/indexResource.js @@ -116,8 +116,12 @@ export function getUiPluginsLink(state: Object) { return getLink(state, "uiPlugins"); } -export function getPluginsLink(state: Object) { - return getLink(state, "plugins"); +export function getAvailablePluginsLink(state: Object) { + return getLink(state, "availablePlugins"); +} + +export function getInstalledPluginsLink(state: Object) { + return getLink(state, "installedPlugins"); } export function getMeLink(state: Object) { @@ -168,6 +172,10 @@ export function getSvnConfigLink(state: Object) { return getLink(state, "svnConfig"); } +export function getLoginInfoLink(state: Object) { + return getLink(state, "loginInfo"); +} + export function getUserAutoCompleteLink(state: Object): string { const link = getLinkCollection(state, "autocomplete").find( i => i.name === "users" diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 6630086964..164ddcbc64 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -1,33 +1,20 @@ //@flow import React from "react"; -import { - fetchRepoByName, - getFetchRepoFailure, - getRepository, - isFetchRepoPending -} from "../modules/repos"; +import {fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos"; -import { connect } from "react-redux"; -import { Redirect, Route, Switch } from "react-router-dom"; -import type { Repository } from "@scm-manager/ui-types"; +import {connect} from "react-redux"; +import {Redirect, Route, Switch} from "react-router-dom"; +import type {Repository} from "@scm-manager/ui-types"; -import { - Loading, - Navigation, - SubNavigation, - NavLink, - Page, - Section, - ErrorPage -} from "@scm-manager/ui-components"; -import { translate } from "react-i18next"; +import {ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation} from "@scm-manager/ui-components"; +import {translate} from "react-i18next"; import RepositoryDetails from "../components/RepositoryDetails"; import EditRepo from "./EditRepo"; import BranchesOverview from "../branches/containers/BranchesOverview"; import CreateBranch from "../branches/containers/CreateBranch"; import Permissions from "../permissions/containers/Permissions"; -import type { History } from "history"; +import type {History} from "history"; import EditRepoNavLink from "../components/EditRepoNavLink"; import BranchRoot from "../branches/containers/BranchRoot"; import ChangesetsRoot from "./ChangesetsRoot"; @@ -35,8 +22,8 @@ import ChangesetView from "./ChangesetView"; import PermissionsNavLink from "../components/PermissionsNavLink"; import Sources from "../sources/containers/Sources"; import RepositoryNavLink from "../components/RepositoryNavLink"; -import { getLinks, getRepositoriesLink } from "../../modules/indexResource"; -import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; +import {getLinks, getRepositoriesLink} from "../../modules/indexResource"; +import {binder, ExtensionPoint} from "@scm-manager/ui-extensions"; type Props = { namespace: string, @@ -125,7 +112,7 @@ class RepositoryRoot extends React.Component<Props> { return ( <Page title={repository.namespace + "/" + repository.name}> <div className="columns"> - <div className="column is-three-quarters is-clipped"> + <div className="column is-three-quarters"> <Switch> <Redirect exact from={this.props.match.url} to={redirectedUrl} /> <Route diff --git a/scm-ui/src/repos/modules/repos.test.js b/scm-ui/src/repos/modules/repos.test.js index ca4b6802b8..31dfc2286c 100644 --- a/scm-ui/src/repos/modules/repos.test.js +++ b/scm-ui/src/repos/modules/repos.test.js @@ -96,7 +96,6 @@ const hitchhikerRestatend: Repository = { description: "restaurant at the end of the universe", namespace: "hitchhiker", name: "restatend", - archived: false, type: "git", _links: { self: { diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index efd92914d3..33211c76f4 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -1,25 +1,20 @@ // @flow import React from "react"; -import { translate } from "react-i18next"; -import type { - RepositoryRole, - PermissionCollection, - PermissionCreateEntry, - SelectValue -} from "@scm-manager/ui-types"; +import {translate} from "react-i18next"; +import type {PermissionCollection, PermissionCreateEntry, RepositoryRole, SelectValue} from "@scm-manager/ui-types"; import { - Subtitle, - SubmitButton, Button, + GroupAutocomplete, LabelWithHelpIcon, Radio, - GroupAutocomplete, + SubmitButton, + Subtitle, UserAutocomplete } from "@scm-manager/ui-components"; import * as validator from "../components/permissionValidation"; import RoleSelector from "../components/RoleSelector"; import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; -import { findVerbsForRole } from "../modules/permissions"; +import {findVerbsForRole} from "../modules/permissions"; type Props = { availableRoles: RepositoryRole[], diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index ea467a56da..41cc366ea6 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -1,44 +1,34 @@ //@flow import React from "react"; -import { connect } from "react-redux"; -import { translate } from "react-i18next"; +import {connect} from "react-redux"; +import {translate} from "react-i18next"; import { + createPermission, + createPermissionReset, + deletePermissionReset, fetchAvailablePermissionsIfNeeded, fetchPermissions, - getFetchAvailablePermissionsFailure, getAvailablePermissions, + getAvailableRepositoryRoles, + getAvailableRepositoryVerbs, + getCreatePermissionFailure, + getDeletePermissionsFailure, + getFetchAvailablePermissionsFailure, getFetchPermissionsFailure, - isFetchAvailablePermissionsPending, - isFetchPermissionsPending, + getModifyPermissionsFailure, getPermissionsOfRepo, hasCreatePermission, - createPermission, isCreatePermissionPending, - getCreatePermissionFailure, - createPermissionReset, - getDeletePermissionsFailure, - getModifyPermissionsFailure, - modifyPermissionReset, - deletePermissionReset, - getAvailableRepositoryRoles, - getAvailableRepositoryVerbs + isFetchAvailablePermissionsPending, + isFetchPermissionsPending, + modifyPermissionReset } from "../modules/permissions"; -import { - Loading, - ErrorPage, - Subtitle, - LabelWithHelpIcon -} from "@scm-manager/ui-components"; -import type { - Permission, - PermissionCollection, - PermissionCreateEntry, - RepositoryRole -} from "@scm-manager/ui-types"; +import {ErrorPage, LabelWithHelpIcon, Loading, Subtitle} from "@scm-manager/ui-components"; +import type {Permission, PermissionCollection, PermissionCreateEntry, RepositoryRole} from "@scm-manager/ui-types"; import SinglePermission from "./SinglePermission"; import CreatePermissionForm from "./CreatePermissionForm"; -import type { History } from "history"; -import { getPermissionsLink } from "../../modules/repos"; +import type {History} from "history"; +import {getPermissionsLink} from "../../modules/repos"; import { getGroupAutoCompleteLink, getRepositoryRolesLink, diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index 549c970aaf..11533a0526 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -1,25 +1,20 @@ // @flow import React from "react"; -import { connect } from "react-redux"; -import { withRouter } from "react-router-dom"; -import type { Branch, Repository } from "@scm-manager/ui-types"; +import {connect} from "react-redux"; +import {withRouter} from "react-router-dom"; +import type {Branch, Repository} from "@scm-manager/ui-types"; import FileTree from "../components/FileTree"; -import { - ErrorNotification, - Loading, - BranchSelector, - Breadcrumb -} from "@scm-manager/ui-components"; -import { translate } from "react-i18next"; +import {BranchSelector, Breadcrumb, ErrorNotification, Loading} from "@scm-manager/ui-components"; +import {translate} from "react-i18next"; import { fetchBranches, getBranches, getFetchBranchesFailure, isFetchBranchesPending } from "../../branches/modules/branches"; -import { compose } from "redux"; +import {compose} from "redux"; import Content from "./Content"; -import { fetchSources, isDirectory } from "../modules/sources"; +import {fetchSources, isDirectory} from "../modules/sources"; type Props = { repository: Repository, diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index 8378bb23e9..48d66e9768 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -497,6 +497,10 @@ form .field:not(.is-grouped) { } } +.modal-card-body div div:last-child { + border-bottom: none; +} + .sub-menu li { line-height: 1; @@ -521,3 +525,10 @@ form .field:not(.is-grouped) { display: none; } } + +// cursor +.has-cursor-pointer { + cursor: pointer; +} + +@import "bulma-popover/css/bulma-popover"; diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 9ab4653e0f..cbeca27d09 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -1798,6 +1798,11 @@ builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" +bulma-popover@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bulma-popover/-/bulma-popover-1.0.0.tgz#fe4b93fa6a68cb233145c16f69ee39ee0f7d4237" + integrity sha512-uc74pIcFIBG7vJMOOYwlcsyiN5HH1LmeDbDame2gLAtiM7EFGsHe58L6wQr6GNDJNrN/adwE2tNzqIb1yjL/Bw== + bulma-tooltip@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/bulma-tooltip/-/bulma-tooltip-2.0.2.tgz#cf0bf5ad2dc75492cbcbd4816e1a005314dc90ac" diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index a74d8dc429..73431b780e 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -464,32 +464,28 @@ <groupId>sonia.scm.maven</groupId> <artifactId>smp-maven-plugin</artifactId> <configuration> - <artifactItems> - <artifactItem> + <smpArtifacts> + <artifact> <groupId>sonia.scm.plugins</groupId> <artifactId>scm-hg-plugin</artifactId> <version>${project.version}</version> - <type>smp</type> - </artifactItem> - <artifactItem> + </artifact> + <artifact> <groupId>sonia.scm.plugins</groupId> <artifactId>scm-svn-plugin</artifactId> <version>${project.version}</version> - <type>smp</type> - </artifactItem> - <artifactItem> + </artifact> + <artifact> <groupId>sonia.scm.plugins</groupId> <artifactId>scm-git-plugin</artifactId> <version>${project.version}</version> - <type>smp</type> - </artifactItem> - <artifactItem> + </artifact> + <artifact> <groupId>sonia.scm.plugins</groupId> <artifactId>scm-legacy-plugin</artifactId> <version>${project.version}</version> - <type>smp</type> - </artifactItem> - </artifactItems> + </artifact> + </smpArtifacts> </configuration> <executions> <execution> diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java new file mode 100644 index 0000000000..3ee4a4ea73 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java @@ -0,0 +1,110 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.plugin.AvailablePlugin; +import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.PluginManager; +import sonia.scm.plugin.PluginPermissions; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Optional; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +public class AvailablePluginResource { + + private final PluginDtoCollectionMapper collectionMapper; + private final PluginManager pluginManager; + private final PluginDtoMapper mapper; + + @Inject + public AvailablePluginResource(PluginDtoCollectionMapper collectionMapper, PluginManager pluginManager, PluginDtoMapper mapper) { + this.collectionMapper = collectionMapper; + this.pluginManager = pluginManager; + this.mapper = mapper; + } + + /** + * Returns a collection of available plugins. + * + * @return collection of available plugins. + */ + @GET + @Path("") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(CollectionDto.class) + @Produces(VndMediaType.PLUGIN_COLLECTION) + public Response getAvailablePlugins() { + PluginPermissions.read().check(); + List<AvailablePlugin> available = pluginManager.getAvailable(); + return Response.ok(collectionMapper.mapAvailable(available)).build(); + } + + /** + * Returns available plugin. + * + * @return available plugin. + */ + @GET + @Path("/{name}") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(PluginDto.class) + @Produces(VndMediaType.PLUGIN) + public Response getAvailablePlugin(@PathParam("name") String name) { + PluginPermissions.read().check(); + Optional<AvailablePlugin> plugin = pluginManager.getAvailable(name); + if (plugin.isPresent()) { + return Response.ok(mapper.mapAvailable(plugin.get())).build(); + } else { + throw notFound(entity("Plugin", name)); + } + } + + /** + * Triggers plugin installation. + * @param name plugin name + * @return HTTP Status. + */ + @POST + @Path("/{name}/install") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response installPlugin(@PathParam("name") String name, @QueryParam("restart") boolean restartAfterInstallation) { + PluginPermissions.manage().check(); + pluginManager.install(name, restartAfterInstallation); + return Response.ok().build(); + } + + @POST + @Path("/install-pending") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response installPending() { + PluginPermissions.manage().check(); + pluginManager.installPendingAndRestart(); + return Response.ok().build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java index 1852d6fdc4..36abd239dd 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java @@ -19,7 +19,6 @@ public class ConfigDto extends HalRepresentation { private String proxyUser; private boolean enableProxy; private String realmDescription; - private boolean enableRepositoryArchive; private boolean disableGroupingGrid; private String dateFormat; private boolean anonymousAccessEnabled; @@ -32,6 +31,7 @@ public class ConfigDto extends HalRepresentation { private long loginAttemptLimitTimeout; private boolean enabledXsrfProtection; private String namespaceStrategy; + private String loginInfoUrl; @Override @SuppressWarnings("squid:S1185") // We want to have this method available in this package diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java index 769de2b705..bb89c90556 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java @@ -33,6 +33,11 @@ class EdisonHalAppender implements HalAppender { embeddedBuilder.with(rel, embedded); } + @Override + public void appendEmbedded(String rel, List<HalRepresentation> embedded) { + embeddedBuilder.with(rel, embedded); + } + private static class EdisonLinkArrayBuilder implements LinkArrayBuilder { private final Links.Builder builder; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index c7b52861dc..d05596abd5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; @@ -7,6 +8,7 @@ import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; import sonia.scm.SCMContextProvider; import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.config.ScmConfiguration; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.PluginPermissions; import sonia.scm.repository.RepositoryRolePermissions; @@ -23,11 +25,13 @@ public class IndexDtoGenerator extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final SCMContextProvider scmContextProvider; + private final ScmConfiguration configuration; @Inject - public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) { + public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider, ScmConfiguration configuration) { this.resourceLinks = resourceLinks; this.scmContextProvider = scmContextProvider; + this.configuration = configuration; } public IndexDto generate() { @@ -36,13 +40,19 @@ public class IndexDtoGenerator extends HalAppenderMapper { builder.self(resourceLinks.index().self()); builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self())); + String loginInfoUrl = configuration.getLoginInfoUrl(); + if (!Strings.isNullOrEmpty(loginInfoUrl)) { + builder.single(link("loginInfo", loginInfoUrl)); + } + if (SecurityUtils.getSubject().isAuthenticated()) { builder.single( link("me", resourceLinks.me().self()), link("logout", resourceLinks.authentication().logout()) ); if (PluginPermissions.read().isPermitted()) { - builder.single(link("plugins", resourceLinks.pluginCollection().self())); + builder.single(link("installedPlugins", resourceLinks.installedPluginCollection().self())); + builder.single(link("availablePlugins", resourceLinks.availablePluginCollection().self())); } if (UserPermissions.list().isPermitted()) { builder.single(link("users", resourceLinks.userCollection().self())); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java similarity index 64% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java index c3b6ea6020..bc9d4b397c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java @@ -3,10 +3,10 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import sonia.scm.plugin.Plugin; -import sonia.scm.plugin.PluginLoader; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginPermissions; -import sonia.scm.plugin.PluginWrapper; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -15,22 +15,21 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; -public class PluginResource { +public class InstalledPluginResource { - private final PluginLoader pluginLoader; private final PluginDtoCollectionMapper collectionMapper; private final PluginDtoMapper mapper; + private final PluginManager pluginManager; @Inject - public PluginResource(PluginLoader pluginLoader, PluginDtoCollectionMapper collectionMapper, PluginDtoMapper mapper) { - this.pluginLoader = pluginLoader; + public InstalledPluginResource(PluginManager pluginManager, PluginDtoCollectionMapper collectionMapper, PluginDtoMapper mapper) { + this.pluginManager = pluginManager; this.collectionMapper = collectionMapper; this.mapper = mapper; } @@ -50,19 +49,19 @@ public class PluginResource { @Produces(VndMediaType.PLUGIN_COLLECTION) public Response getInstalledPlugins() { PluginPermissions.read().check(); - List<PluginWrapper> plugins = new ArrayList<>(pluginLoader.getInstalledPlugins()); - return Response.ok(collectionMapper.map(plugins)).build(); + List<InstalledPlugin> plugins = pluginManager.getInstalled(); + return Response.ok(collectionMapper.mapInstalled(plugins)).build(); } /** * Returns the installed plugin with the given id. * - * @param id id of plugin + * @param name name of plugin * * @return installed plugin with specified id */ @GET - @Path("{id}") + @Path("/{name}") @StatusCodes({ @ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 404, condition = "not found"), @@ -70,18 +69,13 @@ public class PluginResource { }) @TypeHint(PluginDto.class) @Produces(VndMediaType.PLUGIN) - public Response getInstalledPlugin(@PathParam("id") String id) { + public Response getInstalledPlugin(@PathParam("name") String name) { PluginPermissions.read().check(); - Optional<PluginDto> pluginDto = pluginLoader.getInstalledPlugins() - .stream() - .filter(plugin -> id.equals(plugin.getPlugin().getInformation().getId(false))) - .map(mapper::map) - .findFirst(); + Optional<InstalledPlugin> pluginDto = pluginManager.getInstalled(name); if (pluginDto.isPresent()) { - return Response.ok(pluginDto.get()).build(); + return Response.ok(mapper.mapInstalled(pluginDto.get())).build(); } else { - throw notFound(entity(Plugin.class, id)); + throw notFound(entity("Plugin", name)); } } - } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index cf09eeb128..0b419cf542 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -54,5 +54,7 @@ public class MapperModule extends AbstractModule { bind(UIPluginDtoCollectionMapper.class); bind(ScmPathInfoStore.class).in(ServletScopes.REQUEST); + + bind(PluginDtoMapper.class).to(Mappers.getMapper(PluginDtoMapper.class).getClass()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java index 84fbbfe290..a87ad7df17 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java @@ -7,7 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.List; +import java.util.Set; @Getter @Setter @@ -17,7 +17,7 @@ public class MeDto extends HalRepresentation { private String name; private String displayName; private String mail; - private List<String> groups; + private Set<String> groups; MeDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java index b5e1998066..c2bebd389a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java @@ -1,18 +1,16 @@ package sonia.scm.api.v2.resources; -import com.google.common.collect.ImmutableList; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.user.UserPermissions; import javax.inject.Inject; -import java.util.Collections; import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; @@ -22,11 +20,13 @@ public class MeDtoFactory extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final UserManager userManager; + private final GroupCollector groupCollector; @Inject - public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager) { + public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager, GroupCollector groupCollector) { this.resourceLinks = resourceLinks; this.userManager = userManager; + this.groupCollector = groupCollector; } public MeDto create() { @@ -35,16 +35,12 @@ public class MeDtoFactory extends HalAppenderMapper { MeDto dto = createDto(user); mapUserProperties(user, dto); - mapGroups(principals, dto); + mapGroups(user, dto); return dto; } - private void mapGroups(PrincipalCollection principals, MeDto dto) { - Iterable<String> groups = principals.oneByType(GroupNames.class); - if (groups == null) { - groups = Collections.emptySet(); - } - dto.setGroups(ImmutableList.copyOf(groups)); + private void mapGroups(User user, MeDto dto) { + dto.setGroups(groupCollector.collect(user.getName())); } private void mapUserProperties(User user, MeDto dto) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java index d119eca711..bf20d1b67e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java @@ -3,20 +3,26 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; - import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.Set; + @Getter @Setter @NoArgsConstructor +@SuppressWarnings("squid:S2160") // we do not need equals for dto public class PluginDto extends HalRepresentation { private String name; - private String type; private String version; - private String author; + private String displayName; private String description; + private String author; + private String category; + private String avatarUrl; + private boolean pending; + private Set<String> dependencies; public PluginDto(Links links) { add(links); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java index 72178e94f3..7c1ee3d5a6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java @@ -3,10 +3,12 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; -import sonia.scm.plugin.PluginWrapper; +import sonia.scm.plugin.AvailablePlugin; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.PluginPermissions; -import java.util.Collection; import java.util.List; import static de.otto.edison.hal.Embedded.embeddedBuilder; @@ -24,19 +26,41 @@ public class PluginDtoCollectionMapper { this.mapper = mapper; } - public HalRepresentation map(Collection<PluginWrapper> plugins) { - List<PluginDto> dtos = plugins.stream().map(mapper::map).collect(toList()); - return new HalRepresentation(createLinks(), embedDtos(dtos)); + public HalRepresentation mapInstalled(List<InstalledPlugin> plugins) { + List<PluginDto> dtos = plugins.stream().map(mapper::mapInstalled).collect(toList()); + return new HalRepresentation(createInstalledPluginsLinks(), embedDtos(dtos)); } - private Links createLinks() { - String baseUrl = resourceLinks.pluginCollection().self(); + public HalRepresentation mapAvailable(List<AvailablePlugin> plugins) { + List<PluginDto> dtos = plugins.stream().map(mapper::mapAvailable).collect(toList()); + return new HalRepresentation(createAvailablePluginsLinks(plugins), embedDtos(dtos)); + } + + private Links createInstalledPluginsLinks() { + String baseUrl = resourceLinks.installedPluginCollection().self(); Links.Builder linksBuilder = linkingTo() .with(Links.linkingTo().self(baseUrl).build()); return linksBuilder.build(); } + private Links createAvailablePluginsLinks(List<AvailablePlugin> plugins) { + String baseUrl = resourceLinks.availablePluginCollection().self(); + + Links.Builder linksBuilder = linkingTo() + .with(Links.linkingTo().self(baseUrl).build()); + + if (PluginPermissions.manage().isPermitted() && containsPending(plugins)) { + linksBuilder.single(Link.link("installPending", resourceLinks.availablePluginCollection().installPending())); + } + + return linksBuilder.build(); + } + + private boolean containsPending(List<AvailablePlugin> plugins) { + return plugins.stream().anyMatch(AvailablePlugin::isPending); + } + private Embedded embedDtos(List<PluginDto> dtos) { return embeddedBuilder() .with("plugins", dtos) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index d17ecdae70..25faf0a101 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -1,32 +1,69 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Links; -import sonia.scm.plugin.PluginWrapper; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.plugin.AvailablePlugin; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.Plugin; +import sonia.scm.plugin.PluginInformation; +import sonia.scm.plugin.PluginPermissions; + import javax.inject.Inject; +import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; -public class PluginDtoMapper { - - private final ResourceLinks resourceLinks; +@Mapper +public abstract class PluginDtoMapper { @Inject - public PluginDtoMapper(ResourceLinks resourceLinks) { - this.resourceLinks = resourceLinks; + private ResourceLinks resourceLinks; + + public abstract void map(PluginInformation plugin, @MappingTarget PluginDto dto); + + public PluginDto mapInstalled(InstalledPlugin plugin) { + PluginDto dto = createDtoForInstalled(plugin); + map(dto, plugin); + return dto; } - public PluginDto map(PluginWrapper plugin) { - Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.plugin() - .self(plugin.getPlugin().getInformation().getId(false))); + public PluginDto mapAvailable(AvailablePlugin plugin) { + PluginDto dto = createDtoForAvailable(plugin); + map(dto, plugin); + dto.setPending(plugin.isPending()); + return dto; + } - PluginDto pluginDto = new PluginDto(linksBuilder.build()); - pluginDto.setName(plugin.getPlugin().getInformation().getName()); - pluginDto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Miscellaneous"); - pluginDto.setVersion(plugin.getPlugin().getInformation().getVersion()); - pluginDto.setAuthor(plugin.getPlugin().getInformation().getAuthor()); - pluginDto.setDescription(plugin.getPlugin().getInformation().getDescription()); + private void map(PluginDto dto, Plugin plugin) { + dto.setDependencies(plugin.getDescriptor().getDependencies()); + map(plugin.getDescriptor().getInformation(), dto); + if (dto.getCategory() == null) { + dto.setCategory("Miscellaneous"); + } + } - return pluginDto; + private PluginDto createDtoForAvailable(AvailablePlugin plugin) { + PluginInformation information = plugin.getDescriptor().getInformation(); + + Links.Builder links = linkingTo() + .self(resourceLinks.availablePlugin() + .self(information.getName())); + + if (!plugin.isPending() && PluginPermissions.manage().isPermitted()) { + links.single(link("install", resourceLinks.availablePlugin().install(information.getName()))); + } + + return new PluginDto(links.build()); + } + + private PluginDto createDtoForInstalled(InstalledPlugin plugin) { + PluginInformation information = plugin.getDescriptor().getInformation(); + + Links.Builder links = linkingTo() + .self(resourceLinks.installedPlugin() + .self(information.getName())); + + return new PluginDto(links.build()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java index e9b0f0a997..79c46369a3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java @@ -4,18 +4,23 @@ import javax.inject.Inject; import javax.inject.Provider; import javax.ws.rs.Path; -@Path("v2/") +@Path("v2/plugins") public class PluginRootResource { - private Provider<PluginResource> pluginResourceProvider; + private Provider<InstalledPluginResource> installedPluginResourceProvider; + private Provider<AvailablePluginResource> availablePluginResourceProvider; @Inject - public PluginRootResource(Provider<PluginResource> pluginResourceProvider) { - this.pluginResourceProvider = pluginResourceProvider; + public PluginRootResource(Provider<InstalledPluginResource> installedPluginResourceProvider, Provider<AvailablePluginResource> availablePluginResourceProvider) { + this.installedPluginResourceProvider = installedPluginResourceProvider; + this.availablePluginResourceProvider = availablePluginResourceProvider; } - @Path("plugins") - public PluginResource plugins() { - return pluginResourceProvider.get(); + @Path("/installed") + public InstalledPluginResource installedPlugins() { + return installedPluginResourceProvider.get(); } + + @Path("/available") + public AvailablePluginResource availablePlugins() { return availablePluginResourceProvider.get(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 5bca6a6e16..1cfc8b332b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -5,7 +5,6 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryIsNotArchivedException; import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; @@ -63,7 +62,7 @@ public class RepositoryResource { this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; - this.adapter = new SingleResourceManagerAdapter<>(manager, Repository.class, this::handleNotArchived); + this.adapter = new SingleResourceManagerAdapter<>(manager, Repository.class); this.tagRootResource = tagRootResource; this.branchRootResource = branchRootResource; this.changesetRootResource = changesetRootResource; @@ -212,14 +211,6 @@ public class RepositoryResource { @Path("merge/") public MergeResource merge() {return mergeResource.get(); } - private Optional<Response> handleNotArchived(Throwable throwable) { - if (throwable instanceof RepositoryIsNotArchivedException) { - return Optional.of(Response.status(Response.Status.PRECONDITION_FAILED).build()); - } else { - return Optional.empty(); - } - } - private Supplier<Repository> loadBy(String namespace, String name) { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName))); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 1d06659649..bf92c567cd 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -6,6 +6,7 @@ import javax.inject.Inject; import java.net.URI; import java.net.URISyntaxException; +@SuppressWarnings("squid:S1192") // string literals should not be duplicated class ResourceLinks { private final ScmPathInfoStore scmPathInfoStore; @@ -651,35 +652,75 @@ class ResourceLinks { } } - public PluginLinks plugin() { - return new PluginLinks(scmPathInfoStore.get()); + public InstalledPluginLinks installedPlugin() { + return new InstalledPluginLinks(scmPathInfoStore.get()); } - static class PluginLinks { - private final LinkBuilder pluginLinkBuilder; + static class InstalledPluginLinks { + private final LinkBuilder installedPluginLinkBuilder; - PluginLinks(ScmPathInfo pathInfo) { - pluginLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, PluginResource.class); + InstalledPluginLinks(ScmPathInfo pathInfo) { + installedPluginLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, InstalledPluginResource.class); } String self(String id) { - return pluginLinkBuilder.method("plugins").parameters().method("getInstalledPlugin").parameters(id).href(); + return installedPluginLinkBuilder.method("installedPlugins").parameters().method("getInstalledPlugin").parameters(id).href(); } } - public PluginCollectionLinks pluginCollection() { - return new PluginCollectionLinks(scmPathInfoStore.get()); + public InstalledPluginCollectionLinks installedPluginCollection() { + return new InstalledPluginCollectionLinks(scmPathInfoStore.get()); } - static class PluginCollectionLinks { - private final LinkBuilder pluginCollectionLinkBuilder; + static class InstalledPluginCollectionLinks { + private final LinkBuilder installedPluginCollectionLinkBuilder; - PluginCollectionLinks(ScmPathInfo pathInfo) { - pluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, PluginResource.class); + InstalledPluginCollectionLinks(ScmPathInfo pathInfo) { + installedPluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, InstalledPluginResource.class); } String self() { - return pluginCollectionLinkBuilder.method("plugins").parameters().method("getInstalledPlugins").parameters().href(); + return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("getInstalledPlugins").parameters().href(); + } + } + + public AvailablePluginLinks availablePlugin() { + return new AvailablePluginLinks(scmPathInfoStore.get()); + } + + static class AvailablePluginLinks { + private final LinkBuilder availablePluginLinkBuilder; + + AvailablePluginLinks(ScmPathInfo pathInfo) { + availablePluginLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, AvailablePluginResource.class); + } + + String self(String name) { + return availablePluginLinkBuilder.method("availablePlugins").parameters().method("getAvailablePlugin").parameters(name).href(); + } + + String install(String name) { + return availablePluginLinkBuilder.method("availablePlugins").parameters().method("installPlugin").parameters(name).href(); + } + } + + public AvailablePluginCollectionLinks availablePluginCollection() { + return new AvailablePluginCollectionLinks(scmPathInfoStore.get()); + } + + static class AvailablePluginCollectionLinks { + private final LinkBuilder availablePluginCollectionLinkBuilder; + + AvailablePluginCollectionLinks(ScmPathInfo pathInfo) { + availablePluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, AvailablePluginResource.class); + } + + String installPending() { + return availablePluginCollectionLinkBuilder.method("availablePlugins").parameters().method("installPending").parameters().href(); + } + + String self() { + return availablePluginCollectionLinkBuilder.method("availablePlugins").parameters().method("getAvailablePlugins").parameters().href(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index 9c6c0300d6..76371a0b54 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -2,18 +2,16 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; import sonia.scm.ConcurrentModificationException; +import sonia.scm.IllegalIdentifierChangeException; import sonia.scm.Manager; import sonia.scm.ModelObject; import sonia.scm.NotFoundException; import javax.ws.rs.core.Response; -import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; - /** * Adapter from resource http endpoints to managers, for Single resources (e.g. {@code /user/name}). * @@ -28,20 +26,11 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, DTO extends HalRepresentation> { - private final Function<Throwable, Optional<Response>> errorHandler; protected final Manager<MODEL_OBJECT> manager; protected final Class<MODEL_OBJECT> type; SingleResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) { - this(manager, type, e -> Optional.empty()); - } - - SingleResourceManagerAdapter( - Manager<MODEL_OBJECT> manager, - Class<MODEL_OBJECT> type, - Function<Throwable, Optional<Response>> errorHandler) { this.manager = manager; - this.errorHandler = errorHandler; this.type = type; } @@ -65,7 +54,7 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, MODEL_OBJECT existingModelObject = reader.get(); MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); if (!hasSameKey.test(changedModelObject)) { - return Response.status(BAD_REQUEST).entity("illegal change of id").build(); + throw new IllegalIdentifierChangeException("illegal change of id"); } else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) { throw new ConcurrentModificationException(type, keyExtractor.apply(existingModelObject)); @@ -74,12 +63,8 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, } private Response update(MODEL_OBJECT item) { - try { - manager.modify(item); - return Response.noContent().build(); - } catch (RuntimeException ex) { - return createErrorResponse(ex); - } + manager.modify(item); + return Response.noContent().build(); } private boolean modelObjectWasModifiedConcurrently(MODEL_OBJECT existing, MODEL_OBJECT updated) { @@ -100,22 +85,13 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, MODEL_OBJECT item = manager.get(name); if (item != null) { - try { - manager.delete(item); - return Response.noContent().build(); - } catch (RuntimeException ex) { - return createErrorResponse(ex); - } + manager.delete(item); + return Response.noContent().build(); } else { return Response.noContent().build(); } } - private Response createErrorResponse(RuntimeException throwable) { - return errorHandler.apply(throwable) - .orElseThrow(() -> throwable); - } - protected String getId(MODEL_OBJECT item) { return item.getId(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoCollectionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoCollectionMapper.java index f032650d8a..2cb15f7904 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoCollectionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoCollectionMapper.java @@ -4,7 +4,7 @@ import com.google.inject.Inject; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; -import sonia.scm.plugin.PluginWrapper; +import sonia.scm.plugin.InstalledPlugin; import java.util.Collection; import java.util.List; @@ -24,7 +24,7 @@ public class UIPluginDtoCollectionMapper { this.mapper = mapper; } - public HalRepresentation map(Collection<PluginWrapper> plugins) { + public HalRepresentation map(Collection<InstalledPlugin> plugins) { List<UIPluginDto> dtos = plugins.stream().map(mapper::map).collect(toList()); return new HalRepresentation(createLinks(), embedDtos(dtos)); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java index 10ae79b5bf..5eecaa0561 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java @@ -2,7 +2,7 @@ package sonia.scm.api.v2.resources; import com.google.common.base.Strings; import de.otto.edison.hal.Links; -import sonia.scm.plugin.PluginWrapper; +import sonia.scm.plugin.InstalledPlugin; import sonia.scm.util.HttpUtil; import javax.inject.Inject; @@ -25,9 +25,9 @@ public class UIPluginDtoMapper { this.request = request; } - public UIPluginDto map(PluginWrapper plugin) { + public UIPluginDto map(InstalledPlugin plugin) { UIPluginDto dto = new UIPluginDto( - plugin.getPlugin().getInformation().getName(), + plugin.getDescriptor().getInformation().getName(), getScriptResources(plugin) ); @@ -40,8 +40,8 @@ public class UIPluginDtoMapper { return dto; } - private Set<String> getScriptResources(PluginWrapper wrapper) { - Set<String> scriptResources = wrapper.getPlugin().getResources().getScriptResources(); + private Set<String> getScriptResources(InstalledPlugin wrapper) { + Set<String> scriptResources = wrapper.getDescriptor().getResources().getScriptResources(); if (scriptResources != null) { return scriptResources.stream() .map(this::addContextPath) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java index b83f5310e3..1c779653a0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java @@ -4,7 +4,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.plugin.PluginLoader; -import sonia.scm.plugin.PluginWrapper; +import sonia.scm.plugin.InstalledPlugin; import sonia.scm.security.AllowAnonymousAccess; import sonia.scm.web.VndMediaType; @@ -46,7 +46,7 @@ public class UIPluginResource { @TypeHint(CollectionDto.class) @Produces(VndMediaType.UI_PLUGIN_COLLECTION) public Response getInstalledPlugins() { - List<PluginWrapper> plugins = pluginLoader.getInstalledPlugins() + List<InstalledPlugin> plugins = pluginLoader.getInstalledPlugins() .stream() .filter(this::filter) .collect(Collectors.toList()); @@ -85,8 +85,8 @@ public class UIPluginResource { } } - private boolean filter(PluginWrapper plugin) { - return plugin.getPlugin().getResources() != null; + private boolean filter(InstalledPlugin plugin) { + return plugin.getDescriptor().getResources() != null; } } diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java new file mode 100644 index 0000000000..8c6bac8103 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java @@ -0,0 +1,72 @@ +package sonia.scm.group; + +import com.cronutils.utils.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.cache.Cache; +import sonia.scm.cache.CacheManager; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Set; + +/** + * Collect groups for a certain principal. + * <strong>Warning</strong>: The class is only for internal use and should never used directly. + */ +@Singleton +public class DefaultGroupCollector implements GroupCollector { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultGroupCollector.class); + + @VisibleForTesting + static final String CACHE_NAME = "sonia.cache.externalGroups"; + + private final GroupDAO groupDAO; + private final Cache<String, Set<String>> cache; + private final Set<GroupResolver> groupResolvers; + + @Inject + public DefaultGroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set<GroupResolver> groupResolvers) { + this.groupDAO = groupDAO; + this.cache = cacheManager.getCache(CACHE_NAME); + this.groupResolvers = groupResolvers; + } + + @Override + public Set<String> collect(String principal) { + ImmutableSet.Builder<String> builder = ImmutableSet.builder(); + + builder.add(AUTHENTICATED); + builder.addAll(resolveExternalGroups(principal)); + appendInternalGroups(principal, builder); + + Set<String> groups = builder.build(); + LOG.debug("collected following groups for principal {}: {}", principal, groups); + return groups; + } + + private void appendInternalGroups(String principal, ImmutableSet.Builder<String> builder) { + for (Group group : groupDAO.getAll()) { + if (group.isMember(principal)) { + builder.add(group.getName()); + } + } + } + + private Set<String> resolveExternalGroups(String principal) { + Set<String> externalGroups = cache.get(principal); + + if (externalGroups == null) { + ImmutableSet.Builder<String> newExternalGroups = ImmutableSet.builder(); + + for (GroupResolver groupResolver : groupResolvers) { + newExternalGroups.addAll(groupResolver.resolve(principal)); + } + externalGroups = newExternalGroups.build(); + cache.put(principal, externalGroups); + } + return externalGroups; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java index e19d41cc69..ff8c28f51d 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java @@ -9,11 +9,11 @@ import sonia.scm.SCMContext; import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle; import sonia.scm.migration.UpdateException; import sonia.scm.plugin.DefaultPluginLoader; -import sonia.scm.plugin.Plugin; +import sonia.scm.plugin.InstalledPluginDescriptor; import sonia.scm.plugin.PluginException; import sonia.scm.plugin.PluginLoadException; import sonia.scm.plugin.PluginLoader; -import sonia.scm.plugin.PluginWrapper; +import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.PluginsInternal; import sonia.scm.plugin.SmpArchive; import sonia.scm.util.IOUtil; @@ -43,7 +43,7 @@ public final class PluginBootstrap { private final ClassLoaderLifeCycle classLoaderLifeCycle; private final ServletContext servletContext; - private final Set<PluginWrapper> plugins; + private final Set<InstalledPlugin> plugins; private final PluginLoader pluginLoader; PluginBootstrap(ServletContext servletContext, ClassLoaderLifeCycle classLoaderLifeCycle) { @@ -58,7 +58,7 @@ public final class PluginBootstrap { return pluginLoader; } - public Set<PluginWrapper> getPlugins() { + public Set<InstalledPlugin> getPlugins() { return plugins; } @@ -66,7 +66,7 @@ public final class PluginBootstrap { return new DefaultPluginLoader(servletContext, classLoaderLifeCycle.getBootstrapClassLoader(), plugins); } - private Set<PluginWrapper> collectPlugins() { + private Set<InstalledPlugin> collectPlugins() { try { File pluginDirectory = getPluginDirectory(); @@ -105,7 +105,7 @@ public final class PluginBootstrap { PluginIndexEntry entry) throws IOException { URL url = context.getResource(PLUGIN_DIRECTORY.concat(entry.getName())); SmpArchive archive = SmpArchive.create(url); - Plugin plugin = archive.getPlugin(); + InstalledPluginDescriptor plugin = archive.getPlugin(); File directory = PluginsInternal.createPluginDirectory(pluginDirectory, plugin); File checksumFile = PluginsInternal.getChecksumFile(directory); diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java index 7ece64f719..cd34d03b50 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java @@ -50,8 +50,10 @@ import sonia.scm.cache.CacheManager; import sonia.scm.cache.GuavaCacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; +import sonia.scm.group.DefaultGroupCollector; import sonia.scm.group.DefaultGroupDisplayManager; import sonia.scm.group.DefaultGroupManager; +import sonia.scm.group.GroupCollector; import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupDisplayManager; import sonia.scm.group.GroupManager; @@ -195,7 +197,7 @@ class ScmServletModule extends ServletModule { bindDecorated(GroupManager.class, DefaultGroupManager.class, GroupManagerProvider.class); bind(GroupDisplayManager.class, DefaultGroupDisplayManager.class); - + bind(GroupCollector.class, DefaultGroupCollector.class); bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class); // bind sslcontext provider diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java index cc3ef01c56..5612b0395c 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java @@ -85,7 +85,7 @@ public class DefaultPluginLoader implements PluginLoader * @param installedPlugins */ public DefaultPluginLoader(ServletContext servletContext, ClassLoader parent, - Set<PluginWrapper> installedPlugins) + Set<InstalledPlugin> installedPlugins) { this.installedPlugins = installedPlugins; this.uberClassLoader = new UberClassLoader(parent, installedPlugins); @@ -95,7 +95,7 @@ public class DefaultPluginLoader implements PluginLoader try { JAXBContext context = JAXBContext.newInstance(ScmModule.class, - Plugin.class); + InstalledPluginDescriptor.class); modules = getInstalled(parent, context, PATH_MODULECONFIG); @@ -141,7 +141,7 @@ public class DefaultPluginLoader implements PluginLoader * @return */ @Override - public Collection<PluginWrapper> getInstalledPlugins() + public Collection<InstalledPlugin> getInstalledPlugins() { return installedPlugins; } @@ -178,7 +178,7 @@ public class DefaultPluginLoader implements PluginLoader * * @return */ - private Iterable<Plugin> unwrap() + private Iterable<InstalledPluginDescriptor> unwrap() { return PluginsInternal.unwrap(installedPlugins); } @@ -227,7 +227,7 @@ public class DefaultPluginLoader implements PluginLoader private final ExtensionProcessor extensionProcessor; /** Field description */ - private final Set<PluginWrapper> installedPlugins; + private final Set<InstalledPlugin> installedPlugins; /** Field description */ private final Set<ScmModule> modules; diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index ed1f691988..807eaac317 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -35,708 +35,164 @@ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- -import com.github.legman.Subscribe; - -import com.google.common.base.Predicate; -import com.google.common.io.Files; -import com.google.inject.Inject; +import com.google.common.collect.ImmutableList; import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import sonia.scm.SCMContextProvider; -import sonia.scm.cache.Cache; -import sonia.scm.cache.CacheManager; -import sonia.scm.config.ScmConfiguration; -import sonia.scm.config.ScmConfigurationChangedEvent; -import sonia.scm.io.ZipUnArchiver; -import sonia.scm.util.AssertUtil; -import sonia.scm.util.IOUtil; -import sonia.scm.util.SystemUtil; -import sonia.scm.util.Util; -import sonia.scm.version.Version; +import sonia.scm.NotFoundException; +import sonia.scm.event.ScmEventBus; +import sonia.scm.lifecycle.RestartEvent; //~--- JDK imports ------------------------------------------------------------ - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; - -import java.net.URLEncoder; - -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.Map; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; -import javax.xml.bind.JAXB; - -import sonia.scm.net.ahc.AdvancedHttpClient; +import static sonia.scm.ContextEntry.ContextBuilder.entity; /** - * TODO replace aether stuff. - * TODO check AdvancedPluginConfiguration from 1.x * * @author Sebastian Sdorra */ @Singleton -public class DefaultPluginManager implements PluginManager -{ +public class DefaultPluginManager implements PluginManager { - /** Field description */ - public static final String CACHE_NAME = "sonia.cache.plugins"; + private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginManager.class); - /** Field description */ - public static final String ENCODING = "UTF-8"; + private final ScmEventBus eventBus; + private final PluginLoader loader; + private final PluginCenter center; + private final PluginInstaller installer; + private final List<PendingPluginInstallation> pendingQueue = new ArrayList<>(); - /** the logger for DefaultPluginManager */ - private static final Logger logger = - LoggerFactory.getLogger(DefaultPluginManager.class); - - /** enable or disable remote plugins */ - private static final boolean REMOTE_PLUGINS_ENABLED = false; - - /** Field description */ - public static final Predicate<PluginInformation> FILTER_UPDATES = - new StatePluginPredicate(PluginState.UPDATE_AVAILABLE); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * @param context - * @param configuration - * @param pluginLoader - * @param cacheManager - * @param httpClient - */ @Inject - public DefaultPluginManager(SCMContextProvider context, - ScmConfiguration configuration, PluginLoader pluginLoader, - CacheManager cacheManager, AdvancedHttpClient httpClient) - { - this.context = context; - this.configuration = configuration; - this.cache = cacheManager.getCache(CACHE_NAME); - this.httpClient = httpClient; - installedPlugins = new HashMap<>(); - - for (PluginWrapper wrapper : pluginLoader.getInstalledPlugins()) - { - Plugin plugin = wrapper.getPlugin(); - PluginInformation info = plugin.getInformation(); - - if ((info != null) && info.isValid()) - { - installedPlugins.put(info.getId(), plugin); - } - } + public DefaultPluginManager(ScmEventBus eventBus, PluginLoader loader, PluginCenter center, PluginInstaller installer) { + this.eventBus = eventBus; + this.loader = loader; + this.center = center; + this.installer = installer; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ @Override - public void clearCache() - { - if (logger.isDebugEnabled()) - { - logger.debug("clear plugin cache"); - } - - cache.clear(); + public Optional<AvailablePlugin> getAvailable(String name) { + PluginPermissions.read().check(); + return center.getAvailable() + .stream() + .filter(filterByName(name)) + .filter(this::isNotInstalled) + .map(p -> getPending(name).orElse(p)) + .findFirst(); } - /** - * Method description - * - * - * @param config - */ - @Subscribe - public void configChanged(ScmConfigurationChangedEvent config) - { - clearCache(); + private Optional<AvailablePlugin> getPending(String name) { + return pendingQueue + .stream() + .map(PendingPluginInstallation::getPlugin) + .filter(filterByName(name)) + .findFirst(); } - /** - * Method description - * - * - * @param id - */ @Override - public void install(String id) - { + public Optional<InstalledPlugin> getInstalled(String name) { + PluginPermissions.read().check(); + return loader.getInstalledPlugins() + .stream() + .filter(filterByName(name)) + .findFirst(); + } + + @Override + public List<InstalledPlugin> getInstalled() { + PluginPermissions.read().check(); + return ImmutableList.copyOf(loader.getInstalledPlugins()); + } + + @Override + public List<AvailablePlugin> getAvailable() { + PluginPermissions.read().check(); + return center.getAvailable() + .stream() + .filter(this::isNotInstalled) + .map(p -> getPending(p.getDescriptor().getInformation().getName()).orElse(p)) + .collect(Collectors.toList()); + } + + private <T extends Plugin> Predicate<T> filterByName(String name) { + return plugin -> name.equals(plugin.getDescriptor().getInformation().getName()); + } + + private boolean isNotInstalled(AvailablePlugin availablePlugin) { + return !getInstalled(availablePlugin.getDescriptor().getInformation().getName()).isPresent(); + } + + @Override + public void install(String name, boolean restartAfterInstallation) { PluginPermissions.manage().check(); - - PluginCenter center = getPluginCenter(); + List<AvailablePlugin> plugins = collectPluginsToInstall(name); + List<PendingPluginInstallation> pendingInstallations = new ArrayList<>(); + for (AvailablePlugin plugin : plugins) { + try { + PendingPluginInstallation pending = installer.install(plugin); + pendingInstallations.add(pending); + } catch (PluginInstallException ex) { + cancelPending(pendingInstallations); + throw ex; + } + } - // pluginHandler.install(id); - - for (PluginInformation plugin : center.getPlugins()) - { - String pluginId = plugin.getId(); - - if (Util.isNotEmpty(pluginId) && pluginId.equals(id)) - { - plugin.setState(PluginState.INSTALLED); - - // ugly workaround - Plugin newPlugin = new Plugin(); - - // TODO check - // newPlugin.setInformation(plugin); - installedPlugins.put(id, newPlugin); + if (!pendingInstallations.isEmpty()) { + if (restartAfterInstallation) { + restart("plugin installation"); + } else { + pendingQueue.addAll(pendingInstallations); } } } - /** - * Method description - * - * - * @param packageStream - * - * @throws IOException - */ @Override - public void installPackage(InputStream packageStream) throws IOException - { + public void installPendingAndRestart() { PluginPermissions.manage().check(); - - File tempDirectory = Files.createTempDir(); - - try - { - new ZipUnArchiver().extractArchive(packageStream, tempDirectory); - - Plugin plugin = JAXB.unmarshal(new File(tempDirectory, "plugin.xml"), - Plugin.class); - - PluginCondition condition = plugin.getCondition(); - - if ((condition != null) &&!condition.isSupported()) - { - throw new PluginConditionFailedException(condition); - } - - /* - * AetherPluginHandler aph = new AetherPluginHandler(this, context, - * configuration); - * Collection<PluginRepository> repositories = - * Sets.newHashSet(new PluginRepository("package-repository", - * "file://".concat(tempDirectory.getAbsolutePath()))); - * - * aph.setPluginRepositories(repositories); - * - * aph.install(plugin.getInformation().getId()); - */ - plugin.getInformation().setState(PluginState.INSTALLED); - installedPlugins.put(plugin.getInformation().getId(), plugin); - - } - finally - { - IOUtil.delete(tempDirectory); + if (!pendingQueue.isEmpty()) { + restart("install pending plugins"); } } - /** - * Method description - * - * - * @param id - */ - @Override - public void uninstall(String id) - { - PluginPermissions.manage().check(); + private void restart(String cause) { + eventBus.post(new RestartEvent(PluginManager.class, cause)); + } - Plugin plugin = installedPlugins.get(id); + private void cancelPending(List<PendingPluginInstallation> pendingInstallations) { + pendingInstallations.forEach(PendingPluginInstallation::cancel); + } - if (plugin == null) - { - String pluginPrefix = getPluginIdPrefix(id); + private List<AvailablePlugin> collectPluginsToInstall(String name) { + List<AvailablePlugin> plugins = new ArrayList<>(); + collectPluginsToInstall(plugins, name); + return plugins; + } - for (String nid : installedPlugins.keySet()) - { - if (nid.startsWith(pluginPrefix)) - { - id = nid; - plugin = installedPlugins.get(nid); + private boolean isInstalledOrPending(String name) { + return getInstalled(name).isPresent() || getPending(name).isPresent(); + } - break; + private void collectPluginsToInstall(List<AvailablePlugin> plugins, String name) { + if (!isInstalledOrPending(name)) { + AvailablePlugin plugin = getAvailable(name).orElseThrow(() -> NotFoundException.notFound(entity(AvailablePlugin.class, name))); + + Set<String> dependencies = plugin.getDescriptor().getDependencies(); + if (dependencies != null) { + for (String dependency: dependencies){ + collectPluginsToInstall(plugins, dependency); } } - } - if (plugin == null) - { - throw new PluginNotInstalledException(id.concat(" is not install")); - } - - /* - * if (pluginHandler == null) - * { - * getPluginCenter(); - * } - * - * pluginHandler.uninstall(id); - */ - installedPlugins.remove(id); - preparePlugins(getPluginCenter()); - } - - /** - * Method description - * - * - * @param id - */ - @Override - public void update(String id) - { - PluginPermissions.manage().check(); - - String[] idParts = id.split(":"); - String groupId = idParts[0]; - String artefactId = idParts[1]; - PluginInformation installed = null; - - for (PluginInformation info : getInstalled()) - { - if (groupId.equals(info.getGroupId()) - && artefactId.equals(info.getArtifactId())) - { - installed = info; - - break; - } - } - - if (installed == null) - { - StringBuilder msg = new StringBuilder(groupId); - - msg.append(":").append(groupId).append(" is not install"); - - throw new PluginNotInstalledException(msg.toString()); - } - - uninstall(installed.getId()); - install(id); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param id - * - * @return - */ - @Override - public PluginInformation get(String id) - { - PluginPermissions.read().check(); - - PluginInformation result = null; - - for (PluginInformation info : getPluginCenter().getPlugins()) - { - if (id.equals(info.getId())) - { - result = info; - - break; - } - } - - return result; - } - - /** - * Method description - * - * - * @param predicate - * - * @return - */ - @Override - public Set<PluginInformation> get(Predicate<PluginInformation> predicate) - { - AssertUtil.assertIsNotNull(predicate); - PluginPermissions.read().check(); - - Set<PluginInformation> infoSet = new HashSet<>(); - - filter(infoSet, getInstalled(), predicate); - filter(infoSet, getPluginCenter().getPlugins(), predicate); - - return infoSet; - } - - /** - * Method description - * - * - * @return - */ - @Override - public Collection<PluginInformation> getAll() - { - PluginPermissions.read().check(); - - Set<PluginInformation> infoSet = getInstalled(); - - infoSet.addAll(getPluginCenter().getPlugins()); - - return infoSet; - } - - /** - * Method description - * - * - * @return - */ - @Override - public Collection<PluginInformation> getAvailable() - { - PluginPermissions.read().check(); - - Set<PluginInformation> availablePlugins = new HashSet<>(); - Set<PluginInformation> centerPlugins = getPluginCenter().getPlugins(); - - for (PluginInformation info : centerPlugins) - { - if (!installedPlugins.containsKey(info.getId())) - { - availablePlugins.add(info); - } - } - - return availablePlugins; - } - - /** - * Method description - * - * - * @return - */ - @Override - public Set<PluginInformation> getAvailableUpdates() - { - PluginPermissions.read().check(); - - return get(FILTER_UPDATES); - } - - /** - * Method description - * - * - * @return - */ - @Override - public Set<PluginInformation> getInstalled() - { - PluginPermissions.read().check(); - - Set<PluginInformation> infoSet = new LinkedHashSet<>(); - - for (Plugin plugin : installedPlugins.values()) - { - infoSet.add(plugin.getInformation()); - } - - return infoSet; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param url - * @return - */ - private String buildPluginUrl(String url) - { - String os = SystemUtil.getOS(); - String arch = SystemUtil.getArch(); - - try - { - os = URLEncoder.encode(os, ENCODING); - } - catch (UnsupportedEncodingException ex) - { - logger.error(ex.getMessage(), ex); - } - - return url.replace("{version}", context.getVersion()).replace("{os}", - os).replace("{arch}", arch); - } - - /** - * Method description - * - * - * @param target - * @param source - * @param predicate - */ - private void filter(Set<PluginInformation> target, - Collection<PluginInformation> source, - Predicate<PluginInformation> predicate) - { - for (PluginInformation info : source) - { - if (predicate.apply(info)) - { - target.add(info); - } + plugins.add(plugin); + } else { + LOG.info("plugin {} is already installed or installation is pending, skipping installation", name); } } - - /** - * Method description - * - * - * @param available - */ - private void preparePlugin(PluginInformation available) - { - PluginState state = PluginState.AVAILABLE; - - for (PluginInformation installed : getInstalled()) - { - if (isSamePlugin(available, installed)) - { - if (installed.getVersion().equals(available.getVersion())) - { - state = PluginState.INSTALLED; - } - else if (isNewer(available, installed)) - { - state = PluginState.UPDATE_AVAILABLE; - } - else - { - state = PluginState.NEWER_VERSION_INSTALLED; - } - - break; - } - } - - available.setState(state); - } - - /** - * Method description - * - * - * @param pc - */ - private void preparePlugins(PluginCenter pc) - { - Set<PluginInformation> infoSet = pc.getPlugins(); - - if (infoSet != null) - { - Iterator<PluginInformation> pit = infoSet.iterator(); - - while (pit.hasNext()) - { - PluginInformation available = pit.next(); - - if (isCorePluging(available)) - { - pit.remove(); - } - else - { - preparePlugin(available); - } - } - } - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - private PluginCenter getPluginCenter() - { - PluginCenter center = cache.get(PluginCenter.class.getName()); - - if (center == null) - { - synchronized (DefaultPluginManager.class) - { - String pluginUrl = configuration.getPluginUrl(); - - pluginUrl = buildPluginUrl(pluginUrl); - - if (logger.isInfoEnabled()) - { - logger.info("fetch plugin informations from {}", pluginUrl); - } - - /** - * remote plugins are disabled for early 2.0.0-SNAPSHOTS - * TODO enable remote plugins later - */ - if (REMOTE_PLUGINS_ENABLED && Util.isNotEmpty(pluginUrl)) - { - try - { - center = httpClient.get(pluginUrl).request().contentFromXml(PluginCenter.class); - preparePlugins(center); - cache.put(PluginCenter.class.getName(), center); - - /* - * if (pluginHandler == null) - * { - * pluginHandler = new AetherPluginHandler(this, - * SCMContext.getContext(), configuration, - * advancedPluginConfiguration); - * } - * - * pluginHandler.setPluginRepositories(center.getRepositories()); - */ - } - catch (IOException ex) - { - logger.error("could not load plugins from plugin center", ex); - } - } - - if (center == null) - { - center = new PluginCenter(); - } - } - } - - return center; - } - - /** - * Method description - * - * - * @param pluginId - * - * @return - */ - private String getPluginIdPrefix(String pluginId) - { - return pluginId.substring(0, pluginId.lastIndexOf(':')); - } - - /** - * Method description - * - * - * @param available - * - * @return - */ - private boolean isCorePluging(PluginInformation available) - { - boolean core = false; - - for (Plugin installedPlugin : installedPlugins.values()) - { - PluginInformation installed = installedPlugin.getInformation(); - - if (isSamePlugin(available, installed) - && (installed.getState() == PluginState.CORE)) - { - core = true; - - break; - } - } - - return core; - } - - /** - * Method description - * - * - * @param available - * @param installed - * - * @return - */ - private boolean isNewer(PluginInformation available, - PluginInformation installed) - { - boolean result = false; - Version version = Version.parse(available.getVersion()); - - if (version != null) - { - result = version.isNewer(installed.getVersion()); - } - - return result; - } - - /** - * Method description - * - * - * @param p1 - * @param p2 - * - * @return - */ - private boolean isSamePlugin(PluginInformation p1, PluginInformation p2) - { - return p1.getGroupId().equals(p2.getGroupId()) - && p1.getArtifactId().equals(p2.getArtifactId()); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final Cache<String, PluginCenter> cache; - - /** Field description */ - private final AdvancedHttpClient httpClient; - - /** Field description */ - private final ScmConfiguration configuration; - - /** Field description */ - private final SCMContextProvider context; - - /** Field description */ - private final Map<String, Plugin> installedPlugins; } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java index 25b8390e53..c0500245a6 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java @@ -71,11 +71,11 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader //~--- constructors --------------------------------------------------------- - public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins) { + public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<InstalledPlugin> plugins) { this(servletContext, plugins, SCMContext.getContext().getStage()); } - public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins, Stage stage) { + public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<InstalledPlugin> plugins, Stage stage) { this.servletContext = servletContext; this.plugins = plugins; this.cache = createCache(stage); @@ -153,7 +153,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader resources.add(ctxResource); } - for (PluginWrapper wrapper : plugins) + for (InstalledPlugin wrapper : plugins) { URL resource = nonDirectory(wrapper.getWebResourceLoader().getResource(path)); @@ -205,7 +205,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader if (resource == null) { - for (PluginWrapper wrapper : plugins) + for (InstalledPlugin wrapper : plugins) { resource = nonDirectory(wrapper.getWebResourceLoader().getResource(path)); @@ -259,7 +259,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader private final Cache<String, URL> cache; /** Field description */ - private final Iterable<PluginWrapper> plugins; + private final Iterable<InstalledPlugin> plugins; /** Field description */ private final ServletContext servletContext; diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java b/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java index d1fe214f50..ee2514dc3e 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java @@ -63,7 +63,7 @@ public final class ExplodedSmp implements Comparable<ExplodedSmp> * @param path * @param plugin */ - ExplodedSmp(Path path, Plugin plugin) + ExplodedSmp(Path path, InstalledPluginDescriptor plugin) { logger.trace("create exploded scm for plugin {} and dependencies {}", plugin.getInformation().getName(), plugin.getDependencies()); this.path = path; @@ -115,8 +115,8 @@ public final class ExplodedSmp implements Comparable<ExplodedSmp> } else { - String id = plugin.getInformation().getId(false); - String oid = o.plugin.getInformation().getId(false); + String id = plugin.getInformation().getName(false); + String oid = o.plugin.getInformation().getName(false); if (depends.contains(oid) && odepends.contains(id)) { @@ -163,7 +163,7 @@ public final class ExplodedSmp implements Comparable<ExplodedSmp> * * @return plugin descriptor */ - public Plugin getPlugin() + public InstalledPluginDescriptor getPlugin() { return plugin; } @@ -202,5 +202,5 @@ public final class ExplodedSmp implements Comparable<ExplodedSmp> private final Path path; /** plugin object */ - private final Plugin plugin; + private final InstalledPluginDescriptor plugin; } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/OverviewPluginPredicate.java b/scm-webapp/src/main/java/sonia/scm/plugin/OverviewPluginPredicate.java deleted file mode 100644 index b242813e0d..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/plugin/OverviewPluginPredicate.java +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -import com.google.common.base.Predicate; - -/** - * - * @author Sebastian Sdorra - */ -public class OverviewPluginPredicate implements Predicate<PluginInformation> -{ - - /** Field description */ - public static final OverviewPluginPredicate INSTANCE = - new OverviewPluginPredicate(); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param plugin - * - * @return - */ - @Override - public boolean apply(PluginInformation plugin) - { - return plugin.getState() != PluginState.NEWER_VERSION_INSTALLED; - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginInstallation.java b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginInstallation.java new file mode 100644 index 0000000000..fa59930a78 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginInstallation.java @@ -0,0 +1,35 @@ +package sonia.scm.plugin; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +class PendingPluginInstallation { + + private static final Logger LOG = LoggerFactory.getLogger(PendingPluginInstallation.class); + + private final AvailablePlugin plugin; + private final Path file; + + PendingPluginInstallation(AvailablePlugin plugin, Path file) { + this.plugin = plugin; + this.file = file; + } + + public AvailablePlugin getPlugin() { + return plugin; + } + + void cancel() { + String name = plugin.getDescriptor().getInformation().getName(); + LOG.info("cancel installation of plugin {}", name); + try { + Files.delete(file); + } catch (IOException ex) { + throw new PluginFailedToCancelInstallationException("failed to cancel installation of plugin " + name, ex); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java new file mode 100644 index 0000000000..a3817eba0f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java @@ -0,0 +1,55 @@ +package sonia.scm.plugin; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.cache.Cache; +import sonia.scm.cache.CacheManager; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.util.HttpUtil; +import sonia.scm.util.SystemUtil; + +import javax.inject.Inject; +import java.util.Set; + +public class PluginCenter { + + private static final String CACHE_NAME = "sonia.cache.plugins"; + + private static final Logger LOG = LoggerFactory.getLogger(PluginCenter.class); + + private final SCMContextProvider context; + private final ScmConfiguration configuration; + private final PluginCenterLoader loader; + private final Cache<String, Set<AvailablePlugin>> cache; + + @Inject + public PluginCenter(SCMContextProvider context, CacheManager cacheManager, ScmConfiguration configuration, PluginCenterLoader loader) { + this.context = context; + this.configuration = configuration; + this.loader = loader; + this.cache = cacheManager.getCache(CACHE_NAME); + } + + synchronized Set<AvailablePlugin> getAvailable() { + String url = buildPluginUrl(configuration.getPluginUrl()); + Set<AvailablePlugin> plugins = cache.get(url); + if (plugins == null) { + LOG.debug("no cached available plugins found, start fetching"); + plugins = loader.load(url); + cache.put(url, plugins); + } else { + LOG.debug("return available plugins from cache"); + } + return plugins; + } + + private String buildPluginUrl(String url) { + String os = HttpUtil.encode(SystemUtil.getOS()); + String arch = SystemUtil.getArch(); + return url.replace("{version}", context.getVersion()) + .replace("{os}", os) + .replace("{arch}", arch); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDto.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDto.java new file mode 100644 index 0000000000..1a18d696d5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDto.java @@ -0,0 +1,86 @@ +package sonia.scm.plugin; + +import com.google.common.collect.ImmutableList; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@XmlRootElement +@XmlAccessorType(XmlAccessType.FIELD) +public final class PluginCenterDto implements Serializable { + + @XmlElement(name = "_embedded") + private Embedded embedded; + + public Embedded getEmbedded() { + return embedded; + } + + @XmlRootElement(name = "_embedded") + @XmlAccessorType(XmlAccessType.FIELD) + public static class Embedded { + + @XmlElement(name = "plugins") + private List<Plugin> plugins; + + public List<Plugin> getPlugins() { + if (plugins == null) { + plugins = ImmutableList.of(); + } + return plugins; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "plugins") + @Getter + @AllArgsConstructor + public static class Plugin { + + private String name; + private String version; + private String displayName; + private String description; + private String category; + private String author; + private String avatarUrl; + private String sha256; + + @XmlElement(name = "conditions") + private Condition conditions; + + @XmlElement(name = "dependencies") + private Set<String> dependencies; + + @XmlElement(name = "_links") + private Map<String, Link> links; + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "conditions") + @Getter + @AllArgsConstructor + public static class Condition { + + private List<String> os; + private String arch; + private String minVersion; + } + + @XmlAccessorType(XmlAccessType.FIELD) + @Getter + @NoArgsConstructor + @AllArgsConstructor + static class Link { + private String href; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java new file mode 100644 index 0000000000..1b84bca147 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java @@ -0,0 +1,28 @@ +package sonia.scm.plugin; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.HashSet; +import java.util.Set; + +@Mapper +public abstract class PluginCenterDtoMapper { + + static final PluginCenterDtoMapper INSTANCE = Mappers.getMapper(PluginCenterDtoMapper.class); + + abstract PluginInformation map(PluginCenterDto.Plugin plugin); + abstract PluginCondition map(PluginCenterDto.Condition condition); + + Set<AvailablePlugin> map(PluginCenterDto pluginCenterDto) { + Set<AvailablePlugin> plugins = new HashSet<>(); + for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) { + String url = plugin.getLinks().get("download").getHref(); + AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor( + map(plugin), map(plugin.getConditions()), plugin.getDependencies(), url, plugin.getSha256() + ); + plugins.add(new AvailablePlugin(descriptor)); + } + return plugins; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java new file mode 100644 index 0000000000..2b0928891e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java @@ -0,0 +1,42 @@ +package sonia.scm.plugin; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.net.ahc.AdvancedHttpClient; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +class PluginCenterLoader { + + private static final Logger LOG = LoggerFactory.getLogger(PluginCenterLoader.class); + + private final AdvancedHttpClient client; + private final PluginCenterDtoMapper mapper; + + @Inject + public PluginCenterLoader(AdvancedHttpClient client) { + this(client, PluginCenterDtoMapper.INSTANCE); + } + + @VisibleForTesting + PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper) { + this.client = client; + this.mapper = mapper; + } + + Set<AvailablePlugin> load(String url) { + try { + LOG.info("fetch plugins from {}", url); + PluginCenterDto pluginCenterDto = client.get(url).request().contentFromJson(PluginCenterDto.class); + return mapper.map(pluginCenterDto); + } catch (IOException ex) { + LOG.error("failed to load plugins from plugin center, returning empty list"); + return Collections.emptySet(); + } + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginChecksumMismatchException.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginChecksumMismatchException.java new file mode 100644 index 0000000000..1b04c0adf0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginChecksumMismatchException.java @@ -0,0 +1,7 @@ +package sonia.scm.plugin; + +public class PluginChecksumMismatchException extends PluginInstallException { + public PluginChecksumMismatchException(String message) { + super(message); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginDownloadException.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDownloadException.java new file mode 100644 index 0000000000..cb2a119f62 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDownloadException.java @@ -0,0 +1,7 @@ +package sonia.scm.plugin; + +public class PluginDownloadException extends PluginInstallException { + public PluginDownloadException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginFailedToCancelInstallationException.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginFailedToCancelInstallationException.java new file mode 100644 index 0000000000..e3d6c123d6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginFailedToCancelInstallationException.java @@ -0,0 +1,7 @@ +package sonia.scm.plugin; + +public class PluginFailedToCancelInstallationException extends RuntimeException { + public PluginFailedToCancelInstallationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallException.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallException.java new file mode 100644 index 0000000000..d7a840bdc1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallException.java @@ -0,0 +1,12 @@ +package sonia.scm.plugin; + +public class PluginInstallException extends RuntimeException { + + public PluginInstallException(String message) { + super(message); + } + + public PluginInstallException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java new file mode 100644 index 0000000000..6f003c1e31 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java @@ -0,0 +1,76 @@ +package sonia.scm.plugin; + +import com.google.common.hash.HashCode; +import com.google.common.hash.Hashing; +import com.google.common.hash.HashingInputStream; +import sonia.scm.SCMContextProvider; +import sonia.scm.net.ahc.AdvancedHttpClient; + +import javax.inject.Inject; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +@SuppressWarnings("UnstableApiUsage") // guava hash is marked as unstable +class PluginInstaller { + + private final SCMContextProvider context; + private final AdvancedHttpClient client; + + @Inject + public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client) { + this.context = context; + this.client = client; + } + + @SuppressWarnings("squid:S4790") // hashing should be safe + public PendingPluginInstallation install(AvailablePlugin plugin) { + Path file = null; + try (HashingInputStream input = new HashingInputStream(Hashing.sha256(), download(plugin))) { + file = createFile(plugin); + Files.copy(input, file); + + verifyChecksum(plugin, input.hash(), file); + return new PendingPluginInstallation(plugin.install(), file); + } catch (IOException ex) { + cleanup(file); + throw new PluginDownloadException("failed to download plugin", ex); + } + } + + private void cleanup(Path file) { + try { + if (file != null) { + Files.deleteIfExists(file); + } + } catch (IOException e) { + throw new PluginInstallException("failed to cleanup, after broken installation"); + } + } + + private void verifyChecksum(AvailablePlugin plugin, HashCode hash, Path file) { + Optional<String> checksum = plugin.getDescriptor().getChecksum(); + if (checksum.isPresent()) { + String calculatedChecksum = hash.toString(); + if (!checksum.get().equalsIgnoreCase(calculatedChecksum)) { + cleanup(file); + throw new PluginChecksumMismatchException( + String.format("downloaded plugin checksum %s does not match expected %s", calculatedChecksum, checksum.get()) + ); + } + } + } + + private InputStream download(AvailablePlugin plugin) throws IOException { + return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream(); + } + + private Path createFile(AvailablePlugin plugin) throws IOException { + Path directory = context.resolve(Paths.get("plugins")); + Files.createDirectories(directory); + return directory.resolve(plugin.getDescriptor().getInformation().getName() + ".smp"); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginNode.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginNode.java index e28ccff2ff..94a048770f 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginNode.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginNode.java @@ -126,7 +126,7 @@ public final class PluginNode */ public String getId() { - return plugin.getPlugin().getInformation().getId(false); + return plugin.getPlugin().getInformation().getName(false); } /** @@ -157,7 +157,7 @@ public final class PluginNode * * @return */ - public PluginWrapper getWrapper() + public InstalledPlugin getWrapper() { return wrapper; } @@ -170,7 +170,7 @@ public final class PluginNode * * @param wrapper */ - public void setWrapper(PluginWrapper wrapper) + public void setWrapper(InstalledPlugin wrapper) { this.wrapper = wrapper; } @@ -192,5 +192,5 @@ public final class PluginNode private final ExplodedSmp plugin; /** Field description */ - private PluginWrapper wrapper; + private InstalledPlugin wrapper; } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java index c7d669ee63..fced7a01ed 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -59,6 +59,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; +import java.util.function.Predicate; + +import static java.util.stream.Collectors.toList; //~--- JDK imports ------------------------------------------------------------ @@ -68,6 +71,7 @@ import java.util.Set; * * TODO don't mix nio and io */ +@SuppressWarnings("squid:S3725") // performance is not critical, for this type of checks public final class PluginProcessor { @@ -123,7 +127,7 @@ public final class PluginProcessor try { - this.context = JAXBContext.newInstance(Plugin.class); + this.context = JAXBContext.newInstance(InstalledPluginDescriptor.class); } catch (JAXBException ex) { @@ -160,7 +164,7 @@ public final class PluginProcessor * * @throws IOException */ - public Set<PluginWrapper> collectPlugins(ClassLoader classLoader) + public Set<InstalledPlugin> collectPlugins(ClassLoader classLoader) throws IOException { logger.info("collect plugins"); @@ -171,7 +175,11 @@ public final class PluginProcessor extract(archives); - List<Path> dirs = collectPluginDirectories(pluginDirectory); + List<Path> dirs = + collectPluginDirectories(pluginDirectory) + .stream() + .filter(isPluginDirectory()) + .collect(toList()); logger.debug("process {} directories: {}", dirs.size(), dirs); @@ -187,13 +195,17 @@ public final class PluginProcessor logger.trace("create plugin wrappers and build classloaders"); - Set<PluginWrapper> wrappers = createPluginWrappers(classLoader, rootNodes); + Set<InstalledPlugin> wrappers = createPluginWrappers(classLoader, rootNodes); logger.debug("collected {} plugins", wrappers.size()); return ImmutableSet.copyOf(wrappers); } + private Predicate<Path> isPluginDirectory() { + return dir -> Files.exists(dir.resolve(DIRECTORY_METAINF).resolve("scm").resolve("plugin.xml")); + } + /** * Method description * @@ -204,7 +216,7 @@ public final class PluginProcessor * * @throws IOException */ - private void appendPluginWrapper(Set<PluginWrapper> plugins, + private void appendPluginWrapper(Set<InstalledPlugin> plugins, ClassLoader classLoader, PluginNode node) throws IOException { @@ -217,7 +229,7 @@ public final class PluginProcessor for (PluginNode parent : node.getParents()) { - PluginWrapper wrapper = parent.getWrapper(); + InstalledPlugin wrapper = parent.getWrapper(); if (wrapper != null) { @@ -236,8 +248,8 @@ public final class PluginProcessor } - PluginWrapper plugin = - createPluginWrapper(createParentPluginClassLoader(classLoader, parents), + InstalledPlugin plugin = + createPlugin(createParentPluginClassLoader(classLoader, parents), smp); if (plugin != null) @@ -257,7 +269,7 @@ public final class PluginProcessor * * @throws IOException */ - private void appendPluginWrappers(Set<PluginWrapper> plugins, + private void appendPluginWrappers(Set<InstalledPlugin> plugins, ClassLoader classLoader, List<PluginNode> nodes) throws IOException { @@ -318,10 +330,7 @@ public final class PluginProcessor { for (Path parent : parentStream) { - try (DirectoryStream<Path> direcotries = stream(parent, filter)) - { - paths.addAll(direcotries); - } + paths.add(parent); } } @@ -333,7 +342,6 @@ public final class PluginProcessor * * * @param parentClassLoader - * @param directory * @param smp * * @return @@ -375,9 +383,9 @@ public final class PluginProcessor ClassLoader classLoader; URL[] urlArray = urls.toArray(new URL[urls.size()]); - Plugin plugin = smp.getPlugin(); + InstalledPluginDescriptor plugin = smp.getPlugin(); - String id = plugin.getInformation().getId(false); + String id = plugin.getInformation().getName(false); if (smp.getPlugin().isChildFirstClassLoader()) { @@ -435,74 +443,36 @@ public final class PluginProcessor return result; } - /** - * Method description - * - * - * - * @param classLoader - * @param descriptor - * - * @return - */ - private Plugin createPlugin(ClassLoader classLoader, Path descriptor) - { + private InstalledPluginDescriptor createDescriptor(ClassLoader classLoader, Path descriptor) { ClassLoader ctxcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(classLoader); - - try - { - return (Plugin) context.createUnmarshaller().unmarshal( - descriptor.toFile()); - } - catch (JAXBException ex) - { - throw new PluginLoadException( - "could not load plugin desriptor ".concat(descriptor.toString()), ex); - } - finally - { + try { + return (InstalledPluginDescriptor) context.createUnmarshaller().unmarshal(descriptor.toFile()); + } catch (JAXBException ex) { + throw new PluginLoadException("could not load plugin desriptor ".concat(descriptor.toString()), ex); + } finally { Thread.currentThread().setContextClassLoader(ctxcl); } } - /** - * Method description - * - * - * @param classLoader - * @param directory - * @param smp - * - * @return - * - * @throws IOException - */ - private PluginWrapper createPluginWrapper(ClassLoader classLoader, - ExplodedSmp smp) - throws IOException - { - PluginWrapper wrapper = null; + private InstalledPlugin createPlugin(ClassLoader classLoader, ExplodedSmp smp) throws IOException { + InstalledPlugin plugin = null; Path directory = smp.getPath(); - Path descriptor = directory.resolve(PluginConstants.FILE_DESCRIPTOR); + Path descriptorPath = directory.resolve(PluginConstants.FILE_DESCRIPTOR); - if (Files.exists(descriptor)) - { + if (Files.exists(descriptorPath)) { ClassLoader cl = createClassLoader(classLoader, smp); - Plugin plugin = createPlugin(cl, descriptor); + InstalledPluginDescriptor descriptor = createDescriptor(cl, descriptorPath); WebResourceLoader resourceLoader = createWebResourceLoader(directory); - wrapper = new PluginWrapper(plugin, cl, resourceLoader, directory); - } - else - { + plugin = new InstalledPlugin(descriptor, cl, resourceLoader, directory); + } else { logger.warn("found plugin directory without plugin descriptor"); } - return wrapper; + return plugin; } /** @@ -511,18 +481,17 @@ public final class PluginProcessor * * * @param classLoader - * @param smps * @param rootNodes * * @return * * @throws IOException */ - private Set<PluginWrapper> createPluginWrappers(ClassLoader classLoader, - List<PluginNode> rootNodes) + private Set<InstalledPlugin> createPluginWrappers(ClassLoader classLoader, + List<PluginNode> rootNodes) throws IOException { - Set<PluginWrapper> plugins = Sets.newHashSet(); + Set<InstalledPlugin> plugins = Sets.newHashSet(); appendPluginWrappers(plugins, classLoader, rootNodes); diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java index 7e57fb3d57..bd338c0741 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java @@ -86,7 +86,7 @@ public final class PluginTree for (ExplodedSmp smp : smpOrdered) { - Plugin plugin = smp.getPlugin(); + InstalledPluginDescriptor plugin = smp.getPlugin(); if (plugin.getScmVersion() != SCM_VERSION) { diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginsInternal.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginsInternal.java index 52d192da32..242086aa85 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginsInternal.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginsInternal.java @@ -87,8 +87,8 @@ public final class PluginsInternal * * @throws IOException */ - public static Set<PluginWrapper> collectPlugins(ClassLoaderLifeCycle classLoaderLifeCycle, - Path directory) + public static Set<InstalledPlugin> collectPlugins(ClassLoaderLifeCycle classLoaderLifeCycle, + Path directory) throws IOException { PluginProcessor processor = new PluginProcessor(classLoaderLifeCycle, directory); @@ -105,11 +105,11 @@ public final class PluginsInternal * * @return */ - public static File createPluginDirectory(File parent, Plugin plugin) + public static File createPluginDirectory(File parent, InstalledPluginDescriptor plugin) { PluginInformation info = plugin.getInformation(); - return new File(new File(parent, info.getGroupId()), info.getArtifactId()); + return new File(parent, info.getName()); } /** @@ -131,14 +131,14 @@ public final class PluginsInternal if (directory.exists()) { logger.debug("delete directory {} for plugin extraction", - archive.getPlugin().getInformation().getId(false)); + archive.getPlugin().getInformation().getName(false)); IOUtil.delete(directory); } IOUtil.mkdirs(directory); logger.debug("extract plugin {}", - archive.getPlugin().getInformation().getId(false)); + archive.getPlugin().getInformation().getName(false)); archive.extract(directory); Files.write(checksum, checksumFile, Charsets.UTF_8); @@ -159,7 +159,7 @@ public final class PluginsInternal * * @return */ - public static Iterable<Plugin> unwrap(Iterable<PluginWrapper> wrapped) + public static Iterable<InstalledPluginDescriptor> unwrap(Iterable<InstalledPlugin> wrapped) { return Iterables.transform(wrapped, new Unwrap()); } @@ -188,7 +188,7 @@ public final class PluginsInternal * @version Enter version here..., 14/06/05 * @author Enter your name here... */ - private static class Unwrap implements Function<PluginWrapper, Plugin> + private static class Unwrap implements Function<InstalledPlugin, InstalledPluginDescriptor> { /** @@ -200,9 +200,9 @@ public final class PluginsInternal * @return */ @Override - public Plugin apply(PluginWrapper wrapper) + public InstalledPluginDescriptor apply(InstalledPlugin wrapper) { - return wrapper.getPlugin(); + return wrapper.getDescriptor(); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/UberClassLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/UberClassLoader.java index 311cb9e879..62b1073a85 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/UberClassLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/UberClassLoader.java @@ -65,7 +65,7 @@ public final class UberClassLoader extends ClassLoader * @param parent * @param plugins */ - public UberClassLoader(ClassLoader parent, Iterable<PluginWrapper> plugins) + public UberClassLoader(ClassLoader parent, Iterable<InstalledPlugin> plugins) { super(parent); this.plugins = plugins; @@ -87,7 +87,7 @@ public final class UberClassLoader extends ClassLoader } private Class<?> findClassInPlugins(String name) throws ClassNotFoundException { - for (PluginWrapper plugin : plugins) { + for (InstalledPlugin plugin : plugins) { Class<?> clazz = findClass(plugin.getClassLoader(), name); if (clazz != null) { return clazz; @@ -119,7 +119,7 @@ public final class UberClassLoader extends ClassLoader { URL url = null; - for (PluginWrapper plugin : plugins) + for (InstalledPlugin plugin : plugins) { ClassLoader cl = plugin.getClassLoader(); @@ -149,7 +149,7 @@ public final class UberClassLoader extends ClassLoader { List<URL> urls = Lists.newArrayList(); - for (PluginWrapper plugin : plugins) + for (InstalledPlugin plugin : plugins) { ClassLoader cl = plugin.getClassLoader(); @@ -194,5 +194,5 @@ public final class UberClassLoader extends ClassLoader Maps.newConcurrentMap(); /** Field description */ - private final Iterable<PluginWrapper> plugins; + private final Iterable<InstalledPlugin> plugins; } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 1bcd877620..836d95bf42 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -172,9 +172,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { } private void preDelete(Repository toDelete) { - if (configuration.isEnableRepositoryArchive() && !toDelete.isArchived()) { - throw new RepositoryIsNotArchivedException(); - } fireEvent(HandlerEventType.BEFORE_DELETE, toDelete); getHandler(toDelete).delete(toDelete); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java index 66dbb51073..fc653efa52 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java +++ b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java @@ -48,6 +48,8 @@ import sonia.scm.user.User; import sonia.scm.user.UserEvent; import sonia.scm.user.UserModificationEvent; +import javax.inject.Singleton; + /** * Receives all kinds of events, which affects authorization relevant data and fires an * {@link AuthorizationChangedEvent} if authorization data has changed. @@ -55,6 +57,7 @@ import sonia.scm.user.UserModificationEvent; * @author Sebastian Sdorra * @since 1.52 */ +@Singleton @EagerSingleton public class AuthorizationChangedEventProducer { @@ -165,8 +168,7 @@ public class AuthorizationChangedEventProducer { } private boolean isAuthorizationDataModified(Repository repository, Repository beforeModification) { - return repository.isArchived() != beforeModification.isArchived() - || repository.isPublicReadable() != beforeModification.isPublicReadable() + return repository.isPublicReadable() != beforeModification.isPublicReadable() || !(repository.getPermissions().containsAll(beforeModification.getPermissions()) && beforeModification.getPermissions().containsAll(repository.getPermissions())); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java index b237a0a5ff..324dfe9082 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java @@ -104,7 +104,6 @@ public class BearerRealm extends AuthenticatingRealm return helper.authenticationInfoBuilder(accessToken.getSubject()) .withCredentials(bt.getCredentials()) .withScope(Scopes.fromClaims(accessToken.getClaims())) - .withGroups(accessToken.getGroups()) .build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index baff3b951c..28f61df34f 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -52,17 +52,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.Extension; -import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.user.User; import sonia.scm.user.UserPermissions; import sonia.scm.util.Util; import java.util.Collection; +import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -88,19 +89,21 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector /** * Constructs ... - * @param cacheManager + * @param cacheManager * @param repositoryDAO * @param securitySystem * @param repositoryPermissionProvider + * @param groupCollector */ @Inject public DefaultAuthorizationCollector(CacheManager cacheManager, - RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider) + RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider, GroupCollector groupCollector) { this.cache = cacheManager.getCache(CACHE_NAME); this.repositoryDAO = repositoryDAO; this.securitySystem = securitySystem; this.repositoryPermissionProvider = repositoryPermissionProvider; + this.groupCollector = groupCollector; } //~--- methods -------------------------------------------------------------- @@ -145,16 +148,16 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector Preconditions.checkNotNull(user, "no user found in principal collection"); - GroupNames groupNames = principals.oneByType(GroupNames.class); + Set<String> groups = groupCollector.collect(user.getName()); - CacheKey cacheKey = new CacheKey(user.getId(), groupNames); + CacheKey cacheKey = new CacheKey(user.getId(), groups); AuthorizationInfo info = cache.get(cacheKey); if (info == null) { logger.trace("collect AuthorizationInfo for user {}", user.getName()); - info = createAuthorizationInfo(user, groupNames); + info = createAuthorizationInfo(user, groups); cache.put(cacheKey, info); } else if (logger.isTraceEnabled()) @@ -166,7 +169,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private void collectGlobalPermissions(Builder<String> builder, - final User user, final GroupNames groups) + final User user, final Set<String> groups) { Collection<AssignedPermission> globalPermissions = securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input)); @@ -181,7 +184,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private void collectRepositoryPermissions(Builder<String> builder, User user, - GroupNames groups) + Set<String> groups) { for (Repository repository : repositoryDAO.getAll()) { @@ -190,7 +193,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private void collectRepositoryPermissions(Builder<String> builder, - Repository repository, User user, GroupNames groups) + Repository repository, User user, Set<String> groups) { Collection<RepositoryPermission> repositoryPermissions = repository.getPermissions(); @@ -245,7 +248,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector .getVerbs(); } - private AuthorizationInfo createAuthorizationInfo(User user, GroupNames groups) { + private AuthorizationInfo createAuthorizationInfo(User user, Set<String> groups) { Builder<String> builder = ImmutableSet.builder(); collectGlobalPermissions(builder, user, groups); @@ -279,7 +282,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector //~--- get methods ---------------------------------------------------------- - private boolean isUserPermitted(User user, GroupNames groups, + private boolean isUserPermitted(User user, Set<String> groups, PermissionObject perm) { //J- @@ -314,7 +317,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector */ private static class CacheKey { - private CacheKey(String username, GroupNames groupnames) + private CacheKey(String username, Set<String> groupnames) { this.username = username; this.groupnames = groupnames; @@ -356,7 +359,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector //~--- fields ------------------------------------------------------------- /** group names */ - private final GroupNames groupnames; + private final Set<String> groupnames; /** username */ private final String username; @@ -374,4 +377,5 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private final SecuritySystem securitySystem; private final RepositoryPermissionProvider repositoryPermissionProvider; + private final GroupCollector groupCollector; } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java index bacbd9b314..e65c88e679 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java @@ -34,7 +34,6 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.annotations.VisibleForTesting; - import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; @@ -45,21 +44,16 @@ import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; - -import org.apache.shiro.subject.SimplePrincipalCollection; -import sonia.scm.group.GroupNames; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.plugin.Extension; -//~--- JDK imports ------------------------------------------------------------ - import javax.inject.Inject; import javax.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * Default authorizing realm. * @@ -103,6 +97,9 @@ public class DefaultRealm extends AuthorizingRealm matcher.setPasswordService(service); setCredentialsMatcher(helper.wrapCredentialsMatcher(matcher)); setAuthenticationTokenClass(UsernamePasswordToken.class); + + // we cache in the AuthorizationCollector + setCachingEnabled(false); } //~--- methods -------------------------------------------------------------- @@ -149,7 +146,7 @@ public class DefaultRealm extends AuthorizingRealm LOG.trace("principal does not contain scope information, returning all permissions"); log(principals, info, null); } - + return info; } @@ -180,8 +177,6 @@ public class DefaultRealm extends AuthorizingRealm StringBuilder buffer = new StringBuilder("authorization summary: "); buffer.append(SEPARATOR).append("username : ").append(collection.getPrimaryPrincipal()); - buffer.append(SEPARATOR).append("groups : "); - append(buffer, collection.oneByType(GroupNames.class)); buffer.append(SEPARATOR).append("roles : "); append(buffer, original.getRoles()); buffer.append(SEPARATOR).append("scope : "); diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java index cc1d4be7a7..5a74f77502 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java @@ -40,11 +40,9 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.group.ExternalGroupNames; import java.time.Clock; import java.time.Instant; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -139,12 +137,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { return this; } - @Override - public JwtAccessTokenBuilder groups(String... groups) { - Collections.addAll(this.groups, groups); - return this; - } - JwtAccessTokenBuilder refreshExpiration(Instant refreshExpiration) { this.refreshExpiration = refreshExpiration; this.refreshableFor = 0; @@ -206,16 +198,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { claims.setIssuer(issuer); } - if (!groups.isEmpty()) { - claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups); - } else { - Subject currentSubject = SecurityUtils.getSubject(); - ExternalGroupNames externalGroupNames = currentSubject.getPrincipals().oneByType(ExternalGroupNames.class); - if (externalGroupNames != null) { - claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, externalGroupNames.getCollection().toArray(new String[]{})); - } - } - // sign token and create compact version String compact = Jwts.builder() .setClaims(claims) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index e913cead25..173dcb0638 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -7,10 +7,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.lifecycle.RestartEvent; import sonia.scm.event.ScmEventBus; -import sonia.scm.update.repository.MigrationStrategy; +import sonia.scm.lifecycle.RestartEvent; import sonia.scm.update.repository.DefaultMigrationStrategyDAO; +import sonia.scm.update.repository.MigrationStrategy; import sonia.scm.update.repository.V1Repository; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; import sonia.scm.util.ValidationUtil; diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java index 0b96a58385..be40ab3a6d 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java @@ -91,7 +91,6 @@ public class MigrateVerbsToPermissionRoles implements UpdateStep { repository.setHealthCheckFailures(oldRepository.healthCheckFailures); repository.setLastModified(oldRepository.lastModified); repository.setPublicReadable(oldRepository.publicReadable); - repository.setArchived(oldRepository.archived); return repository; } diff --git a/scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java b/scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java new file mode 100644 index 0000000000..f9b40961a3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java @@ -0,0 +1,22 @@ +package sonia.scm.web.filter; + +import sonia.scm.Priority; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.filter.Filters; +import sonia.scm.filter.WebElement; +import sonia.scm.web.UserAgentParser; +import sonia.scm.web.WebTokenGenerator; +import sonia.scm.web.protocol.HttpProtocolServlet; + +import javax.inject.Inject; +import java.util.Set; + +@Priority(Filters.PRIORITY_AUTHENTICATION) +@WebElement(value = HttpProtocolServlet.PATTERN) +public class DefaultHttpProtocolServletAuthenticationFilter extends HttpProtocolServletAuthenticationFilterBase { + + @Inject + public DefaultHttpProtocolServletAuthenticationFilter(ScmConfiguration configuration, Set<WebTokenGenerator> tokenGenerators, UserAgentParser userAgentParser) { + super(configuration, tokenGenerators, userAgentParser); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java b/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java index 0b380c8088..b62b6c63f3 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java +++ b/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java @@ -38,7 +38,6 @@ package sonia.scm.web.security; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Singleton; - import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; @@ -46,21 +45,17 @@ import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContext; -import sonia.scm.group.GroupNames; import sonia.scm.security.Role; import sonia.scm.user.User; import sonia.scm.util.AssertUtil; -//~--- JDK imports ------------------------------------------------------------ - +import javax.xml.bind.JAXB; import java.net.URL; -import javax.xml.bind.JAXB; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -161,7 +156,6 @@ public class DefaultAdministrationContext implements AdministrationContext collection.add(adminUser.getId(), REALM); collection.add(adminUser, REALM); - collection.add(new GroupNames(), REALM); collection.add(AdministrationContextMarker.MARKER, REALM); return collection; diff --git a/scm-webapp/src/main/resources/config/ehcache.xml b/scm-webapp/src/main/resources/config/ehcache.xml deleted file mode 100644 index 19abbde727..0000000000 --- a/scm-webapp/src/main/resources/config/ehcache.xml +++ /dev/null @@ -1,271 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - - Copyright (c) 2010, Sebastian Sdorra - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - 3. Neither the name of SCM-Manager; nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - http://bitbucket.org/sdorra/scm-manager - - ---> - -<!-- - Document : ehcache.xml - Created on : October 14, 2010, 6:54 AM - Author : sdorra - Description: - Purpose of the document follows. ---> - -<ehcache xmlns="http://ehcache.org/ehcache.xsd" - updateCheck="false" - maxBytesLocalDisk="512M"> - - - <!-- - Sets the path to the directory where cache .data files are created. - - If the path is a Java System Property it is replaced by - its value in the running VM. - - The following properties are translated: - user.home - User's home directory - user.dir - User's current working directory - java.io.tmpdir - Default temp file path - --> - - <diskStore path="java.io.tmpdir"/> - - - <!-- - Default Cache configuration. These will applied to caches programmatically created through - the CacheManager. - - The following attributes are required: - - maxElementsInMemory - Sets the maximum number of objects that will be created in memory - eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the - element is never expired. - overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache - has reached the maxInMemory limit. - - The following attributes are optional: - timeToIdleSeconds - Sets the time to idle for an element before it expires. - i.e. The maximum amount of time between accesses before an element expires - Is only used if the element is not eternal. - Optional attribute. A value of 0 means that an Element can idle for infinity. - The default value is 0. - timeToLiveSeconds - Sets the time to live for an element before it expires. - i.e. The maximum time between creation time and when an element expires. - Is only used if the element is not eternal. - Optional attribute. A value of 0 means that and Element can live for infinity. - The default value is 0. - diskPersistent - Whether the disk store persists between restarts of the Virtual Machine. - The default value is false. - diskExpiryThreadIntervalSeconds- The number of seconds between runs of the disk expiry thread. The default value - is 120 seconds. - --> - - <defaultCache - maxEntriesLocalHeap="100" - maxEntriesLocalDisk="10000" - eternal="false" - overflowToDisk="true" - timeToIdleSeconds="1200" - timeToLiveSeconds="2400" - diskPersistent="false" - diskExpiryThreadIntervalSeconds="120" - /> - - <!-- - Authentication cache - average: 1K - --> - <cache - name="sonia.cache.auth" - maxEntriesLocalHeap="1000" - eternal="false" - overflowToDisk="false" - timeToIdleSeconds="30" - timeToLiveSeconds="60" - diskPersistent="false" - /> - - <!-- - Authorization cache - average: 3K - --> - <cache - name="sonia.cache.authorizing" - maxEntriesLocalHeap="1000" - eternal="false" - overflowToDisk="false" - timeToIdleSeconds="1200" - timeToLiveSeconds="2400" - diskPersistent="false" - copyOnRead="true" - /> - - <!-- - PluginCenter cache - average: 30K - --> - <cache - name="sonia.cache.plugins" - maxEntriesLocalHeap="5" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="3600" - diskPersistent="false" - /> - - <!-- - Search cache for users - average: 0.5K - --> - <cache - name="sonia.cache.search.users" - maxEntriesLocalHeap="10000" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="5400" - diskPersistent="false" - /> - - <!-- - Search cache for groups - average: 0.5K - --> - <cache - name="sonia.cache.search.groups" - maxEntriesLocalHeap="1000" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="5400" - diskPersistent="false" - /> - - <!-- new repository api --> - - <!-- - Changeset cache - average: 25K - --> - <cache - name="sonia.cache.cmd.log" - maxEntriesLocalHeap="200" - maxEntriesLocalDisk="10000" - eternal="true" - overflowToDisk="true" - diskPersistent="false" - copyOnRead="true" - copyOnWrite="true" - /> - - <!-- - FileObject cache - average: 1.5K - --> - <cache - name="sonia.cache.cmd.browse" - maxEntriesLocalHeap="3000" - maxEntriesLocalDisk="20000" - eternal="true" - overflowToDisk="true" - diskPersistent="false" - copyOnRead="true" - copyOnWrite="true" - /> - - <!-- - BlameResult cache - average: 15K - --> - <cache - name="sonia.cache.cmd.blame" - maxEntriesLocalHeap="1000" - maxEntriesLocalDisk="10000" - eternal="true" - overflowToDisk="true" - diskPersistent="false" - copyOnRead="true" - copyOnWrite="true" - /> - - <!-- - Tag cache - average: 5K - --> - <cache - name="sonia.cache.cmd.tags" - maxEntriesLocalHeap="500" - eternal="true" - overflowToDisk="false" - diskPersistent="false" - /> - - <!-- - Branch cache - average: 2.5K - --> - <cache - name="sonia.cache.cmd.branches" - maxEntriesLocalHeap="1000" - eternal="true" - overflowToDisk="false" - diskPersistent="false" - /> - - <!-- deprecated old repository api --> - - <cache - name="sonia.cache.repository.changesets" - maxEntriesLocalHeap="200" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="86400" - diskPersistent="false" - /> - - <cache - name="sonia.cache.repository.browser" - maxEntriesLocalHeap="200" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="86400" - diskPersistent="false" - /> - - <cache - name="sonia.cache.repository.blame" - maxEntriesLocalHeap="100" - eternal="false" - overflowToDisk="false" - timeToLiveSeconds="86400" - diskPersistent="false" - /> - -</ehcache> diff --git a/scm-webapp/src/main/resources/config/gcache.xml b/scm-webapp/src/main/resources/config/gcache.xml index eb59f36446..28876558df 100644 --- a/scm-webapp/src/main/resources/config/gcache.xml +++ b/scm-webapp/src/main/resources/config/gcache.xml @@ -40,15 +40,15 @@ /> <!-- - Authentication cache + External group cache average: 1K --> <cache - name="sonia.cache.auth" + name="sonia.cache.externalGroups" maximumSize="1000" - expireAfterAccess="30" - expireAfterWrite="60" + expireAfterAccess="60" + expireAfterWrite="120" /> <!-- diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AvailablePluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AvailablePluginResourceTest.java new file mode 100644 index 0000000000..c108d4ee7a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AvailablePluginResourceTest.java @@ -0,0 +1,208 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.jboss.resteasy.spi.UnhandledException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.AvailablePlugin; +import sonia.scm.plugin.AvailablePluginDescriptor; +import sonia.scm.plugin.PluginCondition; +import sonia.scm.plugin.PluginInformation; +import sonia.scm.plugin.PluginManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; + +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +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.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AvailablePluginResourceTest { + + private Dispatcher dispatcher; + + @Mock + Provider<InstalledPluginResource> installedPluginResourceProvider; + + @Mock + Provider<AvailablePluginResource> availablePluginResourceProvider; + + @Mock + private PluginDtoCollectionMapper collectionMapper; + + @Mock + private PluginManager pluginManager; + + @Mock + private PluginDtoMapper mapper; + + @InjectMocks + AvailablePluginResource availablePluginResource; + + PluginRootResource pluginRootResource; + + private final Subject subject = mock(Subject.class); + + + @BeforeEach + void prepareEnvironment() { + dispatcher = MockDispatcherFactory.createDispatcher(); + pluginRootResource = new PluginRootResource(installedPluginResourceProvider, availablePluginResourceProvider); + when(availablePluginResourceProvider.get()).thenReturn(availablePluginResource); + dispatcher.getRegistry().addSingletonResource(pluginRootResource); + } + + @Nested + class withAuthorization { + + @BeforeEach + void bindSubject() { + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @AfterEach + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void getAvailablePlugins() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin plugin = createPlugin(); + + when(pluginManager.getAvailable()).thenReturn(Collections.singletonList(plugin)); + when(collectionMapper.mapAvailable(Collections.singletonList(plugin))).thenReturn(new MockedResultDto()); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); + request.accept(VndMediaType.PLUGIN_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); + assertThat(response.getContentAsString()).contains("\"marker\":\"x\""); + } + + @Test + void getAvailablePlugin() throws UnsupportedEncodingException, URISyntaxException { + PluginInformation pluginInformation = new PluginInformation(); + pluginInformation.setName("pluginName"); + pluginInformation.setVersion("2.0.0"); + + AvailablePlugin plugin = createPlugin(pluginInformation); + + when(pluginManager.getAvailable("pluginName")).thenReturn(Optional.of(plugin)); + + PluginDto pluginDto = new PluginDto(); + pluginDto.setName("pluginName"); + when(mapper.mapAvailable(plugin)).thenReturn(pluginDto); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available/pluginName"); + request.accept(VndMediaType.PLUGIN); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); + assertThat(response.getContentAsString()).contains("\"name\":\"pluginName\""); + } + + @Test + void installPlugin() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/pluginName/install"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + verify(pluginManager).install("pluginName", false); + assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); + } + + @Test + void installPendingPlugin() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/install-pending"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + verify(pluginManager).installPendingAndRestart(); + assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); + } + } + + private AvailablePlugin createPlugin() { + return createPlugin(new PluginInformation()); + } + + private AvailablePlugin createPlugin(PluginInformation pluginInformation) { + AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor( + pluginInformation, new PluginCondition(), Collections.emptySet(), "https://download.hitchhiker.com", null + ); + return new AvailablePlugin(descriptor); + } + + @Nested + class WithoutAuthorization { + + @BeforeEach + void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldNotGetAvailablePluginsIfMissingPermission() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); + request.accept(VndMediaType.PLUGIN_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + + assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response)); + } + + @Test + void shouldNotGetAvailablePluginIfMissingPermission() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available/pluginName"); + request.accept(VndMediaType.PLUGIN); + MockHttpResponse response = new MockHttpResponse(); + + assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response)); + } + + @Test + void shouldNotInstallPluginIfMissingPermission() throws URISyntaxException { + ThreadContext.unbindSubject(); + MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/pluginName/install"); + request.accept(VndMediaType.PLUGIN); + MockHttpResponse response = new MockHttpResponse(); + + assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response)); + } + } + + public class MockedResultDto extends HalRepresentation { + public String getMarker() { + return "x"; + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java index dd09e50266..4ec30ca1c3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java @@ -37,7 +37,6 @@ public class ConfigDtoToScmConfigurationMapperTest { assertEquals("user" , config.getProxyUser()); assertTrue(config.isEnableProxy()); assertEquals("realm" , config.getRealmDescription()); - assertTrue(config.isEnableRepositoryArchive()); assertTrue(config.isDisableGroupingGrid()); assertEquals("yyyy" , config.getDateFormat()); assertTrue(config.isAnonymousAccessEnabled()); @@ -50,6 +49,7 @@ public class ConfigDtoToScmConfigurationMapperTest { assertEquals(40 , config.getLoginAttemptLimitTimeout()); assertTrue(config.isEnabledXsrfProtection()); assertEquals("username", config.getNamespaceStrategy()); + assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl()); } private ConfigDto createDefaultDto() { @@ -60,7 +60,6 @@ public class ConfigDtoToScmConfigurationMapperTest { configDto.setProxyUser("user"); configDto.setEnableProxy(true); configDto.setRealmDescription("realm"); - configDto.setEnableRepositoryArchive(true); configDto.setDisableGroupingGrid(true); configDto.setDateFormat("yyyy"); configDto.setAnonymousAccessEnabled(true); @@ -73,6 +72,7 @@ public class ConfigDtoToScmConfigurationMapperTest { configDto.setLoginAttemptLimitTimeout(40); configDto.setEnabledXsrfProtection(true); configDto.setNamespaceStrategy("username"); + configDto.setLoginInfoUrl("https://scm-manager.org/login-info"); return configDto; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java index 93099cf5ea..9dfa5fca28 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java @@ -3,9 +3,11 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import org.assertj.core.api.Assertions; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import sonia.scm.SCMContextProvider; +import sonia.scm.config.ScmConfiguration; import java.net.URI; import java.util.Optional; @@ -19,9 +21,22 @@ public class IndexResourceTest { @Rule public final ShiroRule shiroRule = new ShiroRule(); - private final SCMContextProvider scmContextProvider = mock(SCMContextProvider.class); - private final IndexDtoGenerator indexDtoGenerator = new IndexDtoGenerator(ResourceLinksMock.createMock(URI.create("/")), scmContextProvider); - private final IndexResource indexResource = new IndexResource(indexDtoGenerator); + private ScmConfiguration configuration; + private SCMContextProvider scmContextProvider; + private IndexResource indexResource; + + + @Before + public void setUpObjectUnderTest() { + this.configuration = new ScmConfiguration(); + this.scmContextProvider = mock(SCMContextProvider.class); + IndexDtoGenerator generator = new IndexDtoGenerator( + ResourceLinksMock.createMock(URI.create("/")), + scmContextProvider, + configuration + ); + this.indexResource = new IndexResource(generator); + } @Test public void shouldRenderLoginUrlsForUnauthenticatedRequest() { @@ -30,6 +45,22 @@ public class IndexResourceTest { Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent); } + @Test + public void shouldRenderLoginInfoUrl() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isPresent(); + } + + @Test + public void shouldNotRenderLoginInfoUrlWhenNoUrlIsConfigured() { + configuration.setLoginInfoUrl(""); + + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isNotPresent(); + } + @Test public void shouldRenderSelfLinkForUnauthenticatedRequest() { IndexDto index = indexResource.getIndex(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/InstalledPluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/InstalledPluginResourceTest.java new file mode 100644 index 0000000000..7fa0081c5c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/InstalledPluginResourceTest.java @@ -0,0 +1,170 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.jboss.resteasy.spi.UnhandledException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.PluginInformation; +import sonia.scm.plugin.PluginManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +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.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class InstalledPluginResourceTest { + + private Dispatcher dispatcher; + + @Mock + Provider<InstalledPluginResource> installedPluginResourceProvider; + + @Mock + Provider<AvailablePluginResource> availablePluginResourceProvider; + + @Mock + private PluginDtoCollectionMapper collectionMapper; + + @Mock + private PluginDtoMapper mapper; + + @Mock + private PluginManager pluginManager; + + @InjectMocks + InstalledPluginResource installedPluginResource; + + PluginRootResource pluginRootResource; + + private final Subject subject = mock(Subject.class); + + @BeforeEach + void prepareEnvironment() { + dispatcher = MockDispatcherFactory.createDispatcher(); + pluginRootResource = new PluginRootResource(installedPluginResourceProvider, availablePluginResourceProvider); + when(installedPluginResourceProvider.get()).thenReturn(installedPluginResource); + dispatcher.getRegistry().addSingletonResource(pluginRootResource); + } + + @Nested + class withAuthorization { + + @BeforeEach + void bindSubject() { + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @AfterEach + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void getInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException { + InstalledPlugin installedPlugin = createPlugin(); + when(pluginManager.getInstalled()).thenReturn(Collections.singletonList(installedPlugin)); + when(collectionMapper.mapInstalled(Collections.singletonList(installedPlugin))).thenReturn(new MockedResultDto()); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed"); + request.accept(VndMediaType.PLUGIN_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); + assertThat(response.getContentAsString()).contains("\"marker\":\"x\""); + } + + @Test + void getInstalledPlugin() throws UnsupportedEncodingException, URISyntaxException { + PluginInformation pluginInformation = new PluginInformation(); + pluginInformation.setVersion("2.0.0"); + pluginInformation.setName("pluginName"); + InstalledPlugin installedPlugin = createPlugin(pluginInformation); + + when(pluginManager.getInstalled("pluginName")).thenReturn(Optional.of(installedPlugin)); + + PluginDto pluginDto = new PluginDto(); + pluginDto.setName("pluginName"); + when(mapper.mapInstalled(installedPlugin)).thenReturn(pluginDto); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed/pluginName"); + request.accept(VndMediaType.PLUGIN); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); + assertThat(response.getContentAsString()).contains("\"name\":\"pluginName\""); + } + } + + private InstalledPlugin createPlugin() { + return createPlugin(new PluginInformation()); + } + + private InstalledPlugin createPlugin(PluginInformation information) { + InstalledPlugin plugin = mock(InstalledPlugin.class); + InstalledPluginDescriptor descriptor = mock(InstalledPluginDescriptor.class); + lenient().when(descriptor.getInformation()).thenReturn(information); + lenient().when(plugin.getDescriptor()).thenReturn(descriptor); + return plugin; + } + + @Nested + class WithoutAuthorization { + + @BeforeEach + void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldNotGetInstalledPluginsIfMissingPermission() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed"); + request.accept(VndMediaType.PLUGIN_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + + assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response)); + } + + @Test + void shouldNotGetInstalledPluginIfMissingPermission() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed/pluginName"); + request.accept(VndMediaType.PLUGIN); + MockHttpResponse response = new MockHttpResponse(); + + assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response)); + } + } + + public class MockedResultDto extends HalRepresentation { + public String getMarker() { + return "x"; + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java index 8a00c69229..d9572dc04c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java @@ -1,9 +1,9 @@ package sonia.scm.api.v2.resources; +import com.google.common.collect.ImmutableSet; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; -import org.assertj.core.util.Lists; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,7 +12,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.user.UserTestData; @@ -20,7 +20,6 @@ import sonia.scm.user.UserTestData; import java.net.URI; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,6 +32,9 @@ class MeDtoFactoryTest { @Mock private UserManager userManager; + @Mock + private GroupCollector groupCollector; + @Mock private Subject subject; @@ -42,7 +44,7 @@ class MeDtoFactoryTest { void setUpContext() { ThreadContext.bind(subject); ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - meDtoFactory = new MeDtoFactory(resourceLinks, userManager); + meDtoFactory = new MeDtoFactory(resourceLinks, userManager, groupCollector); } @AfterEach @@ -69,24 +71,16 @@ class MeDtoFactoryTest { @Test void shouldCreateMeDtoWithGroups() { - prepareSubject(UserTestData.createTrillian(), "HeartOfGold", "Puzzle42"); + when(groupCollector.collect("trillian")).thenReturn(ImmutableSet.of("HeartOfGold", "Puzzle42")); + prepareSubject(UserTestData.createTrillian()); MeDto dto = meDtoFactory.create(); assertThat(dto.getGroups()).containsOnly("HeartOfGold", "Puzzle42"); } - private void prepareSubject(User user, String... groups) { + private void prepareSubject(User user) { PrincipalCollection collection = mock(PrincipalCollection.class); when(subject.getPrincipals()).thenReturn(collection); - when(collection.oneByType(any(Class.class))).then(ic -> { - Class<?> type = ic.getArgument(0); - if (type.isAssignableFrom(User.class)) { - return user; - } else if (type.isAssignableFrom(GroupNames.class)) { - return new GroupNames(Lists.newArrayList(groups)); - } else { - return null; - } - }); + when(collection.oneByType(User.class)).thenReturn(user); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java index cd2a172c1b..7a3d1b4304 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.ImmutableSet; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.credential.PasswordService; import org.apache.shiro.subject.PrincipalCollection; @@ -16,6 +17,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.ContextEntry; +import sonia.scm.group.GroupCollector; import sonia.scm.user.InvalidPasswordException; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -28,7 +30,12 @@ import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @@ -50,6 +57,9 @@ public class MeResourceTest { @Mock private ScmPathInfoStore scmPathInfoStore; + @Mock + private GroupCollector groupCollector; + @Mock private UserManager userManager; @@ -69,6 +79,7 @@ public class MeResourceTest { when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); + when(groupCollector.collect("trillian")).thenReturn(ImmutableSet.of("group1", "group2")); when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod(); when(userManager.getDefaultType()).thenReturn("xml"); MeResource meResource = new MeResource(meDtoFactory, userManager, passwordService); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java new file mode 100644 index 0000000000..5cf6bdd45a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java @@ -0,0 +1,147 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.collect.ImmutableSet; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.AvailablePlugin; +import sonia.scm.plugin.AvailablePluginDescriptor; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.PluginInformation; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginDtoMapperTest { + + @SuppressWarnings("unused") // Is injected + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("https://hitchhiker.com/")); + + @InjectMocks + private PluginDtoMapperImpl mapper; + + @Mock + private Subject subject; + + @BeforeEach + void bindSubject() { + ThreadContext.bind(subject); + } + + @AfterEach + void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldMapInformation() { + PluginInformation information = createPluginInformation(); + + PluginDto dto = new PluginDto(); + mapper.map(information, dto); + + assertThat(dto.getName()).isEqualTo("scm-cas-plugin"); + assertThat(dto.getVersion()).isEqualTo("1.0.0"); + assertThat(dto.getDisplayName()).isEqualTo("CAS"); + assertThat(dto.getAuthor()).isEqualTo("Sebastian Sdorra"); + assertThat(dto.getCategory()).isEqualTo("Authentication"); + assertThat(dto.getAvatarUrl()).isEqualTo("https://avatar.scm-manager.org/plugins/cas.png"); + } + + private PluginInformation createPluginInformation() { + PluginInformation information = new PluginInformation(); + information.setName("scm-cas-plugin"); + information.setVersion("1.0.0"); + information.setDisplayName("CAS"); + information.setAuthor("Sebastian Sdorra"); + information.setCategory("Authentication"); + information.setAvatarUrl("https://avatar.scm-manager.org/plugins/cas.png"); + return information; + } + + @Test + void shouldAppendInstalledSelfLink() { + InstalledPlugin plugin = createInstalled(); + + PluginDto dto = mapper.mapInstalled(plugin); + assertThat(dto.getLinks().getLinkBy("self").get().getHref()) + .isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin"); + } + + private InstalledPlugin createInstalled(PluginInformation information) { + InstalledPlugin plugin = mock(InstalledPlugin.class, Answers.RETURNS_DEEP_STUBS); + when(plugin.getDescriptor().getInformation()).thenReturn(information); + return plugin; + } + + @Test + void shouldAppendAvailableSelfLink() { + AvailablePlugin plugin = createAvailable(); + + PluginDto dto = mapper.mapAvailable(plugin); + assertThat(dto.getLinks().getLinkBy("self").get().getHref()) + .isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin"); + } + + @Test + void shouldNotAppendInstallLinkWithoutPermissions() { + AvailablePlugin plugin = createAvailable(); + + PluginDto dto = mapper.mapAvailable(plugin); + assertThat(dto.getLinks().getLinkBy("install")).isEmpty(); + } + + @Test + void shouldAppendInstallLink() { + when(subject.isPermitted("plugin:manage")).thenReturn(true); + AvailablePlugin plugin = createAvailable(); + + PluginDto dto = mapper.mapAvailable(plugin); + assertThat(dto.getLinks().getLinkBy("install").get().getHref()) + .isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install"); + } + + @Test + void shouldReturnMiscellaneousIfCategoryIsNull() { + PluginInformation information = createPluginInformation(); + information.setCategory(null); + AvailablePlugin plugin = createAvailable(information); + PluginDto dto = mapper.mapAvailable(plugin); + assertThat(dto.getCategory()).isEqualTo("Miscellaneous"); + } + + @Test + void shouldAppendDependencies() { + AvailablePlugin plugin = createAvailable(); + when(plugin.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("one", "two")); + + PluginDto dto = mapper.mapAvailable(plugin); + assertThat(dto.getDependencies()).containsOnly("one", "two"); + } + + private InstalledPlugin createInstalled() { + return createInstalled(createPluginInformation()); + } + + private AvailablePlugin createAvailable() { + return createAvailable(createPluginInformation()); + } + + private AvailablePlugin createAvailable(PluginInformation information) { + AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class); + when(descriptor.getInformation()).thenReturn(information); + return new AvailablePlugin(descriptor); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index c47250470d..af1e91344a 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -19,7 +19,6 @@ import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryIsNotArchivedException; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -41,7 +40,6 @@ import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; -import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -270,20 +268,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { verify(repositoryManager).delete(anyObject()); } - @Test - public void shouldHandleDeleteIsNotArchivedException() throws Exception { - mockRepository("space", "repo"); - - doThrow(RepositoryIsNotArchivedException.class).when(repositoryManager).delete(anyObject()); - - MockHttpRequest request = MockHttpRequest.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); - MockHttpResponse response = new MockHttpResponse(); - - dispatcher.invoke(request, response); - - assertEquals(SC_PRECONDITION_FAILED, response.getStatus()); - } - @Test public void shouldCreateNewRepositoryInCorrectNamespace() throws Exception { when(repositoryManager.create(any())).thenAnswer(invocation -> { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 83a0f073dd..1aef4e57cb 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -36,6 +36,10 @@ public class ResourceLinksMock { when(resourceLinks.modifications()).thenReturn(new ResourceLinks.ModificationsLinks(uriInfo)); when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo)); when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); + when(resourceLinks.installedPluginCollection()).thenReturn(new ResourceLinks.InstalledPluginCollectionLinks(uriInfo)); + when(resourceLinks.availablePluginCollection()).thenReturn(new ResourceLinks.AvailablePluginCollectionLinks(uriInfo)); + when(resourceLinks.installedPlugin()).thenReturn(new ResourceLinks.InstalledPluginLinks(uriInfo)); + when(resourceLinks.availablePlugin()).thenReturn(new ResourceLinks.AvailablePluginLinks(uriInfo)); when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo)); when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo)); when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo)); @@ -46,7 +50,6 @@ public class ResourceLinksMock { when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo)); when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(uriInfo)); when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo)); - when(resourceLinks.pluginCollection()).thenReturn(new ResourceLinks.PluginCollectionLinks(uriInfo)); return resourceLinks; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java index ee940a9721..2637b20a2f 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java @@ -67,7 +67,6 @@ public class ScmConfigurationToConfigDtoMapperTest { assertEquals("trillian" , dto.getProxyUser()); assertTrue(dto.isEnableProxy()); assertEquals("description" , dto.getRealmDescription()); - assertTrue(dto.isEnableRepositoryArchive()); assertTrue(dto.isDisableGroupingGrid()); assertEquals("dd" , dto.getDateFormat()); assertTrue(dto.isAnonymousAccessEnabled()); @@ -80,6 +79,7 @@ public class ScmConfigurationToConfigDtoMapperTest { assertEquals(2 , dto.getLoginAttemptLimitTimeout()); assertTrue(dto.isEnabledXsrfProtection()); assertEquals("username", dto.getNamespaceStrategy()); + assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); @@ -105,7 +105,6 @@ public class ScmConfigurationToConfigDtoMapperTest { config.setProxyUser("trillian"); config.setEnableProxy(true); config.setRealmDescription("description"); - config.setEnableRepositoryArchive(true); config.setDisableGroupingGrid(true); config.setDateFormat("dd"); config.setAnonymousAccessEnabled(true); @@ -118,6 +117,7 @@ public class ScmConfigurationToConfigDtoMapperTest { config.setLoginAttemptLimitTimeout(2); config.setEnabledXsrfProtection(true); config.setNamespaceStrategy("username"); + config.setLoginInfoUrl("https://scm-manager.org/login-info"); return config; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java index b2dafc8cfe..4987dec644 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java @@ -170,7 +170,7 @@ public class UIRootResourceTest { assertTrue(response.getContentAsString().contains("/scm/my/bundle.js")); } - private void mockPlugins(PluginWrapper... plugins) { + private void mockPlugins(InstalledPlugin... plugins) { when(pluginLoader.getInstalledPlugins()).thenReturn(Lists.newArrayList(plugins)); } @@ -180,16 +180,16 @@ public class UIRootResourceTest { return new PluginResources(scripts, styles); } - private PluginWrapper mockPlugin(String id) { + private InstalledPlugin mockPlugin(String id) { return mockPlugin(id, id, null); } - private PluginWrapper mockPlugin(String id, String name, PluginResources pluginResources) { - PluginWrapper wrapper = mock(PluginWrapper.class); + private InstalledPlugin mockPlugin(String id, String name, PluginResources pluginResources) { + InstalledPlugin wrapper = mock(InstalledPlugin.class); when(wrapper.getId()).thenReturn(id); - Plugin plugin = mock(Plugin.class); - when(wrapper.getPlugin()).thenReturn(plugin); + InstalledPluginDescriptor plugin = mock(InstalledPluginDescriptor.class); + when(wrapper.getDescriptor()).thenReturn(plugin); when(plugin.getResources()).thenReturn(pluginResources); PluginInformation information = mock(PluginInformation.class); diff --git a/scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java new file mode 100644 index 0000000000..edd23151b9 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java @@ -0,0 +1,100 @@ +package sonia.scm.group; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cache.MapCache; +import sonia.scm.cache.MapCacheManager; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultGroupCollectorTest { + + @Mock + private GroupDAO groupDAO; + + @Mock + private GroupResolver groupResolver; + + private MapCacheManager mapCacheManager; + + private Set<GroupResolver> groupResolvers; + + private DefaultGroupCollector collector; + + @BeforeEach + void initCollector() { + groupResolvers = new HashSet<>(); + mapCacheManager = new MapCacheManager(); + collector = new DefaultGroupCollector(groupDAO, mapCacheManager, groupResolvers); + } + + @Test + void shouldAlwaysReturnAuthenticatedGroup() { + Iterable<String> groupNames = collector.collect("trillian"); + assertThat(groupNames).containsOnly("_authenticated"); + } + + @Test + void shouldReturnGroupsFromCache() { + MapCache<String, Set<String>> cache = mapCacheManager.getCache(DefaultGroupCollector.CACHE_NAME); + cache.put("trillian", ImmutableSet.of("awesome", "incredible")); + + Set<String> groups = collector.collect("trillian"); + assertThat(groups).containsOnly("_authenticated", "awesome", "incredible"); + } + + @Test + void shouldNotCallResolverIfExternalGroupsAreCached() { + groupResolvers.add(groupResolver); + + MapCache<String, Set<String>> cache = mapCacheManager.getCache(DefaultGroupCollector.CACHE_NAME); + cache.put("trillian", ImmutableSet.of("awesome", "incredible")); + + Set<String> groups = collector.collect("trillian"); + assertThat(groups).containsOnly("_authenticated", "awesome", "incredible"); + + verify(groupResolver, never()).resolve("trillian"); + } + + @Nested + class WithGroupsFromDao { + + @BeforeEach + void setUpGroupsDao() { + List<Group> groups = Lists.newArrayList( + new Group("xml", "heartOfGold", "trillian"), + new Group("xml", "g42", "dent", "prefect"), + new Group("xml", "fjordsOfAfrican", "dent", "trillian") + ); + when(groupDAO.getAll()).thenReturn(groups); + } + + @Test + void shouldReturnGroupsFromDao() { + Iterable<String> groupNames = collector.collect("trillian"); + assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican"); + } + + @Test + void shouldCombineWithResolvers() { + when(groupResolver.resolve("trillian")).thenReturn(ImmutableSet.of("awesome", "incredible")); + groupResolvers.add(groupResolver); + Iterable<String> groupNames = collector.collect("trillian"); + assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible"); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java deleted file mode 100644 index bc79b9fa20..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.it; - - -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.WebResource; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import sonia.scm.api.v2.resources.ConfigDto; -import sonia.scm.api.v2.resources.RepositoryDto; -import sonia.scm.web.VndMediaType; - -import javax.ws.rs.core.MediaType; -import java.net.URI; -import java.util.Collection; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static sonia.scm.it.IntegrationTestUtil.createAdminClient; -import static sonia.scm.it.IntegrationTestUtil.createResource; -import static sonia.scm.it.IntegrationTestUtil.getLink; -import static sonia.scm.it.IntegrationTestUtil.readJson; -import static sonia.scm.it.IntegrationTestUtil.serialize; -import static sonia.scm.it.RepositoryITUtil.createRepository; -import static sonia.scm.it.RepositoryITUtil.deleteRepository; - -/** - * - * @author Sebastian Sdorra - */ -@RunWith(Parameterized.class) -public class RepositoryArchiveITCase -{ - - /** - * Constructs ... - * - * - * @param type - */ - public RepositoryArchiveITCase(String type) - { - this.type = type; - } - - //~--- methods -------------------------------------------------------------- - - @Parameterized.Parameters(name = "{0}") - public static Collection<String[]> createParameters() { - return IntegrationTestUtil.createRepositoryTypeParameters(); - } - - /** - * Method description - * - */ - @Before - public void createTestRepository() { - client = createAdminClient(); - repository = createRepository(client, readJson("repository-" + type + ".json")); - } - - /** - * Method description - * - */ - @After - public void deleteTestRepository() - { - setArchiveMode(false); - - if (repository != null) - { - deleteRepository(client, repository); - } - } - - /** - * Method description - * - */ - @Test - public void testDeleteAllowed() { - setArchiveMode(true); - - repository.setArchived(true); - - ClientResponse response = createResource(client, - "repositories/" + repository.getNamespace() + "/" + repository.getName()) - .type(VndMediaType.REPOSITORY).put(ClientResponse.class, serialize(repository)); - - assertNotNull(response); - assertEquals(204, response.getStatus()); - response = createResource(client, - "repositories/" + repository.getNamespace() + "/" + repository.getName()).delete(ClientResponse.class); - assertNotNull(response); - assertEquals(204, response.getStatus()); - repository = null; - } - - /** - * Method description - * - */ - @Test - public void testDeleteDenied() - { - setArchiveMode(true); - - URI deleteUrl = getLink(repository, "delete"); - ClientResponse response = createResource(client, deleteUrl).delete(ClientResponse.class); - - assertNotNull(response); - assertEquals(412, response.getStatus()); - response.close(); - } - - /** - * Method description - * - * - * @param archive - */ - private void setArchiveMode(boolean archive) - { - WebResource.Builder resource = createResource(client, "config").type(MediaType.APPLICATION_JSON); - ConfigDto config = resource.get(ConfigDto.class); - - assertNotNull(config); - config.setEnableRepositoryArchive(archive); - - ClientResponse resp = createResource(client, "config").type(VndMediaType.CONFIG).put(ClientResponse.class, config); - - assertNotNull(resp); - assertEquals(204, resp.getStatus()); - } - - /** Field description */ - private ScmClient client; - - /** Field description */ - private RepositoryDto repository; - - /** Field description */ - private String type; -} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java new file mode 100644 index 0000000000..322163ee1a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -0,0 +1,386 @@ +package sonia.scm.plugin; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.shiro.authz.AuthorizationException; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.NotFoundException; +import sonia.scm.event.ScmEventBus; +import sonia.scm.lifecycle.RestartEvent; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.in; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DefaultPluginManagerTest { + + @Mock + private ScmEventBus eventBus; + + @Mock + private PluginLoader loader; + + @Mock + private PluginCenter center; + + @Mock + private PluginInstaller installer; + + @InjectMocks + private DefaultPluginManager manager; + + @Mock + private Subject subject; + + @BeforeEach + void mockInstaller() { + lenient().when(installer.install(any())).then(ic -> { + AvailablePlugin plugin = ic.getArgument(0); + return new PendingPluginInstallation(plugin.install(), null); + }); + } + + @Nested + class WithAdminPermissions { + + @BeforeEach + void setUpSubject() { + ThreadContext.bind(subject); + } + + @AfterEach + void clearThreadContext() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldReturnInstalledPlugins() { + InstalledPlugin review = createInstalled("scm-review-plugin"); + InstalledPlugin git = createInstalled("scm-git-plugin"); + + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(review, git)); + + List<InstalledPlugin> installed = manager.getInstalled(); + assertThat(installed).containsOnly(review, git); + } + + @Test + void shouldReturnReviewPlugin() { + InstalledPlugin review = createInstalled("scm-review-plugin"); + InstalledPlugin git = createInstalled("scm-git-plugin"); + + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(review, git)); + + Optional<InstalledPlugin> plugin = manager.getInstalled("scm-review-plugin"); + assertThat(plugin).contains(review); + } + + @Test + void shouldReturnEmptyForNonInstalledPlugin() { + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of()); + + Optional<InstalledPlugin> plugin = manager.getInstalled("scm-review-plugin"); + assertThat(plugin).isEmpty(); + } + + @Test + void shouldReturnAvailablePlugins() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin git = createAvailable("scm-git-plugin"); + + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); + + List<AvailablePlugin> available = manager.getAvailable(); + assertThat(available).containsOnly(review, git); + } + + @Test + void shouldFilterOutAllInstalled() { + InstalledPlugin installedGit = createInstalled("scm-git-plugin"); + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit)); + + AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin git = createAvailable("scm-git-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); + + List<AvailablePlugin> available = manager.getAvailable(); + assertThat(available).containsOnly(review); + } + + @Test + void shouldReturnAvailable() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin git = createAvailable("scm-git-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); + + Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin"); + assertThat(available).contains(git); + } + + @Test + void shouldReturnEmptyForNonExistingAvailable() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); + + Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin"); + assertThat(available).isEmpty(); + } + + @Test + void shouldReturnEmptyForInstalledPlugin() { + InstalledPlugin installedGit = createInstalled("scm-git-plugin"); + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit)); + + AvailablePlugin git = createAvailable("scm-git-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); + + Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin"); + assertThat(available).isEmpty(); + } + + @Test + void shouldInstallThePlugin() { + AvailablePlugin git = createAvailable("scm-git-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); + + manager.install("scm-git-plugin", false); + + verify(installer).install(git); + verify(eventBus, never()).post(any()); + } + + @Test + void shouldInstallDependingPlugins() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); + AvailablePlugin mail = createAvailable("scm-mail-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); + + manager.install("scm-review-plugin", false); + + verify(installer).install(mail); + verify(installer).install(review); + } + + @Test + void shouldNotInstallAlreadyInstalledDependencies() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); + AvailablePlugin mail = createAvailable("scm-mail-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); + + InstalledPlugin installedMail = createInstalled("scm-mail-plugin"); + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail)); + + manager.install("scm-review-plugin", false); + + ArgumentCaptor<AvailablePlugin> captor = ArgumentCaptor.forClass(AvailablePlugin.class); + verify(installer).install(captor.capture()); + + assertThat(captor.getValue().getDescriptor().getInformation().getName()).isEqualTo("scm-review-plugin"); + } + + @Test + void shouldRollbackOnFailedInstallation() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); + AvailablePlugin mail = createAvailable("scm-mail-plugin"); + when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin")); + AvailablePlugin notification = createAvailable("scm-notification-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail, notification)); + + PendingPluginInstallation pendingNotification = mock(PendingPluginInstallation.class); + doReturn(pendingNotification).when(installer).install(notification); + + PendingPluginInstallation pendingMail = mock(PendingPluginInstallation.class); + doReturn(pendingMail).when(installer).install(mail); + + doThrow(new PluginChecksumMismatchException("checksum does not match")).when(installer).install(review); + + assertThrows(PluginInstallException.class, () -> manager.install("scm-review-plugin", false)); + + verify(pendingNotification).cancel(); + verify(pendingMail).cancel(); + } + + @Test + void shouldInstallNothingIfOneOfTheDependenciesIsNotAvailable() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); + AvailablePlugin mail = createAvailable("scm-mail-plugin"); + when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin")); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); + + assertThrows(NotFoundException.class, () -> manager.install("scm-review-plugin", false)); + + verify(installer, never()).install(any()); + } + + @Test + void shouldSendRestartEventAfterInstallation() { + AvailablePlugin git = createAvailable("scm-git-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); + + manager.install("scm-git-plugin", true); + + verify(installer).install(git); + verify(eventBus).post(any(RestartEvent.class)); + } + + @Test + void shouldNotSendRestartEventIfNoPluginWasInstalled() { + InstalledPlugin gitInstalled = createInstalled("scm-git-plugin"); + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(gitInstalled)); + + manager.install("scm-git-plugin", true); + verify(eventBus, never()).post(any()); + } + + @Test + void shouldNotInstallAlreadyPendingPlugins() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); + + manager.install("scm-review-plugin", false); + manager.install("scm-review-plugin", false); + // only one interaction + verify(installer).install(any()); + } + + @Test + void shouldSendRestartEvent() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); + + manager.install("scm-review-plugin", false); + manager.installPendingAndRestart(); + + verify(eventBus).post(any(RestartEvent.class)); + } + + @Test + void shouldNotSendRestartEventWithoutPendingPlugins() { + manager.installPendingAndRestart(); + + verify(eventBus, never()).post(any()); + } + + @Test + void shouldReturnSingleAvailableAsPending() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); + + manager.install("scm-review-plugin", false); + + Optional<AvailablePlugin> available = manager.getAvailable("scm-review-plugin"); + assertThat(available.get().isPending()).isTrue(); + } + + @Test + void shouldReturnAvailableAsPending() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); + + manager.install("scm-review-plugin", false); + + List<AvailablePlugin> available = manager.getAvailable(); + assertThat(available.get(0).isPending()).isTrue(); + } + + } + + @Nested + class WithoutReadPermissions { + + @BeforeEach + void setUpSubject() { + ThreadContext.bind(subject); + doThrow(AuthorizationException.class).when(subject).checkPermission("plugin:read"); + } + + @AfterEach + void clearThreadContext() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldThrowAuthorizationExceptionsForReadMethods() { + assertThrows(AuthorizationException.class, () -> manager.getInstalled()); + assertThrows(AuthorizationException.class, () -> manager.getInstalled("test")); + assertThrows(AuthorizationException.class, () -> manager.getAvailable()); + assertThrows(AuthorizationException.class, () -> manager.getAvailable("test")); + } + + } + + @Nested + class WithoutManagePermissions { + + @BeforeEach + void setUpSubject() { + ThreadContext.bind(subject); + doThrow(AuthorizationException.class).when(subject).checkPermission("plugin:manage"); + } + + @AfterEach + void clearThreadContext() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldThrowAuthorizationExceptionsForInstallMethod() { + assertThrows(AuthorizationException.class, () -> manager.install("test", false)); + } + + @Test + void shouldThrowAuthorizationExceptionsForInstallPendingAndRestart() { + assertThrows(AuthorizationException.class, () -> manager.installPendingAndRestart()); + } + + } + + private AvailablePlugin createAvailable(String name) { + PluginInformation information = new PluginInformation(); + information.setName(name); + return createAvailable(information); + } + + private InstalledPlugin createInstalled(String name) { + PluginInformation information = new PluginInformation(); + information.setName(name); + return createInstalled(information); + } + + private InstalledPlugin createInstalled(PluginInformation information) { + InstalledPlugin plugin = mock(InstalledPlugin.class, Answers.RETURNS_DEEP_STUBS); + returnInformation(plugin, information); + return plugin; + } + + private AvailablePlugin createAvailable(PluginInformation information) { + AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class); + lenient().when(descriptor.getInformation()).thenReturn(information); + return new AvailablePlugin(descriptor); + } + + private void returnInformation(Plugin mockedPlugin, PluginInformation information) { + when(mockedPlugin.getDescriptor().getInformation()).thenReturn(information); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java index 9e5ebccbfd..7cb534c7ba 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java @@ -102,7 +102,7 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase public void testGetResourceFromCache() { DefaultUberWebResourceLoader resourceLoader = new DefaultUberWebResourceLoader(servletContext, - new ArrayList<PluginWrapper>(), Stage.PRODUCTION); + new ArrayList<InstalledPlugin>(), Stage.PRODUCTION); resourceLoader.getCache().put("/myresource", GITHUB); @@ -131,8 +131,8 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase { File directory = temp.newFolder(); File file = file(directory, "myresource"); - PluginWrapper wrapper = createPluginWrapper(directory); - List<PluginWrapper> plugins = Lists.newArrayList(wrapper); + InstalledPlugin wrapper = createPluginWrapper(directory); + List<InstalledPlugin> plugins = Lists.newArrayList(wrapper); WebResourceLoader resourceLoader = new DefaultUberWebResourceLoader(servletContext, plugins); @@ -170,8 +170,8 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase File directory = temp.newFolder(); File file = file(directory, "myresource"); - PluginWrapper wrapper = createPluginWrapper(directory); - List<PluginWrapper> plugins = Lists.newArrayList(wrapper); + InstalledPlugin wrapper = createPluginWrapper(directory); + List<InstalledPlugin> plugins = Lists.newArrayList(wrapper); UberWebResourceLoader resourceLoader = new DefaultUberWebResourceLoader(servletContext, plugins); @@ -197,11 +197,11 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase WebResourceLoader loader = mock(WebResourceLoader.class); when(loader.getResource("/myresource")).thenReturn(url); - PluginWrapper pluginWrapper = mock(PluginWrapper.class); - when(pluginWrapper.getWebResourceLoader()).thenReturn(loader); + InstalledPlugin installedPlugin = mock(InstalledPlugin.class); + when(installedPlugin.getWebResourceLoader()).thenReturn(loader); WebResourceLoader resourceLoader = - new DefaultUberWebResourceLoader(servletContext, Lists.newArrayList(pluginWrapper)); + new DefaultUberWebResourceLoader(servletContext, Lists.newArrayList(installedPlugin)); assertNull(resourceLoader.getResource("/myresource")); } @@ -214,11 +214,11 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase WebResourceLoader loader = mock(WebResourceLoader.class); when(loader.getResource("/myresource")).thenReturn(url); - PluginWrapper pluginWrapper = mock(PluginWrapper.class); - when(pluginWrapper.getWebResourceLoader()).thenReturn(loader); + InstalledPlugin installedPlugin = mock(InstalledPlugin.class); + when(installedPlugin.getWebResourceLoader()).thenReturn(loader); UberWebResourceLoader resourceLoader = - new DefaultUberWebResourceLoader(servletContext, Lists.newArrayList(pluginWrapper)); + new DefaultUberWebResourceLoader(servletContext, Lists.newArrayList(installedPlugin)); List<URL> resources = resourceLoader.getResources("/myresource"); Assertions.assertThat(resources).isEmpty(); @@ -232,7 +232,7 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase * * @return */ - private PluginWrapper createPluginWrapper(File directory) + private InstalledPlugin createPluginWrapper(File directory) { return createPluginWrapper(directory.toPath()); } @@ -245,9 +245,9 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase * * @return */ - private PluginWrapper createPluginWrapper(Path directory) + private InstalledPlugin createPluginWrapper(Path directory) { - return new PluginWrapper(null, null, new PathWebResourceLoader(directory), + return new InstalledPlugin(null, null, new PathWebResourceLoader(directory), directory); } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/ExplodedSmpTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/ExplodedSmpTest.java index b7bde65677..090d903f49 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/ExplodedSmpTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/ExplodedSmpTest.java @@ -60,12 +60,12 @@ public class ExplodedSmpTest @Test public void testCompareTo() { - ExplodedSmp e1 = create("a", "c", "1", "a:a"); + ExplodedSmp e1 = create("a", "c", "1", "a"); ExplodedSmp e3 = create("a", "a", "1"); ExplodedSmp e2 = create("a", "b", "1"); List<ExplodedSmp> es = list(e1, e2, e3); - is(es, 2, "c"); + is(es, 2, "a"); } /** @@ -75,9 +75,9 @@ public class ExplodedSmpTest @Test(expected = PluginCircularDependencyException.class) public void testCompareToCyclicDependency() { - ExplodedSmp e1 = create("a", "a", "1", "a:c"); - ExplodedSmp e2 = create("a", "b", "1"); - ExplodedSmp e3 = create("a", "c", "1", "a:a"); + ExplodedSmp e1 = create("a", "1", "c"); + ExplodedSmp e2 = create("b", "1"); + ExplodedSmp e3 = create("c", "1", "a"); list(e1, e2, e3); } @@ -89,9 +89,9 @@ public class ExplodedSmpTest @Test public void testCompareToTransitiveDependencies() { - ExplodedSmp e1 = create("a", "a", "1", "a:b"); - ExplodedSmp e2 = create("a", "b", "1"); - ExplodedSmp e3 = create("a", "c", "1", "a:a"); + ExplodedSmp e1 = create("a", "1", "b"); + ExplodedSmp e2 = create("b", "1"); + ExplodedSmp e3 = create("c", "1", "a"); List<ExplodedSmp> es = list(e1, e2, e3); @@ -107,9 +107,9 @@ public class ExplodedSmpTest @Test public void testMultipleDependencies() { - ExplodedSmp e1 = create("a", "a", "1", "a:b", "a:c"); - ExplodedSmp e2 = create("a", "b", "1", "a:c"); - ExplodedSmp e3 = create("a", "c", "1"); + ExplodedSmp e1 = create("a", "1", "b", "c"); + ExplodedSmp e2 = create("b", "1", "c"); + ExplodedSmp e3 = create("c", "1"); List<ExplodedSmp> es = list(e1, e2, e3); is(es, 2, "a"); @@ -119,23 +119,21 @@ public class ExplodedSmpTest * Method description * * - * @param groupId - * @param artifactId + * @param name * @param version * @param dependencies * * @return */ - private ExplodedSmp create(String groupId, String artifactId, String version, + private ExplodedSmp create(String name, String version, String... dependencies) { PluginInformation info = new PluginInformation(); - info.setGroupId(groupId); - info.setArtifactId(artifactId); + info.setName(name); info.setVersion(version); - Plugin plugin = new Plugin(2, info, null, null, false, + InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(2, info, null, null, false, Sets.newSet(dependencies)); return new ExplodedSmp(null, plugin); @@ -170,6 +168,6 @@ public class ExplodedSmpTest */ private void is(List<ExplodedSmp> es, int p, String a) { - assertEquals(a, es.get(p).getPlugin().getInformation().getArtifactId()); + assertEquals(a, es.get(p).getPlugin().getInformation().getName()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PendingPluginInstallationTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PendingPluginInstallationTest.java new file mode 100644 index 0000000000..ae61d6e367 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PendingPluginInstallationTest.java @@ -0,0 +1,46 @@ +package sonia.scm.plugin; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith({MockitoExtension.class, TempDirectory.class}) +class PendingPluginInstallationTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private AvailablePlugin plugin; + + @Test + void shouldDeleteFileOnCancel(@TempDirectory.TempDir Path directory) throws IOException { + Path file = directory.resolve("file"); + Files.write(file, "42".getBytes()); + + when(plugin.getDescriptor().getInformation().getName()).thenReturn("scm-awesome-plugin"); + + PendingPluginInstallation installation = new PendingPluginInstallation(plugin, file); + installation.cancel(); + + assertThat(file).doesNotExist(); + } + + @Test + void shouldThrowExceptionIfCancelFailed(@TempDirectory.TempDir Path directory) { + Path file = directory.resolve("file"); + when(plugin.getDescriptor().getInformation().getName()).thenReturn("scm-awesome-plugin"); + + PendingPluginInstallation installation = new PendingPluginInstallation(plugin, file); + assertThrows(PluginFailedToCancelInstallationException.class, installation::cancel); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java new file mode 100644 index 0000000000..af6449794d --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java @@ -0,0 +1,120 @@ +package sonia.scm.plugin; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static sonia.scm.plugin.PluginCenterDto.Plugin; +import static sonia.scm.plugin.PluginCenterDto.*; + +@ExtendWith(MockitoExtension.class) +class PluginCenterDtoMapperTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private PluginCenterDto dto; + + @InjectMocks + private PluginCenterDtoMapperImpl mapper; + + @Test + void shouldMapSinglePlugin() { + Plugin plugin = new Plugin( + "scm-hitchhiker-plugin", + "SCM Hitchhiker Plugin", + "plugin for hitchhikers", + "Travel", + "2.0.0", + "trillian", + "http://avatar.url", + "555000444", + new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), + ImmutableSet.of("scm-review-plugin"), + ImmutableMap.of("download", new Link("http://download.hitchhiker.com")) + ); + + when(dto.getEmbedded().getPlugins()).thenReturn(Collections.singletonList(plugin)); + AvailablePluginDescriptor descriptor = mapper.map(dto).iterator().next().getDescriptor(); + PluginInformation information = descriptor.getInformation(); + PluginCondition condition = descriptor.getCondition(); + + assertThat(descriptor.getUrl()).isEqualTo("http://download.hitchhiker.com"); + assertThat(descriptor.getChecksum()).contains("555000444"); + + assertThat(information.getAuthor()).isEqualTo(plugin.getAuthor()); + assertThat(information.getCategory()).isEqualTo(plugin.getCategory()); + assertThat(information.getVersion()).isEqualTo(plugin.getVersion()); + assertThat(condition.getArch()).isEqualTo(plugin.getConditions().getArch()); + assertThat(condition.getMinVersion()).isEqualTo(plugin.getConditions().getMinVersion()); + assertThat(condition.getOs().iterator().next()).isEqualTo(plugin.getConditions().getOs().iterator().next()); + assertThat(information.getDescription()).isEqualTo(plugin.getDescription()); + assertThat(information.getName()).isEqualTo(plugin.getName()); + } + + @Test + void shouldMapMultiplePlugins() { + Plugin plugin1 = new Plugin( + "scm-review-plugin", + "SCM Hitchhiker Plugin", + "plugin for hitchhikers", + "Travel", + "2.1.0", + "trillian", + "https://avatar.url", + "12345678aa", + new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), + ImmutableSet.of("scm-review-plugin"), + ImmutableMap.of("download", new Link("http://download.hitchhiker.com/review")) + ); + + Plugin plugin2 = new Plugin( + "scm-hitchhiker-plugin", + "SCM Hitchhiker Plugin", + "plugin for hitchhikers", + "Travel", + "2.0.0", + "dent", + "http://avatar.url", + "555000444", + new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), + ImmutableSet.of("scm-review-plugin"), + ImmutableMap.of("download", new Link("http://download.hitchhiker.com/hitchhiker")) + ); + + when(dto.getEmbedded().getPlugins()).thenReturn(Arrays.asList(plugin1, plugin2)); + + Set<AvailablePlugin> resultSet = mapper.map(dto); + + PluginInformation pluginInformation1 = findPlugin(resultSet, plugin1.getName()); + PluginInformation pluginInformation2 = findPlugin(resultSet, plugin2.getName()); + + assertThat(pluginInformation1.getAuthor()).isEqualTo(plugin1.getAuthor()); + assertThat(pluginInformation1.getVersion()).isEqualTo(plugin1.getVersion()); + assertThat(pluginInformation2.getAuthor()).isEqualTo(plugin2.getAuthor()); + assertThat(pluginInformation2.getVersion()).isEqualTo(plugin2.getVersion()); + assertThat(resultSet.size()).isEqualTo(2); + } + + private PluginInformation findPlugin(Set<AvailablePlugin> resultSet, String name) { + return resultSet + .stream() + .filter(p -> name.equals(p.getDescriptor().getInformation().getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("could not find plugin " + name)) + .getDescriptor() + .getInformation(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java new file mode 100644 index 0000000000..e3ebf995bd --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java @@ -0,0 +1,50 @@ +package sonia.scm.plugin; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.net.ahc.AdvancedHttpClient; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginCenterLoaderTest { + + private static final String PLUGIN_URL = "https://plugins.hitchhiker.com"; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private AdvancedHttpClient client; + + @Mock + private PluginCenterDtoMapper mapper; + + @InjectMocks + private PluginCenterLoader loader; + + @Test + void shouldFetch() throws IOException { + Set<AvailablePlugin> plugins = Collections.emptySet(); + PluginCenterDto dto = new PluginCenterDto(); + when(client.get(PLUGIN_URL).request().contentFromJson(PluginCenterDto.class)).thenReturn(dto); + when(mapper.map(dto)).thenReturn(plugins); + + Set<AvailablePlugin> fetched = loader.load(PLUGIN_URL); + assertThat(fetched).isSameAs(plugins); + } + + @Test + void shouldReturnEmptySetIfPluginCenterNotBeReached() throws IOException { + when(client.get(PLUGIN_URL).request()).thenThrow(new IOException("failed to fetch")); + + Set<AvailablePlugin> fetch = loader.load(PLUGIN_URL); + assertThat(fetch).isEmpty(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java new file mode 100644 index 0000000000..a76b4cb551 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java @@ -0,0 +1,73 @@ +package sonia.scm.plugin; + +import com.google.common.collect.ImmutableSet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; +import sonia.scm.cache.CacheManager; +import sonia.scm.cache.MapCacheManager; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.util.SystemUtil; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginCenterTest { + + private static final String PLUGIN_URL_BASE = "https://plugins.hitchhiker.com/"; + private static final String PLUGIN_URL = PLUGIN_URL_BASE + "{version}"; + + @Mock + private PluginCenterLoader loader; + + @Mock + private SCMContextProvider contextProvider; + + private ScmConfiguration configuration; + + private CacheManager cacheManager; + + private PluginCenter pluginCenter; + + @BeforeEach + void setUpPluginCenter() { + when(contextProvider.getVersion()).thenReturn("2.0.0"); + + cacheManager = new MapCacheManager(); + + configuration = new ScmConfiguration(); + configuration.setPluginUrl(PLUGIN_URL); + + pluginCenter = new PluginCenter(contextProvider, cacheManager, configuration, loader); + } + + @Test + void shouldFetchPlugins() { + Set<AvailablePlugin> plugins = new HashSet<>(); + when(loader.load(PLUGIN_URL_BASE + "2.0.0")).thenReturn(plugins); + + assertThat(pluginCenter.getAvailable()).isSameAs(plugins); + } + + @Test + void shouldCache() { + Set<AvailablePlugin> first = new HashSet<>(); + when(loader.load(anyString())).thenReturn(first, new HashSet<>()); + + assertThat(pluginCenter.getAvailable()).isSameAs(first); + assertThat(pluginCenter.getAvailable()).isSameAs(first); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java new file mode 100644 index 0000000000..3f918cd4fa --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java @@ -0,0 +1,117 @@ +package sonia.scm.plugin; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; +import sonia.scm.net.ahc.AdvancedHttpClient; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith({MockitoExtension.class, TempDirectory.class}) +class PluginInstallerTest { + + @Mock + private SCMContextProvider context; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private AdvancedHttpClient client; + + @InjectMocks + private PluginInstaller installer; + + private Path directory; + + @BeforeEach + void setUpContext(@TempDirectory.TempDir Path directory) { + this.directory = directory; + lenient().when(context.resolve(any())).then(ic -> { + Path arg = ic.getArgument(0); + return directory.resolve(arg); + }); + } + + @Test + void shouldDownloadPlugin() throws IOException { + mockContent("42"); + + installer.install(createGitPlugin()); + + assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).hasContent("42"); + } + + @Test + void shouldReturnPendingPluginInstallation() throws IOException { + mockContent("42"); + AvailablePlugin gitPlugin = createGitPlugin(); + + PendingPluginInstallation pending = installer.install(gitPlugin); + + assertThat(pending).isNotNull(); + assertThat(pending.getPlugin().getDescriptor()).isEqualTo(gitPlugin.getDescriptor()); + assertThat(pending.getPlugin().isPending()).isTrue(); + } + + private void mockContent(String content) throws IOException { + when(client.get("https://download.hitchhiker.com").request().contentAsStream()) + .thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))); + } + + private AvailablePlugin createGitPlugin() { + return createPlugin( + "scm-git-plugin", + "https://download.hitchhiker.com", + "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049" // 42 + ); + } + + @Test + void shouldThrowPluginDownloadException() throws IOException { + when(client.get("https://download.hitchhiker.com").request()).thenThrow(new IOException("failed to download")); + + assertThrows(PluginDownloadException.class, () -> installer.install(createGitPlugin())); + } + + @Test + void shouldThrowPluginChecksumMismatchException() throws IOException { + mockContent("21"); + + assertThrows(PluginChecksumMismatchException.class, () -> installer.install(createGitPlugin())); + assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); + } + + @Test + void shouldThrowPluginDownloadExceptionAndCleanup() throws IOException { + InputStream stream = mock(InputStream.class); + when(stream.read(any(), anyInt(), anyInt())).thenThrow(new IOException("failed to read")); + when(client.get("https://download.hitchhiker.com").request().contentAsStream()).thenReturn(stream); + + assertThrows(PluginDownloadException.class, () -> installer.install(createGitPlugin())); + assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); + } + + + private AvailablePlugin createPlugin(String name, String url, String checksum) { + PluginInformation information = new PluginInformation(); + information.setName(name); + AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor( + information, null, Collections.emptySet(), url, checksum + ); + return new AvailablePlugin(descriptor); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java index 8b352b8e68..bb518ec731 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java @@ -71,37 +71,37 @@ public class PluginProcessorTest /** Field description */ private static final PluginResource PLUGIN_A = new PluginResource("sonia/scm/plugin/scm-a-plugin.smp", "scm-a-plugin.smp", - "sonia.scm.plugins:scm-a-plugin:1.0.0-SNAPSHOT"); + "scm-a-plugin:1.0.0-SNAPSHOT"); /** Field description */ private static final PluginResource PLUGIN_B = new PluginResource("sonia/scm/plugin/scm-b-plugin.smp", "scm-b-plugin.smp", - "sonia.scm.plugins:scm-b-plugin:1.0.0-SNAPSHOT"); + "scm-b-plugin:1.0.0-SNAPSHOT"); /** Field description */ private static final PluginResource PLUGIN_C = new PluginResource("sonia/scm/plugin/scm-c-plugin.smp", "scm-c-plugin.smp", - "sonia.scm.plugins:scm-c-plugin:1.0.0-SNAPSHOT"); + "scm-c-plugin:1.0.0-SNAPSHOT"); /** Field description */ private static final PluginResource PLUGIN_D = new PluginResource("sonia/scm/plugin/scm-d-plugin.smp", "scm-d-plugin.smp", - "sonia.scm.plugins:scm-d-plugin:1.0.0-SNAPSHOT"); + "scm-d-plugin:1.0.0-SNAPSHOT"); /** Field description */ private static final PluginResource PLUGIN_E = new PluginResource("sonia/scm/plugin/scm-e-plugin.smp", "scm-e-plugin.smp", - "sonia.scm.plugins: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 = new PluginResource("sonia/scm/plugin/scm-f-plugin-1.0.0.smp", - "scm-f-plugin.smp", "sonia.scm.plugins: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 = new PluginResource("sonia/scm/plugin/scm-f-plugin-1.0.1.smp", - "scm-f-plugin.smp", "sonia.scm.plugins:scm-f-plugin:1.0.1"); + "scm-f-plugin.smp", "scm-f-plugin:1.0.1"); //~--- methods -------------------------------------------------------------- @@ -129,7 +129,17 @@ public class PluginProcessorTest { copySmp(PLUGIN_A); - PluginWrapper plugin = collectAndGetFirst(); + InstalledPlugin plugin = collectAndGetFirst(); + + assertThat(plugin.getId(), is(PLUGIN_A.id)); + } + + @Test + public void shouldCollectPluginsAndDoNotFailOnNonPluginDirectories() throws IOException { + new File(pluginDirectory, "some-directory").mkdirs(); + + copySmp(PLUGIN_A); + InstalledPlugin plugin = collectAndGetFirst(); assertThat(plugin.getId(), is(PLUGIN_A.id)); } @@ -145,15 +155,15 @@ public class PluginProcessorTest { copySmps(PLUGIN_A, PLUGIN_B); - Set<PluginWrapper> plugins = collectPlugins(); + Set<InstalledPlugin> plugins = collectPlugins(); assertThat(plugins, hasSize(2)); - PluginWrapper a = findPlugin(plugins, PLUGIN_A.id); + InstalledPlugin a = findPlugin(plugins, PLUGIN_A.id); assertNotNull(a); - PluginWrapper b = findPlugin(plugins, PLUGIN_B.id); + InstalledPlugin b = findPlugin(plugins, PLUGIN_B.id); assertNotNull(b); } @@ -178,7 +188,7 @@ public class PluginProcessorTest { copySmp(PLUGIN_A); - PluginWrapper plugin = collectAndGetFirst(); + InstalledPlugin plugin = collectAndGetFirst(); ClassLoader cl = plugin.getClassLoader(); // load parent class @@ -216,9 +226,9 @@ public class PluginProcessorTest { copySmps(PLUGIN_A, PLUGIN_B); - Set<PluginWrapper> plugins = collectPlugins(); + Set<InstalledPlugin> plugins = collectPlugins(); - PluginWrapper plugin = findPlugin(plugins, PLUGIN_B.id); + InstalledPlugin plugin = findPlugin(plugins, PLUGIN_B.id); ClassLoader cl = plugin.getClassLoader(); // load parent class @@ -247,7 +257,7 @@ public class PluginProcessorTest { copySmp(PLUGIN_A); - PluginWrapper plugin = collectAndGetFirst(); + InstalledPlugin plugin = collectAndGetFirst(); WebResourceLoader wrl = plugin.getWebResourceLoader(); assertNotNull(wrl); @@ -269,7 +279,7 @@ public class PluginProcessorTest { copySmp(PLUGIN_F_1_0_0); - PluginWrapper plugin = collectAndGetFirst(); + InstalledPlugin plugin = collectAndGetFirst(); assertThat(plugin.getId(), is(PLUGIN_F_1_0_0.id)); copySmp(PLUGIN_F_1_0_1); @@ -302,9 +312,9 @@ public class PluginProcessorTest * * @throws IOException */ - private PluginWrapper collectAndGetFirst() throws IOException + private InstalledPlugin collectAndGetFirst() throws IOException { - Set<PluginWrapper> plugins = collectPlugins(); + Set<InstalledPlugin> plugins = collectPlugins(); assertThat(plugins, hasSize(1)); @@ -319,7 +329,7 @@ public class PluginProcessorTest * * @throws IOException */ - private Set<PluginWrapper> collectPlugins() throws IOException + private Set<InstalledPlugin> collectPlugins() throws IOException { return processor.collectPlugins(PluginProcessorTest.class.getClassLoader()); } @@ -368,14 +378,14 @@ public class PluginProcessorTest * * @return */ - private PluginWrapper findPlugin(Iterable<PluginWrapper> plugin, - final String id) + private InstalledPlugin findPlugin(Iterable<InstalledPlugin> plugin, + final String id) { - return Iterables.find(plugin, new Predicate<PluginWrapper>() + return Iterables.find(plugin, new Predicate<InstalledPlugin>() { @Override - public boolean apply(PluginWrapper input) + public boolean apply(InstalledPlugin input) { return id.equals(input.getId()); } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java index 06d6c1732c..72c48d4d16 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java @@ -71,7 +71,7 @@ public class PluginTreeTest { PluginCondition condition = new PluginCondition("999", new ArrayList<String>(), "hit"); - Plugin plugin = new Plugin(2, createInfo("a", "b", "1"), null, condition, + InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(2, createInfo("a", "1"), null, condition, false, null); ExplodedSmp smp = createSmp(plugin); @@ -102,7 +102,7 @@ public class PluginTreeTest List<ExplodedSmp> smps = createSmps("a", "b", "c"); List<String> nodes = unwrapIds(new PluginTree(smps).getRootNodes()); - assertThat(nodes, containsInAnyOrder("a:a", "b:b", "c:c")); + assertThat(nodes, containsInAnyOrder("a", "b", "c")); } /** @@ -114,7 +114,7 @@ public class PluginTreeTest @Test(expected = PluginException.class) public void testScmVersion() throws IOException { - Plugin plugin = new Plugin(1, createInfo("a", "b", "1"), null, null, false, + InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(1, createInfo("a", "1"), null, null, false, null); ExplodedSmp smp = createSmp(plugin); @@ -141,34 +141,32 @@ public class PluginTreeTest PluginTree tree = new PluginTree(smps); List<PluginNode> rootNodes = tree.getRootNodes(); - assertThat(unwrapIds(rootNodes), containsInAnyOrder("a:a")); + assertThat(unwrapIds(rootNodes), containsInAnyOrder("a")); PluginNode a = rootNodes.get(0); - assertThat(unwrapIds(a.getChildren()), containsInAnyOrder("b:b", "c:c")); + assertThat(unwrapIds(a.getChildren()), containsInAnyOrder("b", "c")); - PluginNode b = a.getChild("b:b"); + PluginNode b = a.getChild("b"); - assertThat(unwrapIds(b.getChildren()), containsInAnyOrder("c:c")); + assertThat(unwrapIds(b.getChildren()), containsInAnyOrder("c")); } /** * Method description * * - * @param groupId - * @param artifactId + * @param name * @param version * * @return */ - private PluginInformation createInfo(String groupId, String artifactId, + private PluginInformation createInfo(String name, String version) { PluginInformation info = new PluginInformation(); - info.setGroupId(groupId); - info.setArtifactId(artifactId); + info.setName(name); info.setVersion(version); return info; @@ -184,7 +182,7 @@ public class PluginTreeTest * * @throws IOException */ - private ExplodedSmp createSmp(Plugin plugin) throws IOException + private ExplodedSmp createSmp(InstalledPluginDescriptor plugin) throws IOException { return new ExplodedSmp(tempFolder.newFile().toPath(), plugin); } @@ -201,7 +199,7 @@ public class PluginTreeTest */ private ExplodedSmp createSmp(String name) throws IOException { - return createSmp(new Plugin(2, createInfo(name, name, "1.0.0"), null, null, + return createSmp(new InstalledPluginDescriptor(2, createInfo(name, "1.0.0"), null, null, false, null)); } @@ -224,10 +222,10 @@ public class PluginTreeTest for (String d : dependencies) { - dependencySet.add(d.concat(":").concat(d)); + dependencySet.add(d); } - Plugin plugin = new Plugin(2, createInfo(name, name, "1"), null, null, + InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(2, createInfo(name, "1"), null, null, false, dependencySet); return createSmp(plugin); diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java index c2d75358fd..d458f9c72c 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java @@ -90,12 +90,10 @@ class BearerRealmTest { Set<String> groups = ImmutableSet.of("HeartOfGold", "Puzzle42"); when(accessToken.getSubject()).thenReturn("trillian"); - when(accessToken.getGroups()).thenReturn(groups); when(accessToken.getClaims()).thenReturn(new HashMap<>()); when(accessTokenResolver.resolve(bearerToken)).thenReturn(accessToken); when(realmHelper.authenticationInfoBuilder("trillian")).thenReturn(builder); - when(builder.withGroups(groups)).thenReturn(builder); when(builder.withCredentials("__bearer__")).thenReturn(builder); when(builder.withScope(any(Scope.class))).thenReturn(builder); when(builder.build()).thenReturn(authenticationInfo); diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 8e7cb8a70e..930a06d249 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -33,6 +33,7 @@ package sonia.scm.security; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; @@ -49,7 +50,7 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryPermission; @@ -58,8 +59,6 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; -import java.util.Collections; - import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -96,6 +95,9 @@ public class DefaultAuthorizationCollectorTest { @Mock private RepositoryPermissionProvider repositoryPermissionProvider; + @Mock + private GroupCollector groupCollector; + private DefaultAuthorizationCollector collector; @Rule @@ -107,7 +109,7 @@ public class DefaultAuthorizationCollectorTest { @Before public void setUp(){ when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache); - collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider); + collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider, groupCollector); } /** @@ -290,9 +292,13 @@ public class DefaultAuthorizationCollectorTest { SimplePrincipalCollection spc = new SimplePrincipalCollection(); spc.add(user.getName(), "unit"); spc.add(user, "unit"); - spc.add(new GroupNames(group, groups), "unit"); Subject subject = new Subject.Builder().authenticated(true).principals(spc).buildSubject(); shiro.setSubject(subject); + + ImmutableSet.Builder<String> builder = ImmutableSet.builder(); + builder.add(group); + builder.add(groups); + when(groupCollector.collect(user.getName())).thenReturn(builder.build()); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java index b6fea9e897..ba23411b36 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java @@ -36,8 +36,6 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; - import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.DisabledAccountException; @@ -45,43 +43,44 @@ import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.DefaultPasswordService; -import org.apache.shiro.crypto.hash.DefaultHashService; -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.SimplePrincipalCollection; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; -import sonia.scm.user.User; -import sonia.scm.user.UserDAO; -import sonia.scm.user.UserTestData; - -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.authz.permission.WildcardPermissionResolver; +import org.apache.shiro.crypto.hash.DefaultHashService; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.SimplePrincipalCollection; import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.group.GroupDAO; +import sonia.scm.user.User; +import sonia.scm.user.UserDAO; +import sonia.scm.user.UserTestData; + +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -206,32 +205,6 @@ public class DefaultRealmTest ); } - /** - * Method description - * - */ - @Test - public void testGroupCollection() - { - User user = UserTestData.createTrillian(); - //J- - List<Group> groups = Lists.newArrayList( - new Group(DefaultRealm.REALM, "scm", user.getName()), - new Group(DefaultRealm.REALM, "developers", "perfect") - ); - //J+ - - when(groupDAO.getAll()).thenReturn(groups); - - UsernamePasswordToken token = daoUser(user, "secret"); - AuthenticationInfo info = realm.getAuthenticationInfo(token); - GroupNames groupNames = info.getPrincipals().oneByType(GroupNames.class); - - assertNotNull(groupNames); - assertThat(groupNames.getCollection(), hasSize(2)); - assertThat(groupNames, hasItems("scm", GroupNames.AUTHENTICATED)); - } - /** * Method description * @@ -251,12 +224,6 @@ public class DefaultRealmTest assertThat(collection.getRealmNames(), hasSize(1)); assertThat(collection.getRealmNames(), hasItem(DefaultRealm.REALM)); assertEquals(user, collection.oneByType(User.class)); - - GroupNames groups = collection.oneByType(GroupNames.class); - - assertNotNull(groups); - assertThat(groups.getCollection(), hasSize(1)); - assertThat(groups.getCollection(), hasItem(GroupNames.AUTHENTICATED)); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java index e2117235fc..7a8c0ef169 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java @@ -36,27 +36,25 @@ import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.Sets; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.group.ExternalGroupNames; -import java.util.Arrays; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; /** @@ -137,7 +135,6 @@ public class JwtAccessTokenBuilderTest { .issuer("https://www.scm-manager.org") .expiresIn(5, TimeUnit.SECONDS) .custom("a", "b") - .groups("one", "two", "three") .scope(Scope.valueOf("repo:*")) .build(); @@ -154,36 +151,6 @@ public class JwtAccessTokenBuilderTest { assertClaims(new JwtAccessToken(claims, compact)); } - @Test - public void testWithExternalGroups() { - applyExternalGroupsToSubject(true, "external"); - JwtAccessToken token = factory.create().subject("dent").build(); - assertArrayEquals(new String[]{"external"}, token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).map(x -> (String[]) x).get()); - } - - @Test - public void testWithInternalGroups() { - applyExternalGroupsToSubject(false, "external"); - JwtAccessToken token = factory.create().subject("dent").build(); - assertFalse(token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).isPresent()); - } - - private void applyExternalGroupsToSubject(boolean external, String... groups) { - Subject subject = spy(SecurityUtils.getSubject()); - when(subject.getPrincipals()).thenAnswer(invocation -> enrichWithGroups(invocation, groups, external)); - shiro.setSubject(subject); - } - - private Object enrichWithGroups(InvocationOnMock invocation, String[] groups, boolean external) throws Throwable { - PrincipalCollection principals = (PrincipalCollection) spy(invocation.callRealMethod()); - - List<String> groupCollection = Arrays.asList(groups); - if (external) { - when(principals.oneByType(ExternalGroupNames.class)).thenReturn(new ExternalGroupNames(groupCollection)); - } - return principals; - } - private void assertClaims(JwtAccessToken token){ assertThat(token.getId(), not(isEmptyOrNullString())); assertNotNull( token.getIssuedAt() ); @@ -194,6 +161,5 @@ public class JwtAccessTokenBuilderTest { assertEquals(token.getIssuer().get(), "https://www.scm-manager.org"); assertEquals("b", token.getCustom("a").get()); assertEquals("[\"repo:*\"]", token.getScope().toString()); - assertThat(token.getGroups(), containsInAnyOrder("one", "two", "three")); } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java index 9dbe00059e..683c446af7 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -5,8 +5,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.update.repository.MigrationStrategy; import sonia.scm.update.repository.DefaultMigrationStrategyDAO; +import sonia.scm.update.repository.MigrationStrategy; import sonia.scm.update.repository.V1Repository; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; diff --git a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java index 73c7fe6aca..6ab32f3397 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java @@ -11,10 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContextProvider; import sonia.scm.security.AssignedPermission; import sonia.scm.store.ConfigurationEntryStore; -import sonia.scm.store.ConfigurationEntryStoreFactory; -import sonia.scm.store.InMemoryConfigurationEntryStore; import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; -import sonia.scm.update.security.XmlSecurityV1UpdateStep; import javax.xml.bind.JAXBException; import java.io.IOException; diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java index 5ac8f37c01..5481e2e2a5 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java @@ -6,14 +6,12 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryType; -import javax.inject.Inject; import java.util.Arrays; import java.util.Collections; import java.util.List; diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-b-plugin.smp b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-b-plugin.smp index 4106b97945..a70205e3eb 100644 Binary files a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-b-plugin.smp and b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-b-plugin.smp differ diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-c-plugin.smp b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-c-plugin.smp index aee452fac4..b80169b9b5 100644 Binary files a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-c-plugin.smp and b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-c-plugin.smp differ diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-d-plugin.smp b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-d-plugin.smp index ec5c816c12..68509a2ee8 100644 Binary files a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-d-plugin.smp and b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-d-plugin.smp differ diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-e-plugin.smp b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-e-plugin.smp index 68b1facafe..702b5c344f 100644 Binary files a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-e-plugin.smp and b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-e-plugin.smp differ diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-f-plugin-1.0.0.smp b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-f-plugin-1.0.0.smp index cfcaae8427..8f2758f962 100644 Binary files a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-f-plugin-1.0.0.smp and b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-f-plugin-1.0.0.smp differ diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-f-plugin-1.0.1.smp b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-f-plugin-1.0.1.smp index 7132f78277..b5cac4f1e3 100644 Binary files a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-f-plugin-1.0.1.smp and b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-f-plugin-1.0.1.smp differ diff --git a/scm-webapp/src/test/resources/sonia/scm/update/security/config.xml b/scm-webapp/src/test/resources/sonia/scm/update/security/config.xml index a87e80859e..4af47193d9 100644 --- a/scm-webapp/src/test/resources/sonia/scm/update/security/config.xml +++ b/scm-webapp/src/test/resources/sonia/scm/update/security/config.xml @@ -13,7 +13,6 @@ <enableSSL>false</enableSSL> <enablePortForward>false</enablePortForward> <sslPort>8181</sslPort> - <enableRepositoryArchive>false</enableRepositoryArchive> <disableGroupingGrid>false</disableGroupingGrid> <dateFormat>Y-m-d H:i:s</dateFormat> <anonymousAccessEnabled>false</anonymousAccessEnabled>