diff --git a/Jenkinsfile b/Jenkinsfile index 8bb52d6030..e817ac2ba1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -50,6 +50,11 @@ node('docker') { def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}" if (isMainBranch()) { + + 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' archiveArtifacts 'scm-server/target/scm-server-app.*' diff --git a/pom.xml b/pom.xml index 2d8bc13f28..a7108df873 100644 --- a/pom.xml +++ b/pom.xml @@ -351,21 +351,6 @@ test - - - - - commons-beanutils - commons-beanutils - 1.9.3 - - - - commons-collections - commons-collections - 3.2.2 - - @@ -825,11 +810,11 @@ 1.2.3 3.0.1 - 2.0.1 - 3.1.3.Final + 2.1.1 + 3.6.2.Final 1.19.4 2.11.1 - 2.8.6 + 2.9.8 4.0 2.3.0 diff --git a/scm-core/pom.xml b/scm-core/pom.xml index c4bf3a2e6f..f19d50064d 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -93,6 +93,7 @@ javax.ws.rs javax.ws.rs-api + provided @@ -235,7 +236,6 @@ http://download.oracle.com/javase/6/docs/api/ http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/ - http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/ https://google.github.io/guice/api-docs/${guice.version}/javadoc http://www.slf4j.org/api/ http://shiro.apache.org/static/${shiro.version}/apidocs/ diff --git a/scm-core/src/main/java/sonia/scm/HandlerBase.java b/scm-core/src/main/java/sonia/scm/HandlerBase.java index a621f4f697..d960cc6107 100644 --- a/scm-core/src/main/java/sonia/scm/HandlerBase.java +++ b/scm-core/src/main/java/sonia/scm/HandlerBase.java @@ -36,7 +36,6 @@ package sonia.scm; //~--- JDK imports ------------------------------------------------------------ import java.io.Closeable; -import java.io.IOException; /** * The base class of all handlers. diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java index 89cc893131..eba8173de1 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; import org.mapstruct.Mapping; -public abstract class BaseMapper extends LinkAppenderMapper implements InstantAttributeMapper { +public abstract class BaseMapper extends HalAppenderMapper implements InstantAttributeMapper { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes public abstract D map(T modelObject); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java similarity index 57% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java index 162d5d6699..6759f5cb8c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -7,7 +8,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; import java.time.Instant; -import java.util.List; @Getter @Setter @@ -34,16 +34,7 @@ public class ChangesetDto extends HalRepresentation { */ private String description; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + public ChangesetDto(Links links, Embedded embedded) { + super(links, embedded); } - - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation withEmbedded(String rel, List halRepresentations) { - return super.withEmbedded(rel, halRepresentations); - } - - } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java new file mode 100644 index 0000000000..cd7d7ecebe --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java @@ -0,0 +1,14 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Context; +import org.mapstruct.Mapping; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +public interface ChangesetToChangesetDtoMapper { + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + ChangesetDto map(Changeset changeset, @Context Repository repository); + + +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppender.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java similarity index 61% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppender.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java index d3864dc798..a7beaf1f6e 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppender.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java @@ -1,12 +1,14 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.HalRepresentation; + /** - * The {@link LinkAppender} can be used within an {@link LinkEnricher} to append hateoas links to a json response. + * The {@link HalAppender} can be used within an {@link HalEnricher} to append hateoas links to a json response. * * @author Sebastian Sdorra * @since 2.0.0 */ -public interface LinkAppender { +public interface HalAppender { /** * Appends one link to the json response. @@ -14,7 +16,7 @@ public interface LinkAppender { * @param rel name of relation * @param href link uri */ - void appendOne(String rel, String href); + void appendLink(String rel, String href); /** * Returns a builder which is able to append an array of links to the resource. @@ -22,8 +24,15 @@ public interface LinkAppender { * @param rel name of link relation * @return multi link builder */ - LinkArrayBuilder arrayBuilder(String rel); + LinkArrayBuilder linkArrayBuilder(String rel); + /** + * Appends one embedded to the json response. + * + * @param rel name of relation + * @param embeddedItem embedded object + */ + void appendEmbedded(String rel, HalRepresentation embeddedItem); /** * Builder for link arrays. diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppenderMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppenderMapper.java similarity index 56% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppenderMapper.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppenderMapper.java index 7843491b71..dd49b765bc 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppenderMapper.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppenderMapper.java @@ -4,17 +4,17 @@ import com.google.common.annotations.VisibleForTesting; import javax.inject.Inject; -public class LinkAppenderMapper { +public class HalAppenderMapper { @Inject - private LinkEnricherRegistry registry; + private HalEnricherRegistry registry; @VisibleForTesting - void setRegistry(LinkEnricherRegistry registry) { + void setRegistry(HalEnricherRegistry registry) { this.registry = registry; } - protected void appendLinks(LinkAppender appender, Object source, Object... contextEntries) { + protected void applyEnrichers(HalAppender appender, Object source, Object... contextEntries) { // null check is only their to not break existing tests if (registry != null) { @@ -24,10 +24,10 @@ public class LinkAppenderMapper { ctx[i + 1] = contextEntries[i]; } - LinkEnricherContext context = LinkEnricherContext.of(ctx); + HalEnricherContext context = HalEnricherContext.of(ctx); - Iterable enrichers = registry.allByType(source.getClass()); - for (LinkEnricher enricher : enrichers) { + Iterable enrichers = registry.allByType(source.getClass()); + for (HalEnricher enricher : enrichers) { enricher.enrich(context, appender); } } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricher.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricher.java similarity index 51% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricher.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricher.java index c16d6f6482..647a1cf74e 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricher.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricher.java @@ -3,8 +3,8 @@ package sonia.scm.api.v2.resources; import sonia.scm.plugin.ExtensionPoint; /** - * A {@link LinkEnricher} can be used to append hateoas links to a specific json response. - * To register an enricher use the {@link Enrich} annotation or the {@link LinkEnricherRegistry} which is available + * A {@link HalEnricher} can be used to append hal specific attributes, such as links, to the json response. + * To register an enricher use the {@link Enrich} annotation or the {@link HalEnricherRegistry} which is available * via injection. * * Warning: enrichers are always registered as singletons. @@ -14,13 +14,13 @@ import sonia.scm.plugin.ExtensionPoint; */ @ExtensionPoint @FunctionalInterface -public interface LinkEnricher { +public interface HalEnricher { /** - * Enriches the response with hateoas links. + * Enriches the response with hal specific attributes. * * @param context contains the source for the json mapping and related objects - * @param appender can be used to append links to the json response + * @param appender can be used to append links or embedded objects to the json response */ - void enrich(LinkEnricherContext context, LinkAppender appender); + void enrich(HalEnricherContext context, HalAppender appender); } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherContext.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherContext.java similarity index 80% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherContext.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherContext.java index 2808a923e9..36128087b8 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherContext.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherContext.java @@ -7,17 +7,17 @@ import java.util.NoSuchElementException; import java.util.Optional; /** - * Context object for the {@link LinkEnricher}. The context holds the source object for the json and all related - * objects, which can be useful for the link creation. + * Context object for the {@link HalEnricher}. The context holds the source object for the json and all related + * objects, which can be useful for the enrichment. * * @author Sebastian Sdorra * @since 2.0.0 */ -public final class LinkEnricherContext { +public final class HalEnricherContext { private final Map instanceMap; - private LinkEnricherContext(Map instanceMap) { + private HalEnricherContext(Map instanceMap) { this.instanceMap = instanceMap; } @@ -28,12 +28,12 @@ public final class LinkEnricherContext { * * @return context of given entries */ - public static LinkEnricherContext of(Object... instances) { + public static HalEnricherContext of(Object... instances) { ImmutableMap.Builder builder = ImmutableMap.builder(); for (Object instance : instances) { builder.put(instance.getClass(), instance); } - return new LinkEnricherContext(builder.build()); + return new HalEnricherContext(builder.build()); } /** diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherRegistry.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherRegistry.java similarity index 53% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherRegistry.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherRegistry.java index cd95a62ec3..3fadbfa388 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherRegistry.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherRegistry.java @@ -7,34 +7,34 @@ import sonia.scm.plugin.Extension; import javax.inject.Singleton; /** - * The {@link LinkEnricherRegistry} is responsible for binding {@link LinkEnricher} instances to their source types. + * The {@link HalEnricherRegistry} is responsible for binding {@link HalEnricher} instances to their source types. * * @author Sebastian Sdorra * @since 2.0.0 */ @Extension @Singleton -public final class LinkEnricherRegistry { +public final class HalEnricherRegistry { - private final Multimap enrichers = HashMultimap.create(); + private final Multimap enrichers = HashMultimap.create(); /** - * Registers a new {@link LinkEnricher} for the given source type. + * Registers a new {@link HalEnricher} for the given source type. * * @param sourceType type of json mapping source * @param enricher link enricher instance */ - public void register(Class sourceType, LinkEnricher enricher) { + public void register(Class sourceType, HalEnricher enricher) { enrichers.put(sourceType, enricher); } /** - * Returns all registered {@link LinkEnricher} for the given type. + * Returns all registered {@link HalEnricher} for the given type. * * @param sourceType type of json mapping source * @return all registered enrichers */ - public Iterable allByType(Class sourceType) { + public Iterable allByType(Class sourceType) { return enrichers.get(sourceType); } } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java index bf20f26a7a..346ce83816 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java @@ -1,7 +1,7 @@ package sonia.scm.api.v2.resources; /** - * The {@link Index} object can be used to register a {@link LinkEnricher} for the index resource. + * The {@link Index} object can be used to register a {@link HalEnricher} for the index resource. * * @author Sebastian Sdorra * @since 2.0.0 diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java index f8f82804a6..a027a78d79 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java @@ -1,7 +1,7 @@ package sonia.scm.api.v2.resources; /** - * The {@link Me} object can be used to register a {@link LinkEnricher} for the me resource. + * The {@link Me} object can be used to register a {@link HalEnricher} for the me resource. * * @author Sebastian Sdorra * @since 2.0.0 diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PersonDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/PersonDto.java similarity index 100% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PersonDto.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/PersonDto.java diff --git a/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java b/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java index 1fa525d4fd..ef0ec8a5ef 100644 --- a/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java +++ b/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java @@ -1,24 +1,59 @@ package sonia.scm.filter; -import lombok.extern.slf4j.Slf4j; -import sonia.scm.util.WebUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerResponseContext; -import javax.ws.rs.container.ContainerResponseFilter; -import javax.ws.rs.ext.Provider; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.ext.WriterInterceptor; +import javax.ws.rs.ext.WriterInterceptorContext; import java.io.IOException; +import java.io.OutputStream; +import java.util.Locale; import java.util.zip.GZIPOutputStream; -@Provider -@Slf4j -public class GZipResponseFilter implements ContainerResponseFilter { - public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { - if (WebUtil.isGzipSupported(requestContext::getHeaderString)) { - log.trace("compress output with gzip"); - GZIPOutputStream wrappedResponse = new GZIPOutputStream(responseContext.getEntityStream()); - responseContext.getHeaders().add("Content-Encoding", "gzip"); - responseContext.setEntityStream(wrappedResponse); +@javax.ws.rs.ext.Provider +public class GZipResponseFilter implements WriterInterceptor { + + private static final Logger LOG = LoggerFactory.getLogger(GZipResponseFilter.class); + + private final Provider requestProvider; + + @Inject + public GZipResponseFilter(Provider requestProvider) { + this.requestProvider = requestProvider; + } + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + if (isGZipSupported()) { + LOG.trace("compress output with gzip"); + encodeWithGZip(context); + } else { + context.proceed(); } } + + private void encodeWithGZip(WriterInterceptorContext context) throws IOException { + context.getHeaders().remove(HttpHeaders.CONTENT_LENGTH); + context.getHeaders().add(HttpHeaders.CONTENT_ENCODING, "gzip"); + + OutputStream outputStream = context.getOutputStream(); + GZIPOutputStream compressedOutputStream = new GZIPOutputStream(outputStream); + context.setOutputStream(compressedOutputStream); + try { + context.proceed(); + } finally { + compressedOutputStream.finish(); + context.setOutputStream(outputStream); + } + } + + private boolean isGZipSupported() { + Object encoding = requestProvider.get().getHeader(HttpHeaders.ACCEPT_ENCODING); + return encoding != null && encoding.toString().toLowerCase(Locale.ENGLISH).contains("gzip"); + } } diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java b/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java index c80ae9b1c4..0723f44b6c 100644 --- a/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java +++ b/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java @@ -35,7 +35,6 @@ package sonia.scm.net.ahc; import com.google.common.base.Charsets; import com.google.common.base.Strings; -import com.google.common.collect.HashMultimap; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; 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 0bf37054a8..2d65d1cc98 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java @@ -35,8 +35,6 @@ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- -import com.google.inject.Module; - //~--- JDK imports ------------------------------------------------------------ import java.util.Collection; diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index 9941a4253b..663d053ca5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.ConfigurationException; import sonia.scm.io.CommandResult; import sonia.scm.io.ExtendedCommand; +import sonia.scm.plugin.PluginLoader; import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; @@ -67,11 +68,14 @@ public abstract class AbstractSimpleRepositoryHandler verbs; + private Set verbs; /** * Constructs a new {@link RepositoryPermission}. @@ -79,7 +79,7 @@ public class RepositoryPermission implements PermissionObject, Serializable public RepositoryPermission(String name, Collection verbs, boolean groupPermission) { this.name = name; - this.verbs = unmodifiableCollection(new LinkedHashSet<>(verbs)); + this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs)); this.groupPermission = groupPermission; } @@ -109,7 +109,8 @@ public class RepositoryPermission implements PermissionObject, Serializable final RepositoryPermission other = (RepositoryPermission) obj; return Objects.equal(name, other.name) - && CollectionUtils.isEqualCollection(verbs, other.verbs) + && verbs.containsAll(other.verbs) + && verbs.size() == other.verbs.size() && Objects.equal(groupPermission, other.groupPermission); } @@ -209,6 +210,6 @@ public class RepositoryPermission implements PermissionObject, Serializable */ public void setVerbs(Collection verbs) { - this.verbs = verbs; + this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs)); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java index baf03a0aef..45dfc0b2b7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java @@ -5,7 +5,6 @@ import com.google.common.base.Objects; import com.google.common.base.Strings; import sonia.scm.Validateable; import sonia.scm.repository.Person; -import sonia.scm.util.Util; import java.io.Serializable; diff --git a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java index a005583256..8d95131ee6 100644 --- a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java @@ -39,7 +39,6 @@ import com.google.common.base.Objects; 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; diff --git a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java index 4b2e46b665..903f86df90 100644 --- a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java @@ -34,8 +34,6 @@ package sonia.scm.security; //~--- JDK imports ------------------------------------------------------------ -import com.google.common.base.Objects; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; diff --git a/scm-core/src/main/java/sonia/scm/util/Comparables.java b/scm-core/src/main/java/sonia/scm/util/Comparables.java new file mode 100644 index 0000000000..1fb0c5e358 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/util/Comparables.java @@ -0,0 +1,88 @@ +package sonia.scm.util; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; + +public final class Comparables { + + private static final CacheLoader beanInfoCacheLoader = new CacheLoader() { + @Override + public BeanInfo load(Class type) throws IntrospectionException { + return Introspector.getBeanInfo(type); + } + }; + + private static final LoadingCache beanInfoCache = CacheBuilder.newBuilder() + .maximumSize(50) // limit the cache to avoid consuming to much memory on miss usage + .build(beanInfoCacheLoader); + + private Comparables() { + } + + public static Comparator comparator(Class type, String sortBy) { + BeanInfo info = createBeanInfo(type); + PropertyDescriptor propertyDescriptor = findPropertyDescriptor(sortBy, info); + + Method readMethod = propertyDescriptor.getReadMethod(); + checkIfPropertyIsComparable(readMethod, sortBy); + + return new MethodComparator<>(readMethod); + } + + private static void checkIfPropertyIsComparable(Method readMethod, String sortBy) { + checkArgument(isReturnTypeComparable(readMethod), "property %s is not comparable", sortBy); + } + + private static boolean isReturnTypeComparable(Method readMethod) { + return Comparable.class.isAssignableFrom(readMethod.getReturnType()); + } + + private static PropertyDescriptor findPropertyDescriptor(String sortBy, BeanInfo info) { + PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors(); + + Optional sortByPropertyDescriptor = Arrays.stream(propertyDescriptors) + .filter(p -> p.getName().equals(sortBy)) + .findFirst(); + + return sortByPropertyDescriptor.orElseThrow(() -> new IllegalArgumentException("could not find property " + sortBy)); + } + + private static BeanInfo createBeanInfo(Class type) { + return beanInfoCache.getUnchecked(type); + } + + private static class MethodComparator implements Comparator { + + private final Method readMethod; + + private MethodComparator(Method readMethod) { + this.readMethod = readMethod; + } + + @Override + @SuppressWarnings("unchecked") + public int compare(T left, T right) { + try { + Comparable leftResult = (Comparable) readMethod.invoke(left); + Comparable rightResult = (Comparable) readMethod.invoke(right); + return leftResult.compareTo(rightResult); + } catch (IllegalAccessException | InvocationTargetException ex) { + throw new IllegalArgumentException("failed to invoke read method", ex); + } + } + } + +} diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkAppenderMapperTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java similarity index 53% rename from scm-core/src/test/java/sonia/scm/api/v2/resources/LinkAppenderMapperTest.java rename to scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java index 557eac2020..ff658cc26a 100644 --- a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkAppenderMapperTest.java +++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java @@ -11,51 +11,51 @@ import java.util.Optional; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) -class LinkAppenderMapperTest { +class HalAppenderMapperTest { @Mock - private LinkAppender appender; + private HalAppender appender; - private LinkEnricherRegistry registry; - private LinkAppenderMapper mapper; + private HalEnricherRegistry registry; + private HalAppenderMapper mapper; @BeforeEach void beforeEach() { - registry = new LinkEnricherRegistry(); - mapper = new LinkAppenderMapper(); + registry = new HalEnricherRegistry(); + mapper = new HalAppenderMapper(); mapper.setRegistry(registry); } @Test void shouldAppendSimpleLink() { - registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com")); + registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com")); - mapper.appendLinks(appender, "hello"); + mapper.applyEnrichers(appender, "hello"); - verify(appender).appendOne("42", "https://hitchhiker.com"); + verify(appender).appendLink("42", "https://hitchhiker.com"); } @Test void shouldCallMultipleEnrichers() { - registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com")); - registry.register(String.class, (ctx, appender) -> appender.appendOne("21", "https://scm.hitchhiker.com")); + registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com")); + registry.register(String.class, (ctx, appender) -> appender.appendLink("21", "https://scm.hitchhiker.com")); - mapper.appendLinks(appender, "hello"); + mapper.applyEnrichers(appender, "hello"); - verify(appender).appendOne("42", "https://hitchhiker.com"); - verify(appender).appendOne("21", "https://scm.hitchhiker.com"); + verify(appender).appendLink("42", "https://hitchhiker.com"); + verify(appender).appendLink("21", "https://scm.hitchhiker.com"); } @Test void shouldAppendLinkByUsingSourceFromContext() { registry.register(String.class, (ctx, appender) -> { Optional rel = ctx.oneByType(String.class); - appender.appendOne(rel.get(), "https://hitchhiker.com"); + appender.appendLink(rel.get(), "https://hitchhiker.com"); }); - mapper.appendLinks(appender, "42"); + mapper.applyEnrichers(appender, "42"); - verify(appender).appendOne("42", "https://hitchhiker.com"); + verify(appender).appendLink("42", "https://hitchhiker.com"); } @Test @@ -63,12 +63,12 @@ class LinkAppenderMapperTest { registry.register(Integer.class, (ctx, appender) -> { Optional rel = ctx.oneByType(Integer.class); Optional href = ctx.oneByType(String.class); - appender.appendOne(String.valueOf(rel.get()), href.get()); + appender.appendLink(String.valueOf(rel.get()), href.get()); }); - mapper.appendLinks(appender, Integer.valueOf(42), "https://hitchhiker.com"); + mapper.applyEnrichers(appender, Integer.valueOf(42), "https://hitchhiker.com"); - verify(appender).appendOne("42", "https://hitchhiker.com"); + verify(appender).appendLink("42", "https://hitchhiker.com"); } } diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherContextTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java similarity index 72% rename from scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherContextTest.java rename to scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java index 6eb7bb4c84..1aecb5ad46 100644 --- a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherContextTest.java +++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java @@ -7,17 +7,17 @@ import org.junit.jupiter.api.Test; import java.util.NoSuchElementException; -class LinkEnricherContextTest { +class HalEnricherContextTest { @Test void shouldCreateContextFromSingleObject() { - LinkEnricherContext context = LinkEnricherContext.of("hello"); + HalEnricherContext context = HalEnricherContext.of("hello"); assertThat(context.oneByType(String.class)).contains("hello"); } @Test void shouldCreateContextFromMultipleObjects() { - LinkEnricherContext context = LinkEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L)); + HalEnricherContext context = HalEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L)); assertThat(context.oneByType(String.class)).contains("hello"); assertThat(context.oneByType(Integer.class)).contains(42); assertThat(context.oneByType(Long.class)).contains(21L); @@ -25,19 +25,19 @@ class LinkEnricherContextTest { @Test void shouldReturnEmptyOptionalForUnknownTypes() { - LinkEnricherContext context = LinkEnricherContext.of(); + HalEnricherContext context = HalEnricherContext.of(); assertThat(context.oneByType(String.class)).isNotPresent(); } @Test void shouldReturnRequiredObject() { - LinkEnricherContext context = LinkEnricherContext.of("hello"); + HalEnricherContext context = HalEnricherContext.of("hello"); assertThat(context.oneRequireByType(String.class)).isEqualTo("hello"); } @Test void shouldThrowAnNoSuchElementExceptionForUnknownTypes() { - LinkEnricherContext context = LinkEnricherContext.of(); + HalEnricherContext context = HalEnricherContext.of(); assertThrows(NoSuchElementException.class, () -> context.oneRequireByType(String.class)); } diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherRegistryTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java similarity index 53% rename from scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherRegistryTest.java rename to scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java index 07441003d7..6a863d2f04 100644 --- a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherRegistryTest.java +++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java @@ -5,54 +5,54 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -class LinkEnricherRegistryTest { +class HalEnricherRegistryTest { - private LinkEnricherRegistry registry; + private HalEnricherRegistry registry; @BeforeEach void setUpObjectUnderTest() { - registry = new LinkEnricherRegistry(); + registry = new HalEnricherRegistry(); } @Test void shouldRegisterTheEnricher() { - SampleLinkEnricher enricher = new SampleLinkEnricher(); + SampleHalEnricher enricher = new SampleHalEnricher(); registry.register(String.class, enricher); - Iterable enrichers = registry.allByType(String.class); + Iterable enrichers = registry.allByType(String.class); assertThat(enrichers).containsOnly(enricher); } @Test void shouldRegisterMultipleEnrichers() { - SampleLinkEnricher one = new SampleLinkEnricher(); + SampleHalEnricher one = new SampleHalEnricher(); registry.register(String.class, one); - SampleLinkEnricher two = new SampleLinkEnricher(); + SampleHalEnricher two = new SampleHalEnricher(); registry.register(String.class, two); - Iterable enrichers = registry.allByType(String.class); + Iterable enrichers = registry.allByType(String.class); assertThat(enrichers).containsOnly(one, two); } @Test void shouldRegisterEnrichersForDifferentTypes() { - SampleLinkEnricher one = new SampleLinkEnricher(); + SampleHalEnricher one = new SampleHalEnricher(); registry.register(String.class, one); - SampleLinkEnricher two = new SampleLinkEnricher(); + SampleHalEnricher two = new SampleHalEnricher(); registry.register(Integer.class, two); - Iterable enrichers = registry.allByType(String.class); + Iterable enrichers = registry.allByType(String.class); assertThat(enrichers).containsOnly(one); enrichers = registry.allByType(Integer.class); assertThat(enrichers).containsOnly(two); } - private static class SampleLinkEnricher implements LinkEnricher { + private static class SampleHalEnricher implements HalEnricher { @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { + public void enrich(HalEnricherContext context, HalAppender appender) { } } diff --git a/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java b/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java new file mode 100644 index 0000000000..c3648fd4b8 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java @@ -0,0 +1,87 @@ +package sonia.scm.filter; + +import com.google.inject.util.Providers; +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 javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.WriterInterceptorContext; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GZipResponseFilterTest { + + @Mock + private HttpServletRequest request; + + @Mock + private WriterInterceptorContext context; + + @Mock + private MultivaluedMap headers; + + private GZipResponseFilter filter; + + @BeforeEach + void setupObjectUnderTest() { + filter = new GZipResponseFilter(Providers.of(request)); + } + + @Test + void shouldSkipGZipCompression() throws IOException { + when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("deflate, br"); + + filter.aroundWriteTo(context); + + verifySkipped(); + } + + @Test + void shouldSkipGZipCompressionWithoutAcceptEncodingHeader() throws IOException { + filter.aroundWriteTo(context); + + verifySkipped(); + } + + private void verifySkipped() throws IOException { + verify(context, never()).getOutputStream(); + verify(context).proceed(); + } + + + @Nested + class AcceptGZipEncoding { + + @BeforeEach + void setUpContext() { + when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("gzip, deflate, br"); + when(context.getHeaders()).thenReturn(headers); + when(context.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + } + + @Test + void shouldEncode() throws IOException { + filter.aroundWriteTo(context); + + verify(headers).remove(HttpHeaders.CONTENT_LENGTH); + verify(headers).add(HttpHeaders.CONTENT_ENCODING, "gzip"); + + verify(context).setOutputStream(any(GZIPOutputStream.class)); + verify(context, times(2)).setOutputStream(any(OutputStream.class)); + } + + } + + +} diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java index b7fa9ae84a..92ca488ddf 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java @@ -36,7 +36,7 @@ import com.google.common.io.ByteSource; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.UnsupportedEncodingException; + import org.junit.Test; import static org.junit.Assert.*; import static org.hamcrest.Matchers.*; diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java index 2e9383b2e2..d65358a66e 100644 --- a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java @@ -46,4 +46,13 @@ class RepositoryPermissionTest { assertThat(permission1).isNotEqualTo(permission2); } + + @Test + void shouldBeEqualWithRedundantVerbs() { + RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one", "two"), false); + RepositoryPermission permission2 = new RepositoryPermission("name1", asList("one", "two"), false); + permission2.setVerbs(asList("one", "two", "two")); + + assertThat(permission1).isEqualTo(permission2); + } } 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 af4bf37915..7ddeabd8ac 100644 --- a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java @@ -1,7 +1,6 @@ package sonia.scm.security; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.UnknownAccountException; diff --git a/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java b/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java new file mode 100644 index 0000000000..50ae2254e6 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java @@ -0,0 +1,57 @@ +package sonia.scm.util; + +import org.junit.jupiter.api.Test; + +import java.util.Comparator; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ComparablesTest { + + @Test + void shouldCompare() { + One a = new One("a"); + One b = new One("b"); + + Comparator comparable = Comparables.comparator(One.class, "value"); + assertThat(comparable.compare(a, b)).isEqualTo(-1); + } + + @Test + void shouldThrowAnExceptionForNonExistingField() { + assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "awesome")); + } + + @Test + void shouldThrowAnExceptionForNonComparableField() { + assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "nonComparable")); + } + + @Test + void shouldThrowAnExceptionIfTheFieldHasNoGetter() { + assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "incredible")); + } + + private static class One { + + private String value; + private String incredible; + private NonComparable nonComparable; + + One(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public NonComparable getNonComparable() { + return nonComparable; + } + } + + private static class NonComparable {} + +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java index 5b5c00a298..579ef75b71 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java @@ -37,9 +37,6 @@ package sonia.scm.store; import com.google.inject.Inject; import com.google.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.security.KeyGenerator; diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java index 1ce0508616..5b24096eb5 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java @@ -38,7 +38,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.GenericDAO; import sonia.scm.ModelObject; -import sonia.scm.group.xml.XmlGroupDAO; import sonia.scm.store.ConfigurationStore; import java.util.Collection; diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java b/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java index d812eedc35..4b3d9b0f28 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java @@ -1,6 +1,5 @@ package sonia.scm.xml; -import com.google.common.base.Charsets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java index 3c67ca3dc3..c49a65bea2 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java @@ -36,7 +36,6 @@ package sonia.scm.it; import org.apache.http.HttpStatus; import org.assertj.core.api.Assertions; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java index 9386f1d9c5..0a5693ad2e 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java @@ -5,10 +5,8 @@ import io.restassured.response.Response; import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.user.User; import sonia.scm.web.VndMediaType; -import java.net.ConnectException; import java.util.List; import java.util.Map; import java.util.function.Consumer; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java index e078b04b08..7cda4bc9d3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -18,8 +18,6 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; -import static sonia.scm.ContextEntry.ContextBuilder.entity; - /** * RESTful Web Service Resource to manage the configuration of the git plugin. */ diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 95225c9e30..63800e8a02 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -44,6 +44,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.spi.GitRepositoryServiceProvider; import sonia.scm.schedule.Scheduler; import sonia.scm.schedule.Task; @@ -103,9 +104,10 @@ public class GitRepositoryHandler public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, Scheduler scheduler, RepositoryLocationResolver repositoryLocationResolver, - GitWorkdirFactory workdirFactory) + GitWorkdirFactory workdirFactory, + PluginLoader pluginLoader) { - super(storeFactory, repositoryLocationResolver); + super(storeFactory, repositoryLocationResolver, pluginLoader); this.scheduler = scheduler; this.workdirFactory = workdirFactory; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java index a2114a1b6a..6db7d694d5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java @@ -35,7 +35,6 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.inject.Inject; import org.eclipse.jgit.errors.RepositoryNotFoundException; diff --git a/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js b/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js index e34a0ef96f..aadb58eed6 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js +++ b/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js @@ -2,7 +2,7 @@ import React from "react"; -import {apiClient, BranchSelector, ErrorPage, Loading, SubmitButton} from "@scm-manager/ui-components"; +import {apiClient, BranchSelector, ErrorPage, Loading, Subtitle, SubmitButton} from "@scm-manager/ui-components"; import type {Branch, Repository} from "@scm-manager/ui-types"; import {translate} from "react-i18next"; @@ -113,6 +113,7 @@ class RepositoryConfig extends React.Component { if (!(loadingBranches || loadingDefaultBranch)) { return ( <> + {this.renderBranchChangedNotification()}
{ disabled={!this.state.selectedBranchName} /> +
); } else { diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.js b/scm-plugins/scm-git-plugin/src/main/js/index.js index a066247dde..43e3950beb 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/index.js +++ b/scm-plugins/scm-git-plugin/src/main/js/index.js @@ -27,14 +27,9 @@ binder.bind( ); binder.bind("repos.repository-avatar", GitAvatar, gitPredicate); -cfgBinder.bindRepository( - "/configuration", - "scm-git-plugin.repo-config.link", - "configuration", - RepositoryConfig -); -// global config +binder.bind("repo-config.route", RepositoryConfig, gitPredicate); +// global config cfgBinder.bindGlobal( "/git", "scm-git-plugin.config.link", diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json index 1dc0e254c2..cd88897e74 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json @@ -1,9 +1,56 @@ { "scm-git-plugin": { "information": { - "clone" : "Repository Klonen", - "create" : "Neue Repository erstellen", - "replace" : "Eine existierende Repository aktualisieren" + "clone" : "Repository klonen", + "create" : "Neues Repository erstellen", + "replace" : "Ein bestehendes Repository aktualisieren", + "merge": { + "heading": "Merge des Source Branch in den Target Branch", + "checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.", + "update": "2. Update Workspace", + "merge": "3. Merge Source Branch", + "resolve": "4. Merge Konflikte auflösen und korrigierte Dateien dem Index hinzufügen.", + "commit": "5. Commit", + "push": "6. Push des Merge" + } + }, + "config": { + "link": "Git", + "title": "Git Konfiguration", + "gcExpression": "GC Cron Ausdruck", + "gcExpressionHelpText": "Benutze Quartz Cron Ausdrücke (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK), um git GC regelmäßig auszuführen.", + "nonFastForwardDisallowed": "Deaktiviere \"Non Fast-Forward\"", + "nonFastForwardDisallowedHelpText": "Git Pushes ablehnen, die nicht \"fast-forward\" sind, wie \"--force\".", + "disabled": "Deaktiviert", + "disabledHelpText": "Aktiviere oder deaktiviere das Git Plugin", + "submit": "Speichern" + }, + "repo-config": { + "link": "Konfiguration", + "title": "Git Einstellungen", + "default-branch": "Standard Branch", + "submit": "Speichern", + "error": { + "title": "Fehler", + "subtitle": "Ein Fehler ist aufgetreten." + }, + "success": "Der standard Branch wurde geändert!" + } + }, + "permissions" : { + "configuration": { + "read": { + "git": { + "displayName": "Git Konfiguration lesen", + "description": "Darf die git Konfiguration lesen." + } + }, + "write": { + "git": { + "displayName": "Git Konfiguration schreiben", + "description": "Darf die git Konfiguration verändern." + } + } } } } diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json index 2b579801dd..551573fb72 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json @@ -27,6 +27,7 @@ }, "repo-config": { "link": "Configuration", + "title": "Git Settings", "default-branch": "Default branch", "submit": "Submit", "error": { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java index ed34db3008..6a05875aa9 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; import static org.junit.Assert.*; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java index 0c28a28e59..8e657b1050 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java @@ -17,7 +17,7 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.GitRepositoryHandler; @@ -29,6 +29,7 @@ import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.web.GitVndMediaType; import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; @@ -100,7 +101,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "readWrite") - public void shouldGetGitConfig() throws URISyntaxException { + public void shouldGetGitConfig() throws URISyntaxException, UnsupportedEncodingException { MockHttpResponse response = get(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); @@ -115,7 +116,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "readWrite") - public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException { + public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException, UnsupportedEncodingException { when(repositoryHandler.getConfig()).thenReturn(null); MockHttpResponse response = get(); @@ -126,7 +127,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldGetGitConfigWithoutUpdateLink() throws URISyntaxException { + public void shouldGetGitConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpResponse response = get(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); @@ -159,7 +160,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "writeOnly") - public void shouldReadDefaultRepositoryConfig() throws URISyntaxException { + public void shouldReadDefaultRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException { when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X"); @@ -176,7 +177,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldNotHaveUpdateLinkForReadOnlyUser() throws URISyntaxException { + public void shouldNotHaveUpdateLinkForReadOnlyUser() throws URISyntaxException, UnsupportedEncodingException { when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X"); @@ -193,7 +194,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "writeOnly") - public void shouldReadStoredRepositoryConfig() throws URISyntaxException { + public void shouldReadStoredRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException { when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); GitRepositoryConfig gitRepositoryConfig = new GitRepositoryConfig(); gitRepositoryConfig.setDefaultBranch("test"); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java index 40cf36e8dd..62fa8d33b4 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java @@ -11,10 +11,9 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; -import java.io.File; import java.net.URI; import static org.junit.Assert.assertEquals; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java index d2942d08a3..a1e349dd57 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java @@ -14,9 +14,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.api.Command; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.JsonEnricherContext; import sonia.scm.web.VndMediaType; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java index 23b3110567..3362c8a22b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java @@ -40,7 +40,7 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.io.File; import java.io.IOException; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java index cb10e15271..66ec320067 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java @@ -94,7 +94,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { RepositoryLocationResolver locationResolver, File directory) { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - scheduler, locationResolver, gitWorkdirFactory); + scheduler, locationResolver, gitWorkdirFactory, null); repositoryHandler.init(contextProvider); GitConfig config = new GitConfig(); @@ -108,7 +108,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Test public void getDirectory() { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - scheduler, locationResolver, gitWorkdirFactory); + scheduler, locationResolver, gitWorkdirFactory, null); GitConfig config = new GitConfig(); config.setDisabled(false); config.setGcExpression("gc exp"); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java index 630236b20b..f2a4ed4954 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java @@ -35,10 +35,8 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import org.junit.After; -import org.junit.Before; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.GitRepositoryConfig; -import sonia.scm.store.InMemoryConfigurationStore; import sonia.scm.store.InMemoryConfigurationStoreFactory; /** diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java index 817e4641dd..c8d260d503 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java @@ -35,11 +35,9 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; -import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.BlameLine; import sonia.scm.repository.BlameResult; import sonia.scm.repository.GitRepositoryConfig; -import sonia.scm.store.InMemoryConfigurationStoreFactory; import java.io.IOException; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 2ff3c73420..1feceba652 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -32,11 +32,9 @@ package sonia.scm.repository.spi; import org.junit.Test; -import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.GitRepositoryConfig; -import sonia.scm.store.InMemoryConfigurationStoreFactory; import java.io.IOException; import java.util.Collection; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java index e2ab85d9a7..06e9b17fe7 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java @@ -36,10 +36,8 @@ package sonia.scm.repository.spi; import com.google.common.io.Files; import org.junit.Test; -import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.ClearRepositoryCacheEvent; import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.Modifications; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java index dc0822deba..4ed9d5a46a 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java @@ -42,7 +42,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryHandler; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java index 785aa399b1..27fdc7a296 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java @@ -35,14 +35,12 @@ package sonia.scm.installer; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; -import sonia.scm.util.IOUtil; //~--- JDK imports ------------------------------------------------------------ import java.io.File; -import java.io.IOException; + import sonia.scm.net.ahc.AdvancedHttpClient; /** diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java index 4b6998f09a..b1d431c742 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java @@ -39,7 +39,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.io.INIConfiguration; import sonia.scm.io.INIConfigurationReader; -import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.util.ValidationUtil; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index c2c0439fc1..7db9a8becb 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -51,6 +51,7 @@ import sonia.scm.io.INIConfigurationReader; import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.spi.HgRepositoryServiceProvider; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.IOUtil; @@ -111,9 +112,10 @@ public class HgRepositoryHandler @Inject public HgRepositoryHandler(ConfigurationStoreFactory storeFactory, Provider hgContextProvider, - RepositoryLocationResolver repositoryLocationResolver) + RepositoryLocationResolver repositoryLocationResolver, + PluginLoader pluginLoader) { - super(storeFactory, repositoryLocationResolver); + super(storeFactory, repositoryLocationResolver, pluginLoader); this.hgContextProvider = hgContextProvider; try diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index f351ffa572..0897a191a1 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -41,7 +41,6 @@ import com.aragost.javahg.internals.AbstractCommand; import com.aragost.javahg.internals.HgInputStream; import com.google.common.base.Strings; -import com.google.common.collect.Lists; import sonia.scm.repository.FileObject; import sonia.scm.repository.SubRepository; @@ -52,7 +51,6 @@ import java.io.IOException; import java.util.Deque; import java.util.LinkedList; -import java.util.List; /** * Mercurial command to list files of a repository. diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index 93b9699fc9..18b716b665 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -44,7 +44,6 @@ import sonia.scm.web.filter.PermissionFilter; import sonia.scm.repository.HgRepositoryHandler; -import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.util.Set; diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json index 0824a4ad38..37d6d4be2a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json @@ -1,9 +1,48 @@ { "scm-hg-plugin": { "information": { - "clone" : "Repository Klonen", - "create" : "Neue Repository erstellen", - "replace" : "Eine existierende Repository aktualisieren" + "clone" : "Repository klonen", + "create" : "Neues Repository erstellen", + "replace" : "Ein bestehendes Repository aktualisieren" + }, + "config": { + "link": "Mercurial", + "title": "Mercurial Konfiguration", + "hgBinary": "HG Binary", + "hgBinaryHelpText": "Pfad des Mercurial Binary.", + "pythonBinary": "Python Binary", + "pythonBinaryHelpText": "Pfad des Python binary.", + "pythonPath": "Python Module Such Pfad", + "pythonPathHelpText": "Python Module Such Pfad (PYTHONPATH).", + "encoding": "Encoding", + "encodingHelpText": "Repository Encoding.", + "useOptimizedBytecode": "Optimized Bytecode (.pyo)", + "useOptimizedBytecodeHelpText": "Verwende den Python '-O' Switch.", + "showRevisionInId": "Revision anzeigen", + "showRevisionInIdHelpText": "Die Revision als Teil der Node ID anzeigen.", + "enableHttpPostArgs": "HttpPostArgs Protocol aktivieren", + "enableHttpPostArgsHelpText": "Aktiviert das experimentelle HttpPostArgs Protokoll von Mercurial. Das HttpPostArgs Protokoll verwendet den Post Request Body anstatt des HTTP Headers um Meta Informationen zu versenden. Dieses Vorgehen reduziert die Header Größe der Mercurial Requests. HttpPostArgs wird seit Mercurial 3.8 unterstützt.", + "disableHookSSLValidation": "SSL Validierung für Hooks deaktivieren", + "disableHookSSLValidationHelpText": "Deaktiviert die Validierung von SSL Zertifikaten für den Mercurial Hook, der die Repositoryänderungen wieder zurück an den SCM-Manager leitet. Diese Option sollte nur benutzt werden, wenn der SCM-Manager ein selbstsigniertes Zertifikat verwendet.", + "disabled": "Deaktiviert", + "disabledHelpText": "Aktiviert oder deaktiviert das Mercurial Plugin.", + "required": "Dieser Konfigurationswert wird benötigt" + } + }, + "permissions" : { + "configuration": { + "read": { + "hg": { + "displayName": "Mercurial Konfiguration lesen", + "description": "Darf die Mercurial Konfiguration lesen" + } + }, + "write": { + "hg": { + "displayName": "Mercurial Konfiguration schreiben", + "description": "Darf die Mercurial Konfiguration verändern" + } + } } } } diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json index ee1b6c8911..61340ab9cf 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json @@ -21,9 +21,9 @@ "showRevisionInId": "Show Revision", "showRevisionInIdHelpText": "Show revision as part of the node id.", "enableHttpPostArgs": "Enable HttpPostArgs Protocol", - "enableHttpPostArgsHelpText": "Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. This option should only be used, if SCM-Manager uses a self signed certificate.", + "enableHttpPostArgsHelpText": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.", "disableHookSSLValidation": "Disable SSL Validation on Hooks", - "disableHookSSLValidationHelpText": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.", + "disableHookSSLValidationHelpText": "Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. This option should only be used, if SCM-Manager uses a self signed certificate.", "disabled": "Disabled", "disabledHelpText": "Enable or disable the Mercurial plugin.", "required": "This configuration value is required" diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java index 4b66444bbe..1f88bfe665 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java @@ -14,7 +14,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java index 524e33e265..6e181f4886 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java @@ -3,11 +3,9 @@ package sonia.scm.api.v2.resources; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.HgConfig; -import java.io.File; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java index 65b9c262cb..bcd9543d28 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java @@ -14,7 +14,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import javax.inject.Provider; import javax.servlet.http.HttpServletResponse; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java index 7cae1d9f7e..80f8ec32b1 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java @@ -6,7 +6,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.net.URI; import java.util.Arrays; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java index f1558b6efb..473ddfe4b4 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java @@ -17,7 +17,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.installer.HgPackage; import sonia.scm.installer.HgPackageReader; import sonia.scm.net.ahc.AdvancedHttpClient; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java index c4431da6d5..0b5d7b14d0 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java @@ -6,7 +6,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.installer.HgPackage; import sonia.scm.installer.HgPackages; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java index df59954971..e0253ad86a 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java @@ -16,15 +16,15 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; import javax.inject.Provider; import javax.servlet.http.HttpServletResponse; -import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; @@ -99,7 +99,7 @@ public class HgConfigResourceTest { @Test @SubjectAware(username = "readWrite") - public void shouldGetHgConfigEvenWhenItsEmpty() throws URISyntaxException { + public void shouldGetHgConfigEvenWhenItsEmpty() throws URISyntaxException, UnsupportedEncodingException { when(repositoryHandler.getConfig()).thenReturn(null); MockHttpResponse response = get(); @@ -110,7 +110,7 @@ public class HgConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldGetHgConfigWithoutUpdateLink() throws URISyntaxException { + public void shouldGetHgConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpResponse response = get(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java index 84343cdf72..a3430aac43 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java @@ -3,8 +3,6 @@ package sonia.scm.api.v2.resources; import sonia.scm.installer.HgPackage; import sonia.scm.repository.HgConfig; -import java.io.File; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java index 81c50f3d58..d4bc8be549 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java @@ -11,7 +11,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.HgConfig; import java.net.URI; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java index c45d9ab358..ed222f5119 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java @@ -77,7 +77,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Override protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) { - HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver); + HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null); handler.init(contextProvider); HgTestUtil.checkForSkip(handler); @@ -87,7 +87,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Test public void getDirectory() { - HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver); + HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null); HgConfig hgConfig = new HgConfig(); hgConfig.setHgBinary("hg"); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java index 68f7e18a76..131dad0837 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java @@ -105,7 +105,7 @@ public final class HgTestUtil RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(context, repoDao, new InitialRepositoryLocationResolver()); HgRepositoryHandler handler = - new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver); + new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null); Path repoDir = directory.toPath(); when(repoDao.getPath(any())).thenReturn(repoDir); handler.init(context); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java index 7d74024630..efe9983951 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java @@ -8,7 +8,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import static org.mockito.Matchers.anyInt; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java index b3a4a0c2a4..f9bc77bbda 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java @@ -48,8 +48,6 @@ import javax.servlet.http.HttpServletRequest; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import static sonia.scm.web.WireProtocolRequestMockFactory.CMDS_HEADS_KNOWN_NODES; import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.BOOKMARKS; import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.PHASES; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java index 519dadfd6c..9237127c88 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java @@ -37,7 +37,7 @@ import com.google.common.collect.Lists; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java index 86f99cd517..639a16968c 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java @@ -53,9 +53,9 @@ import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.logging.SVNKitLogger; import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.SvnRepositoryServiceProvider; -import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.Util; @@ -97,9 +97,10 @@ public class SvnRepositoryHandler @Inject public SvnRepositoryHandler(ConfigurationStoreFactory storeFactory, HookEventFacade eventFacade, - RepositoryLocationResolver repositoryLocationResolver) + RepositoryLocationResolver repositoryLocationResolver, + PluginLoader pluginLoader) { - super(storeFactory, repositoryLocationResolver); + super(storeFactory, repositoryLocationResolver, pluginLoader); // register logger SVNDebugLog.setDefaultLog(new SVNKitLogger()); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index e2f58b593b..df266a11af 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -36,7 +36,6 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Strings; -import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tmatesoft.svn.core.SVNDirEntry; @@ -53,7 +52,6 @@ import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; import java.util.Collection; -import java.util.List; //~--- JDK imports ------------------------------------------------------------ diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java index 4b4f655b12..580bc0b77d 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java @@ -4,6 +4,8 @@ import lombok.extern.slf4j.Slf4j; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.io.SVNRepository; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.admin.SVNLookClient; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Modifications; import sonia.scm.repository.Repository; @@ -19,23 +21,45 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif super(context, repository); } - @Override - @SuppressWarnings("unchecked") - public Modifications getModifications(String revision) { - Modifications modifications = null; - log.debug("get modifications {}", revision); + public Modifications getModifications(String revisionOrTransactionId) { + Modifications modifications; try { - long revisionNumber = SvnUtil.parseRevision(revision, repository); - SVNRepository repo = open(); - Collection entries = repo.log(null, null, revisionNumber, - revisionNumber, true, true); - if (Util.isNotEmpty(entries)) { - modifications = SvnUtil.createModifications(entries.iterator().next(), revision); + if (SvnUtil.isTransactionEntryId(revisionOrTransactionId)) { + modifications = getModificationsFromTransaction(SvnUtil.getTransactionId(revisionOrTransactionId)); + } else { + modifications = getModificationFromRevision(revisionOrTransactionId); } + return modifications; } catch (SVNException ex) { - throw new InternalRepositoryException(repository, "could not open repository", ex); + throw new InternalRepositoryException( + repository, + "failed to get svn modifications for " + revisionOrTransactionId, + ex + ); } + } + + @SuppressWarnings("unchecked") + private Modifications getModificationFromRevision(String revision) throws SVNException { + log.debug("get svn modifications from revision: {}", revision); + long revisionNumber = SvnUtil.getRevisionNumber(revision, repository); + SVNRepository repo = open(); + Collection entries = repo.log(null, null, revisionNumber, + revisionNumber, true, true); + if (Util.isNotEmpty(entries)) { + return SvnUtil.createModifications(entries.iterator().next(), revision); + } + return null; + } + + private Modifications getModificationsFromTransaction(String transaction) throws SVNException { + log.debug("get svn modifications from transaction: {}", transaction); + final Modifications modifications = new Modifications(); + SVNLookClient client = SVNClientManager.newInstance().getLookClient(); + client.doGetChanged(context.getDirectory(), transaction, + e -> SvnUtil.appendModification(modifications, e.getType(), e.getPath()), true); + return modifications; } @@ -44,5 +68,4 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif return getModifications(request.getRevision()); } - } diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json index 7c58498ef1..58a18482b2 100644 --- a/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json @@ -1,7 +1,42 @@ { "scm-svn-plugin": { "information": { - "checkout" : "Repository auschecken" + "checkout": "Repository auschecken" + }, + "config": { + "link": "Subversion", + "title": "Subversion Konfiguration", + "compatibility": "Version Kompatibilität", + "compatibilityHelpText": "Gibt an, mit welcher Subversion Version die Repositories kompatibel sind.", + "compatibility-values": { + "none": "Keine Kompatibilität", + "pre14": "Vor 1.4 kompatibel", + "pre15": "Vor 1.5 kompatibel", + "pre16": "Vor 1.6 kompatibel", + "pre17": "Vor 1.7 kompatibel", + "with17": "Mit 1.7 kompatibel" + }, + "enabledGZip": "GZip Compression aktivieren", + "enabledGZipHelpText": "Aktiviert GZip Kompression für SVN Responses", + "disabled": "Deaktiviert", + "disabledHelpText": "Aktiviert oder deaktiviert das SVN Plugin", + "required": "Dieser Konfigurationswert wird benötigt" + } + }, + "permissions": { + "configuration": { + "read": { + "svn": { + "displayName": "Subversion Konfiguration lesen", + "description": "Darf die Subversion Konfiguration lesen" + } + }, + "write": { + "svn": { + "displayName": "Subversion Konfiguration schreiben", + "description": "Darf die Subversion Konfiguration verändern" + } + } } } } diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json index 2a363c77cd..a796027afc 100644 --- a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json @@ -7,7 +7,7 @@ "link": "Subversion", "title": "Subversion Configuration", "compatibility": "Version Compatibility", - "compatibilityHelpText": "Specifies with which subversion version repositories are compatible.", + "compatibilityHelpText": "Specifies with which Subversion version repositories are compatible.", "compatibility-values": { "none": "No compatibility", "pre14": "Pre 1.4 Compatible", @@ -17,9 +17,9 @@ "with17": "With 1.7 Compatible" }, "enabledGZip": "Enable GZip Compression", - "enabledGZipHelpText": "Enable GZip compression for svn responses.", + "enabledGZipHelpText": "Enable GZip compression for SVN responses.", "disabled": "Disabled", - "disabledHelpText": "Enable or disable the Git plugin", + "disabledHelpText": "Enable or disable the SVN plugin", "required": "This configuration value is required" } }, diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java index 8ab947fbaf..27ca6d5635 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Compatibility; import sonia.scm.repository.SvnConfig; diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java index 3077bb34f3..f7ccf039b2 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java @@ -16,14 +16,14 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.SvnConfig; import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.web.SvnVndMediaType; import javax.servlet.http.HttpServletResponse; -import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; @@ -98,7 +98,7 @@ public class SvnConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldGetSvnConfigWithoutUpdateLink() throws URISyntaxException { + public void shouldGetSvnConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpResponse response = get(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java index 6bbff499e1..07ead15322 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java @@ -11,11 +11,10 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Compatibility; import sonia.scm.repository.SvnConfig; -import java.io.File; import java.net.URI; import static org.junit.Assert.assertEquals; diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java index 7b22e15c94..c81c6311e1 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java @@ -32,14 +32,10 @@ package sonia.scm.repository; -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.repository.api.HookContextFactory; import sonia.scm.repository.spi.HookEventFacade; -import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; @@ -47,7 +43,7 @@ import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -93,7 +89,7 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) { - SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, locationResolver); + SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, locationResolver, null); handler.init(contextProvider); @@ -109,7 +105,7 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { public void getDirectory() { when(factory.withType(any())).thenCallRealMethod(); SvnRepositoryHandler repositoryHandler = new SvnRepositoryHandler(factory, - facade, locationResolver); + facade, locationResolver, null); SvnConfig svnConfig = new SvnConfig(); repositoryHandler.setConfig(svnConfig); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java index f2511a9ad9..0cfeaa3a1c 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java @@ -39,8 +39,6 @@ import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.Modifications; -import java.io.IOException; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; diff --git a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java index 3efe78c820..ccaeee8631 100644 --- a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java +++ b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java @@ -59,7 +59,7 @@ public class DummyRepositoryHandler private final Set existingRepoNames = new HashSet<>(); public DummyRepositoryHandler(ConfigurationStoreFactory storeFactory, RepositoryLocationResolver repositoryLocationResolver) { - super(storeFactory, repositoryLocationResolver); + super(storeFactory, repositoryLocationResolver, null); } @Override diff --git a/scm-ui-components/packages/ui-components/src/BranchSelector.js b/scm-ui-components/packages/ui-components/src/BranchSelector.js index 99c93fd677..c473527472 100644 --- a/scm-ui-components/packages/ui-components/src/BranchSelector.js +++ b/scm-ui-components/packages/ui-components/src/BranchSelector.js @@ -13,10 +13,11 @@ const styles = { minWidthOfLabel: { minWidth: "4.5rem" }, - wrapper: { - padding: "1rem 1.5rem 0.25rem 1.5rem", - border: "1px solid #eee", - borderRadius: "5px 5px 0 0" + labelSizing: { + fontSize: "1rem !important" + }, + noBottomMargin: { + marginBottom: "0 !important" } }; @@ -52,9 +53,9 @@ class BranchSelector extends React.Component { return (
{ classes.minWidthOfLabel )} > - +
-
+
void }; class DownloadButton extends React.Component { render() { - const { displayName, url } = this.props; + const { displayName, url, disabled, onClick } = this.props; + const onClickOrDefault = !!onClick ? onClick : () => {}; return ( - + diff --git a/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js index 1b2b37bb19..56964b016b 100644 --- a/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js +++ b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js @@ -36,9 +36,9 @@ class ConfigurationBinder { binder.bind("config.navigation", ConfigNavLink, configPredicate); // route for global configuration, passes the link from the index resource to component - const ConfigRoute = ({ url, links }) => { + const ConfigRoute = ({ url, links, ...additionalProps }) => { const link = links[linkName].href; - return this.route(url + to, ); + return this.route(url + to, ); }; // bind config route to extension point @@ -63,9 +63,36 @@ class ConfigurationBinder { // route for global configuration, passes the current repository to component - const RepoRoute = ({url, repository}) => { - const link = repository._links[linkName].href - return this.route(url + to, ); + const RepoRoute = ({url, repository, ...additionalProps}) => { + const link = repository._links[linkName].href; + return this.route(url + to, ); + }; + + // bind config route to extension point + binder.bind("repository.route", RepoRoute, repoPredicate); + } + + bindRepositorySetting(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) { + + // create predicate based on the link name of the current repository route + // if the linkname is not available, the navigation link and the route are not bound to the extension points + const repoPredicate = (props: Object) => { + return props.repository && props.repository._links && props.repository._links[linkName]; + }; + + // create NavigationLink with translated label + const RepoNavLink = translate(this.i18nNamespace)(({t, url}) => { + return this.navLink(url + "/settings" + to, labelI18nKey, t); + }); + + // bind navigation link to extension point + binder.bind("repository.subnavigation", RepoNavLink, repoPredicate); + + + // route for global configuration, passes the current repository to component + const RepoRoute = ({url, repository, ...additionalProps}) => { + const link = repository._links[linkName].href; + return this.route(url + "/settings" + to, ); }; // bind config route to extension point diff --git a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js index e5c04eb613..24b1ced28a 100644 --- a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js +++ b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js @@ -10,7 +10,8 @@ type Props = { buttonLabel: string, fieldLabel: string, errorMessage: string, - helpText?: string + helpText?: string, + validateEntry?: string => boolean }; type State = { @@ -25,6 +26,15 @@ class AddEntryToTableField extends React.Component { }; } + isValid = () => { + const {validateEntry} = this.props; + if (!this.state.entryToAdd || this.state.entryToAdd === "" || !validateEntry) { + return true; + } else { + return validateEntry(this.state.entryToAdd); + } + }; + render() { const { disabled, @@ -39,7 +49,7 @@ class AddEntryToTableField extends React.Component { label={fieldLabel} errorMessage={errorMessage} onChange={this.handleAddEntryChange} - validationError={false} + validationError={!this.isValid()} value={this.state.entryToAdd} onReturnPressed={this.appendEntry} disabled={disabled} @@ -48,7 +58,7 @@ class AddEntryToTableField extends React.Component {
); diff --git a/scm-ui-components/packages/ui-components/src/navigation/NavLink.js b/scm-ui-components/packages/ui-components/src/navigation/NavLink.js index 53b124ef31..98c3138a8f 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/NavLink.js +++ b/scm-ui-components/packages/ui-components/src/navigation/NavLink.js @@ -28,7 +28,7 @@ class NavLink extends React.Component { let showIcon = null; if (icon) { - showIcon = (<>{" "}); + showIcon = (<>{" "}); } return ( diff --git a/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js b/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js index f2401729d6..897c63138e 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js +++ b/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js @@ -50,8 +50,19 @@ class PrimaryNavigation extends React.Component { createNavigationItems = () => { const navigationItems = []; + const { t, links } = this.props; + + const props = { + links, + label: t("primary-navigation.first-menu") + }; const append = this.createNavigationAppender(navigationItems); + if (binder.hasExtension("primary-navigation.first-menu", props)) { + navigationItems.push( + + ); + } append("/repos", "/(repo|repos)", "primary-navigation.repositories", "repositories"); append("/users", "/(user|users)", "primary-navigation.users", "users"); append("/groups", "/(group|groups)", "primary-navigation.groups", "groups"); diff --git a/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js b/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js new file mode 100644 index 0000000000..0a6612a173 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js @@ -0,0 +1,65 @@ +//@flow +import * as React from "react"; +import { Link, Route } from "react-router-dom"; + +type Props = { + to: string, + icon?: string, + label: string, + activeOnlyWhenExact?: boolean, + activeWhenMatch?: (route: any) => boolean, + children?: React.Node +}; + +class SubNavigation extends React.Component { + static defaultProps = { + activeOnlyWhenExact: false + }; + + isActive(route: any) { + const { activeWhenMatch } = this.props; + return route.match || (activeWhenMatch && activeWhenMatch(route)); + } + + renderLink = (route: any) => { + const { to, icon, label } = this.props; + + let defaultIcon = "fas fa-cog"; + if (icon) { + defaultIcon = icon; + } + + let children = null; + if (this.isActive(route)) { + children =
    {this.props.children}
; + } + + return ( +
  • + + {label} + + {children} +
  • + ); + }; + + render() { + const { to, activeOnlyWhenExact } = this.props; + + // removes last part of url + let parents = to.split("/"); + parents.splice(-1, 1); + let parent = parents.join("/"); + + return ( + + ); + } +} + +export default SubNavigation; diff --git a/scm-ui-components/packages/ui-components/src/navigation/index.js b/scm-ui-components/packages/ui-components/src/navigation/index.js index ca82073b56..b696f98328 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/index.js +++ b/scm-ui-components/packages/ui-components/src/navigation/index.js @@ -3,6 +3,7 @@ export { default as NavAction } from "./NavAction.js"; export { default as NavLink } from "./NavLink.js"; export { default as Navigation } from "./Navigation.js"; +export { default as SubNavigation } from "./SubNavigation.js"; export { default as PrimaryNavigation } from "./PrimaryNavigation.js"; export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink.js"; export { default as Section } from "./Section.js"; diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js index 857ff8c827..232b0e3577 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js @@ -25,7 +25,7 @@ class ChangesetDiff extends React.Component { render() { const { changeset, t } = this.props; if (!this.isDiffSupported(changeset)) { - return {t("changesets.diff.not-supported")}; + return {t("changesets.changeset.diffNotSupported")}; } else { const url = this.createUrl(changeset); return ; diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetList.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetList.js index 74ec816369..4cd8ec319c 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetList.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetList.js @@ -21,7 +21,7 @@ class ChangesetList extends React.Component { /> ); }); - return
    {content}
    ; + return <>{content}; } } diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index 4a4b7c89cf..f32d764ce8 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -1,7 +1,7 @@ { "login": { "title": "Anmeldung", - "subtitle": "Bitte anmelden um fortzufahren.", + "subtitle": "Bitte anmelden, um fortzufahren.", "logo-alt": "SCM-Manager", "username-placeholder": "Benutzername", "password-placeholder": "Passwort", @@ -10,13 +10,13 @@ "logout": { "error": { "title": "Abmeldung fehlgeschlagen", - "subtitle": "Während der Abmeldung ist ein Fehler aufgetreten" + "subtitle": "Während der Abmeldung ist ein Fehler aufgetreten." } }, "app": { "error": { "title": "Fehler", - "subtitle": "Ein unbekannter Fehler ist aufgetreten" + "subtitle": "Ein unbekannter Fehler ist aufgetreten." } }, "error-notification": { @@ -43,8 +43,10 @@ "previous": "Zurück" }, "profile": { - "navigation-label": "Navigation", - "actions-label": "Aktionen", + "navigationLabel": "Profil Navigation", + "informationNavLink": "Information", + "changePasswordNavLink": "Passwort ändern", + "settingsNavLink": "Einstellungen", "username": "Benutzername", "displayName": "Anzeigename", "mail": "E-Mail", @@ -52,21 +54,21 @@ "information": "Informationen", "change-password": "Passwort ändern", "error-title": "Fehler", - "error-subtitle": "Das Profil kann nicht angezeigt werden", + "error-subtitle": "Das Profil kann nicht angezeigt werden.", "error": "Fehler", "error-message": "'me' ist nicht definiert" }, "password": { "label": "Passwort", "newPassword": "Neues Passwort", - "passwordHelpText": "Plaintext Passwort des Benutzers.", + "passwordHelpText": "Klartext Passwort des Benutzers.", "passwordConfirmHelpText": "Passwort zur Bestätigen wiederholen.", "currentPassword": "Aktuelles Passwort", "currentPasswordHelpText": "Dieses Passwort wird momentan bereits verwendet.", "confirmPassword": "Passwort wiederholen", - "passwordInvalid": "Das Passwort muss zwischen 6 und 32 Zeichen lang sein", - "passwordConfirmFailed": "Passwörter müssen identisch sein", + "passwordInvalid": "Das Passwort muss zwischen 6 und 32 Zeichen lang sein!", + "passwordConfirmFailed": "Passwörter müssen identisch sein!", "submit": "Speichern", - "changedSuccessfully": "Passwort erfolgreich geändert" + "changedSuccessfully": "Passwort erfolgreich geändert!" } } diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index 0d8390a9bd..5767a2b376 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -1,12 +1,10 @@ { "config": { - "navigation-title": "Navigation" - }, - "global-config": { + "navigationLabel": "Einstellungs Navigation", + "globalConfigurationNavLink": "Globale Einstellungen", "title": "Einstellungen", - "navigation-label": "Globale Einstellungen", - "error-title": "Fehler", - "error-subtitle": "Unbekannter Einstellungen Fehler" + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Einstellungen Fehler" }, "config-form": { "submit": "Speichern", @@ -47,12 +45,12 @@ "login-attempt": { "name": "Anmeldeversuche", "login-attempt-limit": "Limit für Anmeldeversuche", - "login-attempt-limit-timeout": "Timeout bei fehlgeschlagenen Anmeldeversuche" + "login-attempt-limit-timeout": "Timeout bei fehlgeschlagenen Anmeldeversuchen" }, "general-settings": { "realm-description": "Realm Beschreibung", "enable-repository-archive": "Repository Archiv aktivieren", - "disable-grouping-grid": "Gruppen deaktiviern", + "disable-grouping-grid": "Gruppen deaktivieren", "date-format": "Datumsformat", "anonymous-access-enabled": "Anonyme Zugriffe erlauben", "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", @@ -67,27 +65,27 @@ "plugin-url-invalid": "Dies ist keine gültige URL" }, "help": { - "realmDescriptionHelpText": "Beschreibung des authentication realm", - "dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der momentjs Dokumentation beschrieben.", + "realmDescriptionHelpText": "Beschreibung des Authentication Realm.", + "dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.", "pluginRepositoryHelpText": "Die URL des Plugin Repositories. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur", "enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.", "enableRepositoryArchiveHelpText": "Repository Archive aktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.", "disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.", "allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf öffentliche Repositories.", - "skipFailedAuthenticatorsHelpText": "Die Kette der Authentifikatoren wird nicht beendet wenn ein Authentifikator einen Benutzer findet, ihn aber nicht erfolgreich authentifizieren kann.", + "skipFailedAuthenticatorsHelpText": "Die Kette der Authentifikatoren wird nicht beendet, wenn ein Authentifikator einen Benutzer findet, ihn aber nicht erfolgreich authentifizieren kann.", "adminGroupsHelpText": "Namen von Gruppen mit Admin-Berechtigungen.", "adminUsersHelpText": "Namen von Benutzern mit Admin-Berechtigungen.", "forceBaseUrlHelpText": "Zugriffe, die von einer anderen URL kommen, werden auf die Base URL weiter geleitet.", "baseUrlHelpText": "Die URL der Applikation mit Kontextpfad, z.B. http://localhost:8080/scm", "loginAttemptLimitHelpText": "Maximale Anzahl von Anmeldeversuchen. Durch Verwendung von -1 wird die Begrenzung der Anmeldeversuche deaktiviert.", - "loginAttemptLimitTimeoutHelpText": "Timeout in Sekunden für Benutzer, die vorübergehend wegen zu vieler fehlgeschlagener Anmeldeversuche deaktiviert wurden.", - "enableProxyHelpText": "Proxy aktiviern", + "loginAttemptLimitTimeoutHelpText": "Timeout in Sekunden für Benutzer, die vorübergehend wegen zu vieler fehlgeschlagener Anmeldeversuche, deaktiviert wurden.", + "enableProxyHelpText": "Proxy aktivieren", "proxyPortHelpText": "Der Proxy Port", "proxyPasswordHelpText": "Das Passwort für die Proxy Server Anmeldung.", "proxyServerHelpText": "Der Proxy Server", "proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.", "proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", "enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.", - "defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces" + "defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces." } } diff --git a/scm-ui/public/locales/de/groups.json b/scm-ui/public/locales/de/groups.json index 86528ee82a..1b2504a644 100644 --- a/scm-ui/public/locales/de/groups.json +++ b/scm-ui/public/locales/de/groups.json @@ -11,20 +11,23 @@ "title": "Gruppen", "subtitle": "Verwaltung der Gruppen" }, - "single-group": { - "error-title": "Fehler", - "error-subtitle": "Unbekannter Gruppen Fehler", - "navigation-label": "Navigation", - "actions-label": "Aktionen", - "information-label": "Informationen", - "back-label": "Zurück" + "singleGroup": { + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Gruppen Fehler", + "menu": { + "navigationLabel": "Gruppen Navigation", + "informationNavLink": "Informationen", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell", + "setPermissionsNavLink": "Berechtigungen" + } }, "add-group": { "title": "Gruppe erstellen", - "subtitle": "Erstllen einer neuen Gruppe" + "subtitle": "Erstellen einer neuen Gruppe" }, "create-group-button": { - "label": "Erstellen" + "label": "Gruppe erstellen" }, "edit-group-button": { "label": "Bearbeiten" @@ -41,30 +44,28 @@ }, "add-member-autocomplete": { "placeholder": "Benutzername eingeben", - "loading": "suche...", + "loading": "Suche...", "no-options": "Kein Vorschlag für Benutzername verfügbar" - }, - -"group-form": { + }, + "groupForm": { + "subtitle": "Gruppe bearbeiten", "submit": "Speichern", - "name-error": "Name ist ungültig", - "description-error": "Beschreibung ist ungültig", + "nameError": "Name ist ungültig", + "descriptionError": "Beschreibung ist ungültig", "help": { - "nameHelpText": "Einzigartiger Name der Gruppe", + "nameHelpText": "Eindeutiger Name der Gruppe", "descriptionHelpText": "Eine kurze Beschreibung der Gruppe", "memberHelpText": "Benutzername des Mitglieds der Gruppe" } }, - "delete-group-button": { - "label": "Löschen", - "confirm-alert": { + "deleteGroup": { + "subtitle": "Gruppe löschen", + "button": "Löschen", + "confirmAlert": { "title": "Gruppe löschen", "message": "Soll die Gruppe wirklich gelöscht werden?", "submit": "Ja", "cancel": "Nein" } - }, - "set-permissions-button": { - "label": "Berechtigungen ändern" } } diff --git a/scm-ui/public/locales/de/permissions.json b/scm-ui/public/locales/de/permissions.json index d1280808e5..57c061743a 100644 --- a/scm-ui/public/locales/de/permissions.json +++ b/scm-ui/public/locales/de/permissions.json @@ -1,8 +1,6 @@ { - "form": { - "submit-button": { - "label": "Berechtigungen speichern" - }, - "set-permissions-successful": "Berechtigungen erfolgreich gespeichert" + "setPermissions": { + "button": "Berechtigungen speichern", + "setPermissionsSuccessful": "Berechtigungen erfolgreich gespeichert" } } diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json index 038d5cdc54..f10157b4de 100644 --- a/scm-ui/public/locales/de/repos.json +++ b/scm-ui/public/locales/de/repos.json @@ -8,44 +8,58 @@ "lastModified": "Zuletzt bearbeitet" }, "validation": { - "name-invalid": "Der Name des Repositories ist ungültig", + "name-invalid": "Der Name des Repository ist ungültig", "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein" }, + "help": { + "nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.", + "typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).", + "contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.", + "descriptionHelpText": "Eine kurze Beschreibung des Repository." + }, + "repositoryRoot": { + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Repository Fehler", + "menu": { + "navigationLabel": "Repository Navigation", + "informationNavLink": "Informationen", + "historyNavLink": "Commits", + "sourcesNavLink": "Sources", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell", + "permissionsNavLink": "Berechtigungen" + } + }, "overview": { "title": "Repositories", "subtitle": "Übersicht aller verfügbaren Repositories", - "create-button": "Erstellen" - }, - "repository-root": { - "error-title": "Fehler", - "error-subtitle": "Unbekannter Repository Fehler", - "actions-label": "Aktionen", - "back-label": "Zurück", - "navigation-label": "Navigation", - "history": "Commits", - "information": "Informationen", - "permissions": "Berechtigungen", - "sources": "Sources" + "createButton": "Repository erstellen" }, "create": { - "title": "Repository Erstellen", - "subtitle": "Erstellen eines neuen Repositories" + "title": "Repository erstellen", + "subtitle": "Erstellen eines neuen Repository" }, - "repository-form": { - "submit": "Speichern" - }, - "edit-nav-link": { - "label": "Bearbeiten" - }, - "delete-nav-action": { - "label": "Löschen", - "confirm-alert": { - "title": "Repository löschen", - "message": "Soll das Repository wirklich gelöscht werden?", - "submit": "Ja", - "cancel": "Nein" + "changesets": { + "errorTitle": "Fehler", + "errorSubtitle": "Changesets konnten nicht abgerufen werden", + "branchSelectorLabel": "Branches", + "changeset": { + "description": "Beschreibung", + "summary": "Changeset {{id}} wurde committet {{time}}", + "diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt", + "id": "ID", + "contact": "Kontakt", + "date": "Datum" + }, + "author": { + "name": "Autor", + "mail": "Mail" } }, + "repositoryForm": { + "subtitle": "Repository bearbeiten", + "submit": "Speichern" + }, "sources": { "file-tree": { "name": "Name", @@ -65,28 +79,8 @@ "size": "Größe" } }, - "changesets": { - "diff": { - "not-supported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt" - }, - "error-title": "Fehler", - "error-subtitle": "Changesets konnten nicht abgerufen werden", - "changeset": { - "id": "ID", - "description": "Beschreibung", - "contact": "Kontakt", - "date": "Datum", - "summary": "Changeset {{id}} wurde committet {{time}}" - }, - "author": { - "name": "Autor", - "mail": "Mail" - } - }, - "branch-selector": { - "label": "Branches" - }, "permission": { + "title": "Berechtigungen bearbeiten", "user": "Benutzer", "group": "Gruppe", "error-title": "Fehler", @@ -120,7 +114,7 @@ "help": { "groupPermissionHelpText": "Zeigt ob es sich bei der Berechtigung um eine Gruppenberechtigung handelt. Wenn hier kein Haken gesetzt ist, handelt es sich um eine Benutzerberechtigung.", "nameHelpText": "Verwaltung von Berechtigungen für Benutzer und Gruppen", - "roleHelpText": "READ = read; WRITE = read und write; OWNER = read, write und auch die Möglichkeit Einstellungen und Berechtigungen zu verwalten. Wenn hier nichts angezeigt wird den Erweitert-Button benutzen um Details zu sehen.", + "roleHelpText": "READ = read; WRITE = read und write; OWNER = read, write und auch die Möglichkeit Einstellungen und Berechtigungen zu verwalten. Wenn hier nichts angezeigt wird, den Erweitert-Button benutzen, um Details zu sehen.", "permissionsHelpText": "Hier können individuelle Berechtigungen unabhängig von vordefinierten Rollen vergeben werden." }, "autocomplete": { @@ -138,10 +132,14 @@ } } }, - "help": { - "nameHelpText": "Der Name des Repositories. Dieser wird Teil der URL des Repositories sein.", - "typeHelpText": "Der Typ des Repositories (Mercurial, Git oder Subversion).", - "contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.", - "descriptionHelpText": "Eine kurze Beschreibung des Repositories." + "deleteRepo": { + "subtitle": "Repository löschen", + "button": "Löschen", + "confirmAlert": { + "title": "Repository löschen", + "message": "Soll das Repository wirklich gelöscht werden?", + "submit": "Ja", + "cancel": "Nein" + } } } diff --git a/scm-ui/public/locales/de/users.json b/scm-ui/public/locales/de/users.json index bb033e02dd..b38dd9c2fb 100644 --- a/scm-ui/public/locales/de/users.json +++ b/scm-ui/public/locales/de/users.json @@ -7,62 +7,58 @@ "admin": "Admin", "active": "Aktiv", "type": "Typ", - "creationDate": "Erstell", + "creationDate": "Erstellt", "lastModified": "Zuletzt bearbeitet" }, - "users": { - "title": "Benutzer", - "subtitle": "Verwaltung der Benutzer" - }, - "create-user-button": { - "label": "Erstellen" - }, - "delete-user-button": { - "label": "Löschen", - "confirm-alert": { - "title": "Benutzer löschen", - "message": "Soll der Benutzer wirklich gelöscht werden?", - "submit": "Ja", - "cancel": "Nein" - } - }, - "edit-user-button": { - "label": "Bearbeiten" - }, - "set-password-button": { - "label": "Passwort ändern" - }, - "set-permissions-button": { - "label": "Berechtigungen ändern" - }, - "user-form": { - "submit": "Speichern" - }, - "add-user": { - "title": "Benutzer erstellen", - "subtitle": "Erstellen eines neuen Benutzers" - }, - "single-user": { - "error-title": "Fehler", - "error-subtitle": "Unbekannter Benutzer Fehler", - "navigation-label": "Navigation", - "actions-label": "Aktionen", - "information-label": "Informationen", - "back-label": "Zurück" - }, "validation": { "mail-invalid": "Diese E-Mail ist ungültig", "name-invalid": "Dieser Name ist ungültig", "displayname-invalid": "Dieser Anzeigename ist ungültig" }, - "password": { - "set-password-successful": "Das Passwort wurde erfolgreich gespeichert." - }, "help": { - "usernameHelpText": "Einzigartiger Name des Benutzers.", - "displayNameHelpText": "Anzeigename des Benutzers.", - "mailHelpText": "E-Mail Adresse des Benutzers.", + "usernameHelpText": "Einzigartiger Name des Benutzers", + "displayNameHelpText": "Anzeigename des Benutzers", + "mailHelpText": "E-Mail Adresse des Benutzers", "adminHelpText": "Ein Administrator kann Repositories, Gruppen und Benutzer erstellen, bearbeiten und löschen.", - "activeHelpText": "Aktivierung oder Deaktivierung eines Benutzers." + "activeHelpText": "Aktivierung oder Deaktivierung eines Benutzers" + }, + "users": { + "title": "Benutzer", + "subtitle": "Verwaltung der Benutzer", + "createButton": "Benutzer erstellen" + }, + "singleUser": { + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Benutzer Fehler", + "menu": { + "navigationLabel": "Benutzer Navigation", + "informationNavLink": "Informationen", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell", + "setPasswordNavLink": "Passwort", + "setPermissionsNavLink": "Berechtigungen" + } + }, + "addUser": { + "title": "Benutzer erstellen", + "subtitle": "Erstellen eines neuen Benutzers" + }, + "deleteUser": { + "subtitle": "Benutzer löschen", + "button": "Löschen", + "confirmAlert": { + "title": "Benutzer löschen", + "message": "Soll der Benutzer wirklich gelöscht werden?", + "submit": "Ja", + "cancel": "Nein" + } + }, + "singleUserPassword": { + "button": "Passwort setzen", + "setPasswordSuccessful": "Das Passwort wurde erfolgreich gespeichert." + }, + "userForm": { + "subtitle": "Benutzer bearbeiten", + "button": "Speichern" } } diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index 6c3fa6628a..fed749a200 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -1,7 +1,7 @@ { "login": { "title": "Login", - "subtitle": "Please login to proceed.", + "subtitle": "Please login to proceed", "logo-alt": "SCM-Manager", "username-placeholder": "Your Username", "password-placeholder": "Your Password", @@ -22,7 +22,7 @@ "error-notification": { "prefix": "Error", "loginLink": "You can login here again.", - "timeout": "The session has expired.", + "timeout": "The session has expired", "wrong-login-credentials": "Invalid credentials" }, "loading": { @@ -43,14 +43,16 @@ "previous": "Previous" }, "profile": { - "navigation-label": "Navigation", - "actions-label": "Actions", + "navigationLabel": "Profile Navigation", + "informationNavLink": "Information", + "changePasswordNavLink": "Change password", + "settingsNavLink": "Settings", "username": "Username", "displayName": "Display Name", "mail": "E-Mail", "groups": "Groups", "information": "Information", - "change-password": "Change password", + "change-password": "Change Password", "error-title": "Error", "error-subtitle": "Cannot display profile", "error": "Error", @@ -59,14 +61,14 @@ "password": { "label": "Password", "newPassword": "New password", - "passwordHelpText": "Plain text password of the user.", - "passwordConfirmHelpText": "Repeat the password for confirmation.", + "passwordHelpText": "Plain text password of the user", + "passwordConfirmHelpText": "Repeat the password for confirmation", "currentPassword": "Current password", "currentPasswordHelpText": "The password currently in use", "confirmPassword": "Confirm password", "passwordInvalid": "Password has to be between 6 and 32 characters", "passwordConfirmFailed": "Passwords have to be identical", "submit": "Submit", - "changedSuccessfully": "Password successfully changed" + "changedSuccessfully": "Password changed successfully" } } diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 1a33da8c8b..b08c5c2d1b 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -1,12 +1,10 @@ { "config": { - "navigation-title": "Navigation" - }, - "global-config": { + "navigationLabel": "Configuration Navigation", + "globalConfigurationNavLink": "Global Configuration", "title": "Configuration", - "navigation-label": "Global Configuration", - "error-title": "Error", - "error-subtitle": "Unknown Config Error" + "errorTitle": "Error", + "errorSubtitle": "Unknown Config Error" }, "config-form": { "submit": "Submit", @@ -67,17 +65,17 @@ "plugin-url-invalid": "This is not a valid url" }, "help": { - "realmDescriptionHelpText": "Enter authentication realm description", - "dateFormatHelpText": "Moments date format. Please have a look at the momentjs documentation.", + "realmDescriptionHelpText": "Enter authentication realm description.", + "dateFormatHelpText": "Moments date format. Please have a look at the MomentJS documentation.", "pluginRepositoryHelpText": "The url of the plugin repository. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture", - "enableForwardingHelpText": "Enbale mod_proxy port forwarding.", + "enableForwardingHelpText": "Enable mod_proxy port forwarding.", "enableRepositoryArchiveHelpText": "Enable repository archives. A complete page reload is required after a change of this value.", "disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.", "allowAnonymousAccessHelpText": "Anonymous users have read access on public repositories.", "skipFailedAuthenticatorsHelpText": "Do not stop the authentication chain, if an authenticator finds the user but fails to authenticate the user.", "adminGroupsHelpText": "Names of groups with admin permissions.", "adminUsersHelpText": "Names of users with admin permissions.", - "forceBaseUrlHelpText": "Redirects to the base url if the request comes from a other url", + "forceBaseUrlHelpText": "Redirects to the base url if the request comes from a other url.", "baseUrlHelpText": "The url of the application (with context path), i.e. http://localhost:8080/scm", "loginAttemptLimitHelpText": "Maximum allowed login attempts. Use -1 to disable the login attempt limit.", "loginAttemptLimitTimeoutHelpText": "Timeout in seconds for users which are temporary disabled, because of too many failed login attempts.", @@ -86,8 +84,8 @@ "proxyPasswordHelpText": "The password for the proxy server authentication.", "proxyServerHelpText": "The proxy server", "proxyUserHelpText": "The username for the proxy server authentication.", - "proxyExcludesHelpText": "Glob patterns for hostnames which should be excluded from proxy settings.", - "enableXsrfProtectionHelpText": "Enable Xsrf Cookie Protection. Note: This feature is still experimental.", - "defaultNameSpaceStrategyHelpText": "The default namespace strategy" + "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", + "enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", + "defaultNameSpaceStrategyHelpText": "The default namespace strategy." } } diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json index 3fbe088029..3b34e5722b 100644 --- a/scm-ui/public/locales/en/groups.json +++ b/scm-ui/public/locales/en/groups.json @@ -11,60 +11,61 @@ "title": "Groups", "subtitle": "Create, read, update and delete groups" }, - "single-group": { - "error-title": "Error", - "error-subtitle": "Unknown group error", - "navigation-label": "Navigation", - "actions-label": "Actions", - "information-label": "Information", - "back-label": "Back" + "singleGroup": { + "errorTitle": "Error", + "errorSubtitle": "Unknown group error", + "menu": { + "navigationLabel": "Group Navigation", + "informationNavLink": "Information", + "settingsNavLink": "Settings", + "generalNavLink": "General", + "setPermissionsNavLink": "Permissions" + } }, "add-group": { "title": "Create Group", "subtitle": "Create a new group" }, "create-group-button": { - "label": "Create" + "label": "Create Group" }, "edit-group-button": { "label": "Edit" }, "add-member-button": { - "label": "Add member" + "label": "Add Member" }, "remove-member-button": { - "label": "Remove member" + "label": "Remove Member" }, "add-member-textfield": { - "label": "Add member", + "label": "Add Member", "error": "Invalid member name" }, "add-member-autocomplete": { - "placeholder": "Enter member", + "placeholder": "Enter Member", "loading": "Loading...", "no-options": "No suggestion available" - }, - -"group-form": { + }, + "groupForm": { + "subtitle": "Edit Group", "submit": "Submit", - "name-error": "Group name is invalid", - "description-error": "Description is invalid", + "nameError": "Group name is invalid", + "descriptionError": "Description is invalid", "help": { "nameHelpText": "Unique name of the group", "descriptionHelpText": "A short description of the group", "memberHelpText": "Usernames of the group members" } }, - "delete-group-button": { - "label": "Delete", - "confirm-alert": { + "deleteGroup": { + "subtitle": "Delete Group", + "button": "Delete", + "confirmAlert": { "title": "Delete Group", "message": "Do you really want to delete the group?", "submit": "Yes", "cancel": "No" } - }, - "set-permissions-button": { - "label": "Set permissions" } } diff --git a/scm-ui/public/locales/en/permissions.json b/scm-ui/public/locales/en/permissions.json index 52059db60a..9c3663e77b 100644 --- a/scm-ui/public/locales/en/permissions.json +++ b/scm-ui/public/locales/en/permissions.json @@ -1,8 +1,6 @@ { - "form": { - "submit-button": { - "label": "Set permissions" - }, - "set-permissions-successful": "Permissions set successfully" + "setPermissions": { + "button": "Set permissions", + "setPermissionsSuccessful": "Permissions set successfully" } } diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 7eef6f79b1..c30eade1a9 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -11,41 +11,55 @@ "name-invalid": "The repository name is invalid", "contact-invalid": "Contact must be a valid mail address" }, + "help": { + "nameHelpText": "The name of the repository. This name will be part of the repository url.", + "typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).", + "contactHelpText": "Email address of the person who is responsible for this repository.", + "descriptionHelpText": "A short description of the repository." + }, + "repositoryRoot": { + "errorTitle": "Error", + "errorSubtitle": "Unknown repository error", + "menu": { + "navigationLabel": "Repository Navigation", + "informationNavLink": "Information", + "historyNavLink": "Commits", + "sourcesNavLink": "Sources", + "settingsNavLink": "Settings", + "generalNavLink": "General", + "permissionsNavLink": "Permissions" + } + }, "overview": { "title": "Repositories", "subtitle": "Overview of available repositories", - "create-button": "Create" - }, - "repository-root": { - "error-title": "Error", - "error-subtitle": "Unknown repository error", - "actions-label": "Actions", - "back-label": "Back", - "navigation-label": "Navigation", - "history": "Commits", - "information": "Information", - "permissions": "Permissions", - "sources": "Sources" + "createButton": "Create Repository" }, "create": { "title": "Create Repository", "subtitle": "Create a new repository" }, - "repository-form": { - "submit": "Save" - }, - "edit-nav-link": { - "label": "Edit" - }, - "delete-nav-action": { - "label": "Delete", - "confirm-alert": { - "title": "Delete repository", - "message": "Do you really want to delete the repository?", - "submit": "Yes", - "cancel": "No" + "changesets": { + "errorTitle": "Error", + "errorSubtitle": "Could not fetch changesets", + "branchSelectorLabel": "Branches", + "changeset": { + "description": "Description", + "summary": "Changeset {{id}} was committed {{time}}", + "diffNotSupported": "Diff of changesets is not supported by the type of repository", + "id": "ID", + "contact": "Contact", + "date": "Date" + }, + "author": { + "name": "Author", + "mail": "Mail" } }, + "repositoryForm": { + "subtitle": "Edit Repository", + "submit": "Save" + }, "sources": { "file-tree": { "name": "Name", @@ -65,28 +79,8 @@ "size": "Size" } }, - "changesets": { - "diff": { - "not-supported": "Diff of changesets is not supported by the type of repository" - }, - "error-title": "Error", - "error-subtitle": "Could not fetch changesets", - "changeset": { - "id": "ID", - "description": "Description", - "contact": "Contact", - "date": "Date", - "summary": "Changeset {{id}} was committed {{time}}" - }, - "author": { - "name": "Author", - "mail": "Mail" - } - }, - "branch-selector": { - "label": "Branches" - }, "permission": { + "title": "Edit Permissions", "user": "User", "group": "Group", "error-title": "Error", @@ -119,9 +113,9 @@ }, "help": { "groupPermissionHelpText": "States if a permission is a group permission. If this is not checked, it is a user permission.", - "nameHelpText": "Manage permissions for a specific user or group", + "nameHelpText": "Manage permissions for a specific user or group.", "roleHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions. If nothing is selected here, use the 'Advanced' Button to see detailed permissions.", - "permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles" + "permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles." }, "autocomplete": { "no-group-options": "No group suggestion available", @@ -132,16 +126,20 @@ }, "advanced": { "dialog": { - "title": "Advanced permissions", + "title": "Advanced Permissions", "submit": "Submit", "abort": "Abort" } } }, - "help": { - "nameHelpText": "The name of the repository. This name will be part of the repository url.", - "typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).", - "contactHelpText": "Email address of the person who is responsible for this repository.", - "descriptionHelpText": "A short description of the repository." + "deleteRepo": { + "subtitle": "Delete Repository", + "button": "Delete", + "confirmAlert": { + "title": "Delete repository", + "message": "Do you really want to delete the repository?", + "submit": "Yes", + "cancel": "No" + } } } diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index afe86deb9b..2b72d85cbc 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -10,59 +10,55 @@ "creationDate": "Creation Date", "lastModified": "Last Modified" }, - "users": { - "title": "Users", - "subtitle": "Create, read, update and delete users" - }, - "create-user-button": { - "label": "Create" - }, - "delete-user-button": { - "label": "Delete", - "confirm-alert": { - "title": "Delete user", - "message": "Do you really want to delete the user?", - "submit": "Yes", - "cancel": "No" - } - }, - "edit-user-button": { - "label": "Edit" - }, - "set-password-button": { - "label": "Set password" - }, - "set-permissions-button": { - "label": "Set permissions" - }, - "user-form": { - "submit": "Submit" - }, - "add-user": { - "title": "Create User", - "subtitle": "Create a new user" - }, - "single-user": { - "error-title": "Error", - "error-subtitle": "Unknown user error", - "navigation-label": "Navigation", - "actions-label": "Actions", - "information-label": "Information", - "back-label": "Back" - }, "validation": { "mail-invalid": "This email is invalid", "name-invalid": "This name is invalid", "displayname-invalid": "This displayname is invalid" }, - "password": { - "set-password-successful": "Password successfully set" - }, "help": { "usernameHelpText": "Unique name of the user.", "displayNameHelpText": "Display name of the user.", "mailHelpText": "Email address of the user.", "adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.", - "activeHelpText": "Activate or deactive the user." + "activeHelpText": "Activate or deactivate the user." + }, + "users": { + "title": "Users", + "subtitle": "Create, read, update and delete users", + "createButton": "Create User" + }, + "singleUser": { + "errorTitle": "Error", + "errorSubtitle": "Unknown user error", + "menu": { + "navigationLabel": "User Navigation", + "informationNavLink": "Information", + "settingsNavLink": "Settings", + "generalNavLink": "General", + "setPasswordNavLink": "Password", + "setPermissionsNavLink": "Permissions" + } + }, + "addUser": { + "title": "Create User", + "subtitle": "Create a new user" + }, + "deleteUser": { + "subtitle": "Delete User", + "button": "Delete", + "confirmAlert": { + "title": "Delete user", + "message": "Do you really want to delete the user?", + "submit": "Yes", + "cancel": "No" + } + }, + "singleUserPassword": { + "button": "Set password", + "setPasswordSuccessful": "Password successfully set" + }, + "userForm": { + "subtitle": "Edit User", + "button": "Submit" } } diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 60dc9bb75f..04de525c95 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -1,86 +1,87 @@ -// @flow -import React from "react"; -import { translate } from "react-i18next"; -import { Route } from "react-router"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; - -import type { Links } from "@scm-manager/ui-types"; -import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; -import GlobalConfig from "./GlobalConfig"; -import type { History } from "history"; -import {connect} from "react-redux"; -import {compose} from "redux"; -import { getLinks } from "../../modules/indexResource"; - -type Props = { - links: Links, - - // context objects - t: string => string, - match: any, - history: History -}; - -class Config extends React.Component { - stripEndingSlash = (url: string) => { - if (url.endsWith("/")) { - return url.substring(0, url.length - 2); - } - return url; - }; - - matchedUrl = () => { - return this.stripEndingSlash(this.props.match.url); - }; - - render() { - const { links, t } = this.props; - - const url = this.matchedUrl(); - const extensionProps = { - links, - url - }; - - return ( - -
    -
    - - -
    -
    - -
    - - -
    -
    -
    -
    -
    - ); - } -} - -const mapStateToProps = (state: any) => { - const links = getLinks(state); - return { - links - }; -}; - -export default compose( - connect(mapStateToProps), - translate("config") -)(Config); - +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Route } from "react-router"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; + +import type { Links } from "@scm-manager/ui-types"; +import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; +import GlobalConfig from "./GlobalConfig"; +import type { History } from "history"; +import { connect } from "react-redux"; +import { compose } from "redux"; +import { getLinks } from "../../modules/indexResource"; + +type Props = { + links: Links, + + // context objects + t: string => string, + match: any, + history: History +}; + +class Config extends React.Component { + stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 2); + } + return url; + }; + + matchedUrl = () => { + return this.stripEndingSlash(this.props.match.url); + }; + + render() { + const { links, t } = this.props; + + const url = this.matchedUrl(); + const extensionProps = { + links, + url + }; + + return ( + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    +
    +
    + ); + } +} + +const mapStateToProps = (state: any) => { + const links = getLinks(state); + return { + links + }; +}; + +export default compose( + connect(mapStateToProps), + translate("config") +)(Config); diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 71be3fdd7f..eac8e27bee 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -78,8 +78,8 @@ class GlobalConfig extends React.Component { if (error) { return ( @@ -91,7 +91,7 @@ class GlobalConfig extends React.Component { return (
    - + <Title title={t("config.title")} /> {this.renderConfigChangedNotification()} <ConfigForm submitForm={config => this.modifyConfig(config)} diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index e8c5352d58..3ee9b141ef 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -143,7 +143,6 @@ class Login extends React.Component<Props, State> { /> <SubmitButton label={t("login.submit")} - disabled={this.isInValid()} fullWidth={true} loading={loading} /> diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index d2bc50faf2..e963fb00f9 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -10,7 +10,7 @@ import Login from "../containers/Login"; import Logout from "../containers/Logout"; import { ProtectedRoute } from "@scm-manager/ui-components"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import {binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import AddUser from "../users/containers/AddUser"; import SingleUser from "../users/containers/SingleUser"; @@ -32,10 +32,15 @@ type Props = { class Main extends React.Component<Props> { render() { const { authenticated, links } = this.props; + const redirectUrlFactory = binder.getExtension("main.redirect", this.props); + let url ="/repos"; + if (redirectUrlFactory){ + url = redirectUrlFactory(this.props); + } return ( <div className="main"> <Switch> - <Redirect exact path="/" to="/repos" /> + <Redirect exact path="/" to={url}/> <Route exact path="/login" component={Login} /> <Route path="/logout" component={Logout} /> <ProtectedRoute diff --git a/scm-ui/src/containers/Profile.js b/scm-ui/src/containers/Profile.js index 3464e125dd..e9bdbaa4b9 100644 --- a/scm-ui/src/containers/Profile.js +++ b/scm-ui/src/containers/Profile.js @@ -12,11 +12,13 @@ import { ErrorPage, Page, Navigation, + SubNavigation, Section, NavLink } from "@scm-manager/ui-components"; import ChangeUserPassword from "./ChangeUserPassword"; import ProfileInfo from "./ProfileInfo"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { me: Me, @@ -57,26 +59,43 @@ class Profile extends React.Component<Props, State> { ); } + const extensionProps = { + me, + url + }; + return ( <Page title={me.displayName}> <div className="columns"> <div className="column is-three-quarters"> <Route path={url} exact render={() => <ProfileInfo me={me} />} /> <Route - path={`${url}/password`} + path={`${url}/settings/password`} render={() => <ChangeUserPassword me={me} />} /> </div> <div className="column"> <Navigation> - <Section label={t("profile.navigation-label")}> - <NavLink to={`${url}`} icon="fas fa-info-circle" label={t("profile.information")} /> - </Section> - <Section label={t("profile.actions-label")}> + <Section label={t("profile.navigationLabel")}> <NavLink - to={`${url}/password`} - label={t("profile.change-password")} + to={`${url}`} + icon="fas fa-info-circle" + label={t("profile.informationNavLink")} /> + <SubNavigation + to={`${url}/settings/password`} + label={t("profile.settingsNavLink")} + > + <NavLink + to={`${url}/settings/password`} + label={t("profile.changePasswordNavLink")} + /> + <ExtensionPoint + name="profile.subnavigation" + props={extensionProps} + renderAll={true} + /> + </SubNavigation> </Section> </Navigation> </div> diff --git a/scm-ui/src/containers/ProfileInfo.js b/scm-ui/src/containers/ProfileInfo.js index 4c333174d1..49c79f1fe8 100644 --- a/scm-ui/src/containers/ProfileInfo.js +++ b/scm-ui/src/containers/ProfileInfo.js @@ -28,30 +28,20 @@ class ProfileInfo extends React.Component<Props, State> { <div className="media-content"> <table className="table"> <tbody> - <tr> - <td className="has-text-weight-semibold">{t("profile.username")}</td> - <td>{me.name}</td> - </tr> - <tr> - <td className="has-text-weight-semibold">{t("profile.displayName")}</td> - <td>{me.displayName}</td> - </tr> - <tr> - <td className="has-text-weight-semibold">{t("profile.mail")}</td> - <td> - <MailLink address={me.mail} /> - </td> - </tr> - <tr> - <td className="has-text-weight-semibold">{t("profile.groups")}</td> - <td className="content"> - <ul> - {me.groups.map((group) => { - return <li>{group}</li>; - })} - </ul> - </td> - </tr> + <tr> + <td className="has-text-weight-semibold">{t("profile.username")}</td> + <td>{me.name}</td> + </tr> + <tr> + <td className="has-text-weight-semibold">{t("profile.displayName")}</td> + <td>{me.displayName}</td> + </tr> + <tr> + <td className="has-text-weight-semibold">{t("profile.mail")}</td> + <td> + <MailLink address={me.mail} /> + </td> + </tr> </tbody> </table> </div> diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index 7cc2ee5d24..8693cb9b47 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -2,6 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import { + Subtitle, AutocompleteAddEntryToTableField, LabelWithHelpIcon, MemberNameTable, @@ -71,59 +72,67 @@ class GroupForm extends React.Component<Props, State> { }; render() { - const { t, loading } = this.props; + const { loading, t } = this.props; const { group } = this.state; let nameField = null; + let subtitle = null; if (!this.props.group) { + // create new group nameField = ( <InputField label={t("group.name")} - errorMessage={t("group-form.name-error")} + errorMessage={t("groupForm.nameError")} onChange={this.handleGroupNameChange} value={group.name} validationError={this.state.nameValidationError} - helpText={t("group-form.help.nameHelpText")} + helpText={t("groupForm.help.nameHelpText")} /> ); + } else { + // edit existing group + subtitle = <Subtitle subtitle={t("groupForm.subtitle")} />; } return ( - <form onSubmit={this.submit}> - {nameField} - <Textarea - label={t("group.description")} - errorMessage={t("group-form.description-error")} - onChange={this.handleDescriptionChange} - value={group.description} - validationError={false} - helpText={t("group-form.help.descriptionHelpText")} - /> - <LabelWithHelpIcon - label={t("group.members")} - helpText={t("group-form.help.memberHelpText")} - /> - <MemberNameTable - members={group.members} - memberListChanged={this.memberListChanged} - /> + <> + {subtitle} + <form onSubmit={this.submit}> + {nameField} + <Textarea + label={t("group.description")} + errorMessage={t("groupForm.descriptionError")} + onChange={this.handleDescriptionChange} + value={group.description} + validationError={false} + helpText={t("groupForm.help.descriptionHelpText")} + /> + <LabelWithHelpIcon + label={t("group.members")} + helpText={t("groupForm.help.memberHelpText")} + /> + <MemberNameTable + members={group.members} + memberListChanged={this.memberListChanged} + /> - <AutocompleteAddEntryToTableField - addEntry={this.addMember} - disabled={false} - buttonLabel={t("add-member-button.label")} - fieldLabel={t("add-member-textfield.label")} - errorMessage={t("add-member-textfield.error")} - loadSuggestions={this.props.loadUserSuggestions} - placeholder={t("add-member-autocomplete.placeholder")} - loadingMessage={t("add-member-autocomplete.loading")} - noOptionsMessage={t("add-member-autocomplete.no-options")} - /> - <SubmitButton - disabled={!this.isValid()} - label={t("group-form.submit")} - loading={loading} - /> - </form> + <AutocompleteAddEntryToTableField + addEntry={this.addMember} + disabled={false} + buttonLabel={t("add-member-button.label")} + fieldLabel={t("add-member-textfield.label")} + errorMessage={t("add-member-textfield.error")} + loadSuggestions={this.props.loadUserSuggestions} + placeholder={t("add-member-autocomplete.placeholder")} + loadingMessage={t("add-member-autocomplete.loading")} + noOptionsMessage={t("add-member-autocomplete.no-options")} + /> + <SubmitButton + disabled={!this.isValid()} + label={t("groupForm.submit")} + loading={loading} + /> + </form> + </> ); } diff --git a/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.js b/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.js deleted file mode 100644 index 71119b6e9f..0000000000 --- a/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.js +++ /dev/null @@ -1,56 +0,0 @@ -// @flow -import React from "react"; -import { translate } from "react-i18next"; -import type { Group } from "@scm-manager/ui-types"; -import { NavAction, confirmAlert } from "@scm-manager/ui-components"; - -type Props = { - group: Group, - confirmDialog?: boolean, - t: string => string, - deleteGroup: (group: Group) => void -}; - -export class DeleteGroupNavLink extends React.Component<Props> { - static defaultProps = { - confirmDialog: true - }; - - deleteGroup = () => { - this.props.deleteGroup(this.props.group); - }; - - confirmDelete = () => { - const { t } = this.props; - confirmAlert({ - title: t("delete-group-button.confirm-alert.title"), - message: t("delete-group-button.confirm-alert.message"), - buttons: [ - { - label: t("delete-group-button.confirm-alert.submit"), - onClick: () => this.deleteGroup(), - }, - { - label: t("delete-group-button.confirm-alert.cancel"), - onClick: () => null - } - ] - }); - }; - - isDeletable = () => { - return this.props.group._links.delete; - }; - - render() { - const { confirmDialog, t } = this.props; - const action = confirmDialog ? this.confirmDelete : this.deleteGroup; - - if (!this.isDeletable()) { - return null; - } - return <NavAction icon="fas fa-times" label={t("delete-group-button.label")} action={action} />; - } -} - -export default translate("groups")(DeleteGroupNavLink); diff --git a/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.test.js b/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.test.js deleted file mode 100644 index 49f8d95c63..0000000000 --- a/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { mount, shallow } from "enzyme"; -import "../../../tests/enzyme"; -import "../../../tests/i18n"; -import DeleteGroupNavLink from "./DeleteGroupNavLink"; - -import { confirmAlert } from "@scm-manager/ui-components"; -jest.mock("@scm-manager/ui-components", () => ({ - confirmAlert: jest.fn(), - NavAction: require.requireActual("@scm-manager/ui-components").NavAction -})); - -describe("DeleteGroupNavLink", () => { - it("should render nothing, if the delete link is missing", () => { - const group = { - _links: {} - }; - - const navLink = shallow( - <DeleteGroupNavLink group={group} deleteGroup={() => {}} /> - ); - expect(navLink.text()).toBe(""); - }); - - it("should render the navLink", () => { - const group = { - _links: { - delete: { - href: "/groups" - } - } - }; - - const navLink = mount( - <DeleteGroupNavLink group={group} deleteGroup={() => {}} /> - ); - expect(navLink.text()).not.toBe(""); - }); - - it("should open the confirm dialog on navLink click", () => { - const group = { - _links: { - delete: { - href: "/groups" - } - } - }; - - const navLink = mount( - <DeleteGroupNavLink group={group} deleteGroup={() => {}} /> - ); - navLink.find("a").simulate("click"); - - expect(confirmAlert.mock.calls.length).toBe(1); - }); - - it("should call the delete group function with delete url", () => { - const group = { - _links: { - delete: { - href: "/groups" - } - } - }; - - let calledUrl = null; - function capture(group) { - calledUrl = group._links.delete.href; - } - - const navLink = mount( - <DeleteGroupNavLink - group={group} - confirmDialog={false} - deleteGroup={capture} - /> - ); - navLink.find("a").simulate("click"); - - expect(calledUrl).toBe("/groups"); - }); -}); diff --git a/scm-ui/src/groups/components/navLinks/EditGroupNavLink.js b/scm-ui/src/groups/components/navLinks/EditGroupNavLink.js index e8bcd26385..9713c5c5c9 100644 --- a/scm-ui/src/groups/components/navLinks/EditGroupNavLink.js +++ b/scm-ui/src/groups/components/navLinks/EditGroupNavLink.js @@ -1,29 +1,28 @@ //@flow import React from "react"; +import type { Group } from "@scm-manager/ui-types"; import { NavLink } from "@scm-manager/ui-components"; import { translate } from "react-i18next"; -import type { Group } from "@scm-manager/ui-types"; type Props = { - t: string => string, + group: Group, editUrl: string, - group: Group + t: string => string }; -type State = {}; - -class EditGroupNavLink extends React.Component<Props, State> { - render() { - const { t, editUrl } = this.props; - if (!this.isEditable()) { - return null; - } - return <NavLink to={editUrl} icon="fas fa-cog" label={t("edit-group-button.label")} />; - } - +class EditGroupNavLink extends React.Component<Props> { isEditable = () => { return this.props.group._links.update; }; + + render() { + const { t, editUrl } = this.props; + + if (!this.isEditable()) { + return null; + } + return <NavLink to={editUrl} label={t("singleGroup.menu.generalNavLink")} />; + } } export default translate("groups")(EditGroupNavLink); diff --git a/scm-ui/src/groups/components/navLinks/editGroupNavLink.test.js b/scm-ui/src/groups/components/navLinks/EditGroupNavLink.test.js similarity index 100% rename from scm-ui/src/groups/components/navLinks/editGroupNavLink.test.js rename to scm-ui/src/groups/components/navLinks/EditGroupNavLink.test.js diff --git a/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js b/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js index a1314f9987..b2d7062f5e 100644 --- a/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js +++ b/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js @@ -17,7 +17,7 @@ class ChangePermissionNavLink extends React.Component<Props> { if (!this.hasPermissionToSetPermission()) { return null; } - return <NavLink to={permissionsUrl} label={t("set-permissions-button.label")} />; + return <NavLink to={permissionsUrl} label={t("singleGroup.menu.setPermissionsNavLink")} />; } hasPermissionToSetPermission = () => { diff --git a/scm-ui/src/groups/components/navLinks/index.js b/scm-ui/src/groups/components/navLinks/index.js index e589e5b6c9..992e9ab9a3 100644 --- a/scm-ui/src/groups/components/navLinks/index.js +++ b/scm-ui/src/groups/components/navLinks/index.js @@ -1,3 +1,2 @@ -export { default as DeleteGroupNavLink } from "./DeleteGroupNavLink"; export { default as EditGroupNavLink } from "./EditGroupNavLink"; export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; diff --git a/scm-ui/src/groups/containers/DeleteGroup.js b/scm-ui/src/groups/containers/DeleteGroup.js new file mode 100644 index 0000000000..d497e4434d --- /dev/null +++ b/scm-ui/src/groups/containers/DeleteGroup.js @@ -0,0 +1,113 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Group } from "@scm-manager/ui-types"; +import { + Subtitle, + DeleteButton, + confirmAlert, + ErrorNotification +} from "@scm-manager/ui-components"; +import { + deleteGroup, + getDeleteGroupFailure, + isDeleteGroupPending +} from "../modules/groups"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import type { History } from "history"; + +type Props = { + loading: boolean, + error: Error, + group: Group, + confirmDialog?: boolean, + deleteGroup: (group: Group, callback?: () => void) => void, + + // context props + history: History, + t: string => string +}; + +export class DeleteGroup extends React.Component<Props> { + static defaultProps = { + confirmDialog: true + }; + + deleteGroup = () => { + this.props.deleteGroup(this.props.group, this.groupDeleted); + }; + + groupDeleted = () => { + this.props.history.push("/groups"); + }; + + confirmDelete = () => { + const { t } = this.props; + confirmAlert({ + title: t("deleteGroup.confirmAlert.title"), + message: t("deleteGroup.confirmAlert.message"), + buttons: [ + { + label: t("deleteGroup.confirmAlert.submit"), + onClick: () => this.deleteGroup() + }, + { + label: t("deleteGroup.confirmAlert.cancel"), + onClick: () => null + } + ] + }); + }; + + isDeletable = () => { + return this.props.group._links.delete; + }; + + render() { + const { loading, error, confirmDialog, t } = this.props; + const action = confirmDialog ? this.confirmDelete : this.deleteGroup; + + if (!this.isDeletable()) { + return null; + } + + return ( + <> + <Subtitle subtitle={t("deleteGroup.subtitle")} /> + <ErrorNotification error={error} /> + <div className="columns"> + <div className="column"> + <DeleteButton + label={t("deleteGroup.button")} + action={action} + loading={loading} + /> + </div> + </div> + </> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const loading = isDeleteGroupPending(state, ownProps.group.name); + const error = getDeleteGroupFailure(state, ownProps.group.name); + return { + loading, + error + }; +}; + +const mapDispatchToProps = dispatch => { + return { + deleteGroup: (group: Group, callback?: () => void) => { + dispatch(deleteGroup(group, callback)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(translate("groups")(DeleteGroup))); diff --git a/scm-ui/src/groups/containers/EditGroup.js b/scm-ui/src/groups/containers/EditGroup.js index 223ea1eef6..a1f9f31e46 100644 --- a/scm-ui/src/groups/containers/EditGroup.js +++ b/scm-ui/src/groups/containers/EditGroup.js @@ -3,9 +3,9 @@ import React from "react"; import { connect } from "react-redux"; import GroupForm from "../components/GroupForm"; import { + modifyGroup, getModifyGroupFailure, isModifyGroupPending, - modifyGroup, modifyGroupReset } from "../modules/groups"; import type { History } from "history"; @@ -13,12 +13,13 @@ import { withRouter } from "react-router-dom"; import type { Group } from "@scm-manager/ui-types"; import { ErrorNotification } from "@scm-manager/ui-components"; import { getUserAutoCompleteLink } from "../../modules/indexResource"; +import DeleteGroup from "./DeleteGroup"; type Props = { group: Group, + fetchGroup: (name: string) => void, modifyGroup: (group: Group, callback?: () => void) => void, modifyGroupReset: Group => void, - fetchGroup: (name: string) => void, autocompleteLink: string, history: History, loading?: boolean, @@ -54,7 +55,7 @@ class EditGroup extends React.Component<Props> { }; render() { - const { group, loading, error } = this.props; + const { loading, error, group } = this.props; return ( <div> <ErrorNotification error={error} /> @@ -66,6 +67,8 @@ class EditGroup extends React.Component<Props> { loading={loading} loadUserSuggestions={this.loadUserAutocompletion} /> + <hr /> + <DeleteGroup group={group} /> </div> ); } diff --git a/scm-ui/src/groups/containers/SingleGroup.js b/scm-ui/src/groups/containers/SingleGroup.js index 19c5d53c01..f8d88d7a7d 100644 --- a/scm-ui/src/groups/containers/SingleGroup.js +++ b/scm-ui/src/groups/containers/SingleGroup.js @@ -6,33 +6,30 @@ import { ErrorPage, Loading, Navigation, + SubNavigation, Section, NavLink } from "@scm-manager/ui-components"; import { Route } from "react-router"; import { Details } from "./../components/table"; import { - DeleteGroupNavLink, EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks"; import type { Group } from "@scm-manager/ui-types"; import type { History } from "history"; import { - deleteGroup, fetchGroupByName, getGroupByName, isFetchGroupPending, - getFetchGroupFailure, - getDeleteGroupFailure, - isDeleteGroupPending + getFetchGroupFailure } from "../modules/groups"; import { translate } from "react-i18next"; import EditGroup from "./EditGroup"; import { getGroupsLink } from "../../modules/indexResource"; import SetPermissions from "../../permissions/components/SetPermissions"; -import {ExtensionPoint} from "@scm-manager/ui-extensions"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { name: string, @@ -42,7 +39,6 @@ type Props = { groupLink: string, // dispatcher functions - deleteGroup: (group: Group, callback?: () => void) => void, fetchGroupByName: (string, string) => void, // context objects @@ -63,14 +59,6 @@ class SingleGroup extends React.Component<Props> { return url; }; - deleteGroup = (group: Group) => { - this.props.deleteGroup(group, this.groupDeleted); - }; - - groupDeleted = () => { - this.props.history.push("/groups"); - }; - matchedUrl = () => { return this.stripEndingSlash(this.props.match.url); }; @@ -81,8 +69,8 @@ class SingleGroup extends React.Component<Props> { if (error) { return ( <ErrorPage - title={t("single-group.error-title")} - subtitle={t("single-group.error-subtitle")} + title={t("singleGroup.errorTitle")} + subtitle={t("singleGroup.errorSubtitle")} error={error} /> ); @@ -109,15 +97,17 @@ class SingleGroup extends React.Component<Props> { component={() => <Details group={group} />} /> <Route - path={`${url}/edit`} + path={`${url}/settings/general`} exact component={() => <EditGroup group={group} />} /> <Route - path={`${url}/permissions`} + path={`${url}/settings/permissions`} exact component={() => ( - <SetPermissions selectedPermissionsLink={group._links.permissions} /> + <SetPermissions + selectedPermissionsLink={group._links.permissions} + /> )} /> <ExtensionPoint @@ -128,33 +118,35 @@ class SingleGroup extends React.Component<Props> { </div> <div className="column"> <Navigation> - <Section label={t("single-group.navigation-label")}> + <Section label={t("singleGroup.menu.navigationLabel")}> <NavLink to={`${url}`} icon="fas fa-info-circle" - label={t("single-group.information-label")} - /> - <SetPermissionsNavLink - group={group} - permissionsUrl={`${url}/permissions`} + label={t("singleGroup.menu.informationNavLink")} /> <ExtensionPoint name="group.navigation" props={extensionProps} renderAll={true} /> - </Section> - <Section label={t("single-group.actions-label")}> - <DeleteGroupNavLink - group={group} - deleteGroup={this.deleteGroup} - /> - <EditGroupNavLink group={group} editUrl={`${url}/edit`} /> - <NavLink - to="/groups" - icon="fas fa-undo-alt" - label={t("single-group.back-label")} - /> + <SubNavigation + to={`${url}/settings/general`} + label={t("singleGroup.menu.settingsNavLink")} + > + <EditGroupNavLink + group={group} + editUrl={`${url}/settings/general`} + /> + <SetPermissionsNavLink + group={group} + permissionsUrl={`${url}/settings/permissions`} + /> + <ExtensionPoint + name="group.subnavigation" + props={extensionProps} + renderAll={true} + /> + </SubNavigation> </Section> </Navigation> </div> @@ -167,10 +159,8 @@ class SingleGroup extends React.Component<Props> { const mapStateToProps = (state, ownProps) => { const name = ownProps.match.params.name; const group = getGroupByName(state, name); - const loading = - isFetchGroupPending(state, name) || isDeleteGroupPending(state, name); - const error = - getFetchGroupFailure(state, name) || getDeleteGroupFailure(state, name); + const loading = isFetchGroupPending(state, name); + const error = getFetchGroupFailure(state, name); const groupLink = getGroupsLink(state); return { @@ -186,9 +176,6 @@ const mapDispatchToProps = dispatch => { return { fetchGroupByName: (link: string, name: string) => { dispatch(fetchGroupByName(link, name)); - }, - deleteGroup: (group: Group, callback?: () => void) => { - dispatch(deleteGroup(group, callback)); } }; }; diff --git a/scm-ui/src/permissions/components/SetPermissions.js b/scm-ui/src/permissions/components/SetPermissions.js index e7e561d739..d78177a745 100644 --- a/scm-ui/src/permissions/components/SetPermissions.js +++ b/scm-ui/src/permissions/components/SetPermissions.js @@ -113,7 +113,7 @@ class SetPermissions extends React.Component<Props, State> { message = ( <Notification type={"success"} - children={t("form.set-permissions-successful")} + children={t("setPermissions.setPermissionsSuccessful")} onClose={() => this.onClose()} /> ); @@ -128,7 +128,7 @@ class SetPermissions extends React.Component<Props, State> { <SubmitButton disabled={!this.state.permissionsChanged} loading={loading} - label={t("form.submit-button.label")} + label={t("setPermissions.button")} /> </form> ); diff --git a/scm-ui/src/repos/components/DeleteNavAction.js b/scm-ui/src/repos/components/DeleteNavAction.js deleted file mode 100644 index fe35a9caf1..0000000000 --- a/scm-ui/src/repos/components/DeleteNavAction.js +++ /dev/null @@ -1,58 +0,0 @@ -//@flow -import React from "react"; -import { translate } from "react-i18next"; -import { NavAction, confirmAlert } from "@scm-manager/ui-components"; -import type { Repository } from "@scm-manager/ui-types"; - -type Props = { - repository: Repository, - confirmDialog?: boolean, - delete: Repository => void, - - // context props - t: string => string -}; - -class DeleteNavAction extends React.Component<Props> { - static defaultProps = { - confirmDialog: true - }; - - delete = () => { - this.props.delete(this.props.repository); - }; - - confirmDelete = () => { - const { t } = this.props; - confirmAlert({ - title: t("delete-nav-action.confirm-alert.title"), - message: t("delete-nav-action.confirm-alert.message"), - buttons: [ - { - label: t("delete-nav-action.confirm-alert.submit"), - onClick: () => this.delete() - }, - { - label: t("delete-nav-action.confirm-alert.cancel"), - onClick: () => null - } - ] - }); - }; - - isDeletable = () => { - return this.props.repository._links.delete; - }; - - render() { - const { confirmDialog, t } = this.props; - const action = confirmDialog ? this.confirmDelete : this.delete(); - - if (!this.isDeletable()) { - return null; - } - return <NavAction action={action} icon="fas fa-times" label={t("delete-nav-action.label")} />; - } -} - -export default translate("repos")(DeleteNavAction); diff --git a/scm-ui/src/repos/components/DeleteNavAction.test.js b/scm-ui/src/repos/components/DeleteNavAction.test.js deleted file mode 100644 index 7c2191864a..0000000000 --- a/scm-ui/src/repos/components/DeleteNavAction.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { mount, shallow } from "enzyme"; -import "../../tests/enzyme"; -import "../../tests/i18n"; -import DeleteNavAction from "./DeleteNavAction"; - -import { confirmAlert } from "@scm-manager/ui-components"; -jest.mock("@scm-manager/ui-components", () => ({ - confirmAlert: jest.fn(), - NavAction: require.requireActual("@scm-manager/ui-components").NavAction -})); - -describe("DeleteNavAction", () => { - it("should render nothing, if the delete link is missing", () => { - const repository = { - _links: {} - }; - - const navLink = shallow( - <DeleteNavAction repository={repository} delete={() => {}} /> - ); - expect(navLink.text()).toBe(""); - }); - - it("should render the navLink", () => { - const repository = { - _links: { - delete: { - href: "/repositories" - } - } - }; - - const navLink = mount( - <DeleteNavAction repository={repository} delete={() => {}} /> - ); - expect(navLink.text()).not.toBe(""); - }); - - it("should open the confirm dialog on navLink click", () => { - const repository = { - _links: { - delete: { - href: "/repositorys" - } - } - }; - - const navLink = mount( - <DeleteNavAction repository={repository} delete={() => {}} /> - ); - navLink.find("a").simulate("click"); - - expect(confirmAlert.mock.calls.length).toBe(1); - }); - - it("should call the delete repository function with delete url", () => { - const repository = { - _links: { - delete: { - href: "/repos" - } - } - }; - - let calledUrl = null; - function capture(repository) { - calledUrl = repository._links.delete.href; - } - - const navLink = mount( - <DeleteNavAction - repository={repository} - confirmDialog={false} - delete={capture} - /> - ); - navLink.find("a").simulate("click"); - - expect(calledUrl).toBe("/repos"); - }); -}); diff --git a/scm-ui/src/repos/components/EditNavLink.js b/scm-ui/src/repos/components/EditRepoNavLink.js similarity index 57% rename from scm-ui/src/repos/components/EditNavLink.js rename to scm-ui/src/repos/components/EditRepoNavLink.js index a42625a154..9a502cdafb 100644 --- a/scm-ui/src/repos/components/EditNavLink.js +++ b/scm-ui/src/repos/components/EditRepoNavLink.js @@ -1,22 +1,28 @@ //@flow import React from "react"; +import type { Repository } from "@scm-manager/ui-types"; import { NavLink } from "@scm-manager/ui-components"; import { translate } from "react-i18next"; -import type { Repository } from "@scm-manager/ui-types"; -type Props = { editUrl: string, t: string => string, repository: Repository }; +type Props = { + repository: Repository, + editUrl: string, + t: string => string +}; -class EditNavLink extends React.Component<Props> { +class EditRepoNavLink extends React.Component<Props> { isEditable = () => { return this.props.repository._links.update; }; + render() { + const { editUrl, t } = this.props; + if (!this.isEditable()) { return null; } - const { editUrl, t } = this.props; - return <NavLink to={editUrl} icon="fas fa-cog" label={t("edit-nav-link.label")} />; + return <NavLink to={editUrl} label={t("repositoryRoot.menu.generalNavLink")} />; } } -export default translate("repos")(EditNavLink); +export default translate("repos")(EditRepoNavLink); diff --git a/scm-ui/src/repos/components/EditNavLink.test.js b/scm-ui/src/repos/components/EditRepoNavLink.test.js similarity index 71% rename from scm-ui/src/repos/components/EditNavLink.test.js rename to scm-ui/src/repos/components/EditRepoNavLink.test.js index fdb13ade8d..22bb06fae0 100644 --- a/scm-ui/src/repos/components/EditNavLink.test.js +++ b/scm-ui/src/repos/components/EditRepoNavLink.test.js @@ -3,9 +3,9 @@ import { shallow, mount } from "enzyme"; import "../../tests/enzyme"; import "../../tests/i18n"; import ReactRouterEnzymeContext from "react-router-enzyme-context"; -import EditNavLink from "./EditNavLink"; +import EditRepoNavLink from "./EditRepoNavLink"; -describe("EditNavLink", () => { +describe("GeneralNavLink", () => { const options = new ReactRouterEnzymeContext(); it("should render nothing, if the modify link is missing", () => { @@ -14,7 +14,7 @@ describe("EditNavLink", () => { }; const navLink = shallow( - <EditNavLink repository={repository} editUrl="" />, + <EditRepoNavLink repository={repository} editUrl="" />, options.get() ); expect(navLink.text()).toBe(""); @@ -30,9 +30,9 @@ describe("EditNavLink", () => { }; const navLink = mount( - <EditNavLink repository={repository} editUrl="" />, + <EditRepoNavLink repository={repository} editUrl="" />, options.get() ); - expect(navLink.text()).toBe(" edit-nav-link.label"); + expect(navLink.text()).toBe("repositoryRoot.menu.generalNavLink"); }); }); diff --git a/scm-ui/src/repos/components/PermissionsNavLink.js b/scm-ui/src/repos/components/PermissionsNavLink.js index 3a6f97588b..773ad94246 100644 --- a/scm-ui/src/repos/components/PermissionsNavLink.js +++ b/scm-ui/src/repos/components/PermissionsNavLink.js @@ -20,7 +20,7 @@ class PermissionsNavLink extends React.Component<Props> { } const { permissionUrl, t } = this.props; return ( - <NavLink to={permissionUrl} icon="fas fa-lock" label={t("repository-root.permissions")} /> + <NavLink to={permissionUrl} label={t("repositoryRoot.menu.permissionsNavLink")} /> ); } } diff --git a/scm-ui/src/repos/components/PermissionsNavLink.test.js b/scm-ui/src/repos/components/PermissionsNavLink.test.js index 901175caa0..3f6a95fe7d 100644 --- a/scm-ui/src/repos/components/PermissionsNavLink.test.js +++ b/scm-ui/src/repos/components/PermissionsNavLink.test.js @@ -33,6 +33,6 @@ describe("PermissionsNavLink", () => { <PermissionsNavLink repository={repository} permissionUrl="" />, options.get() ); - expect(navLink.text()).toBe(" repository-root.permissions"); + expect(navLink.text()).toBe("repositoryRoot.menu.permissionsNavLink"); }); }); diff --git a/scm-ui/src/repos/components/form/RepositoryForm.js b/scm-ui/src/repos/components/form/RepositoryForm.js index 8f5d932778..fcd88f0417 100644 --- a/scm-ui/src/repos/components/form/RepositoryForm.js +++ b/scm-ui/src/repos/components/form/RepositoryForm.js @@ -2,6 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import { + Subtitle, InputField, Select, SubmitButton, @@ -81,30 +82,39 @@ class RepositoryForm extends React.Component<Props, State> { const { loading, t } = this.props; const repository = this.state.repository; - return ( - <form onSubmit={this.submit}> - {this.renderCreateOnlyFields()} - <InputField - label={t("repository.contact")} - onChange={this.handleContactChange} - value={repository ? repository.contact : ""} - validationError={this.state.contactValidationError} - errorMessage={t("validation.contact-invalid")} - helpText={t("help.contactHelpText")} - /> + let subtitle = null; + if (this.props.repository) { + // edit existing repo + subtitle = <Subtitle subtitle={t("repositoryForm.subtitle")} />; + } - <Textarea - label={t("repository.description")} - onChange={this.handleDescriptionChange} - value={repository ? repository.description : ""} - helpText={t("help.descriptionHelpText")} - /> - <SubmitButton - disabled={!this.isValid()} - loading={loading} - label={t("repository-form.submit")} - /> - </form> + return ( + <> + {subtitle} + <form onSubmit={this.submit}> + {this.renderCreateOnlyFields()} + <InputField + label={t("repository.contact")} + onChange={this.handleContactChange} + value={repository ? repository.contact : ""} + validationError={this.state.contactValidationError} + errorMessage={t("validation.contact-invalid")} + helpText={t("help.contactHelpText")} + /> + + <Textarea + label={t("repository.description")} + onChange={this.handleDescriptionChange} + value={repository ? repository.description : ""} + helpText={t("help.descriptionHelpText")} + /> + <SubmitButton + disabled={!this.isValid()} + loading={loading} + label={t("repositoryForm.submit")} + /> + </form> + </> ); } diff --git a/scm-ui/src/repos/components/list/RepositoryEntry.js b/scm-ui/src/repos/components/list/RepositoryEntry.js index 28a6fe1ad3..b8b03a3523 100644 --- a/scm-ui/src/repos/components/list/RepositoryEntry.js +++ b/scm-ui/src/repos/components/list/RepositoryEntry.js @@ -64,7 +64,7 @@ class RepositoryEntry extends React.Component<Props> { return ( <RepositoryEntryLink iconClass="fa-cog fa-lg" - to={repositoryLink + "/edit"} + to={repositoryLink + "/settings/general"} /> ); } diff --git a/scm-ui/src/repos/containers/ChangesetView.js b/scm-ui/src/repos/containers/ChangesetView.js index 80ab0b71d6..dc53b5d798 100644 --- a/scm-ui/src/repos/containers/ChangesetView.js +++ b/scm-ui/src/repos/containers/ChangesetView.js @@ -37,8 +37,8 @@ class ChangesetView extends React.Component<Props> { if (error) { return ( <ErrorPage - title={t("changeset-error.title")} - subtitle={t("changeset-error.subtitle")} + title={t("changesets.errorTitle")} + subtitle={t("changesets.errorSubtitle")} error={error} /> ); diff --git a/scm-ui/src/repos/containers/Changesets.js b/scm-ui/src/repos/containers/Changesets.js index 95bf0459a6..5edc677e60 100644 --- a/scm-ui/src/repos/containers/Changesets.js +++ b/scm-ui/src/repos/containers/Changesets.js @@ -1,8 +1,13 @@ // @flow import React from "react"; -import {withRouter} from "react-router-dom"; -import type {Branch, Changeset, PagedCollection, Repository} from "@scm-manager/ui-types"; +import { withRouter } from "react-router-dom"; +import type { + Branch, + Changeset, + PagedCollection, + Repository +} from "@scm-manager/ui-types"; import { fetchChangesets, getChangesets, @@ -11,9 +16,15 @@ import { selectListAsCollection } from "../modules/changesets"; -import {connect} from "react-redux"; -import {ErrorNotification, getPageFromMatch, LinkPaginator, ChangesetList, Loading} from "@scm-manager/ui-components"; -import {compose} from "redux"; +import { connect } from "react-redux"; +import { + ErrorNotification, + getPageFromMatch, + LinkPaginator, + ChangesetList, + Loading +} from "@scm-manager/ui-components"; +import { compose } from "redux"; type Props = { repository: Repository, @@ -64,13 +75,21 @@ class Changesets extends React.Component<Props> { renderList = () => { const { repository, changesets } = this.props; - return <ChangesetList repository={repository} changesets={changesets} />; + return ( + <div className="panel-block"> + <ChangesetList repository={repository} changesets={changesets} /> + </div> + ); }; renderPaginator = () => { const { page, list } = this.props; if (list) { - return <LinkPaginator page={page} collection={list} />; + return ( + <div className="panel-footer"> + <LinkPaginator page={page} collection={list} /> + </div> + ); } return null; }; diff --git a/scm-ui/src/repos/containers/ChangesetsRoot.js b/scm-ui/src/repos/containers/ChangesetsRoot.js index 2eea64b8e1..f6e0c3d359 100644 --- a/scm-ui/src/repos/containers/ChangesetsRoot.js +++ b/scm-ui/src/repos/containers/ChangesetsRoot.js @@ -89,10 +89,12 @@ class BranchRoot extends React.Component<Props> { const changesets = <Changesets repository={repository} branch={branch} />; return ( - <> + <div className="panel"> + <div className="panel-heading"> {this.renderBranchSelector()} + </div> <Route path={`${url}/:page?`} component={() => changesets} /> - </> + </div> ); } @@ -101,7 +103,7 @@ class BranchRoot extends React.Component<Props> { if (repository._links.branches) { return ( <BranchSelector - label={t("branch-selector.label")} + label={t("changesets.branchSelectorLabel")} branches={branches} selectedBranch={selected} selected={(b: Branch) => { diff --git a/scm-ui/src/repos/containers/DeleteRepo.js b/scm-ui/src/repos/containers/DeleteRepo.js new file mode 100644 index 0000000000..b621a1998b --- /dev/null +++ b/scm-ui/src/repos/containers/DeleteRepo.js @@ -0,0 +1,114 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Repository } from "@scm-manager/ui-types"; +import { + Subtitle, + DeleteButton, + confirmAlert, + ErrorNotification +} from "@scm-manager/ui-components"; +import { + deleteRepo, + getDeleteRepoFailure, + isDeleteRepoPending +} from "../modules/repos"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import type { History } from "history"; + +type Props = { + loading: boolean, + error: Error, + repository: Repository, + confirmDialog?: boolean, + deleteRepo: (Repository, () => void) => void, + + // context props + history: History, + t: string => string +}; + +class DeleteRepo extends React.Component<Props> { + static defaultProps = { + confirmDialog: true + }; + + deleted = () => { + this.props.history.push("/repos"); + }; + + deleteRepo = () => { + this.props.deleteRepo(this.props.repository, this.deleted); + }; + + confirmDelete = () => { + const { t } = this.props; + confirmAlert({ + title: t("deleteRepo.confirmAlert.title"), + message: t("deleteRepo.confirmAlert.message"), + buttons: [ + { + label: t("deleteRepo.confirmAlert.submit"), + onClick: () => this.deleteRepo() + }, + { + label: t("deleteRepo.confirmAlert.cancel"), + onClick: () => null + } + ] + }); + }; + + isDeletable = () => { + return this.props.repository._links.delete; + }; + + render() { + const { loading, error, confirmDialog, t } = this.props; + const action = confirmDialog ? this.confirmDelete : this.deleteRepo; + + if (!this.isDeletable()) { + return null; + } + + return ( + <> + <Subtitle subtitle={t("deleteRepo.subtitle")} /> + <ErrorNotification error={error} /> + <div className="columns"> + <div className="column"> + <DeleteButton + label={t("deleteRepo.button")} + action={action} + loading={loading} + /> + </div> + </div> + </> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const { namespace, name } = ownProps.repository; + const loading = isDeleteRepoPending(state, namespace, name); + const error = getDeleteRepoFailure(state, namespace, name); + return { + loading, + error + }; +}; + +const mapDispatchToProps = dispatch => { + return { + deleteRepo: (repo: Repository, callback: () => void) => { + dispatch(deleteRepo(repo, callback)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(translate("repos")(DeleteRepo))); diff --git a/scm-ui/src/repos/containers/Edit.js b/scm-ui/src/repos/containers/EditRepo.js similarity index 69% rename from scm-ui/src/repos/containers/Edit.js rename to scm-ui/src/repos/containers/EditRepo.js index 816dae8de9..2e5bb69172 100644 --- a/scm-ui/src/repos/containers/Edit.js +++ b/scm-ui/src/repos/containers/EditRepo.js @@ -1,8 +1,9 @@ // @flow import React from "react"; import { connect } from "react-redux"; -import { translate } from "react-i18next"; +import { withRouter } from "react-router-dom"; import RepositoryForm from "../components/form"; +import DeleteRepo from "./DeleteRepo"; import type { Repository } from "@scm-manager/ui-types"; import { modifyRepo, @@ -10,34 +11,55 @@ import { getModifyRepoFailure, modifyRepoReset } from "../modules/repos"; -import { withRouter } from "react-router-dom"; import type { History } from "history"; import { ErrorNotification } from "@scm-manager/ui-components"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { - repository: Repository, - modifyRepo: (Repository, () => void) => void, - modifyRepoReset: Repository => void, loading: boolean, error: Error, + modifyRepo: (Repository, () => void) => void, + modifyRepoReset: Repository => void, + // context props - t: string => string, - history: History + repository: Repository, + history: History, + match: any }; -class Edit extends React.Component<Props> { +class EditRepo extends React.Component<Props> { componentDidMount() { const { modifyRepoReset, repository } = this.props; modifyRepoReset(repository); } + repoModified = () => { const { history, repository } = this.props; history.push(`/repo/${repository.namespace}/${repository.name}`); }; + stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 2); + } + return url; + }; + + matchedUrl = () => { + return this.stripEndingSlash(this.props.match.url); + }; + render() { - const { loading, error } = this.props; + const { loading, error, repository } = this.props; + + const url = this.matchedUrl(); + + const extensionProps = { + repository, + url + }; + return ( <div> <ErrorNotification error={error} /> @@ -48,6 +70,13 @@ class Edit extends React.Component<Props> { this.props.modifyRepo(repo, this.repoModified); }} /> + <hr /> + <ExtensionPoint + name="repo-config.route" + props={extensionProps} + renderAll={true} + /> + <DeleteRepo repository={repository} /> </div> ); } @@ -77,4 +106,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("repos")(withRouter(Edit))); +)(withRouter(EditRepo)); diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index bbafe14539..598b6c94f2 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -90,7 +90,7 @@ class Overview extends React.Component<Props> { if (showCreateButton) { return ( <CreateButton - label={t("overview.create-button")} + label={t("overview.createButton")} link="/repos/create" /> ); diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 07b6681752..d0d722e1fd 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -1,27 +1,39 @@ //@flow import React from "react"; -import {deleteRepo, fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos"; +import { + fetchRepoByName, + getFetchRepoFailure, + getRepository, + isFetchRepoPending +} from "../modules/repos"; -import {connect} from "react-redux"; -import {Route, Switch} from "react-router-dom"; -import type {Repository} from "@scm-manager/ui-types"; +import { connect } from "react-redux"; +import { Route, Switch } from "react-router-dom"; +import type { Repository } from "@scm-manager/ui-types"; -import {ErrorPage, Loading, Navigation, NavLink, Page, Section} from "@scm-manager/ui-components"; -import {translate} from "react-i18next"; +import { + ErrorPage, + Loading, + Navigation, + SubNavigation, + NavLink, + Page, + Section +} from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; import RepositoryDetails from "../components/RepositoryDetails"; -import DeleteNavAction from "../components/DeleteNavAction"; -import Edit from "../containers/Edit"; +import EditRepo from "./EditRepo"; import Permissions from "../permissions/containers/Permissions"; -import type {History} from "history"; -import EditNavLink from "../components/EditNavLink"; +import type { History } from "history"; +import EditRepoNavLink from "../components/EditRepoNavLink"; import BranchRoot from "./ChangesetsRoot"; import ChangesetView from "./ChangesetView"; import PermissionsNavLink from "../components/PermissionsNavLink"; import Sources from "../sources/containers/Sources"; import RepositoryNavLink from "../components/RepositoryNavLink"; -import {getRepositoriesLink} from "../../modules/indexResource"; +import {getLinks, getRepositoriesLink} from "../../modules/indexResource"; import {ExtensionPoint} from "@scm-manager/ui-extensions"; type Props = { @@ -31,10 +43,10 @@ type Props = { loading: boolean, error: Error, repoLink: string, + indexLinks: Object, // dispatch functions fetchRepoByName: (link: string, namespace: string, name: string) => void, - deleteRepo: (repository: Repository, () => void) => void, // context props t: string => string, @@ -60,14 +72,6 @@ class RepositoryRoot extends React.Component<Props> { return this.stripEndingSlash(this.props.match.url); }; - deleted = () => { - this.props.history.push("/repos"); - }; - - delete = (repository: Repository) => { - this.props.deleteRepo(repository, this.deleted); - }; - matches = (route: any) => { const url = this.matchedUrl(); const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`); @@ -75,13 +79,13 @@ class RepositoryRoot extends React.Component<Props> { }; render() { - const { loading, error, repository, t } = this.props; + const { loading, error, indexLinks, repository, t } = this.props; if (error) { return ( <ErrorPage - title={t("repository-root.error-title")} - subtitle={t("repository-root.error-subtitle")} + title={t("repositoryRoot.errorTitle")} + subtitle={t("repositoryRoot.errorSubtitle")} error={error} /> ); @@ -95,7 +99,8 @@ class RepositoryRoot extends React.Component<Props> { const extensionProps = { repository, - url + url, + indexLinks }; return ( @@ -109,11 +114,11 @@ class RepositoryRoot extends React.Component<Props> { component={() => <RepositoryDetails repository={repository} />} /> <Route - path={`${url}/edit`} - component={() => <Edit repository={repository} />} + path={`${url}/settings/general`} + component={() => <EditRepo repository={repository} />} /> <Route - path={`${url}/permissions`} + path={`${url}/settings/permissions`} render={() => ( <Permissions namespace={this.props.repository.namespace} @@ -168,14 +173,18 @@ class RepositoryRoot extends React.Component<Props> { </div> <div className="column"> <Navigation> - <Section label={t("repository-root.navigation-label")}> - <NavLink to={url} icon="fas fa-info-circle" label={t("repository-root.information")} /> + <Section label={t("repositoryRoot.menu.navigationLabel")}> + <NavLink + to={url} + icon="fas fa-info-circle" + label={t("repositoryRoot.menu.informationNavLink")} + /> <RepositoryNavLink repository={repository} linkName="changesets" to={`${url}/changesets/`} icon="fas fa-code-branch" - label={t("repository-root.history")} + label={t("repositoryRoot.menu.historyNavLink")} activeWhenMatch={this.matches} activeOnlyWhenExact={false} /> @@ -184,23 +193,32 @@ class RepositoryRoot extends React.Component<Props> { linkName="sources" to={`${url}/sources`} icon="fas fa-code" - label={t("repository-root.sources")} + label={t("repositoryRoot.menu.sourcesNavLink")} activeOnlyWhenExact={false} /> - <PermissionsNavLink - permissionUrl={`${url}/permissions`} - repository={repository} - /> <ExtensionPoint name="repository.navigation" props={extensionProps} renderAll={true} /> - </Section> - <Section label={t("repository-root.actions-label")}> - <DeleteNavAction repository={repository} delete={this.delete} /> - <EditNavLink repository={repository} editUrl={`${url}/edit`} /> - <NavLink to="/repos" icon="fas fa-undo" label={t("repository-root.back-label")} /> + <SubNavigation + to={`${url}/settings/general`} + label={t("repositoryRoot.menu.settingsNavLink")} + > + <EditRepoNavLink + repository={repository} + editUrl={`${url}/settings/general`} + /> + <PermissionsNavLink + permissionUrl={`${url}/settings/permissions`} + repository={repository} + /> + <ExtensionPoint + name="repository.subnavigation" + props={extensionProps} + renderAll={true} + /> + </SubNavigation> </Section> </Navigation> </div> @@ -216,13 +234,15 @@ const mapStateToProps = (state, ownProps) => { const loading = isFetchRepoPending(state, namespace, name); const error = getFetchRepoFailure(state, namespace, name); const repoLink = getRepositoriesLink(state); + const indexLinks = getLinks(state); return { namespace, name, repository, loading, error, - repoLink + repoLink, + indexLinks }; }; @@ -230,9 +250,6 @@ const mapDispatchToProps = dispatch => { return { fetchRepoByName: (link: string, namespace: string, name: string) => { dispatch(fetchRepoByName(link, namespace, name)); - }, - deleteRepo: (repository: Repository, callback: () => void) => { - dispatch(deleteRepo(repository, callback)); } }; }; diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 7c20f503c5..38afea441b 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -24,6 +24,7 @@ import { import { Loading, ErrorPage, + Subtitle, LabelWithHelpIcon } from "@scm-manager/ui-components"; import type { @@ -143,6 +144,7 @@ class Permissions extends React.Component<Props> { return ( <div> + <Subtitle subtitle={t("permission.title")} /> <table className="has-background-light table is-hoverable is-fullwidth"> <thead> <tr> diff --git a/scm-ui/src/repos/sources/components/FileTree.js b/scm-ui/src/repos/sources/components/FileTree.js index cbe03df62f..18ef1b01c5 100644 --- a/scm-ui/src/repos/sources/components/FileTree.js +++ b/scm-ui/src/repos/sources/components/FileTree.js @@ -108,32 +108,34 @@ class FileTree extends React.Component<Props> { } return ( - <table className="table table-hover table-sm is-fullwidth"> - <thead> - <tr> - <th className={classes.iconColumn} /> - <th>{t("sources.file-tree.name")}</th> - <th className="is-hidden-mobile"> - {t("sources.file-tree.length")} - </th> - <th className="is-hidden-mobile"> - {t("sources.file-tree.lastModified")} - </th> - <th className="is-hidden-mobile"> - {t("sources.file-tree.description")} - </th> - </tr> - </thead> - <tbody> - {files.map(file => ( - <FileTreeLeaf - key={file.name} - file={file} - baseUrl={baseUrlWithRevision} - /> - ))} - </tbody> - </table> + <div className="panel-block"> + <table className="table table-hover table-sm is-fullwidth"> + <thead> + <tr> + <th className={classes.iconColumn} /> + <th>{t("sources.file-tree.name")}</th> + <th className="is-hidden-mobile"> + {t("sources.file-tree.length")} + </th> + <th className="is-hidden-mobile"> + {t("sources.file-tree.lastModified")} + </th> + <th className="is-hidden-mobile"> + {t("sources.file-tree.description")} + </th> + </tr> + </thead> + <tbody> + {files.map(file => ( + <FileTreeLeaf + key={file.name} + file={file} + baseUrl={baseUrlWithRevision} + /> + ))} + </tbody> + </table> + </div> ); } } diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index a7e9874058..2f3f5ba853 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -29,9 +29,6 @@ type State = { }; const styles = { - toCenterContent: { - display: "block" - }, pointer: { cursor: "pointer" }, @@ -126,7 +123,6 @@ class Content extends React.Component<Props, State> { <div className={classNames( "panel-block", - classes.toCenterContent, classes.hasBackground )} > @@ -161,7 +157,7 @@ class Content extends React.Component<Props, State> { } render() { - const { file, revision, repository, path, classes } = this.props; + const { file, revision, repository, path } = this.props; const { showHistory } = this.state; const header = this.showHeader(); @@ -180,13 +176,11 @@ class Content extends React.Component<Props, State> { return ( <div> - <nav className="panel"> - <article className="panel-heading">{header}</article> + <div className="panel"> + <div className="panel-heading">{header}</div> {moreInformation} - <div className={classNames("panel-block", classes.toCenterContent)}> - {content} - </div> - </nav> + {content} + </div> </div> ); } diff --git a/scm-ui/src/repos/sources/containers/HistoryView.js b/scm-ui/src/repos/sources/containers/HistoryView.js index 98400248d9..d13b5904d2 100644 --- a/scm-ui/src/repos/sources/containers/HistoryView.js +++ b/scm-ui/src/repos/sources/containers/HistoryView.js @@ -79,12 +79,16 @@ class HistoryView extends React.Component<Props, State> { const currentPage = page + 1; return ( <> - <ChangesetList repository={repository} changesets={changesets} /> - <StatePaginator - page={currentPage} - collection={pageCollection} - updatePage={(newPage: number) => this.updatePage(newPage)} - /> + <div className="panel-block"> + <ChangesetList repository={repository} changesets={changesets} /> + </div> + <div className="panel-footer"> + <StatePaginator + page={currentPage} + collection={pageCollection} + updatePage={(newPage: number) => this.updatePage(newPage)} + /> + </div> </> ); } diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index 810e2309ee..05705a61d1 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -93,8 +93,10 @@ class Sources extends React.Component<Props> { if (currentFileIsDirectory) { return ( - <div className={"has-border-around"}> - {this.renderBranchSelector()} + <div className="panel"> + <div className="panel-heading"> + {this.renderBranchSelector()} + </div> <FileTree repository={repository} revision={revision} @@ -118,7 +120,7 @@ class Sources extends React.Component<Props> { <BranchSelector branches={branches} selectedBranch={revision} - label={t("branch-selector.label")} + label={t("changesets.branchSelectorLabel")} selected={(b: Branch) => { this.branchSelected(b); }} diff --git a/scm-ui/src/repos/sources/containers/SourcesView.js b/scm-ui/src/repos/sources/containers/SourcesView.js index 1e76c6d6bf..0f729beb2e 100644 --- a/scm-ui/src/repos/sources/containers/SourcesView.js +++ b/scm-ui/src/repos/sources/containers/SourcesView.js @@ -90,7 +90,7 @@ class SourcesView extends React.Component<Props, State> { const sources = this.showSources(); - return <>{sources}</>; + return <div className="panel-block">{sources}</div>; } } diff --git a/scm-ui/src/users/components/SetUserPassword.js b/scm-ui/src/users/components/SetUserPassword.js index d318025f21..a0fe844b0c 100644 --- a/scm-ui/src/users/components/SetUserPassword.js +++ b/scm-ui/src/users/components/SetUserPassword.js @@ -90,7 +90,7 @@ class SetUserPassword extends React.Component<Props, State> { message = ( <Notification type={"success"} - children={t("password.set-password-successful")} + children={t("singleUserPassword.setPasswordSuccessful")} onClose={() => this.onClose()} /> ); @@ -108,7 +108,7 @@ class SetUserPassword extends React.Component<Props, State> { <SubmitButton disabled={!this.state.passwordValid} loading={loading} - label={t("user-form.submit")} + label={t("singleUserPassword.button")} /> </form> ); diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index 9bf36d19d0..c8fae1ff0a 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -3,6 +3,7 @@ import React from "react"; import { translate } from "react-i18next"; import type { User } from "@scm-manager/ui-types"; import { + Subtitle, Checkbox, InputField, PasswordConfirmation, @@ -113,7 +114,9 @@ class UserForm extends React.Component<Props, State> { let nameField = null; let passwordChangeField = null; + let subtitle = null; if (!this.props.user) { + // create new user nameField = ( <div className="column is-half"> <InputField @@ -130,59 +133,65 @@ class UserForm extends React.Component<Props, State> { passwordChangeField = ( <PasswordConfirmation passwordChanged={this.handlePasswordChange} /> ); + } else { + // edit existing user + subtitle = <Subtitle subtitle={t("userForm.subtitle")} />; } return ( - <form onSubmit={this.submit}> - <div className="columns is-multiline"> - {nameField} - <div className="column is-half"> - <InputField - label={t("user.displayName")} - onChange={this.handleDisplayNameChange} - value={user ? user.displayName : ""} - validationError={this.state.displayNameValidationError} - errorMessage={t("validation.displayname-invalid")} - helpText={t("help.displayNameHelpText")} - /> + <> + {subtitle} + <form onSubmit={this.submit}> + <div className="columns is-multiline"> + {nameField} + <div className="column is-half"> + <InputField + label={t("user.displayName")} + onChange={this.handleDisplayNameChange} + value={user ? user.displayName : ""} + validationError={this.state.displayNameValidationError} + errorMessage={t("validation.displayname-invalid")} + helpText={t("help.displayNameHelpText")} + /> + </div> + <div className="column is-half"> + <InputField + label={t("user.mail")} + onChange={this.handleEmailChange} + value={user ? user.mail : ""} + validationError={this.state.mailValidationError} + errorMessage={t("validation.mail-invalid")} + helpText={t("help.mailHelpText")} + /> + </div> </div> - <div className="column is-half"> - <InputField - label={t("user.mail")} - onChange={this.handleEmailChange} - value={user ? user.mail : ""} - validationError={this.state.mailValidationError} - errorMessage={t("validation.mail-invalid")} - helpText={t("help.mailHelpText")} - /> + <div className="columns"> + <div className="column"> + {passwordChangeField} + <Checkbox + label={t("user.admin")} + onChange={this.handleAdminChange} + checked={user ? user.admin : false} + helpText={t("help.adminHelpText")} + /> + <Checkbox + label={t("user.active")} + onChange={this.handleActiveChange} + checked={user ? user.active : false} + helpText={t("help.activeHelpText")} + /> + </div> </div> - </div> - <div className="columns"> - <div className="column"> - {passwordChangeField} - <Checkbox - label={t("user.admin")} - onChange={this.handleAdminChange} - checked={user ? user.admin : false} - helpText={t("help.adminHelpText")} - /> - <Checkbox - label={t("user.active")} - onChange={this.handleActiveChange} - checked={user ? user.active : false} - helpText={t("help.activeHelpText")} - /> + <div className="columns"> + <div className="column"> + <SubmitButton + disabled={!this.isValid()} + loading={loading} + label={t("userForm.button")} + /> + </div> </div> - </div> - <div className="columns"> - <div className="column"> - <SubmitButton - disabled={!this.isValid()} - loading={loading} - label={t("user-form.submit")} - /> - </div> - </div> - </form> + </form> + </> ); } diff --git a/scm-ui/src/users/components/buttons/CreateUserButton.js b/scm-ui/src/users/components/buttons/CreateUserButton.js deleted file mode 100644 index f34820cd0d..0000000000 --- a/scm-ui/src/users/components/buttons/CreateUserButton.js +++ /dev/null @@ -1,20 +0,0 @@ -//@flow -import React from "react"; -import { translate } from "react-i18next"; -import { CreateButton } from "@scm-manager/ui-components"; - -// TODO remove -type Props = { - t: string => string -}; - -class CreateUserButton extends React.Component<Props> { - render() { - const { t } = this.props; - return ( - <CreateButton label={t("create-user-button.label")} link="/users/add" /> - ); - } -} - -export default translate("users")(CreateUserButton); diff --git a/scm-ui/src/users/components/navLinks/DeleteUserNavLink.js b/scm-ui/src/users/components/navLinks/DeleteUserNavLink.js deleted file mode 100644 index 80c355e999..0000000000 --- a/scm-ui/src/users/components/navLinks/DeleteUserNavLink.js +++ /dev/null @@ -1,56 +0,0 @@ -// @flow -import React from "react"; -import { translate } from "react-i18next"; -import type { User } from "@scm-manager/ui-types"; -import { NavAction, confirmAlert } from "@scm-manager/ui-components"; - -type Props = { - user: User, - confirmDialog?: boolean, - t: string => string, - deleteUser: (user: User) => void -}; - -class DeleteUserNavLink extends React.Component<Props> { - static defaultProps = { - confirmDialog: true - }; - - deleteUser = () => { - this.props.deleteUser(this.props.user); - }; - - confirmDelete = () => { - const { t } = this.props; - confirmAlert({ - title: t("delete-user-button.confirm-alert.title"), - message: t("delete-user-button.confirm-alert.message"), - buttons: [ - { - label: t("delete-user-button.confirm-alert.submit"), - onClick: () => this.deleteUser() - }, - { - label: t("delete-user-button.confirm-alert.cancel"), - onClick: () => null - } - ] - }); - }; - - isDeletable = () => { - return this.props.user._links.delete; - }; - - render() { - const { confirmDialog, t } = this.props; - const action = confirmDialog ? this.confirmDelete : this.deleteUser; - - if (!this.isDeletable()) { - return null; - } - return <NavAction icon="fas fa-times" label={t("delete-user-button.label")} action={action} />; - } -} - -export default translate("users")(DeleteUserNavLink); diff --git a/scm-ui/src/users/components/navLinks/DeleteUserNavLink.test.js b/scm-ui/src/users/components/navLinks/DeleteUserNavLink.test.js deleted file mode 100644 index 500235ab94..0000000000 --- a/scm-ui/src/users/components/navLinks/DeleteUserNavLink.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { mount, shallow } from "enzyme"; -import "../../../tests/enzyme"; -import "../../../tests/i18n"; -import DeleteUserNavLink from "./DeleteUserNavLink"; - -import { confirmAlert } from "@scm-manager/ui-components"; -jest.mock("@scm-manager/ui-components", () => ({ - confirmAlert: jest.fn(), - NavAction: require.requireActual("@scm-manager/ui-components").NavAction -})); - -describe("DeleteUserNavLink", () => { - it("should render nothing, if the delete link is missing", () => { - const user = { - _links: {} - }; - - const navLink = shallow( - <DeleteUserNavLink user={user} deleteUser={() => {}} /> - ); - expect(navLink.text()).toBe(""); - }); - - it("should render the navLink", () => { - const user = { - _links: { - delete: { - href: "/users" - } - } - }; - - const navLink = mount( - <DeleteUserNavLink user={user} deleteUser={() => {}} /> - ); - expect(navLink.text()).not.toBe(""); - }); - - it("should open the confirm dialog on navLink click", () => { - const user = { - _links: { - delete: { - href: "/users" - } - } - }; - - const navLink = mount( - <DeleteUserNavLink user={user} deleteUser={() => {}} /> - ); - navLink.find("a").simulate("click"); - - expect(confirmAlert.mock.calls.length).toBe(1); - }); - - it("should call the delete user function with delete url", () => { - const user = { - _links: { - delete: { - href: "/users" - } - } - }; - - let calledUrl = null; - function capture(user) { - calledUrl = user._links.delete.href; - } - - const navLink = mount( - <DeleteUserNavLink - user={user} - confirmDialog={false} - deleteUser={capture} - /> - ); - navLink.find("a").simulate("click"); - - expect(calledUrl).toBe("/users"); - }); -}); diff --git a/scm-ui/src/users/components/navLinks/EditUserNavLink.js b/scm-ui/src/users/components/navLinks/EditUserNavLink.js index 8be8dbc621..051bf9a4bd 100644 --- a/scm-ui/src/users/components/navLinks/EditUserNavLink.js +++ b/scm-ui/src/users/components/navLinks/EditUserNavLink.js @@ -1,28 +1,28 @@ //@flow import React from "react"; -import { translate } from "react-i18next"; import type { User } from "@scm-manager/ui-types"; import { NavLink } from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; type Props = { - t: string => string, user: User, - editUrl: String + editUrl: String, + t: string => string }; class EditUserNavLink extends React.Component<Props> { + isEditable = () => { + return this.props.user._links.update; + }; + render() { const { t, editUrl } = this.props; if (!this.isEditable()) { return null; } - return <NavLink to={editUrl} icon="fas fa-cog" label={t("edit-user-button.label")} />; + return <NavLink to={editUrl} label={t("singleUser.menu.generalNavLink")} />; } - - isEditable = () => { - return this.props.user._links.update; - }; } export default translate("users")(EditUserNavLink); diff --git a/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js index 46e931e788..79234308aa 100644 --- a/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js +++ b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js @@ -17,7 +17,7 @@ class ChangePasswordNavLink extends React.Component<Props> { if (!this.hasPermissionToSetPassword()) { return null; } - return <NavLink to={passwordUrl} label={t("set-password-button.label")} />; + return <NavLink to={passwordUrl} label={t("singleUser.menu.setPasswordNavLink")} />; } hasPermissionToSetPassword = () => { diff --git a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js index 3c593d9427..84b0f9da76 100644 --- a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js +++ b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js @@ -17,7 +17,7 @@ class ChangePermissionNavLink extends React.Component<Props> { if (!this.hasPermissionToSetPermission()) { return null; } - return <NavLink to={permissionsUrl} label={t("set-permissions-button.label")} />; + return <NavLink to={permissionsUrl} label={t("singleUser.menu.setPermissionsNavLink")} />; } hasPermissionToSetPermission = () => { diff --git a/scm-ui/src/users/components/navLinks/index.js b/scm-ui/src/users/components/navLinks/index.js index eb39bb6726..cb97c57e3f 100644 --- a/scm-ui/src/users/components/navLinks/index.js +++ b/scm-ui/src/users/components/navLinks/index.js @@ -1,4 +1,3 @@ -export { default as DeleteUserNavLink } from "./DeleteUserNavLink"; export { default as EditUserNavLink } from "./EditUserNavLink"; export { default as SetPasswordNavLink } from "./SetPasswordNavLink"; export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; diff --git a/scm-ui/src/users/containers/AddUser.js b/scm-ui/src/users/containers/AddUser.js index f19f974265..069df04187 100644 --- a/scm-ui/src/users/containers/AddUser.js +++ b/scm-ui/src/users/containers/AddUser.js @@ -49,8 +49,8 @@ class AddUser extends React.Component<Props> { return ( <Page - title={t("add-user.title")} - subtitle={t("add-user.subtitle")} + title={t("addUser.title")} + subtitle={t("addUser.subtitle")} error={error} showContentOnError={true} > diff --git a/scm-ui/src/users/containers/DeleteUser.js b/scm-ui/src/users/containers/DeleteUser.js new file mode 100644 index 0000000000..b8b42fd9e8 --- /dev/null +++ b/scm-ui/src/users/containers/DeleteUser.js @@ -0,0 +1,113 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { User } from "@scm-manager/ui-types"; +import { + Subtitle, + DeleteButton, + confirmAlert, + ErrorNotification +} from "@scm-manager/ui-components"; +import { + deleteUser, + getDeleteUserFailure, + isDeleteUserPending +} from "../modules/users"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import type { History } from "history"; + +type Props = { + loading: boolean, + error: Error, + user: User, + confirmDialog?: boolean, + deleteUser: (user: User, callback?: () => void) => void, + + // context props + history: History, + t: string => string +}; + +class DeleteUser extends React.Component<Props> { + static defaultProps = { + confirmDialog: true + }; + + userDeleted = () => { + this.props.history.push("/users"); + }; + + deleteUser = () => { + this.props.deleteUser(this.props.user, this.userDeleted); + }; + + confirmDelete = () => { + const { t } = this.props; + confirmAlert({ + title: t("deleteUser.confirmAlert.title"), + message: t("deleteUser.confirmAlert.message"), + buttons: [ + { + label: t("deleteUser.confirmAlert.submit"), + onClick: () => this.deleteUser() + }, + { + label: t("deleteUser.confirmAlert.cancel"), + onClick: () => null + } + ] + }); + }; + + isDeletable = () => { + return this.props.user._links.delete; + }; + + render() { + const { loading, error, confirmDialog, t } = this.props; + const action = confirmDialog ? this.confirmDelete : this.deleteUser; + + if (!this.isDeletable()) { + return null; + } + + return ( + <> + <Subtitle subtitle={t("deleteUser.subtitle")} /> + <ErrorNotification error={error} /> + <div className="columns"> + <div className="column"> + <DeleteButton + label={t("deleteUser.button")} + action={action} + loading={loading} + /> + </div> + </div> + </> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const loading = isDeleteUserPending(state, ownProps.user.name); + const error = getDeleteUserFailure(state, ownProps.user.name); + return { + loading, + error + }; +}; + +const mapDispatchToProps = dispatch => { + return { + deleteUser: (user: User, callback?: () => void) => { + dispatch(deleteUser(user, callback)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(translate("users")(DeleteUser))); diff --git a/scm-ui/src/users/containers/EditUser.js b/scm-ui/src/users/containers/EditUser.js index 55062ecb5b..942d5182e7 100644 --- a/scm-ui/src/users/containers/EditUser.js +++ b/scm-ui/src/users/containers/EditUser.js @@ -2,7 +2,8 @@ import React from "react"; import { connect } from "react-redux"; import { withRouter } from "react-router-dom"; -import UserForm from "./../components/UserForm"; +import UserForm from "../components/UserForm"; +import DeleteUser from "./DeleteUser"; import type { User } from "@scm-manager/ui-types"; import { modifyUser, @@ -31,6 +32,7 @@ class EditUser extends React.Component<Props> { const { modifyUserReset, user } = this.props; modifyUserReset(user); } + userModified = (user: User) => () => { this.props.history.push(`/user/${user.name}`); }; @@ -49,11 +51,22 @@ class EditUser extends React.Component<Props> { user={user} loading={loading} /> + <hr /> + <DeleteUser user={user} /> </div> ); } } +const mapStateToProps = (state, ownProps) => { + const loading = isModifyUserPending(state, ownProps.user.name); + const error = getModifyUserFailure(state, ownProps.user.name); + return { + loading, + error + }; +}; + const mapDispatchToProps = dispatch => { return { modifyUser: (user: User, callback?: () => void) => { @@ -65,15 +78,6 @@ const mapDispatchToProps = dispatch => { }; }; -const mapStateToProps = (state, ownProps) => { - const loading = isModifyUserPending(state, ownProps.user.name); - const error = getModifyUserFailure(state, ownProps.user.name); - return { - loading, - error - }; -}; - export default connect( mapStateToProps, mapDispatchToProps diff --git a/scm-ui/src/users/containers/SingleUser.js b/scm-ui/src/users/containers/SingleUser.js index 4a827ee75a..9a1633a162 100644 --- a/scm-ui/src/users/containers/SingleUser.js +++ b/scm-ui/src/users/containers/SingleUser.js @@ -5,6 +5,7 @@ import { Page, Loading, Navigation, + SubNavigation, Section, NavLink, ErrorPage @@ -16,24 +17,16 @@ import type { User } from "@scm-manager/ui-types"; import type { History } from "history"; import { fetchUserByName, - deleteUser, getUserByName, isFetchUserPending, - getFetchUserFailure, - isDeleteUserPending, - getDeleteUserFailure + getFetchUserFailure } from "../modules/users"; - -import { - DeleteUserNavLink, - EditUserNavLink, - SetPasswordNavLink, - SetPermissionsNavLink -} from "./../components/navLinks"; +import { EditUserNavLink, SetPasswordNavLink, SetPermissionsNavLink } from "./../components/navLinks"; import { translate } from "react-i18next"; import { getUsersLink } from "../../modules/indexResource"; import SetUserPassword from "../components/SetUserPassword"; import SetPermissions from "../../permissions/components/SetPermissions"; +import {ExtensionPoint} from "@scm-manager/ui-extensions"; type Props = { name: string, @@ -42,8 +35,7 @@ type Props = { error: Error, usersLink: string, - // dispatcher functions - deleteUser: (user: User, callback?: () => void) => void, + // dispatcher function fetchUserByName: (string, string) => void, // context objects @@ -57,14 +49,6 @@ class SingleUser extends React.Component<Props> { this.props.fetchUserByName(this.props.usersLink, this.props.name); } - userDeleted = () => { - this.props.history.push("/users"); - }; - - deleteUser = (user: User) => { - this.props.deleteUser(user, this.userDeleted); - }; - stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -82,8 +66,8 @@ class SingleUser extends React.Component<Props> { if (error) { return ( <ErrorPage - title={t("single-user.error-title")} - subtitle={t("single-user.error-subtitle")} + title={t("singleUser.errorTitle")} + subtitle={t("singleUser.errorSubtitle")} error={error} /> ); @@ -95,21 +79,26 @@ class SingleUser extends React.Component<Props> { const url = this.matchedUrl(); + const extensionProps = { + user, + url + }; + return ( <Page title={user.displayName}> <div className="columns"> <div className="column is-three-quarters"> <Route path={url} exact component={() => <Details user={user} />} /> <Route - path={`${url}/edit`} + path={`${url}/settings/general`} component={() => <EditUser user={user} />} /> <Route - path={`${url}/password`} + path={`${url}/settings/password`} component={() => <SetUserPassword user={user} />} /> <Route - path={`${url}/permissions`} + path={`${url}/settings/permissions`} component={() => ( <SetPermissions selectedPermissionsLink={user._links.permissions} @@ -119,25 +108,34 @@ class SingleUser extends React.Component<Props> { </div> <div className="column"> <Navigation> - <Section label={t("single-user.navigation-label")}> + <Section label={t("singleUser.menu.navigationLabel")}> <NavLink to={`${url}`} icon="fas fa-info-circle" - label={t("single-user.information-label")} + label={t("singleUser.menu.informationNavLink")} /> - <EditUserNavLink user={user} editUrl={`${url}/edit`} /> - <SetPasswordNavLink - user={user} - passwordUrl={`${url}/password`} - /> - <SetPermissionsNavLink - user={user} - permissionsUrl={`${url}/permissions`} - /> - </Section> - <Section label={t("single-user.actions-label")}> - <DeleteUserNavLink user={user} deleteUser={this.deleteUser} /> - <NavLink to="/users" icon="fas fa-undo" label={t("single-user.back-label")} /> + <SubNavigation + to={`${url}/settings/general`} + label={t("singleUser.menu.settingsNavLink")} + > + <EditUserNavLink + user={user} + editUrl={`${url}/settings/general`} + /> + <SetPasswordNavLink + user={user} + passwordUrl={`${url}/settings/password`} + /> + <SetPermissionsNavLink + user={user} + permissionsUrl={`${url}/settings/permissions`} + /> + <ExtensionPoint + name="user.subnavigation" + props={extensionProps} + renderAll={true} + /> + </SubNavigation> </Section> </Navigation> </div> @@ -150,10 +148,8 @@ class SingleUser extends React.Component<Props> { const mapStateToProps = (state, ownProps) => { const name = ownProps.match.params.name; const user = getUserByName(state, name); - const loading = - isFetchUserPending(state, name) || isDeleteUserPending(state, name); - const error = - getFetchUserFailure(state, name) || getDeleteUserFailure(state, name); + const loading = isFetchUserPending(state, name); + const error = getFetchUserFailure(state, name); const usersLink = getUsersLink(state); return { usersLink, @@ -168,9 +164,6 @@ const mapDispatchToProps = dispatch => { return { fetchUserByName: (link: string, name: string) => { dispatch(fetchUserByName(link, name)); - }, - deleteUser: (user: User, callback?: () => void) => { - dispatch(deleteUser(user, callback)); } }; }; diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index cbb33dc68b..041ac226b4 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -1,143 +1,143 @@ -// @flow -import React from "react"; -import type { History } from "history"; -import { connect } from "react-redux"; -import { translate } from "react-i18next"; - -import { - fetchUsersByPage, - fetchUsersByLink, - getUsersFromState, - selectListAsCollection, - isPermittedToCreateUsers, - isFetchUsersPending, - getFetchUsersFailure -} from "../modules/users"; - -import { Page, Paginator } from "@scm-manager/ui-components"; -import { UserTable } from "./../components/table"; -import type { User, PagedCollection } from "@scm-manager/ui-types"; -import CreateUserButton from "../components/buttons/CreateUserButton"; -import { getUsersLink } from "../../modules/indexResource"; - -type Props = { - users: User[], - loading: boolean, - error: Error, - canAddUsers: boolean, - list: PagedCollection, - page: number, - usersLink: string, - - // context objects - t: string => string, - history: History, - - // dispatch functions - fetchUsersByPage: (link: string, page: number) => void, - fetchUsersByLink: (link: string) => void -}; - -class Users extends React.Component<Props> { - componentDidMount() { - this.props.fetchUsersByPage(this.props.usersLink, this.props.page); - } - - onPageChange = (link: string) => { - this.props.fetchUsersByLink(link); - }; - - /** - * reflect page transitions in the uri - */ - componentDidUpdate() { - const { page, list } = this.props; - if (list && (list.page || list.page === 0)) { - // backend starts paging by 0 - const statePage: number = list.page + 1; - if (page !== statePage) { - this.props.history.push(`/users/${statePage}`); - } - } - } - - render() { - const { users, loading, error, t } = this.props; - return ( - <Page - title={t("users.title")} - subtitle={t("users.subtitle")} - loading={loading || !users} - error={error} - > - <UserTable users={users} /> - {this.renderPaginator()} - {this.renderCreateButton()} - </Page> - ); - } - - renderPaginator() { - const { list } = this.props; - if (list) { - return <Paginator collection={list} onPageChange={this.onPageChange} />; - } - return null; - } - - renderCreateButton() { - if (this.props.canAddUsers) { - return <CreateUserButton />; - } else { - return; - } - } -} - -const getPageFromProps = props => { - let page = props.match.params.page; - if (page) { - page = parseInt(page, 10); - } else { - page = 1; - } - return page; -}; - -const mapStateToProps = (state, ownProps) => { - const users = getUsersFromState(state); - const loading = isFetchUsersPending(state); - const error = getFetchUsersFailure(state); - - const usersLink = getUsersLink(state); - - const page = getPageFromProps(ownProps); - const canAddUsers = isPermittedToCreateUsers(state); - const list = selectListAsCollection(state); - - return { - users, - loading, - error, - canAddUsers, - list, - page, - usersLink - }; -}; - -const mapDispatchToProps = dispatch => { - return { - fetchUsersByPage: (link: string, page: number) => { - dispatch(fetchUsersByPage(link, page)); - }, - fetchUsersByLink: (link: string) => { - dispatch(fetchUsersByLink(link)); - } - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(translate("users")(Users)); +// @flow +import React from "react"; +import type { History } from "history"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; + +import { + fetchUsersByPage, + fetchUsersByLink, + getUsersFromState, + selectListAsCollection, + isPermittedToCreateUsers, + isFetchUsersPending, + getFetchUsersFailure +} from "../modules/users"; + +import { Page, CreateButton, Paginator } from "@scm-manager/ui-components"; +import { UserTable } from "./../components/table"; +import type { User, PagedCollection } from "@scm-manager/ui-types"; +import { getUsersLink } from "../../modules/indexResource"; + +type Props = { + users: User[], + loading: boolean, + error: Error, + canAddUsers: boolean, + list: PagedCollection, + page: number, + usersLink: string, + + // context objects + t: string => string, + history: History, + + // dispatch functions + fetchUsersByPage: (link: string, page: number) => void, + fetchUsersByLink: (link: string) => void +}; + +class Users extends React.Component<Props> { + componentDidMount() { + this.props.fetchUsersByPage(this.props.usersLink, this.props.page); + } + + onPageChange = (link: string) => { + this.props.fetchUsersByLink(link); + }; + + /** + * reflect page transitions in the uri + */ + componentDidUpdate() { + const { page, list } = this.props; + if (list && (list.page || list.page === 0)) { + // backend starts paging by 0 + const statePage: number = list.page + 1; + if (page !== statePage) { + this.props.history.push(`/users/${statePage}`); + } + } + } + + render() { + const { users, loading, error, t } = this.props; + return ( + <Page + title={t("users.title")} + subtitle={t("users.subtitle")} + loading={loading || !users} + error={error} + > + <UserTable users={users} /> + {this.renderPaginator()} + {this.renderCreateButton()} + </Page> + ); + } + + renderPaginator() { + const { list } = this.props; + if (list) { + return <Paginator collection={list} onPageChange={this.onPageChange} />; + } + return null; + } + + renderCreateButton() { + const { t } = this.props; + if (this.props.canAddUsers) { + return <CreateButton label={t("users.createButton")} link="/users/add" />; + } else { + return; + } + } +} + +const getPageFromProps = props => { + let page = props.match.params.page; + if (page) { + page = parseInt(page, 10); + } else { + page = 1; + } + return page; +}; + +const mapStateToProps = (state, ownProps) => { + const users = getUsersFromState(state); + const loading = isFetchUsersPending(state); + const error = getFetchUsersFailure(state); + + const usersLink = getUsersLink(state); + + const page = getPageFromProps(ownProps); + const canAddUsers = isPermittedToCreateUsers(state); + const list = selectListAsCollection(state); + + return { + users, + loading, + error, + canAddUsers, + list, + page, + usersLink + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchUsersByPage: (link: string, page: number) => { + dispatch(fetchUsersByPage(link, page)); + }, + fetchUsersByLink: (link: string) => { + dispatch(fetchUsersByLink(link)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("users")(Users)); diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index ce9e164482..0e7b0fc420 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -94,14 +94,6 @@ $fa-font-path: "webfonts"; } } -//border around options -.has-border-around { - border-top: 1px solid #eee; - border-left: 1px solid #eee; - border-right: 1px solid #eee; - border-bottom: 1px solid #eee; -} - // multiline Columns .columns.is-multiline { .column.is-half { @@ -201,6 +193,33 @@ $fa-font-path: "webfonts"; } } +// panels +.panel { + .panel-heading > .field { + margin-bottom: 0; // replace selector margin + } + .panel-block { + display: block; + } + + .panel-footer { + background-color: whitesmoke; + border-radius: 0 0 4px 4px; + color: #363636; + font-size: 1.25em; + font-weight: 300; + line-height: 1.25; + padding: 0.5em 0.75em; + + border-left: 1px solid #dbdbdb; + border-right: 1px solid #dbdbdb; + + &:last-child { + border-bottom: 1px solid #dbdbdb; + } + } +} + // forms .field:not(.is-grouped) { margin-bottom: 1rem; @@ -283,36 +302,54 @@ $fa-font-path: "webfonts"; } .menu-list { a { - border-radius: 0; color: #333; padding: 1rem; - border-top: 1px solid #eee; - border-left: 1px solid #eee; - border-right: 1px solid #eee; &.is-active { color: $blue; background-color: #fff; - - &:before { - position: relative; - content: " "; - background: $blue; - height: 53px; - width: 2px; - display: block; - left: -17px; - float: left; - top: -16px; - } } } - > li:first-child > a { + + > li { + ul { + margin: 0; + border-top: 1px solid #eee; + + li { + border-right: none; + } + li:last-child { + border-bottom: none; + } + } + + > a.is-active:before { + position: relative; + content: " "; + background: $blue; + height: 53px; + width: 2px; + display: block; + left: -17px; + float: left; + top: -16px; + } + + border-radius: 0; + border-top: 1px solid #eee; + border-left: 1px solid #eee; + border-right: 1px solid #eee; + } + > li:first-child { border-top: none; } - li:last-child > a { + li:last-child { border-bottom: 1px solid #eee; } + div { + margin-bottom: 0; + } } // modal @@ -321,3 +358,28 @@ $fa-font-path: "webfonts"; justify-content: flex-end; // pulled-right } } + +.sub-menu li { + line-height: 1; + + a { + padding: 0.75rem 1rem; + } + + a:before { + font-family: "Font Awesome 5 Free"; + font-weight: 900; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; + content: "\f105"; + padding-right: 5px; + } + + i { + display: none; + } +} diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index c83cea36d5..45da9fec49 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -115,9 +115,9 @@ </dependency> <dependency> - <groupId>javax</groupId> - <artifactId>javaee-api</artifactId> - <version>7.0</version> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>jackson-jaxrs-json-provider</artifactId> + <version>${jackson.version}</version> </dependency> <!-- rest api --> @@ -158,6 +158,24 @@ <version>${resteasy.version}</version> </dependency> + <dependency> + <groupId>org.hibernate</groupId> + <artifactId>hibernate-validator</artifactId> + <version>5.3.6.Final</version> + </dependency> + + <dependency> + <groupId>javax.el</groupId> + <artifactId>javax.el-api</artifactId> + <version>2.2.4</version> + </dependency> + + <dependency> + <groupId>org.glassfish.web</groupId> + <artifactId>javax.el</artifactId> + <version>2.2.4</version> + </dependency> + <!-- injection --> <dependency> @@ -203,18 +221,6 @@ <version>1.4.01</version> </dependency> - <!-- only for BeanComparator, replace with own implementation --> - - <dependency> - <groupId>commons-beanutils</groupId> - <artifactId>commons-beanutils</artifactId> - </dependency> - - <dependency> - <groupId>commons-collections</groupId> - <artifactId>commons-collections</artifactId> - </dependency> - <!-- fix installation of httpasswd-plugin https://groups.google.com/d/topic/scmmanager/eN7UtG8TwW8/discussion @@ -255,19 +261,19 @@ <dependency> <groupId>com.github.sdorra</groupId> <artifactId>web-resources</artifactId> - <version>1.0.2</version> + <version>1.0.4</version> </dependency> <dependency> <groupId>com.github.sdorra</groupId> <artifactId>spotter-core</artifactId> - <version>1.1.0</version> + <version>1.2.1</version> </dependency> <dependency> <groupId>org.apache.tika</groupId> <artifactId>tika-core</artifactId> - <version>1.18</version> + <version>1.20</version> </dependency> <!-- test scope --> @@ -561,8 +567,6 @@ <selenium.version>2.53.1</selenium.version> <wagon.version>1.0</wagon.version> <mustache.version>0.8.17</mustache.version> - <resteasy.version>3.1.4.Final</resteasy.version> - <jackson.version>2.8.9</jackson.version> <netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server> <sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria> <sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey> diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/NotAllowedExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/NotAllowedExceptionMapper.java new file mode 100644 index 0000000000..1d268b6855 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/NotAllowedExceptionMapper.java @@ -0,0 +1,12 @@ +package sonia.scm.api.rest; + +import javax.ws.rs.NotAllowedException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +@Provider +public class NotAllowedExceptionMapper extends StatusExceptionMapper<NotAllowedException> { + public NotAllowedExceptionMapper() { + super(NotAllowedException.class, Response.Status.METHOD_NOT_ALLOWED); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java index 4253c456fb..dfc0bd2a5d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java @@ -37,7 +37,6 @@ package sonia.scm.api.rest.resources; import com.google.common.annotations.VisibleForTesting; import com.google.common.net.UrlEscapers; -import org.apache.commons.beanutils.BeanComparator; import org.apache.shiro.authz.AuthorizationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,6 +46,7 @@ import sonia.scm.ModelObject; import sonia.scm.PageResult; import sonia.scm.api.rest.RestExceptionResult; import sonia.scm.util.AssertUtil; +import sonia.scm.util.Comparables; import sonia.scm.util.Util; import javax.ws.rs.core.CacheControl; @@ -56,15 +56,10 @@ import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.util.Arrays; +import java.net.URI; import java.util.Collection; import java.util.Comparator; import java.util.Date; -import java.net.URI; //~--- JDK imports ------------------------------------------------------------ @@ -510,21 +505,11 @@ public abstract class AbstractManagerResource<T extends ModelObject> { return builder.build(); } - @SuppressWarnings("unchecked") - private Comparator<T> createComparator(String sortBy, boolean desc) - { - checkSortByField(sortBy); - Comparator comparator; - - if (desc) - { - comparator = new BeanReverseComparator(sortBy); + private Comparator<T> createComparator(String sortBy, boolean desc) { + Comparator<T> comparator = Comparables.comparator(type, sortBy); + if (desc) { + comparator = comparator.reversed(); } - else - { - comparator = new BeanComparator(sortBy); - } - return comparator; } @@ -558,21 +543,6 @@ public abstract class AbstractManagerResource<T extends ModelObject> { return items; } - // We have to handle IntrospectionException here, because it's a checked exception - // It shouldn't occur really - so creating a new unchecked exception would be over-engineered here - @SuppressWarnings("squid:S00112") - private void checkSortByField(String sortBy) { - try { - BeanInfo info = Introspector.getBeanInfo(type); - PropertyDescriptor[] pds = info.getPropertyDescriptors(); - if (Arrays.stream(pds).noneMatch(p -> p.getName().equals(sortBy))) { - throw new IllegalArgumentException("sortBy"); - } - } catch (IntrospectionException e) { - throw new RuntimeException("error introspecting model type " + type.getName(), e); - } - } - protected PageResult<T> fetchPage(String sortBy, boolean desc, int pageNumber, int pageSize) { AssertUtil.assertPositive(pageNumber); @@ -608,51 +578,4 @@ public abstract class AbstractManagerResource<T extends ModelObject> { return lastModified; } - - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 11/06/09 - * @author Enter your name here... - */ - private static class BeanReverseComparator extends BeanComparator - { - - /** Field description */ - private static final long serialVersionUID = -8535047820348790009L; - - //~--- constructors ------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param sortby - */ - private BeanReverseComparator(String sortby) - { - super(sortby); - } - - //~--- methods ------------------------------------------------------------ - - /** - * Method description - * - * - * @param o1 - * @param o2 - * - * @return - */ - @Override - @SuppressWarnings("unchecked") - public int compare(Object o1, Object o2) - { - return super.compare(o1, o2) * -1; - } - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java index d2ce744c19..79b5dbc2ae 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java @@ -6,8 +6,6 @@ import sonia.scm.repository.api.CatCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.util.IOUtil; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import java.io.IOException; import java.io.OutputStream; diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java index d177e05a5e..b7f994b967 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java @@ -42,7 +42,6 @@ import sonia.scm.repository.api.RepositoryService; import sonia.scm.util.IOUtil; import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import java.io.IOException; import java.io.OutputStream; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java index f5bdc850ab..343d9c8bc8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -12,8 +13,7 @@ public class BranchDto extends HalRepresentation { private String name; private String revision; - @Override - protected HalRepresentation add(Links links) { - return super.add(links); + BranchDto(Links links, Embedded embedded) { + super(links, embedded); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index 658abbded8..71b1127ad8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -26,7 +26,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; -import java.util.List; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java index 7e6f0c074c..c940b1ffd9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java @@ -1,11 +1,11 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.repository.Branch; import sonia.scm.repository.NamespaceAndName; @@ -15,7 +15,7 @@ import static de.otto.edison.hal.Link.linkBuilder; import static de.otto.edison.hal.Links.linkingTo; @Mapper -public abstract class BranchToBranchDtoMapper extends LinkAppenderMapper { +public abstract class BranchToBranchDtoMapper extends HalAppenderMapper { @Inject private ResourceLinks resourceLinks; @@ -23,16 +23,17 @@ public abstract class BranchToBranchDtoMapper extends LinkAppenderMapper { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes public abstract BranchDto map(Branch branch, @Context NamespaceAndName namespaceAndName); - @AfterMapping - void appendLinks(Branch source, @MappingTarget BranchDto target, @Context NamespaceAndName namespaceAndName) { + @ObjectFactory + BranchDto createDto(@Context NamespaceAndName namespaceAndName, Branch branch) { Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.branch().self(namespaceAndName, target.getName())) - .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build()) - .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()) - .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()); + .self(resourceLinks.branch().self(namespaceAndName, branch.getName())) + .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, branch.getName())).build()) + .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()) + .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()); - appendLinks(new EdisonLinkAppender(linksBuilder), source, namespaceAndName); + Embedded.Builder embeddedBuilder = Embedded.embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), branch, namespaceAndName); - target.add(linksBuilder.build()); + return new BranchDto(linksBuilder.build(), embeddedBuilder.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 4c9620564b..f77823eaac 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 @@ -23,7 +23,9 @@ public class ConfigDto extends HalRepresentation { private boolean disableGroupingGrid; private String dateFormat; private boolean anonymousAccessEnabled; + @NoBlankStrings private Set<String> adminGroups; + @NoBlankStrings private Set<String> adminUsers; private String baseUrl; private boolean forceBaseUrl; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java index bf3a11fb9c..c646dceab4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java @@ -9,6 +9,7 @@ import sonia.scm.util.ScmConfigurationUtil; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; @@ -71,7 +72,7 @@ public class ConfigResource { @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - public Response update(ConfigDto configDto) { + public Response update(@Valid ConfigDto configDto) { // This *could* be moved to ScmConfiguration or ScmConfigurationUtil classes. // But to where to check? load() or store()? Leave it for now, SCMv1 legacy that can be cleaned up later. diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java similarity index 64% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java index 219062d320..479a43aef1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java @@ -1,11 +1,11 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.repository.Branch; import sonia.scm.repository.Changeset; import sonia.scm.repository.Repository; @@ -19,11 +19,12 @@ import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @Mapper -public abstract class ChangesetToChangesetDtoMapper extends LinkAppenderMapper implements InstantAttributeMapper { +public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMapper implements InstantAttributeMapper, ChangesetToChangesetDtoMapper{ @Inject private RepositoryServiceFactory serviceFactory; @@ -31,7 +32,6 @@ public abstract class ChangesetToChangesetDtoMapper extends LinkAppenderMapper i @Inject private ResourceLinks resourceLinks; - @Inject private BranchCollectionToDtoMapper branchCollectionToDtoMapper; @@ -46,31 +46,35 @@ public abstract class ChangesetToChangesetDtoMapper extends LinkAppenderMapper i public abstract ChangesetDto map(Changeset changeset, @Context Repository repository); - @AfterMapping - void appendLinks(Changeset source, @MappingTarget ChangesetDto target, @Context Repository repository) { + @ObjectFactory + ChangesetDto createDto(@Context Repository repository, Changeset source) { String namespace = repository.getNamespace(); String name = repository.getName(); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + try (RepositoryService repositoryService = serviceFactory.create(repository)) { if (repositoryService.isSupported(Command.TAGS)) { - target.withEmbedded("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name, + embeddedBuilder.with("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name, getListOfObjects(source.getTags(), tagName -> new Tag(tagName, source.getId())))); } if (repositoryService.isSupported(Command.BRANCHES)) { - target.withEmbedded("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name, + embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name, getListOfObjects(source.getBranches(), branchName -> new Branch(branchName, source.getId())))); } } - target.withEmbedded("parents", getListOfObjects(source.getParents(), parent -> changesetToParentDtoMapper.map(new Changeset(parent, 0L, null), repository))); + embeddedBuilder.with("parents", getListOfObjects(source.getParents(), parent -> changesetToParentDtoMapper.map(new Changeset(parent, 0L, null), repository))); Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), target.getId())) - .single(link("diff", resourceLinks.diff().self(namespace, name, target.getId()))) - .single(link("modifications", resourceLinks.modifications().self(namespace, name, target.getId()))); + .self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), source.getId())) + .single(link("diff", resourceLinks.diff().self(namespace, name, source.getId()))) + .single(link("sources", resourceLinks.source().self(namespace, name, source.getId()))) + .single(link("modifications", resourceLinks.modifications().self(namespace, name, source.getId()))); - appendLinks(new EdisonLinkAppender(linksBuilder), source, repository); - target.add(linksBuilder.build()); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), source, repository); + + return new ChangesetDto(linksBuilder.build(), embeddedBuilder.build()); } private <T> List<T> getListOfObjects(List<String> list, Function<String, T> mapFunction) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonLinkAppender.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java similarity index 52% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonLinkAppender.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java index c4e699cb58..769de2b705 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonLinkAppender.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java @@ -1,27 +1,36 @@ package sonia.scm.api.v2.resources; +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 java.util.ArrayList; import java.util.List; -class EdisonLinkAppender implements LinkAppender { +class EdisonHalAppender implements HalAppender { - private final Links.Builder builder; + private final Links.Builder linkBuilder; + private final Embedded.Builder embeddedBuilder; - EdisonLinkAppender(Links.Builder builder) { - this.builder = builder; + EdisonHalAppender(Links.Builder linkBuilder, Embedded.Builder embeddedBuilder) { + this.linkBuilder = linkBuilder; + this.embeddedBuilder = embeddedBuilder; } @Override - public void appendOne(String rel, String href) { - builder.single(Link.link(rel, href)); + public void appendLink(String rel, String href) { + linkBuilder.single(Link.link(rel, href)); } @Override - public LinkArrayBuilder arrayBuilder(String rel) { - return new EdisonLinkArrayBuilder(builder, rel); + public LinkArrayBuilder linkArrayBuilder(String rel) { + return new EdisonLinkArrayBuilder(linkBuilder, rel); + } + + @Override + public void appendEmbedded(String rel, HalRepresentation embedded) { + embeddedBuilder.with(rel, embedded); } private static class EdisonLinkArrayBuilder implements LinkArrayBuilder { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java index bd889d5de5..d155fbede6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java @@ -5,7 +5,6 @@ import lombok.Getter; import lombok.Setter; import sonia.scm.ContextEntry; -import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java index c183d731c6..0bce564e35 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -27,10 +28,8 @@ public class FileObjectDto extends HalRepresentation { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String revision; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + public FileObjectDto(Links links, Embedded embedded) { + super(links, embedded); } public void setChildren(List<FileObjectDto> children) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java index 2432d5168c..608dea9f26 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java @@ -1,24 +1,22 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.SubRepository; import javax.inject.Inject; -import java.util.List; -import java.util.stream.Collectors; - +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; @Mapper -public abstract class FileObjectToFileObjectDtoMapper extends LinkAppenderMapper implements InstantAttributeMapper { +public abstract class FileObjectToFileObjectDtoMapper extends HalAppenderMapper implements InstantAttributeMapper { @Inject private ResourceLinks resourceLinks; @@ -28,20 +26,21 @@ public abstract class FileObjectToFileObjectDtoMapper extends LinkAppenderMapper abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); - @AfterMapping - void addLinks(FileObject fileObject, @MappingTarget FileObjectDto dto, @Context NamespaceAndName namespaceAndName, @Context String revision) { + @ObjectFactory + FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context String revision, FileObject fileObject) { String path = removeFirstSlash(fileObject.getPath()); Links.Builder links = Links.linkingTo(); - if (dto.isDirectory()) { + if (fileObject.isDirectory()) { links.self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path)); } else { links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path)); links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path))); } - appendLinks(new EdisonLinkAppender(links), fileObject, namespaceAndName, revision); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(links, embeddedBuilder), fileObject, namespaceAndName, revision); - dto.add(links.build()); + return new FileObjectDto(links.build(), embeddedBuilder.build()); } private String removeFirstSlash(String source) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index 4c111e6707..6c13dc33a5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -10,7 +10,6 @@ import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java index 760beab1da..a150570316 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java @@ -1,12 +1,12 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.Pattern; import java.time.Instant; @@ -28,13 +28,7 @@ public class GroupDto extends HalRepresentation { private Map<String, String> properties; private List<String> members; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); - } - - public HalRepresentation withMembers(List<MemberDto> members) { - return super.withEmbedded("members", members); + GroupDto(Links links, Embedded embedded) { + super(links, embedded); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDtoToGroupMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDtoToGroupMapper.java index be1aca5814..3812f700da 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDtoToGroupMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDtoToGroupMapper.java @@ -4,8 +4,6 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; import sonia.scm.group.Group; -import java.time.Instant; - @Mapper public abstract class GroupDtoToGroupMapper extends BaseDtoMapper { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java index bf866af350..7d5ddae548 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java @@ -1,9 +1,9 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.group.Group; import sonia.scm.group.GroupPermissions; import sonia.scm.security.PermissionPermissions; @@ -12,6 +12,7 @@ import javax.inject.Inject; import java.util.List; import java.util.stream.Collectors; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @@ -23,28 +24,26 @@ public abstract class GroupToGroupDtoMapper extends BaseMapper<Group, GroupDto> @Inject private ResourceLinks resourceLinks; - @AfterMapping - void appendLinks(Group group, @MappingTarget GroupDto target) { - Links.Builder linksBuilder = linkingTo().self(resourceLinks.group().self(target.getName())); + @ObjectFactory + GroupDto createDto(Group group) { + Links.Builder linksBuilder = linkingTo().self(resourceLinks.group().self(group.getName())); if (GroupPermissions.delete(group).isPermitted()) { - linksBuilder.single(link("delete", resourceLinks.group().delete(target.getName()))); + linksBuilder.single(link("delete", resourceLinks.group().delete(group.getName()))); } if (GroupPermissions.modify(group).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.group().update(target.getName()))); + linksBuilder.single(link("update", resourceLinks.group().update(group.getName()))); } if (PermissionPermissions.read().isPermitted()) { - linksBuilder.single(link("permissions", resourceLinks.groupPermissions().permissions(target.getName()))); + linksBuilder.single(link("permissions", resourceLinks.groupPermissions().permissions(group.getName()))); } - appendLinks(new EdisonLinkAppender(linksBuilder), group); - - target.add(linksBuilder.build()); - } - - @AfterMapping - void mapMembers(Group group, @MappingTarget GroupDto target) { + Embedded.Builder embeddedBuilder = embeddedBuilder(); List<MemberDto> memberDtos = group.getMembers().stream().map(this::createMember).collect(Collectors.toList()); - target.withMembers(memberDtos); + embeddedBuilder.with("members", memberDtos); + + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), group); + + return new GroupDto(linksBuilder.build(), embeddedBuilder.build()); } private MemberDto createMember(String name) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java index 9346420f58..16f945332d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -9,8 +10,8 @@ public class IndexDto extends HalRepresentation { private final String version; - IndexDto(String version, Links links) { - super(links); + IndexDto(Links links, Embedded embedded, String version) { + super(links, embedded); this.version = version; } } 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 3eff661385..90445bcdc2 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,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.google.common.collect.Lists; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; @@ -13,9 +14,10 @@ import sonia.scm.user.UserPermissions; import javax.inject.Inject; import java.util.List; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; -public class IndexDtoGenerator extends LinkAppenderMapper { +public class IndexDtoGenerator extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final SCMContextProvider scmContextProvider; @@ -61,8 +63,9 @@ public class IndexDtoGenerator extends LinkAppenderMapper { builder.single(link("login", resourceLinks.authentication().jsonLogin())); } - appendLinks(new EdisonLinkAppender(builder), new Index()); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(builder, embeddedBuilder), new Index()); - return new IndexDto(scmContextProvider.getVersion(), builder.build()); + return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistration.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistration.java index 890e268ed5..8472eb9fc1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistration.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistration.java @@ -10,30 +10,30 @@ import javax.servlet.ServletContextListener; import java.util.Set; /** - * Registers every {@link LinkEnricher} which is annotated with an {@link Enrich} annotation. + * Registers every {@link HalEnricher} which is annotated with an {@link Enrich} annotation. */ @Extension public class LinkEnricherAutoRegistration implements ServletContextListener { private static final Logger LOG = LoggerFactory.getLogger(LinkEnricherAutoRegistration.class); - private final LinkEnricherRegistry registry; - private final Set<LinkEnricher> enrichers; + private final HalEnricherRegistry registry; + private final Set<HalEnricher> enrichers; @Inject - public LinkEnricherAutoRegistration(LinkEnricherRegistry registry, Set<LinkEnricher> enrichers) { + public LinkEnricherAutoRegistration(HalEnricherRegistry registry, Set<HalEnricher> enrichers) { this.registry = registry; this.enrichers = enrichers; } @Override public void contextInitialized(ServletContextEvent sce) { - for (LinkEnricher enricher : enrichers) { + for (HalEnricher enricher : enrichers) { Enrich annotation = enricher.getClass().getAnnotation(Enrich.class); if (annotation != null) { registry.register(annotation.value(), enricher); } else { - LOG.warn("found LinkEnricher extension {} without Enrich annotation", enricher.getClass()); + LOG.warn("found HalEnricher extension {} without Enrich annotation", enricher.getClass()); } } } 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 859a6481f6..c74f16ad70 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 @@ -28,7 +28,7 @@ public class MapperModule extends AbstractModule { bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass()); bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass()); - bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass()); + bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(DefaultChangesetToChangesetDtoMapper.class).getClass()); bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass()); bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.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 5488faca28..84fbbfe290 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 @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -18,9 +19,7 @@ public class MeDto extends HalRepresentation { private String mail; private List<String> groups; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + 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 082db7fd94..b5e1998066 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,6 +1,7 @@ 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; @@ -13,10 +14,11 @@ 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; import static de.otto.edison.hal.Links.linkingTo; -public class MeDtoFactory extends LinkAppenderMapper { +public class MeDtoFactory extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final UserManager userManager; @@ -29,15 +31,11 @@ public class MeDtoFactory extends LinkAppenderMapper { public MeDto create() { PrincipalCollection principals = getPrincipalCollection(); - - MeDto dto = new MeDto(); - User user = principals.oneByType(User.class); + MeDto dto = createDto(user); mapUserProperties(user, dto); mapGroups(principals, dto); - - appendLinks(user, dto); return dto; } @@ -61,21 +59,22 @@ public class MeDtoFactory extends LinkAppenderMapper { } - private void appendLinks(User user, MeDto target) { + private MeDto createDto(User user) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self()); if (UserPermissions.delete(user).isPermitted()) { - linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName()))); + linksBuilder.single(link("delete", resourceLinks.me().delete(user.getName()))); } if (UserPermissions.modify(user).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.me().update(target.getName()))); + linksBuilder.single(link("update", resourceLinks.me().update(user.getName()))); } if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) { linksBuilder.single(link("password", resourceLinks.me().passwordChange())); } - appendLinks(new EdisonLinkAppender(linksBuilder), new Me(), user); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), new Me(), user); - target.add(linksBuilder.build()); + return new MeDto(linksBuilder.build(), embeddedBuilder.build()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStrings.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStrings.java new file mode 100644 index 0000000000..ba5e20ffbd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStrings.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Constraint(validatedBy = NoBlankStringsValidator.class) +@Documented +public @interface NoBlankStrings { + + String message() default "collection must not contain empty strings"; + + Class<?>[] groups() default {}; + + Class<? extends Payload>[] payload() default {}; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStringsValidator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStringsValidator.java new file mode 100644 index 0000000000..6bae44164e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStringsValidator.java @@ -0,0 +1,23 @@ +package sonia.scm.api.v2.resources; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Collection; + +public class NoBlankStringsValidator implements ConstraintValidator<NoBlankStrings, Collection> { + + @Override + public void initialize(NoBlankStrings constraintAnnotation) { + } + + @Override + public boolean isValid(Collection object, ConstraintValidatorContext constraintContext) { + if ( object == null || object.isEmpty()) { + return true; + } + return object.stream() + .map(x -> x.toString()) + .map(s -> ((String) s).trim()) + .noneMatch(s -> ((String) s).isEmpty()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java index 87d1aeca9f..31269d468e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java @@ -1,7 +1,6 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Links; -import org.mapstruct.Context; import sonia.scm.security.PermissionDescriptor; import sonia.scm.security.PermissionPermissions; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index a9bd5c2424..e1e1260a4d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -23,7 +23,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; -import static java.util.Arrays.asList; import static java.util.Collections.singletonList; public class RepositoryCollectionResource { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index ddfe432d73..8b48311bba 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -1,9 +1,11 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; @@ -13,7 +15,7 @@ import java.time.Instant; import java.util.List; import java.util.Map; -@Getter @Setter +@Getter @Setter @NoArgsConstructor public class RepositoryDto extends HalRepresentation { @Email @@ -31,9 +33,7 @@ public class RepositoryDto extends HalRepresentation { private String type; protected Map<String, String> properties; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + RepositoryDto(Links links, Embedded embedded) { + super(links, embedded); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java index 09683db488..fe8c2c19b1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import com.fasterxml.jackson.annotation.JsonInclude; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -9,7 +8,6 @@ import lombok.Setter; import lombok.ToString; import org.hibernate.validator.constraints.NotEmpty; -import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import java.util.Collection; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 19929b63ba..9e680b7e5c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -1,11 +1,11 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.repository.Feature; import sonia.scm.repository.HealthCheckFailure; import sonia.scm.repository.Repository; @@ -17,6 +17,7 @@ import sonia.scm.repository.api.ScmProtocol; import java.util.List; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; import static java.util.stream.Collectors.toList; @@ -33,17 +34,17 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit abstract HealthCheckFailureDto toDto(HealthCheckFailure failure); - @AfterMapping - void appendLinks(Repository repository, @MappingTarget RepositoryDto target) { - Links.Builder linksBuilder = linkingTo().self(resourceLinks.repository().self(target.getNamespace(), target.getName())); + @ObjectFactory + RepositoryDto createDto(Repository repository) { + Links.Builder linksBuilder = linkingTo().self(resourceLinks.repository().self(repository.getNamespace(), repository.getName())); if (RepositoryPermissions.delete(repository).isPermitted()) { - linksBuilder.single(link("delete", resourceLinks.repository().delete(target.getNamespace(), target.getName()))); + linksBuilder.single(link("delete", resourceLinks.repository().delete(repository.getNamespace(), repository.getName()))); } if (RepositoryPermissions.modify(repository).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.repository().update(target.getNamespace(), target.getName()))); + linksBuilder.single(link("update", resourceLinks.repository().update(repository.getNamespace(), repository.getName()))); } if (RepositoryPermissions.permissionRead(repository).isPermitted()) { - linksBuilder.single(link("permissions", resourceLinks.repositoryPermission().all(target.getNamespace(), target.getName()))); + linksBuilder.single(link("permissions", resourceLinks.repositoryPermission().all(repository.getNamespace(), repository.getName()))); } try (RepositoryService repositoryService = serviceFactory.create(repository)) { if (RepositoryPermissions.pull(repository).isPermitted()) { @@ -53,26 +54,27 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit linksBuilder.array(protocolLinks); } if (repositoryService.isSupported(Command.TAGS)) { - linksBuilder.single(link("tags", resourceLinks.tag().all(target.getNamespace(), target.getName()))); + linksBuilder.single(link("tags", resourceLinks.tag().all(repository.getNamespace(), repository.getName()))); } if (repositoryService.isSupported(Command.BRANCHES)) { - linksBuilder.single(link("branches", resourceLinks.branchCollection().self(target.getNamespace(), target.getName()))); + linksBuilder.single(link("branches", resourceLinks.branchCollection().self(repository.getNamespace(), repository.getName()))); } if (repositoryService.isSupported(Feature.INCOMING_REVISION)) { - linksBuilder.single(link("incomingChangesets", resourceLinks.incoming().changesets(target.getNamespace(), target.getName()))); - linksBuilder.single(link("incomingDiff", resourceLinks.incoming().diff(target.getNamespace(), target.getName()))); + linksBuilder.single(link("incomingChangesets", resourceLinks.incoming().changesets(repository.getNamespace(), repository.getName()))); + linksBuilder.single(link("incomingDiff", resourceLinks.incoming().diff(repository.getNamespace(), repository.getName()))); } if (repositoryService.isSupported(Command.MERGE)) { - linksBuilder.single(link("merge", resourceLinks.merge().merge(target.getNamespace(), target.getName()))); - linksBuilder.single(link("mergeDryRun", resourceLinks.merge().dryRun(target.getNamespace(), target.getName()))); + linksBuilder.single(link("merge", resourceLinks.merge().merge(repository.getNamespace(), repository.getName()))); + linksBuilder.single(link("mergeDryRun", resourceLinks.merge().dryRun(repository.getNamespace(), repository.getName()))); } } - linksBuilder.single(link("changesets", resourceLinks.changeset().all(target.getNamespace(), target.getName()))); - linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(target.getNamespace(), target.getName()))); + linksBuilder.single(link("changesets", resourceLinks.changeset().all(repository.getNamespace(), repository.getName()))); + linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(repository.getNamespace(), repository.getName()))); - appendLinks(new EdisonLinkAppender(linksBuilder), repository); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), repository); - target.add(linksBuilder.build()); + return new RepositoryDto(linksBuilder.build(), embeddedBuilder.build()); } private Link createProtocolLink(ScmProtocol protocol) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java index 8af036f5a3..a3d4c5d17e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -15,10 +16,8 @@ public class TagDto extends HalRepresentation { private String revision; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + TagDto(Links links, Embedded embedded) { + super(links, embedded); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java index 5ede1cb55b..3a7faf3155 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java @@ -1,21 +1,22 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Tag; import javax.inject.Inject; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @Mapper -public abstract class TagToTagDtoMapper extends LinkAppenderMapper { +public abstract class TagToTagDtoMapper extends HalAppenderMapper { @Inject private ResourceLinks resourceLinks; @@ -23,15 +24,16 @@ public abstract class TagToTagDtoMapper extends LinkAppenderMapper { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes public abstract TagDto map(Tag tag, @Context NamespaceAndName namespaceAndName); - @AfterMapping - void appendLinks(Tag tag, @MappingTarget TagDto target, @Context NamespaceAndName namespaceAndName) { + @ObjectFactory + TagDto createDto(@Context NamespaceAndName namespaceAndName, Tag tag) { Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())) - .single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision()))) - .single(link("changeset", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision()))); + .self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), tag.getName())) + .single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), tag.getRevision()))) + .single(link("changeset", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), tag.getRevision()))); - appendLinks(new EdisonLinkAppender(linksBuilder), tag, namespaceAndName); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), tag, namespaceAndName); - target.add(linksBuilder.build()); + return new TagDto(linksBuilder.build(), embeddedBuilder.build()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index a4fe9adb94..a7442a2262 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -11,7 +11,6 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java index 9dc5b850bd..0ee7b2f82c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -33,9 +34,7 @@ public class UserDto extends HalRepresentation { private String type; private Map<String, String> properties; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + UserDto(Links links, Embedded embedded) { + super(links, embedded); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java index 3c7e9fd7f1..ac641e3e66 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java @@ -1,10 +1,10 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.security.PermissionPermissions; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -12,6 +12,7 @@ import sonia.scm.user.UserPermissions; import javax.inject.Inject; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @@ -31,25 +32,26 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> { @Inject private ResourceLinks resourceLinks; - @AfterMapping - protected void appendLinks(User user, @MappingTarget UserDto target) { - Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(target.getName())); + @ObjectFactory + UserDto createDto(User user) { + Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(user.getName())); if (UserPermissions.delete(user).isPermitted()) { - linksBuilder.single(link("delete", resourceLinks.user().delete(target.getName()))); + linksBuilder.single(link("delete", resourceLinks.user().delete(user.getName()))); } if (UserPermissions.modify(user).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.user().update(target.getName()))); + linksBuilder.single(link("update", resourceLinks.user().update(user.getName()))); if (userManager.isTypeDefault(user)) { - linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName()))); + linksBuilder.single(link("password", resourceLinks.user().passwordChange(user.getName()))); } } if (PermissionPermissions.read().isPermitted()) { - linksBuilder.single(link("permissions", resourceLinks.userPermissions().permissions(target.getName()))); + linksBuilder.single(link("permissions", resourceLinks.userPermissions().permissions(user.getName()))); } - appendLinks(new EdisonLinkAppender(linksBuilder), user); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), user); - target.add(linksBuilder.build()); + return new UserDto(linksBuilder.build(), embeddedBuilder.build()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index afaa28bfe8..be5a1e7ac2 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -33,12 +33,9 @@ package sonia.scm.boot; //~--- non-JDK imports -------------------------------------------------------- -import com.github.legman.Subscribe; - import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.io.Files; -import com.google.inject.servlet.GuiceFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java index b77a927a2d..fc52ef4eff 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java @@ -42,7 +42,6 @@ import org.slf4j.MDC; import sonia.scm.SCMContext; import sonia.scm.security.DefaultKeyGenerator; -import sonia.scm.security.KeyGenerator; import sonia.scm.web.filter.HttpFilter; //~--- JDK imports ------------------------------------------------------------ diff --git a/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java index 508e804d1f..e7d020ae18 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java @@ -51,8 +51,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; - //~--- JDK imports ------------------------------------------------------------ /** diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java index 64e26405a1..5b895a34fa 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java @@ -35,7 +35,6 @@ import io.jsonwebtoken.Claims; import java.util.Collections; import java.util.Date; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java b/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java index 6b6b06aa9c..1fab500d79 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java +++ b/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java @@ -1,7 +1,5 @@ package sonia.scm.security; -import org.apache.commons.collections.CollectionUtils; - import java.util.Collection; import java.util.Collections; import java.util.Objects; @@ -33,8 +31,9 @@ public class RepositoryRole { if (this == o) return true; if (!(o instanceof RepositoryRole)) return false; RepositoryRole that = (RepositoryRole) o; - return name.equals(that.name) && - CollectionUtils.isEqualCollection(this.verbs, that.verbs); + return name.equals(that.name) + && this.verbs.containsAll(that.verbs) + && this.verbs.size() == that.verbs.size(); } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java b/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java index 77683bd6be..e58945a346 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java @@ -17,9 +17,6 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Set; -import static sonia.scm.util.HttpUtil.AUTHENTICATION_REALM; -import static sonia.scm.util.HttpUtil.HEADER_WWW_AUTHENTICATE; - @Priority(Filters.PRIORITY_AUTHENTICATION) @WebElement(value = HttpProtocolServlet.PATTERN) public class HttpProtocolServletAuthenticationFilter extends AuthenticationFilter { diff --git a/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java b/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java index b074781fec..08a4a33493 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java @@ -78,8 +78,9 @@ public class I18nServlet extends HttpServlet { @VisibleForTesting @Override protected void doGet(HttpServletRequest req, HttpServletResponse response) { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json"); try (PrintWriter out = response.getWriter()) { - response.setContentType("application/json"); String path = req.getServletPath(); Function<String, Optional<JsonNode>> jsonFileProvider = usedPath -> Optional.empty(); BiConsumer<String, JsonNode> createdJsonFileConsumer = (usedPath, jsonNode) -> log.debug("A json File is created from the path {}", usedPath); diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json new file mode 100644 index 0000000000..96eb8e8e9b --- /dev/null +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -0,0 +1,81 @@ +{ + "permissions": { + "repository": { + "read,pull": { + "*": { + "displayName": "Alle Repositories lesen", + "description": "Darf alle Repositories lesen und klonen." + } + }, + "read,pull,push": { + "*": { + "displayName": "Alle Repositories schreiben", + "description": "Darf alle Repositories lesen, klonen und schreiben." + } + }, + "*": { + "*": { + "displayName": "Alle Repositories besitzen (Owner)", + "description": "Darf alle Repositories lesen, klonen, schreiben, konfigurieren und löschen." + } + }, + "create": { + "displayName": "Repositories erstellen", + "description": "Darf Repositories erstellen." + } + }, + "user": { + "*": { + "displayName": "Benutzer administrieren", + "description": "Darf Benutzer administrieren." + } + }, + "group": { + "*": { + "displayName": "Gruppen administrieren", + "description": "Darf Gruppen administrieren." + } + }, + "unknown": "Unbekannte Berechtigung" + }, + "verbs": { + "repository": { + "read": { + "displayName": "Lesen", + "description": "Darf das Repository im SCM-Manager sehen." + }, + "modify": { + "displayName": "Modifizieren", + "description": "Darf die Eigenschaften des Repository verändern." + }, + "delete": { + "displayName": "Löschen", + "description": "Darf das Repository löschen." + }, + "pull": { + "displayName": "Pull/Checkout", + "description": "Darf pull/checkout auf das Repository ausführen." + }, + "push": { + "displayName": "Push/Commit", + "description": "Darf push/commit auf das Repository ausführen und damit den Inhalt verändern." + }, + "permissionRead": { + "displayName": "Berechtigungen lesen", + "description": "Darf die Berechtigungen des Repository sehen." + }, + "permissionWrite": { + "displayName": "Berechtigungen modifizieren", + "description": "Darf die Berechtigungen des Repository bearbeiten." + }, + "healthCheck": { + "displayName": "Health Check", + "description": "Darf den Repository Health Check ausführen." + }, + "*": { + "displayName": "Alle Repository Rechte", + "description": "Darf im Repository Kontext alles ausführen. Dies beinhaltet alle Repository Berechtigungen." + } + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java index 696174d6e0..fd9745be83 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java @@ -21,7 +21,7 @@ import java.util.Comparator; import static java.util.Collections.emptyList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java index b60775a73b..8bdbacafc2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java @@ -3,7 +3,6 @@ package sonia.scm.api.v2; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; -import com.google.common.collect.Lists; import com.google.common.io.Resources; import org.junit.Test; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index 4994c11b08..9216922e19 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -82,7 +82,7 @@ public class BranchRootResourceTest extends RepositoryTestBase { @InjectMocks - private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java index d2e202576a..3e64ab95b6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java @@ -24,12 +24,12 @@ class BranchToBranchDtoMapperTest { @Test void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(Branch.class, (ctx, appender) -> { NamespaceAndName namespaceAndName = ctx.oneRequireByType(NamespaceAndName.class); Branch branch = ctx.oneRequireByType(Branch.class); - appender.appendOne("ka", "http://" + namespaceAndName.logString() + "/" + branch.getName()); + appender.appendLink("ka", "http://" + namespaceAndName.logString() + "/" + branch.getName()); }); mapper.setRegistry(registry); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java index 69695279e6..7653fbe122 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import org.assertj.core.api.Assertions; import org.junit.Test; import sonia.scm.PageResult; import sonia.scm.repository.Changeset; @@ -17,7 +16,7 @@ public class ChangesetCollectionToDtoMapperTest { public static final Repository REPOSITORY = new Repository("", "git", "space", "name"); public static final Changeset CHANGESET = new Changeset(); - private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper = mock(ChangesetToChangesetDtoMapper.class); + private final DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper = mock(DefaultChangesetToChangesetDtoMapperImpl.class); private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, ResourceLinksMock.createMock(URI.create("/"))); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java index d5c0f91f81..952c8504f6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java @@ -65,7 +65,7 @@ public class ChangesetRootResourceTest extends RepositoryTestBase { private ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; @InjectMocks - private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; private ChangesetRootResource changesetRootResource; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index 033824cbea..66689c6ac6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -18,6 +18,7 @@ import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -68,7 +69,7 @@ public class ConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldGetGlobalConfig() throws URISyntaxException { + public void shouldGetGlobalConfig() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -92,11 +93,7 @@ public class ConfigResourceTest { @Test @SubjectAware(username = "readWrite") public void shouldUpdateConfig() throws URISyntaxException, IOException { - URL url = Resources.getResource("sonia/scm/api/v2/config-test-update.json"); - byte[] configJson = Resources.toByteArray(url); - MockHttpRequest request = MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2) - .contentType(VndMediaType.CONFIG) - .content(configJson); + MockHttpRequest request = post("sonia/scm/api/v2/config-test-update.json"); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -113,11 +110,7 @@ public class ConfigResourceTest { @Test @SubjectAware(username = "readOnly") public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, IOException { - URL url = Resources.getResource("sonia/scm/api/v2/config-test-update.json"); - byte[] configJson = Resources.toByteArray(url); - MockHttpRequest request = MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2) - .contentType(VndMediaType.CONFIG) - .content(configJson); + MockHttpRequest request = post("sonia/scm/api/v2/config-test-update.json"); MockHttpResponse response = new MockHttpResponse(); thrown.expectMessage("Subject does not have permission [configuration:write:global]"); @@ -125,6 +118,36 @@ public class ConfigResourceTest { dispatcher.invoke(request, response); } + @Test + @SubjectAware(username = "readWrite") + public void shouldFailForEmptyAdminUsers() throws URISyntaxException, IOException { + MockHttpRequest request = post("sonia/scm/api/v2/config-test-empty-admin-user.json"); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldFailForEmptyAdminGroups() throws URISyntaxException, IOException { + MockHttpRequest request = post("sonia/scm/api/v2/config-test-empty-admin-group.json"); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); + } + + private MockHttpRequest post(String resourceName) throws IOException, URISyntaxException { + URL url = Resources.getResource(resourceName); + byte[] configJson = Resources.toByteArray(url); + return MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2) + .contentType(VndMediaType.CONFIG) + .content(configJson); + } + private static ScmConfiguration createConfiguration() { ScmConfiguration scmConfiguration = new ScmConfiguration(); scmConfiguration.setProxyPassword("heartOfGold"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonHalAppenderTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonHalAppenderTest.java new file mode 100644 index 0000000000..ff149c5dc5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonHalAppenderTest.java @@ -0,0 +1,61 @@ +package sonia.scm.api.v2.resources; + +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Links.linkingTo; +import static org.assertj.core.api.Assertions.assertThat; + +class EdisonHalAppenderTest { + + private Links.Builder linksBuilder; + private Embedded.Builder embeddedBuilder; + private EdisonHalAppender appender; + + @BeforeEach + void prepare() { + linksBuilder = linkingTo(); + embeddedBuilder = embeddedBuilder(); + appender = new EdisonHalAppender(linksBuilder, embeddedBuilder); + } + + @Test + void shouldAppendOneLink() { + appender.appendLink("self", "https://scm.hitchhiker.com"); + + Links links = linksBuilder.build(); + assertThat(links.getLinkBy("self").get().getHref()).isEqualTo("https://scm.hitchhiker.com"); + } + + @Test + void shouldAppendMultipleLinks() { + appender.linkArrayBuilder("items") + .append("one", "http://one") + .append("two", "http://two") + .build(); + + List<Link> items = linksBuilder.build().getLinksBy("items"); + assertThat(items).hasSize(2); + } + + @Test + void shouldAppendEmbedded() { + HalRepresentation one = new HalRepresentation(); + appender.appendEmbedded("one", one); + + HalRepresentation two = new HalRepresentation(); + appender.appendEmbedded("two", new HalRepresentation()); + + Embedded embedded = embeddedBuilder.build(); + assertThat(embedded.getItemsBy("one")).containsOnly(one); + assertThat(embedded.getItemsBy("two")).containsOnly(two); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonLinkAppenderTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonLinkAppenderTest.java deleted file mode 100644 index e97415cc09..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonLinkAppenderTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package sonia.scm.api.v2.resources; - -import de.otto.edison.hal.Link; -import de.otto.edison.hal.Links; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static de.otto.edison.hal.Links.linkingTo; -import static org.assertj.core.api.Assertions.assertThat; - -class EdisonLinkAppenderTest { - - private Links.Builder builder; - private EdisonLinkAppender appender; - - @BeforeEach - void prepare() { - builder = linkingTo(); - appender = new EdisonLinkAppender(builder); - } - - @Test - void shouldAppendOneLink() { - appender.appendOne("self", "https://scm.hitchhiker.com"); - - Links links = builder.build(); - assertThat(links.getLinkBy("self").get().getHref()).isEqualTo("https://scm.hitchhiker.com"); - } - - @Test - void shouldAppendMultipleLinks() { - appender.arrayBuilder("items") - .append("one", "http://one") - .append("two", "http://two") - .build(); - - List<Link> items = builder.build().getLinksBy("items"); - assertThat(items).hasSize(2); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java index 52c9a434c0..a8b3c15158 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java @@ -66,7 +66,7 @@ public class FileHistoryResourceTest extends RepositoryTestBase { private FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper; @InjectMocks - private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; private FileHistoryRootResource fileHistoryRootResource; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java index b25410210f..55058a1684 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java @@ -73,13 +73,13 @@ public class FileObjectToFileObjectDtoMapperTest { @Test public void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(FileObject.class, (ctx, appender) -> { NamespaceAndName repository = ctx.oneRequireByType(NamespaceAndName.class); FileObject fo = ctx.oneRequireByType(FileObject.class); String rev = ctx.oneRequireByType(String.class); - appender.appendOne("hog", "http://" + repository.logString() + "/" + fo.getName() + "/" + rev); + appender.appendLink("hog", "http://" + repository.logString() + "/" + fo.getName() + "/" + rev); }); mapper.setRegistry(registry); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index 646e9d0839..3e2d0f9663 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -24,6 +24,7 @@ import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -100,7 +101,7 @@ public class GroupRootResourceTest { } @Test - public void shouldGetGroup() throws URISyntaxException { + public void shouldGetGroup() throws URISyntaxException, UnsupportedEncodingException { Group group = createDummyGroup(); when(groupManager.get("admin")).thenReturn(group); @@ -305,7 +306,7 @@ public class GroupRootResourceTest { } @Test - public void shouldGetAll() throws URISyntaxException { + public void shouldGetAll() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2); MockHttpResponse response = new MockHttpResponse(); @@ -317,7 +318,7 @@ public class GroupRootResourceTest { } @Test - public void shouldGetPermissionLink() throws URISyntaxException { + public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin"); MockHttpResponse response = new MockHttpResponse(); @@ -329,7 +330,7 @@ public class GroupRootResourceTest { } @Test - public void shouldGetPermissions() throws URISyntaxException { + public void shouldGetPermissions() throws URISyntaxException, UnsupportedEncodingException { when(permissionAssigner.readPermissionsForGroup("admin")).thenReturn(singletonList(new PermissionDescriptor("something:*"))); MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin/permissions"); MockHttpResponse response = new MockHttpResponse(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java index b681dff21f..045124ad91 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java @@ -11,7 +11,6 @@ import org.mockito.InjectMocks; import sonia.scm.group.Group; import java.net.URI; -import java.net.URISyntaxException; import java.util.stream.IntStream; import static java.util.stream.Collectors.toList; @@ -91,10 +90,10 @@ public class GroupToGroupDtoMapperTest { @Test public void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(Group.class, (ctx, appender) -> { Group group = ctx.oneRequireByType(Group.class); - appender.appendOne("some", "http://" + group.getName()); + appender.appendLink("some", "http://" + group.getName()); }); mapper.setRegistry(registry); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/HalEnricherAutoRegistrationTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/HalEnricherAutoRegistrationTest.java new file mode 100644 index 0000000000..314dcf11c2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/HalEnricherAutoRegistrationTest.java @@ -0,0 +1,64 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.collect.ImmutableSet; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +class HalEnricherAutoRegistrationTest { + + @Test + void shouldRegisterAllAvailableLinkEnrichers() { + HalEnricher one = new One(); + HalEnricher two = new Two(); + HalEnricher three = new Three(); + HalEnricher four = new Four(); + Set<HalEnricher> enrichers = ImmutableSet.of(one, two, three, four); + + HalEnricherRegistry registry = new HalEnricherRegistry(); + + LinkEnricherAutoRegistration autoRegistration = new LinkEnricherAutoRegistration(registry, enrichers); + autoRegistration.contextInitialized(null); + + assertThat(registry.allByType(String.class)).containsOnly(one, two); + assertThat(registry.allByType(Integer.class)).containsOnly(three); + } + + @Enrich(String.class) + public static class One implements HalEnricher { + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + + } + } + + @Enrich(String.class) + public static class Two implements HalEnricher { + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + + } + } + + @Enrich(Integer.class) + public static class Three implements HalEnricher { + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + + } + } + + public static class Four implements HalEnricher { + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java index e4495a0455..b965c2f2c3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java @@ -74,7 +74,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase { private IncomingChangesetCollectionToDtoMapper incomingChangesetCollectionToDtoMapper; @InjectMocks - private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; private IncomingRootResource incomingRootResource; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistrationTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistrationTest.java deleted file mode 100644 index a2b72abc49..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistrationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package sonia.scm.api.v2.resources; - -import com.google.common.collect.ImmutableSet; -import org.junit.jupiter.api.Test; - -import java.util.Set; - -import static org.assertj.core.api.Java6Assertions.assertThat; - -class LinkEnricherAutoRegistrationTest { - - @Test - void shouldRegisterAllAvailableLinkEnrichers() { - LinkEnricher one = new One(); - LinkEnricher two = new Two(); - LinkEnricher three = new Three(); - LinkEnricher four = new Four(); - Set<LinkEnricher> enrichers = ImmutableSet.of(one, two, three, four); - - LinkEnricherRegistry registry = new LinkEnricherRegistry(); - - LinkEnricherAutoRegistration autoRegistration = new LinkEnricherAutoRegistration(registry, enrichers); - autoRegistration.contextInitialized(null); - - assertThat(registry.allByType(String.class)).containsOnly(one, two); - assertThat(registry.allByType(Integer.class)).containsOnly(three); - } - - @Enrich(String.class) - public static class One implements LinkEnricher { - - @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { - - } - } - - @Enrich(String.class) - public static class Two implements LinkEnricher { - - @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { - - } - } - - @Enrich(Integer.class) - public static class Three implements LinkEnricher { - - @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { - - } - } - - public static class Four implements LinkEnricher { - - @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { - - } - } - -} 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 138387938b..8a00c69229 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 @@ -15,7 +15,6 @@ import org.mockito.quality.Strictness; import sonia.scm.group.GroupNames; import sonia.scm.user.User; import sonia.scm.user.UserManager; -import sonia.scm.user.UserPermissions; import sonia.scm.user.UserTestData; import java.net.URI; @@ -170,12 +169,12 @@ class MeDtoFactoryTest { void shouldAppendLinks() { prepareSubject(UserTestData.createTrillian()); - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); meDtoFactory.setRegistry(registry); registry.register(Me.class, (ctx, appender) -> { User user = ctx.oneRequireByType(User.class); - appender.appendOne("profile", "http://hitchhiker.com/users/" + user.getName()); + appender.appendLink("profile", "http://hitchhiker.com/users/" + user.getName()); }); MeDto dto = meDtoFactory.create(); 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 052a059959..cd2a172c1b 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 @@ -22,12 +22,12 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @@ -78,7 +78,7 @@ public class MeResourceTest { } @Test - public void shouldReturnCurrentlyAuthenticatedUser() throws URISyntaxException { + public void shouldReturnCurrentlyAuthenticatedUser() throws URISyntaxException, UnsupportedEncodingException { applyUserToSubject(originalUser); MockHttpRequest request = MockHttpRequest.get("/" + MeResource.ME_PATH_V2); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NoBlankStringsValidatorTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NoBlankStringsValidatorTest.java new file mode 100644 index 0000000000..1929b0bb06 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NoBlankStringsValidatorTest.java @@ -0,0 +1,28 @@ +package sonia.scm.api.v2.resources; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static java.util.Collections.emptySet; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NoBlankStringsValidatorTest { + + @Test + void shouldAcceptNonEmptyElements() { + assertTrue(new NoBlankStringsValidator().isValid(Arrays.asList("not", "empty"), null)); + } + + @Test + void shouldFailForEmptyElements() { + assertFalse(new NoBlankStringsValidator().isValid(Arrays.asList("one", "", "three"), null)); + } + + @Test + void shouldAcceptEmptyList() { + assertTrue(new NoBlankStringsValidator().isValid(emptySet(), null)); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java index 2795562b14..4472acb2c5 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java @@ -36,6 +36,7 @@ import sonia.scm.repository.RepositoryPermission; import sonia.scm.web.VndMediaType; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -53,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -214,7 +215,12 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { .expectedResponseStatus(200) .path(PATH_OF_ALL_PERMISSIONS + expectedPermission.getName()) .responseValidator((response) -> { - String body = response.getContentAsString(); + String body = null; + try { + body = response.getContentAsString(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } ObjectMapper mapper = new ObjectMapper(); try { RepositoryPermissionDto actualRepositoryPermissionDto = mapper.readValue(body, RepositoryPermissionDto.class); @@ -268,13 +274,21 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestPOSTPermission .content("{\"name\" : \"" + newPermission.getName() + "\" , \"verbs\" : [\"read\",\"pull\",\"push\"], \"groupPermission\" : true}") .expectedResponseStatus(201) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("POST response has no body") .isBlank()) ); assertGettingExpectedPermissions(expectedPermissions, PERMISSION_WRITE); } + private String getContentAsString(MockHttpResponse response) { + try { + return response.getContentAsString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("could not get content from response", e); + } + } + @Test public void shouldNotAddExistingPermission() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); @@ -296,7 +310,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"verbs\" : [\"*\"], \"groupPermission\" : false}") .path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName()) .expectedResponseStatus(204) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("PUT response has no body") .isBlank()) ); @@ -312,7 +326,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestDELETEPermission .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) .expectedResponseStatus(204) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("DELETE response has no body") .isBlank()) ); @@ -327,7 +341,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestDELETEPermission .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) .expectedResponseStatus(204) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("DELETE response has no body") .isBlank()) ); @@ -335,7 +349,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestDELETEPermission .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) .expectedResponseStatus(204) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("DELETE response has no body") .isBlank()) ); @@ -346,7 +360,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestGETAllPermissions .expectedResponseStatus(200) .responseValidator((response) -> { - String body = response.getContentAsString(); + String body = getContentAsString(response); ObjectMapper mapper = new ObjectMapper(); try { HalRepresentation halRepresentation = mapper.readValue(body, HalRepresentation.class); 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 bf4366f0b2..1f6ed6b3a7 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 @@ -27,6 +27,7 @@ import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -41,9 +42,9 @@ 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.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyObject; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -120,7 +121,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { } @Test - public void shouldFindExistingRepository() throws URISyntaxException { + public void shouldFindExistingRepository() throws URISyntaxException, UnsupportedEncodingException { mockRepository("space", "repo"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); @@ -133,7 +134,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { } @Test - public void shouldMapProperties() throws URISyntaxException { + public void shouldMapProperties() throws URISyntaxException, UnsupportedEncodingException { Repository repository = mockRepository("space", "repo"); repository.setProperty("testKey", "testValue"); @@ -146,7 +147,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { } @Test - public void shouldGetAll() throws URISyntaxException { + public void shouldGetAll() throws URISyntaxException, UnsupportedEncodingException { PageResult<Repository> singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); when(repositoryManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 8469e966c8..4b02508ae8 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -23,7 +23,7 @@ import static java.util.stream.Stream.of; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -211,10 +211,10 @@ public class RepositoryToRepositoryDtoMapperTest { @Test public void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(Repository.class, (ctx, appender) -> { Repository repository = ctx.oneRequireByType(Repository.class); - appender.appendOne("id", "http://" + repository.getId()); + appender.appendLink("id", "http://" + repository.getId()); }); mapper.setRegistry(registry); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java index aa8eb3e7ab..cd3c18de27 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java @@ -22,11 +22,11 @@ class TagToTagDtoMapperTest { @Test void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(Tag.class, (ctx, appender) -> { NamespaceAndName repository = ctx.oneRequireByType(NamespaceAndName.class); Tag tag = ctx.oneRequireByType(Tag.class); - appender.appendOne("yo", "http://" + repository.logString() + "/" + tag.getName()); + appender.appendLink("yo", "http://" + repository.logString() + "/" + tag.getName()); }); mapper.setRegistry(registry); 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 99a1435923..b2dafc8cfe 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 @@ -16,6 +16,7 @@ import sonia.scm.plugin.*; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.HashSet; @@ -87,7 +88,7 @@ public class UIRootResourceTest { } @Test - public void shouldReturnPlugin() throws URISyntaxException { + public void shouldReturnPlugin() throws URISyntaxException, UnsupportedEncodingException { mockPlugins(mockPlugin("awesome", "Awesome", createPluginResources("my/awesome.bundle.js"))); MockHttpRequest request = MockHttpRequest.get("/v2/ui/plugins/awesome"); @@ -101,7 +102,7 @@ public class UIRootResourceTest { } @Test - public void shouldReturnPlugins() throws URISyntaxException { + public void shouldReturnPlugins() throws URISyntaxException, UnsupportedEncodingException { mockPlugins( mockPlugin("awesome", "Awesome", createPluginResources("my/awesome.bundle.js")), mockPlugin("special", "Special", createPluginResources("my/special.bundle.js")) @@ -120,7 +121,7 @@ public class UIRootResourceTest { } @Test - public void shouldNotReturnPluginsWithoutResources() throws URISyntaxException { + public void shouldNotReturnPluginsWithoutResources() throws URISyntaxException, UnsupportedEncodingException { mockPlugins( mockPlugin("awesome", "Awesome", createPluginResources("my/awesome.bundle.js")), mockPlugin("special") diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java index 19f247b3b2..552009b73f 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java @@ -10,7 +10,6 @@ import sonia.scm.user.User; import java.time.Instant; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; public class UserDtoToUserMapperTest { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 88142e4d50..4047dfadd2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -14,7 +14,6 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Spy; import sonia.scm.ContextEntry; import sonia.scm.NotFoundException; import sonia.scm.PageResult; @@ -26,6 +25,7 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -35,8 +35,8 @@ import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; @@ -76,7 +76,7 @@ public class UserRootResourceTest { private User originalUser; @Before - public void prepareEnvironment() throws Exception { + public void prepareEnvironment() { initMocks(this); originalUser = createDummyUser("Neo"); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); @@ -97,7 +97,7 @@ public class UserRootResourceTest { } @Test - public void shouldCreateFullResponseForAdmin() throws URISyntaxException { + public void shouldCreateFullResponseForAdmin() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo"); MockHttpResponse response = new MockHttpResponse(); @@ -137,7 +137,7 @@ public class UserRootResourceTest { @Test @SubjectAware(username = "unpriv") - public void shouldCreateLimitedResponseForSimpleUser() throws URISyntaxException { + public void shouldCreateLimitedResponseForSimpleUser() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo"); MockHttpResponse response = new MockHttpResponse(); @@ -331,7 +331,7 @@ public class UserRootResourceTest { } @Test - public void shouldCreatePageForOnePageOnly() throws URISyntaxException { + public void shouldCreatePageForOnePageOnly() throws URISyntaxException, UnsupportedEncodingException { PageResult<User> singletonPageResult = createSingletonPageResult(1); when(userManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2); @@ -347,7 +347,7 @@ public class UserRootResourceTest { } @Test - public void shouldCreatePageForMultiplePages() throws URISyntaxException { + public void shouldCreatePageForMultiplePages() throws URISyntaxException, UnsupportedEncodingException { PageResult<User> singletonPageResult = createSingletonPageResult(3); when(userManager.getPage(any(), eq(1), eq(1))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?page=1&pageSize=1"); @@ -365,7 +365,7 @@ public class UserRootResourceTest { } @Test - public void shouldGetPermissionLink() throws URISyntaxException { + public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo"); MockHttpResponse response = new MockHttpResponse(); @@ -377,7 +377,7 @@ public class UserRootResourceTest { } @Test - public void shouldGetPermissions() throws URISyntaxException { + public void shouldGetPermissions() throws URISyntaxException, UnsupportedEncodingException { when(permissionAssigner.readPermissionsForUser("Neo")).thenReturn(singletonList(new PermissionDescriptor("something:*"))); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions"); MockHttpResponse response = new MockHttpResponse(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java index 9924dae81b..ae1d75dddf 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java @@ -155,8 +155,8 @@ public class UserToUserDtoMapperTest { public void shouldAppendLink() { User trillian = UserTestData.createTrillian(); - LinkEnricherRegistry registry = new LinkEnricherRegistry(); - registry.register(User.class, (ctx, appender) -> appender.appendOne("sample", "http://" + ctx.oneByType(User.class).get().getName())); + HalEnricherRegistry registry = new HalEnricherRegistry(); + registry.register(User.class, (ctx, appender) -> appender.appendLink("sample", "http://" + ctx.oneByType(User.class).get().getName())); mapper.setRegistry(registry); UserDto userDto = mapper.map(trillian); diff --git a/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java b/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java index b8b538c82b..eac4a12340 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java @@ -2,7 +2,6 @@ package sonia.scm.boot; import com.github.legman.Subscribe; import com.google.common.base.Charsets; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; diff --git a/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java b/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java index a26cf3b215..c9d8c594b4 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java @@ -13,7 +13,6 @@ import java.util.Enumeration; import java.util.Set; import java.util.Vector; -import static org.junit.Assert.*; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java index 8cdb162740..a367d171a1 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java @@ -136,10 +136,13 @@ public class GitLfsITCase { } private void createUser(User user) { - UserDto dto = new UserToUserDtoMapperImpl(){ - @Override - protected void appendLinks(User user, UserDto target) {} - }.map(user); + UserDto dto = new UserDto(); + dto.setName(user.getName()); + dto.setMail(user.getMail()); + dto.setDisplayName(user.getDisplayName()); + dto.setType(user.getType()); + dto.setActive(user.isActive()); + dto.setAdmin(user.isAdmin()); dto.setPassword(user.getPassword()); createResource(adminClient, "users") .accept("*/*") diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java index a60c884b64..399f20cd3f 100644 --- a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java +++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java @@ -41,6 +41,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.io.ByteSource; +import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,8 +50,6 @@ import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.config.ScmConfiguration; -import static org.hamcrest.Matchers.*; - import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -136,7 +135,7 @@ public class DefaultAdvancedHttpResponseTest connection, 200, "OK"); Multimap<String, String> headers = response.getHeaders(); - assertThat(headers.get("Test"), contains("One", "Two")); + assertThat(headers.get("Test"), Matchers.contains("One", "Two")); assertTrue(headers.get("Test-2").isEmpty()); } @@ -144,8 +143,7 @@ public class DefaultAdvancedHttpResponseTest /** Field description */ private final DefaultAdvancedHttpClient client = - new DefaultAdvancedHttpClient(new ScmConfiguration(), - new HashSet<ContentTransformer>(), new SSLContextProvider()); + new DefaultAdvancedHttpClient(new ScmConfiguration(), new HashSet<>(), new SSLContextProvider()); /** Field description */ @Mock diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/MultiParentClassLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/MultiParentClassLoaderTest.java index ae65f5c1ae..df31977de1 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/MultiParentClassLoaderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/MultiParentClassLoaderTest.java @@ -29,9 +29,6 @@ package sonia.scm.plugin; -import com.google.common.base.Enums; -import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; import java.io.IOException; import java.net.URL; import java.util.Arrays; diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java index efaeb702fe..baf4c659cc 100644 --- a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java +++ b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java @@ -32,12 +32,11 @@ package sonia.scm.schedule; import org.junit.Test; -import static org.junit.Assert.*; + import static org.mockito.Mockito.*; -import static org.hamcrest.Matchers.*; + import org.junit.Before; import org.junit.runner.RunWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.quartz.JobKey; 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 5c7aa08f37..c2d75358fd 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java @@ -33,18 +33,13 @@ package sonia.scm.security; import com.google.common.collect.ImmutableSet; import org.apache.shiro.authc.AuthenticationInfo; -import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; -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.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.stubbing.Answer; import java.util.HashMap; import java.util.Set; @@ -52,7 +47,6 @@ import java.util.Set; 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.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java index cce3fea2b1..f59991f2cc 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java @@ -47,7 +47,6 @@ import sonia.scm.store.ConfigurationEntryStoreFactory; import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.in; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; diff --git a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java index 8e261b75cc..ab31d751fd 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java @@ -45,9 +45,6 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import sonia.scm.NotFoundException; -import sonia.scm.repository.InitialRepositoryLocationResolver; -import sonia.scm.repository.RepositoryDAO; -import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.user.xml.XmlUserDAO; diff --git a/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java b/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java index 29c7dea358..5f95a171d2 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java @@ -3,7 +3,7 @@ package sonia.scm.web.cgi; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import javax.servlet.http.HttpServletRequest; diff --git a/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java index a912f738e2..bb3c7b5f1e 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java @@ -2,8 +2,6 @@ package sonia.scm.web.i18n; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.sdorra.shiro.ShiroRule; -import com.github.sdorra.shiro.SubjectAware; import com.google.common.base.Charsets; import com.google.common.io.Files; import org.apache.commons.lang3.StringUtils; @@ -37,17 +35,11 @@ import java.util.Enumeration; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.Silent.class) -@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") public class I18nServletTest { - @Rule - public ShiroRule shiro = new ShiroRule(); - private static final String GIT_PLUGIN_JSON = json( "{", "'scm-git-plugin': {", @@ -88,15 +80,15 @@ public class I18nServletTest { public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock - PluginLoader pluginLoader; + private PluginLoader pluginLoader; @Mock - CacheManager cacheManager; + private CacheManager cacheManager; @Mock - ClassLoader classLoader; + private ClassLoader classLoader; - I18nServlet servlet; + private I18nServlet servlet; @Mock private Cache cache; @@ -106,9 +98,9 @@ public class I18nServletTest { @SuppressWarnings("unchecked") public void init() throws IOException { resources = Collections.enumeration(Lists.newArrayList( - createFileFromString(SVN_PLUGIN_JSON).toURL(), - createFileFromString(GIT_PLUGIN_JSON).toURL(), - createFileFromString(HG_PLUGIN_JSON).toURL() + createFileFromString(SVN_PLUGIN_JSON).toURI().toURL(), + createFileFromString(GIT_PLUGIN_JSON).toURI().toURL(), + createFileFromString(HG_PLUGIN_JSON).toURI().toURL() )); when(pluginLoader.getUberClassLoader()).thenReturn(classLoader); when(cacheManager.getCache(I18nServlet.CACHE_NAME)).thenReturn(cache); @@ -194,6 +186,8 @@ public class I18nServletTest { assertJson(json); verify(cache).get(path); verify(cache).put(eq(path), any()); + + verifyHeaders(response); } @Test @@ -221,6 +215,8 @@ public class I18nServletTest { verify(cache, never()).put(eq(path), any()); verify(cache).get(path); assertJson(json); + + verifyHeaders(response); } @Test @@ -234,11 +230,16 @@ public class I18nServletTest { assertJson(jsonNodeOptional.orElse(null)); } + private void verifyHeaders(HttpServletResponse response) { + verify(response).setCharacterEncoding("UTF-8"); + verify(response).setContentType("application/json"); + } + public void assertJson(JsonNode actual) throws IOException { assertJson(actual.toString()); } - public void assertJson(String actual) throws IOException { + private void assertJson(String actual) throws IOException { assertThat(actual) .isNotEmpty() .contains(StringUtils.deleteWhitespace(GIT_PLUGIN_JSON.substring(1, GIT_PLUGIN_JSON.length() - 1))) @@ -246,7 +247,7 @@ public class I18nServletTest { .contains(StringUtils.deleteWhitespace(SVN_PLUGIN_JSON.substring(1, SVN_PLUGIN_JSON.length() - 1))); } - public File createFileFromString(String json) throws IOException { + private File createFileFromString(String json) throws IOException { File file = temporaryFolder.newFile(); Files.write(json.getBytes(Charsets.UTF_8), file); return file; diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-group.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-group.json new file mode 100644 index 0000000000..f665c29ee7 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-group.json @@ -0,0 +1,3 @@ +{ + "adminGroups": [""] +} diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-user.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-user.json new file mode 100644 index 0000000000..61efcb1609 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-user.json @@ -0,0 +1,3 @@ +{ + "adminUsers": [""] +}