diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000..0609164d73
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+# ignore everything except scm-server.tar.gz
+**
+!scm-server/target/*.tar.gz
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index 9dda3b659b..8b4bf8dee1 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1 +1,2 @@
+# Keep this version number in sync with Jenkinsfile
distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000..781a3e890b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,20 @@
+FROM openjdk:8u171-alpine3.8
+
+ENV SCM_HOME=/var/lib/scm
+
+RUN set -x \
+ && apk add --no-cache mercurial bash \
+ && addgroup -S -g 1000 scm \
+ && adduser -S -s /bin/false -G scm -h /opt/scm-server -D -H -u 1000 scm \
+ && mkdir ${SCM_HOME} \
+ && chown scm:scm ${SCM_HOME}
+
+ADD scm-server/target/scm-server-app.tar.gz /opt
+RUN chown -R scm:scm /opt/scm-server
+
+WORKDIR /opt/scm-server
+VOLUME [ "${SCM_HOME}", "/opt/scm-server/var/log" ]
+EXPOSE 8080
+USER scm
+
+ENTRYPOINT [ "/opt/scm-server/bin/scm-server" ]
diff --git a/Jenkinsfile b/Jenkinsfile
index cd02d15487..57cc3b901f 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -4,7 +4,7 @@
@Library('github.com/cloudogu/ces-build-lib@59d3e94')
import com.cloudogu.ces.cesbuildlib.*
-node() { // No specific label
+node('docker') {
// Change this as when we go back to default - necessary for proper SonarQube analysis
mainBranch = "2.0.0-m3"
@@ -12,15 +12,14 @@ node() { // No specific label
properties([
// Keep only the last 10 build to preserve space
buildDiscarder(logRotator(numToKeepStr: '10')),
+ disableConcurrentBuilds()
])
- catchError {
+ timeout(activity: true, time: 20, unit: 'MINUTES') {
- Maven mvn = setupMavenBuild()
- // Maven build specified it must be 1.8.0-101 or newer
- def javaHome = tool 'JDK-1.8.0-101+'
+ catchError {
- withEnv(["JAVA_HOME=${javaHome}", "PATH=${env.JAVA_HOME}/bin:${env.PATH}"]) {
+ Maven mvn = setupMavenBuild()
stage('Checkout') {
checkout scm
@@ -46,24 +45,45 @@ node() { // No specific label
currentBuild.result = 'UNSTABLE'
}
}
+
+ def commitHash = getCommitHash()
+ def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}"
+
+ if (isMainBranch()) {
+ stage('Docker') {
+ def image = docker.build('cloudogu/scm-manager')
+ docker.withRegistry('', 'hub.docker.com-cesmarvin') {
+ image.push(dockerImageTag)
+ image.push('latest')
+ }
+ }
+
+ stage('Deployment') {
+ build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [
+ string(name: 'changeset', value: commitHash),
+ string(name: 'imageTag', value: dockerImageTag)
+ ]
+ }
+ }
}
+
+ // Archive Unit and integration test results, if any
+ junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml'
+
+ // Find maven warnings and visualize in job
+ warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true
+
+ mailIfStatusChanged(commitAuthorEmail)
}
-
- // Archive Unit and integration test results, if any
- junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml'
-
- // Find maven warnings and visualize in job
- warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true
-
- mailIfStatusChanged(commitAuthorEmail)
}
String mainBranch
Maven setupMavenBuild() {
- Maven mvn = new MavenWrapper(this)
+ // Keep this version number in sync with .mvn/maven-wrapper.properties
+ Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8")
- if (mainBranch.equals(env.BRANCH_NAME)) {
+ if (isMainBranch()) {
// Release starts javadoc, which takes very long, so do only for certain branches
mvn.additionalArgs += ' -DperformRelease'
// JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet.
@@ -90,7 +110,7 @@ void analyzeWith(Maven mvn) {
"-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager "
} else {
mvnArgs += " -Dsonar.branch.name=${env.BRANCH_NAME} "
- if (!mainBranch.equals(env.BRANCH_NAME)) {
+ if (!isMainBranch()) {
// Avoid exception "The main branch must not have a target" on main branch
mvnArgs += " -Dsonar.branch.target=${mainBranch} "
}
@@ -99,6 +119,10 @@ void analyzeWith(Maven mvn) {
}
}
+boolean isMainBranch() {
+ return mainBranch.equals(env.BRANCH_NAME)
+}
+
boolean waitForQualityGateWebhookToBeCalled() {
boolean isQualityGateSucceeded = true
timeout(time: 2, unit: 'MINUTES') { // Needed when there is no webhook for example
@@ -115,6 +139,10 @@ String getCommitAuthorComplete() {
new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{author}"'
}
+String getCommitHash() {
+ new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{node}"'
+}
+
String getCommitAuthorEmail() {
def matcher = getCommitAuthorComplete() =~ "<(.*?)>"
matcher ? matcher[0][1] : ""
diff --git a/deployments/helm/.helmignore b/deployments/helm/.helmignore
new file mode 100644
index 0000000000..f0c1319444
--- /dev/null
+++ b/deployments/helm/.helmignore
@@ -0,0 +1,21 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
diff --git a/deployments/helm/Chart.yaml b/deployments/helm/Chart.yaml
new file mode 100644
index 0000000000..c5b5fff4cc
--- /dev/null
+++ b/deployments/helm/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+appVersion: "1.0"
+description: A Helm chart for SCM-Manager
+name: scm-manager
+version: 0.1.0
diff --git a/deployments/helm/templates/NOTES.txt b/deployments/helm/templates/NOTES.txt
new file mode 100644
index 0000000000..a58c8f124a
--- /dev/null
+++ b/deployments/helm/templates/NOTES.txt
@@ -0,0 +1,19 @@
+1. Get the application URL by running these commands:
+{{- if .Values.ingress.enabled }}
+{{- range .Values.ingress.hosts }}
+ http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+ export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "scm-manager.fullname" . }})
+ export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+ echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+ NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+ You can watch the status of by running 'kubectl get svc -w {{ include "scm-manager.fullname" . }}'
+ export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "scm-manager.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+ echo http://$SERVICE_IP:{{ .Values.service.port }}
+{{- else if contains "ClusterIP" .Values.service.type }}
+ export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ include "scm-manager.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+ echo "Visit http://127.0.0.1:8080 to use your application"
+ kubectl port-forward $POD_NAME 8080:80
+{{- end }}
diff --git a/deployments/helm/templates/_helpers.tpl b/deployments/helm/templates/_helpers.tpl
new file mode 100644
index 0000000000..23d4e1b03e
--- /dev/null
+++ b/deployments/helm/templates/_helpers.tpl
@@ -0,0 +1,32 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "scm-manager.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "scm-manager.fullname" -}}
+{{- if .Values.fullnameOverride -}}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- if contains $name .Release.Name -}}
+{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "scm-manager.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
diff --git a/deployments/helm/templates/configmap.yaml b/deployments/helm/templates/configmap.yaml
new file mode 100644
index 0000000000..dd52b6fa8c
--- /dev/null
+++ b/deployments/helm/templates/configmap.yaml
@@ -0,0 +1,160 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "scm-manager.fullname" . }}
+ labels:
+ app: {{ include "scm-manager.name" . }}
+ chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ release: "{{ .Release.Name }}"
+ heritage: "{{ .Release.Service }}"
+data:
+ server-config.xml: |
+
+
+
+
+
+
+ 16384
+ 16384
+
+ {{- if .Values.ingress.enabled -}}
+
+
+
+
+ {{- end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /scm
+
+ /var/webapp/scm-webapp.war
+
+
+ org.eclipse.jetty.servlet.Default.dirAllowed
+ false
+
+
+ /work/scm
+
+
+
+
+ /
+
+
+
+
+
+ /var/webapp/docroot
+
+
+
+
+
+ /work/docroot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ logging.xml: |
+
+
+
+ <--
+ in a container environment we only need stdout
+ -->
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/deployments/helm/templates/deployment.yaml b/deployments/helm/templates/deployment.yaml
new file mode 100644
index 0000000000..928daa5f06
--- /dev/null
+++ b/deployments/helm/templates/deployment.yaml
@@ -0,0 +1,77 @@
+apiVersion: apps/v1beta2
+kind: Deployment
+metadata:
+ name: {{ include "scm-manager.fullname" . }}
+ labels:
+ app: {{ include "scm-manager.name" . }}
+ chart: {{ include "scm-manager.chart" . }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+spec:
+ replicas: 1 # could not be scaled
+ strategy:
+ type: Recreate
+ selector:
+ matchLabels:
+ app: {{ include "scm-manager.name" . }}
+ release: {{ .Release.Name }}
+ template:
+ metadata:
+ labels:
+ app: {{ include "scm-manager.name" . }}
+ release: {{ .Release.Name }}
+ spec:
+ initContainers:
+ - name: volume-permissions
+ image: alpine:3.8
+ imagePullPolicy: IfNotPresent
+ command: ['sh', '-c', 'chown 1000:1000 /data']
+ volumeMounts:
+ - name: data
+ mountPath: /data
+ containers:
+ - name: {{ .Chart.Name }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: 8080
+ protocol: TCP
+ livenessProbe:
+ httpGet:
+ path: /scm
+ port: http
+ readinessProbe:
+ httpGet:
+ path: /scm
+ port: http
+ resources:
+{{ toYaml .Values.resources | indent 12 }}
+ volumeMounts:
+ - name: data
+ mountPath: /var/lib/scm
+ - name: config
+ mountPath: /opt/scm-server/conf
+ volumes:
+ - name: data
+ {{- if .Values.persistence.enabled }}
+ persistentVolumeClaim:
+ claimName: {{ include "scm-manager.fullname" . }}
+ {{- else }}
+ emptyDir: {}
+ {{- end }}
+ - name: config
+ configMap:
+ name: {{ include "scm-manager.fullname" . }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+{{ toYaml . | indent 8 }}
+ {{- end }}
+ {{- with .Values.affinity }}
+ affinity:
+{{ toYaml . | indent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+{{ toYaml . | indent 8 }}
+ {{- end }}
diff --git a/deployments/helm/templates/ingress.yaml b/deployments/helm/templates/ingress.yaml
new file mode 100644
index 0000000000..66912c9d96
--- /dev/null
+++ b/deployments/helm/templates/ingress.yaml
@@ -0,0 +1,38 @@
+{{- if .Values.ingress.enabled -}}
+{{- $fullName := include "scm-manager.fullname" . -}}
+{{- $ingressPath := .Values.ingress.path -}}
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+ name: {{ $fullName }}
+ labels:
+ app: {{ include "scm-manager.name" . }}
+ chart: {{ include "scm-manager.chart" . }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+{{- with .Values.ingress.annotations }}
+ annotations:
+{{ toYaml . | indent 4 }}
+{{- end }}
+spec:
+{{- if .Values.ingress.tls }}
+ tls:
+ {{- range .Values.ingress.tls }}
+ - hosts:
+ {{- range .hosts }}
+ - {{ . | quote }}
+ {{- end }}
+ secretName: {{ .secretName }}
+ {{- end }}
+{{- end }}
+ rules:
+ {{- range .Values.ingress.hosts }}
+ - host: {{ . | quote }}
+ http:
+ paths:
+ - path: {{ $ingressPath }}
+ backend:
+ serviceName: {{ $fullName }}
+ servicePort: http
+ {{- end }}
+{{- end }}
diff --git a/deployments/helm/templates/pvc.yaml b/deployments/helm/templates/pvc.yaml
new file mode 100644
index 0000000000..0e7d0f6db4
--- /dev/null
+++ b/deployments/helm/templates/pvc.yaml
@@ -0,0 +1,24 @@
+{{- if .Values.persistence.enabled -}}
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+ name: {{ include "scm-manager.fullname" . }}
+ labels:
+ app: {{ include "scm-manager.name" . }}
+ chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ release: "{{ .Release.Name }}"
+ heritage: "{{ .Release.Service }}"
+spec:
+ accessModes:
+ - {{ .Values.persistence.accessMode | quote }}
+ resources:
+ requests:
+ storage: {{ .Values.persistence.size | quote }}
+{{- if .Values.persistence.storageClass }}
+{{- if (eq "-" .Values.persistence.storageClass) }}
+ storageClassName: ""
+{{- else }}
+ storageClassName: "{{ .Values.persistence.storageClass }}"
+{{- end }}
+{{- end }}
+{{- end -}}
diff --git a/deployments/helm/templates/service.yaml b/deployments/helm/templates/service.yaml
new file mode 100644
index 0000000000..f3a8207908
--- /dev/null
+++ b/deployments/helm/templates/service.yaml
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "scm-manager.fullname" . }}
+ labels:
+ app: {{ include "scm-manager.name" . }}
+ chart: {{ include "scm-manager.chart" . }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: 8080
+ protocol: TCP
+ name: http
+ selector:
+ app: {{ include "scm-manager.name" . }}
+ release: {{ .Release.Name }}
diff --git a/deployments/helm/values.yaml b/deployments/helm/values.yaml
new file mode 100644
index 0000000000..d54088aa8b
--- /dev/null
+++ b/deployments/helm/values.yaml
@@ -0,0 +1,65 @@
+# Default values for scm-manager.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+# replicaCount: 1
+
+image:
+ repository: cloudogu/scm-manager
+ # TODO change after release, to something more stable
+ tag: latest
+ pullPolicy: Always
+
+nameOverride: ""
+fullnameOverride: ""
+
+service:
+ type: LoadBalancer
+ port: 80
+
+ingress:
+ enabled: false
+ annotations: {}
+ # kubernetes.io/ingress.class: nginx
+ # kubernetes.io/tls-acme: "true"
+ path: /
+ hosts:
+ - scm-manager.local
+ tls: []
+ # - secretName: scm-manager-tls
+ # hosts:
+ # - scm-manager.local
+
+## Enable persistence using Persistent Volume Claims
+## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
+##
+persistence:
+ enabled: true
+ ## ghost data Persistent Volume Storage Class
+ ## If defined, storageClassName:
+ ## If set to "-", storageClassName: "", which disables dynamic provisioning
+ ## If undefined (the default) or set to null, no storageClassName spec is
+ ## set, choosing the default provisioner. (gp2 on AWS, standard on
+ ## GKE, AWS & OpenStack)
+ ##
+ # storageClass: "-"
+ accessMode: ReadWriteOnce
+ size: 12Gi
+
+resources:
+ # We usually recommend not to specify default resources and to leave this as a conscious
+ # choice for the user. This also increases chances charts run on environments with little
+ # resources, such as Minikube. If you do want to specify resources, uncomment the following
+ # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+ limits:
+ cpu: 2000m
+ memory: 2048Mi
+ requests:
+ cpu: 50m
+ memory: 256Mi
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
diff --git a/pom.xml b/pom.xml
index 8ca4318230..df49023265 100644
--- a/pom.xml
+++ b/pom.xml
@@ -188,15 +188,14 @@
test
-
- com.github.sdorra.shiro-static-permissions
+ com.github.sdorrassp-lib${ssp.version}
- com.github.sdorra.shiro-static-permissions
+ com.github.sdorrassp-processor${ssp.version}true
@@ -430,7 +429,9 @@
org.codehaus.mojoanimal-sniffer-maven-plugin
- 1.17
+
+ 1.16org.codehaus.mojo.signature
@@ -763,7 +764,7 @@
9.2.10.v20150310
- 967c8fd521
+ 1.1.01.4.0
diff --git a/scm-core/pom.xml b/scm-core/pom.xml
index 17ca03b115..3c90fec779 100644
--- a/scm-core/pom.xml
+++ b/scm-core/pom.xml
@@ -32,6 +32,13 @@
scm-annotations2.0.0-SNAPSHOT
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
@@ -87,6 +94,12 @@
javax.ws.rs-api
+
+ org.jboss.resteasy
+ resteasy-jaxrs
+ test
+
+
@@ -153,14 +166,13 @@
provided
-
- com.github.sdorra.shiro-static-permissions
+ com.github.sdorrassp-lib
- com.github.sdorra.shiro-static-permissions
+ com.github.sdorrassp-processortrue
diff --git a/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java b/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java
deleted file mode 100644
index 727e9a8160..0000000000
--- a/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-package sonia.scm;
-
-/**
- *
- * @author Sebastian Sdorra
- * @since 1.17
- */
-public class ArgumentIsInvalidException extends IllegalStateException
-{
-
- /**
- * Constructs ...
- *
- */
- public ArgumentIsInvalidException()
- {
- super();
- }
-
- /**
- * Constructs ...
- *
- *
- * @param s
- */
- public ArgumentIsInvalidException(String s)
- {
- super(s);
- }
-
- /**
- * Constructs ...
- *
- *
- * @param cause
- */
- public ArgumentIsInvalidException(Throwable cause)
- {
- super(cause);
- }
-
- /**
- * Constructs ...
- *
- *
- * @param message
- * @param cause
- */
- public ArgumentIsInvalidException(String message, Throwable cause)
- {
- super(message, cause);
- }
-}
diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java
index 0d8c14c61b..8a7ae642bd 100644
--- a/scm-core/src/main/java/sonia/scm/NotFoundException.java
+++ b/scm-core/src/main/java/sonia/scm/NotFoundException.java
@@ -7,9 +7,4 @@ public class NotFoundException extends Exception {
public NotFoundException() {
}
-
-
- public NotFoundException(String message) {
- super(message);
- }
}
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 e4cf8ecb5d..d7f299d989 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,14 +3,8 @@ package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import org.mapstruct.Mapping;
-import java.time.Instant;
-
-public abstract class BaseMapper {
+public abstract class BaseMapper implements InstantAttributeMapper {
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract D map(T modelObject);
-
- protected Instant mapTime(Long epochMilli) {
- return epochMilli == null? null: Instant.ofEpochMilli(epochMilli);
- }
}
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/InstantAttributeMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/InstantAttributeMapper.java
new file mode 100644
index 0000000000..468bdfc137
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/InstantAttributeMapper.java
@@ -0,0 +1,9 @@
+package sonia.scm.api.v2.resources;
+
+import java.time.Instant;
+
+public interface InstantAttributeMapper {
+ default Instant mapTime(Long epochMilli) {
+ return epochMilli == null? null: Instant.ofEpochMilli(epochMilli);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java
index 6f6831b058..0797134c9f 100644
--- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java
@@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources;
import com.google.common.collect.ImmutableList;
import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.Arrays;
@@ -14,7 +13,7 @@ import java.util.Arrays;
* builder for each method.
*
*
- * LinkBuilder builder = new LinkBuilder(uriInfo, MainResource.class, SubResource.class);
+ * LinkBuilder builder = new LinkBuilder(pathInfo, MainResource.class, SubResource.class);
* Link link = builder
* .method("sub")
* .parameters("param")
@@ -25,16 +24,16 @@ import java.util.Arrays;
*/
@SuppressWarnings("WeakerAccess") // Non-public will result in IllegalAccessError for plugins
public class LinkBuilder {
- private final UriInfo uriInfo;
+ private final ScmPathInfo pathInfo;
private final Class[] classes;
private final ImmutableList calls;
- public LinkBuilder(UriInfo uriInfo, Class... classes) {
- this(uriInfo, classes, ImmutableList.of());
+ public LinkBuilder(ScmPathInfo pathInfo, Class... classes) {
+ this(pathInfo, classes, ImmutableList.of());
}
- private LinkBuilder(UriInfo uriInfo, Class[] classes, ImmutableList calls) {
- this.uriInfo = uriInfo;
+ private LinkBuilder(ScmPathInfo pathInfo, Class[] classes, ImmutableList calls) {
+ this.pathInfo = pathInfo;
this.classes = classes;
this.calls = calls;
}
@@ -51,7 +50,7 @@ public class LinkBuilder {
throw new IllegalStateException("not enough methods for all classes");
}
- URI baseUri = uriInfo.getBaseUri();
+ URI baseUri = pathInfo.getApiRestUri();
URI relativeUri = createRelativeUri();
return baseUri.resolve(relativeUri);
}
@@ -61,7 +60,7 @@ public class LinkBuilder {
}
private LinkBuilder add(String method, String[] parameters) {
- return new LinkBuilder(uriInfo, classes, appendNewCall(method, parameters));
+ return new LinkBuilder(pathInfo, classes, appendNewCall(method, parameters));
}
private ImmutableList appendNewCall(String method, String[] parameters) {
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java
new file mode 100644
index 0000000000..fa975520c1
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java
@@ -0,0 +1,14 @@
+package sonia.scm.api.v2.resources;
+
+import java.net.URI;
+
+public interface ScmPathInfo {
+
+ String REST_API_PATH = "/api/rest";
+
+ URI getApiRestUri();
+
+ default URI getRootUri() {
+ return getApiRestUri().resolve("../..");
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java
new file mode 100644
index 0000000000..c88bd4a2b5
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java
@@ -0,0 +1,18 @@
+package sonia.scm.api.v2.resources;
+
+public class ScmPathInfoStore {
+
+ private ScmPathInfo pathInfo;
+
+ public ScmPathInfo get() {
+ return pathInfo;
+ }
+
+ public void set(ScmPathInfo info) {
+ if (this.pathInfo != null) {
+ throw new IllegalStateException("UriInfo already set");
+ }
+ this.pathInfo = info;
+ }
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java
deleted file mode 100644
index 2f61383cfd..0000000000
--- a/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package sonia.scm.api.v2.resources;
-
-import javax.ws.rs.core.UriInfo;
-
-public class UriInfoStore {
-
- private UriInfo uriInfo;
-
- public UriInfo get() {
- return uriInfo;
- }
-
- public void set(UriInfo uriInfo) {
- if (this.uriInfo != null) {
- throw new IllegalStateException("UriInfo already set");
- }
- this.uriInfo = uriInfo;
- }
-}
diff --git a/scm-core/src/main/java/sonia/scm/config/Configuration.java b/scm-core/src/main/java/sonia/scm/config/Configuration.java
index e9bf3528d5..823c50b155 100644
--- a/scm-core/src/main/java/sonia/scm/config/Configuration.java
+++ b/scm-core/src/main/java/sonia/scm/config/Configuration.java
@@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions;
@StaticPermissions(
value = "configuration",
permissions = {"read", "write"},
- globalPermissions = {}
+ globalPermissions = {"list"}
)
public interface Configuration extends PermissionObject {
}
diff --git a/scm-core/src/main/java/sonia/scm/filter/Filters.java b/scm-core/src/main/java/sonia/scm/filter/Filters.java
index b6a45811bc..b1f5ea47cf 100644
--- a/scm-core/src/main/java/sonia/scm/filter/Filters.java
+++ b/scm-core/src/main/java/sonia/scm/filter/Filters.java
@@ -31,6 +31,8 @@
package sonia.scm.filter;
+import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
+
/**
* Useful constants for filter implementations.
*
@@ -44,26 +46,26 @@ public final class Filters
public static final String PATTERN_ALL = "/*";
/** Field description */
- public static final String PATTERN_CONFIG = "/api/rest/config*";
+ public static final String PATTERN_CONFIG = REST_API_PATH + "/config*";
/** Field description */
public static final String PATTERN_DEBUG = "/debug.html";
/** Field description */
- public static final String PATTERN_GROUPS = "/api/rest/groups*";
+ public static final String PATTERN_GROUPS = REST_API_PATH + "/groups*";
/** Field description */
- public static final String PATTERN_PLUGINS = "/api/rest/plugins*";
+ public static final String PATTERN_PLUGINS = REST_API_PATH + "/plugins*";
/** Field description */
public static final String PATTERN_RESOURCE_REGEX =
"^/(?:resources|api|plugins|index)[\\./].*(?:html|\\.css|\\.js|\\.xml|\\.json|\\.txt)";
/** Field description */
- public static final String PATTERN_RESTAPI = "/api/rest/*";
+ public static final String PATTERN_RESTAPI = REST_API_PATH + "/*";
/** Field description */
- public static final String PATTERN_USERS = "/api/rest/users*";
+ public static final String PATTERN_USERS = REST_API_PATH + "/users*";
/** authentication priority */
public static final int PRIORITY_AUTHENTICATION = 5000;
diff --git a/scm-core/src/main/java/sonia/scm/group/Group.java b/scm-core/src/main/java/sonia/scm/group/Group.java
index 98d9dcc7a3..5e7f596c58 100644
--- a/scm-core/src/main/java/sonia/scm/group/Group.java
+++ b/scm-core/src/main/java/sonia/scm/group/Group.java
@@ -60,7 +60,7 @@ import java.util.List;
*
* @author Sebastian Sdorra
*/
-@StaticPermissions("group")
+@StaticPermissions(value = "group", globalPermissions = {"create", "list"})
@XmlRootElement(name = "groups")
@XmlAccessorType(XmlAccessType.FIELD)
public class Group extends BasicPropertiesAware
diff --git a/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java b/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java
index 94b31ac844..454003c922 100644
--- a/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java
+++ b/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java
@@ -33,9 +33,8 @@ package sonia.scm.plugin;
//~--- JDK imports ------------------------------------------------------------
-import java.net.URL;
-
import javax.servlet.ServletContext;
+import java.net.URL;
/**
* The WebResourceLoader is able to load web resources. The resources are loaded
@@ -53,9 +52,11 @@ public interface WebResourceLoader
* Returns a {@link URL} for the given path. The method will return null if no
* resources could be found for the given path.
*
+ * Note: The path is a web path and uses "/" as path separator
+ *
* @param path resource path
*
* @return url object for the given path or null
*/
- public URL getResource(String path);
+ URL getResource(String path);
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java
index 18ede493f6..7397fecabe 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java
@@ -41,7 +41,6 @@ import sonia.scm.util.ValidationUtil;
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.util.ArrayList;
import java.util.Date;
@@ -84,12 +83,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
*/
private String id;
- /**
- * List of files changed by this changeset
- */
- @XmlElement(name = "modifications")
- private Modifications modifications;
-
/**
* parent changeset ids
*/
@@ -137,7 +130,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
&& Objects.equal(parents, other.parents)
&& Objects.equal(tags, other.tags)
&& Objects.equal(branches, other.branches)
- && Objects.equal(modifications, other.modifications)
&& Objects.equal(properties, other.properties);
//J+
}
@@ -152,7 +144,7 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
public int hashCode()
{
return Objects.hashCode(id, date, author, description, parents, tags,
- branches, modifications, properties);
+ branches, properties);
}
/**
@@ -184,11 +176,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
out.append("branches: ").append(Util.toString(branches)).append("\n");
out.append("tags: ").append(Util.toString(tags)).append("\n");
- if (modifications != null)
- {
- out.append("modifications: \n").append(modifications);
- }
-
return out.toString();
}
@@ -285,21 +272,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
}
- /**
- * Returns the file modifications, which was done with this changeset.
- *
- *
- * @return file modifications
- */
- public Modifications getModifications()
- {
- if (modifications == null)
- {
- modifications = new Modifications();
- }
-
- return modifications;
- }
/**
* Return the ids of the parent changesets.
@@ -402,17 +374,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
this.id = id;
}
- /**
- * Sets the file modification of the changeset.
- *
- *
- * @param modifications file modifications
- */
- public void setModifications(Modifications modifications)
- {
- this.modifications = modifications;
- }
-
/**
* Sets the parents of the changeset.
*
diff --git a/scm-core/src/main/java/sonia/scm/repository/EscapeUtil.java b/scm-core/src/main/java/sonia/scm/repository/EscapeUtil.java
deleted file mode 100644
index a06472256d..0000000000
--- a/scm-core/src/main/java/sonia/scm/repository/EscapeUtil.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.repository;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.collect.Lists;
-
-import org.apache.commons.lang.StringEscapeUtils;
-
-import sonia.scm.util.Util;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import java.util.List;
-
-/**
- *
- * @author Sebastian Sdorra
- * @since 1.15
- */
-public final class EscapeUtil
-{
-
- /**
- * Constructs ...
- *
- */
- private EscapeUtil() {}
-
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param result
- */
- public static void escape(BrowserResult result)
- {
- result.setBranch(escape(result.getBranch()));
- result.setTag(escape(result.getTag()));
-
- for (FileObject fo : result)
- {
- escape(fo);
- }
- }
-
- /**
- * Method description
- *
- *
- * @param result
- * @since 1.17
- */
- public static void escape(BlameResult result)
- {
- for (BlameLine line : result.getBlameLines())
- {
- escape(line);
- }
- }
-
- /**
- * Method description
- *
- *
- * @param line
- * @since 1.17
- */
- public static void escape(BlameLine line)
- {
- line.setDescription(escape(line.getDescription()));
- escape(line.getAuthor());
- }
-
- /**
- * Method description
- *
- *
- * @param fo
- */
- public static void escape(FileObject fo)
- {
- fo.setDescription(escape(fo.getDescription()));
- fo.setName(fo.getName());
- fo.setPath(fo.getPath());
- }
-
- /**
- * Method description
- *
- *
- * @param changeset
- */
- public static void escape(Changeset changeset)
- {
- changeset.setDescription(escape(changeset.getDescription()));
- escape(changeset.getAuthor());
- changeset.setBranches(escapeList(changeset.getBranches()));
- changeset.setTags(escapeList(changeset.getTags()));
- }
-
- /**
- * Method description
- *
- *
- * @param person
- * @since 1.17
- */
- public static void escape(Person person)
- {
- if (person != null)
- {
- person.setName(escape(person.getName()));
- person.setMail(escape(person.getMail()));
- }
- }
-
- /**
- * Method description
- *
- *
- * @param result
- */
- public static void escape(ChangesetPagingResult result)
- {
- for (Changeset c : result)
- {
- escape(c);
- }
- }
-
- /**
- * Method description
- *
- *
- * @param value
- *
- * @return
- */
- public static String escape(String value)
- {
- return StringEscapeUtils.escapeHtml(value);
- }
-
- /**
- * Method description
- *
- *
- * @param values
- *
- * @return
- */
- public static List escapeList(List values)
- {
- if (Util.isNotEmpty(values))
- {
- List newList = Lists.newArrayList();
-
- for (String v : values)
- {
- newList.add(StringEscapeUtils.escapeHtml(v));
- }
-
- values = newList;
- }
-
- return values;
- }
-}
diff --git a/scm-core/src/main/java/sonia/scm/repository/Modifications.java b/scm-core/src/main/java/sonia/scm/repository/Modifications.java
index 4079d8f574..a679b0429f 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Modifications.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Modifications.java
@@ -67,7 +67,8 @@ public class Modifications implements Serializable
* Constructs ...
*
*/
- public Modifications() {}
+ public Modifications() {
+ }
/**
* Constructs ...
@@ -218,6 +219,10 @@ public class Modifications implements Serializable
return removed;
}
+ public String getRevision() {
+ return revision;
+ }
+
//~--- set methods ----------------------------------------------------------
/**
@@ -253,8 +258,14 @@ public class Modifications implements Serializable
this.removed = removed;
}
+ public void setRevision(String revision) {
+ this.revision = revision;
+ }
+
//~--- fields ---------------------------------------------------------------
+ private String revision;
+
/** list of added files */
@XmlElement(name = "added")
@XmlElementWrapper(name = "added")
diff --git a/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessor.java b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessor.java
new file mode 100644
index 0000000000..e5870e91aa
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessor.java
@@ -0,0 +1,23 @@
+package sonia.scm.repository;
+
+import sonia.scm.plugin.ExtensionPoint;
+
+
+/**
+ * A pre processor for {@link Modifications} objects. A pre processor is able to
+ * modify the object before it is delivered to the user interface.
+ *
+ * @author Mohamed Karray
+ * @since 2.0
+ */
+@ExtensionPoint
+public interface ModificationsPreProcessor extends PreProcessor {
+
+ /**
+ * Process the given modifications.
+ *
+ * @param modifications modifications to process
+ */
+ @Override
+ void process(Modifications modifications);
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessorFactory.java b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessorFactory.java
new file mode 100644
index 0000000000..71a6a38efa
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessorFactory.java
@@ -0,0 +1,26 @@
+package sonia.scm.repository;
+
+import sonia.scm.plugin.ExtensionPoint;
+
+
+/**
+ * This factory create a {@link ModificationsPreProcessor}
+ *
+ * @author Mohamed Karray
+ * @since 2.0
+ */
+@ExtensionPoint
+public interface ModificationsPreProcessorFactory extends PreProcessorFactory {
+
+ /**
+ * Create a new {@link ModificationsPreProcessor} for the given repository.
+ *
+ *
+ * @param repository repository
+ *
+ * @return {@link ModificationsPreProcessor} for the given repository
+ */
+ @Override
+ ModificationsPreProcessor createPreProcessor(Repository repository);
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java b/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java
index b44da13ad1..2a1d9c0340 100644
--- a/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java
+++ b/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java
@@ -36,17 +36,15 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.util.Util;
-//~--- JDK imports ------------------------------------------------------------
-
import java.util.Collection;
import java.util.Set;
+//~--- JDK imports ------------------------------------------------------------
+
/**
*
* @author Sebastian Sdorra
@@ -73,14 +71,18 @@ public class PreProcessorUtil
* @param fileObjectPreProcessorFactorySet
* @param blameLinePreProcessorSet
* @param blameLinePreProcessorFactorySet
+ * @param modificationsPreProcessorFactorySet
+ * @param modificationsPreProcessorSet
*/
@Inject
public PreProcessorUtil(Set changesetPreProcessorSet,
- Set changesetPreProcessorFactorySet,
- Set fileObjectPreProcessorSet,
- Set fileObjectPreProcessorFactorySet,
- Set blameLinePreProcessorSet,
- Set blameLinePreProcessorFactorySet)
+ Set changesetPreProcessorFactorySet,
+ Set fileObjectPreProcessorSet,
+ Set fileObjectPreProcessorFactorySet,
+ Set blameLinePreProcessorSet,
+ Set blameLinePreProcessorFactorySet,
+ Set modificationsPreProcessorFactorySet,
+ Set modificationsPreProcessorSet)
{
this.changesetPreProcessorSet = changesetPreProcessorSet;
this.changesetPreProcessorFactorySet = changesetPreProcessorFactorySet;
@@ -88,6 +90,8 @@ public class PreProcessorUtil
this.fileObjectPreProcessorFactorySet = fileObjectPreProcessorFactorySet;
this.blameLinePreProcessorSet = blameLinePreProcessorSet;
this.blameLinePreProcessorFactorySet = blameLinePreProcessorFactorySet;
+ this.modificationsPreProcessorFactorySet = modificationsPreProcessorFactorySet;
+ this.modificationsPreProcessorSet = modificationsPreProcessorSet;
}
//~--- methods --------------------------------------------------------------
@@ -107,14 +111,7 @@ public class PreProcessorUtil
blameLine.getLineNumber(), repository.getName());
}
- EscapeUtil.escape(blameLine);
-
- PreProcessorHandler handler =
- new PreProcessorHandler(blameLinePreProcessorFactorySet,
- blameLinePreProcessorSet, repository);
-
- handler.callPreProcessors(blameLine);
- handler.callPreProcessorFactories(blameLine);
+ handlePreProcess(repository,blameLine,blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
}
/**
@@ -125,22 +122,6 @@ public class PreProcessorUtil
* @param blameResult
*/
public void prepareForReturn(Repository repository, BlameResult blameResult)
- {
- prepareForReturn(repository, blameResult, true);
- }
-
- /**
- * Method description
- *
- *
- * @param repository
- * @param blameResult
- * @param escape
- *
- * @since 1.35
- */
- public void prepareForReturn(Repository repository, BlameResult blameResult,
- boolean escape)
{
if (logger.isTraceEnabled())
{
@@ -148,17 +129,7 @@ public class PreProcessorUtil
repository.getName());
}
- if (escape)
- {
- EscapeUtil.escape(blameResult);
- }
-
- PreProcessorHandler handler =
- new PreProcessorHandler(blameLinePreProcessorFactorySet,
- blameLinePreProcessorSet, repository);
-
- handler.callPreProcessors(blameResult.getBlameLines());
- handler.callPreProcessorFactories(blameResult.getBlameLines());
+ handlePreProcessForIterable(repository, blameResult.getBlameLines(),blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
}
/**
@@ -170,39 +141,13 @@ public class PreProcessorUtil
*/
public void prepareForReturn(Repository repository, Changeset changeset)
{
- prepareForReturn(repository, changeset, true);
+ logger.trace("prepare changeset {} of repository {} for return", changeset.getId(), repository.getName());
+ handlePreProcess(repository, changeset, changesetPreProcessorFactorySet, changesetPreProcessorSet);
}
- /**
- * Method description
- *
- *
- * @param repository
- * @param changeset
- * @param escape
- *
- * @since 1.35
- */
- public void prepareForReturn(Repository repository, Changeset changeset,
- boolean escape)
- {
- if (logger.isTraceEnabled())
- {
- logger.trace("prepare changeset {} of repository {} for return",
- changeset.getId(), repository.getName());
- }
-
- if (escape)
- {
- EscapeUtil.escape(changeset);
- }
-
- PreProcessorHandler handler =
- new PreProcessorHandler(changesetPreProcessorFactorySet,
- changesetPreProcessorSet, repository);
-
- handler.callPreProcessors(changeset);
- handler.callPreProcessorFactories(changeset);
+ public void prepareForReturn(Repository repository, Modifications modifications) {
+ logger.trace("prepare modifications {} of repository {} for return", modifications, repository.getName());
+ handlePreProcess(repository, modifications, modificationsPreProcessorFactorySet, modificationsPreProcessorSet);
}
/**
@@ -213,22 +158,6 @@ public class PreProcessorUtil
* @param result
*/
public void prepareForReturn(Repository repository, BrowserResult result)
- {
- prepareForReturn(repository, result, true);
- }
-
- /**
- * Method description
- *
- *
- * @param repository
- * @param result
- * @param escape
- *
- * @since 1.35
- */
- public void prepareForReturn(Repository repository, BrowserResult result,
- boolean escape)
{
if (logger.isTraceEnabled())
{
@@ -236,17 +165,7 @@ public class PreProcessorUtil
repository.getName());
}
- if (escape)
- {
- EscapeUtil.escape(result);
- }
-
- PreProcessorHandler handler =
- new PreProcessorHandler(fileObjectPreProcessorFactorySet,
- fileObjectPreProcessorSet, repository);
-
- handler.callPreProcessors(result);
- handler.callPreProcessorFactories(result);
+ handlePreProcessForIterable(repository, result,fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet);
}
/**
@@ -255,12 +174,8 @@ public class PreProcessorUtil
*
* @param repository
* @param result
- * @param escape
- *
- * @since 1.35
*/
- public void prepareForReturn(Repository repository,
- ChangesetPagingResult result, boolean escape)
+ public void prepareForReturn(Repository repository, ChangesetPagingResult result)
{
if (logger.isTraceEnabled())
{
@@ -268,30 +183,23 @@ public class PreProcessorUtil
repository.getName());
}
- if (escape)
- {
- EscapeUtil.escape(result);
- }
-
- PreProcessorHandler handler =
- new PreProcessorHandler(changesetPreProcessorFactorySet,
- changesetPreProcessorSet, repository);
-
- handler.callPreProcessors(result);
- handler.callPreProcessorFactories(result);
+ handlePreProcessForIterable(repository,result,changesetPreProcessorFactorySet, changesetPreProcessorSet);
}
- /**
- * Method description
- *
- *
- * @param repository
- * @param result
- */
- public void prepareForReturn(Repository repository,
- ChangesetPagingResult result)
- {
- prepareForReturn(repository, result, true);
+ private , P extends PreProcessor> void handlePreProcess(Repository repository, T processedObject,
+ Collection factories,
+ Collection
preProcessors) {
+ PreProcessorHandler handler = new PreProcessorHandler(factories, preProcessors, repository);
+ handler.callPreProcessors(processedObject);
+ handler.callPreProcessorFactories(processedObject);
+ }
+
+ private , F extends PreProcessorFactory, P extends PreProcessor> void handlePreProcessForIterable(Repository repository, I processedObjects,
+ Collection factories,
+ Collection
preProcessors) {
+ PreProcessorHandler handler = new PreProcessorHandler(factories, preProcessors, repository);
+ handler.callPreProcessors(processedObjects);
+ handler.callPreProcessorFactories(processedObjects);
}
//~--- inner classes --------------------------------------------------------
@@ -454,6 +362,10 @@ public class PreProcessorUtil
/** Field description */
private final Collection changesetPreProcessorSet;
+ private final Collection modificationsPreProcessorFactorySet;
+
+ private final Collection modificationsPreProcessorSet;
+
/** Field description */
private final Collection fileObjectPreProcessorFactorySet;
diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java
index 0d8c6a6af8..cad36f2d88 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Repository.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java
@@ -40,7 +40,6 @@ import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import sonia.scm.BasicPropertiesAware;
import sonia.scm.ModelObject;
-import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import sonia.scm.util.ValidationUtil;
@@ -349,17 +348,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
// do not copy health check results
}
- /**
- * Creates the url of the repository.
- *
- * @param baseUrl base url of the server including the context path
- * @return url of the repository
- * @since 1.17
- */
- public String createUrl(String baseUrl) {
- return HttpUtil.concatenate(baseUrl, type, namespace, name);
- }
-
/**
* Returns true if the {@link Repository} is the same as the obj argument.
*
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
index 2c4d958d8d..1e2fdccf42 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
@@ -38,7 +38,6 @@ package sonia.scm.repository;
import sonia.scm.AlreadyExistsException;
import sonia.scm.TypeManager;
-import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
@@ -99,29 +98,6 @@ public interface RepositoryManager
*/
public Collection getConfiguredTypes();
- /**
- * Returns the {@link Repository} associated to the request uri.
- *
- *
- * @param request the current http request
- *
- * @return associated to the request uri
- * @since 1.9
- */
- public Repository getFromRequest(HttpServletRequest request);
-
- /**
- * Returns the {@link Repository} associated to the request uri.
- *
- *
- *
- * @param uri request uri without context path
- *
- * @return associated to the request uri
- * @since 1.9
- */
- public Repository getFromUri(String uri);
-
/**
* Returns a {@link RepositoryHandler} by the given type (hg, git, svn ...).
*
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
index aa4117af34..87df960da7 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
@@ -39,7 +39,6 @@ import sonia.scm.AlreadyExistsException;
import sonia.scm.ManagerDecorator;
import sonia.scm.Type;
-import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
@@ -120,34 +119,6 @@ public class RepositoryManagerDecorator
return decorated;
}
- /**
- * {@inheritDoc}
- *
- *
- * @param request
- *
- * @return
- */
- @Override
- public Repository getFromRequest(HttpServletRequest request)
- {
- return decorated.getFromRequest(request);
- }
-
- /**
- * {@inheritDoc}
- *
- *
- * @param uri
- *
- * @return
- */
- @Override
- public Repository getFromUri(String uri)
- {
- return decorated.getFromUri(uri);
- }
-
/**
* {@inheritDoc}
*
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java
index 070221117a..9dd866daa4 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java
@@ -44,8 +44,8 @@ import sonia.scm.NotFoundException;
public class RepositoryNotFoundException extends NotFoundException
{
- /** Field description */
private static final long serialVersionUID = -6583078808900520166L;
+ private static final String TYPE_REPOSITORY = "repository";
//~--- constructors ---------------------------------------------------------
@@ -55,10 +55,14 @@ public class RepositoryNotFoundException extends NotFoundException
*
*/
public RepositoryNotFoundException(Repository repository) {
- super("repository", repository.getName() + "/" + repository.getNamespace());
+ super(TYPE_REPOSITORY, repository.getName() + "/" + repository.getNamespace());
}
public RepositoryNotFoundException(String repositoryId) {
- super("repository", repositoryId);
+ super(TYPE_REPOSITORY, repositoryId);
+ }
+
+ public RepositoryNotFoundException(NamespaceAndName namespaceAndName) {
+ super(TYPE_REPOSITORY, namespaceAndName.toString());
}
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java
index 1a5300ad21..cea8574d14 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java
@@ -6,13 +6,13 @@
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
+ * this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
@@ -26,35 +26,21 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
- *
*/
-
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.throwingproviders.CheckedProvider;
-import sonia.scm.security.ScmSecurityException;
-
/**
*
* @author Sebastian Sdorra
* @since 1.10
*/
-public interface RepositoryProvider extends CheckedProvider
-{
-
- /**
- * Method description
- *
- *
- * @return
- *
- * @throws ScmSecurityException
- */
+public interface RepositoryProvider extends CheckedProvider {
@Override
- public Repository get() throws ScmSecurityException;
+ Repository get();
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java
index 5a34345dce..f55abd4598 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java
@@ -185,7 +185,7 @@ public final class BlameCommandBuilder
if (!disablePreProcessors && (result != null))
{
- preProcessorUtil.prepareForReturn(repository, result, !disableEscaping);
+ preProcessorUtil.prepareForReturn(repository, result);
}
return result;
@@ -210,24 +210,6 @@ public final class BlameCommandBuilder
return this;
}
- /**
- * Disable html escaping for the returned blame lines. By default all
- * blame lines are html escaped.
- *
- *
- * @param disableEscaping true to disable the html escaping
- *
- * @return {@code this}
- *
- * @since 1.35
- */
- public BlameCommandBuilder setDisableEscaping(boolean disableEscaping)
- {
- this.disableEscaping = disableEscaping;
-
- return this;
- }
-
/**
* Disable the execution of pre processors.
*
@@ -362,9 +344,6 @@ public final class BlameCommandBuilder
/** the cache */
private final Cache cache;
- /** disable escaping */
- private boolean disableEscaping = false;
-
/** disable change */
private boolean disableCache = false;
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
index eeae45b893..fe39aa0a05 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
@@ -179,7 +179,7 @@ public final class BrowseCommandBuilder
if (!disablePreProcessors && (result != null))
{
- preProcessorUtil.prepareForReturn(repository, result, !disableEscaping);
+ preProcessorUtil.prepareForReturn(repository, result);
List fileObjects = result.getFiles();
@@ -212,24 +212,6 @@ public final class BrowseCommandBuilder
return this;
}
- /**
- * Disable html escaping for the returned file objects. By default all
- * file objects are html escaped.
- *
- *
- * @param disableEscaping true to disable the html escaping
- *
- * @return {@code this}
- *
- * @since 1.35
- */
- public BrowseCommandBuilder setDisableEscaping(boolean disableEscaping)
- {
- this.disableEscaping = disableEscaping;
-
- return this;
- }
-
/**
* Disabling the last commit means that every call to
* {@link FileObject#getDescription()} and
@@ -433,9 +415,6 @@ public final class BrowseCommandBuilder
/** cache */
private final Cache cache;
- /** disable escaping */
- private boolean disableEscaping = false;
-
/** disables the cache */
private boolean disableCache = false;
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java
index ccb0d8c2c0..2f844cfbfb 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java
@@ -61,5 +61,11 @@ public enum Command
/**
* @since 1.43
*/
- BUNDLE, UNBUNDLE;
+ BUNDLE, UNBUNDLE,
+
+ /**
+ * @since 2.0
+ */
+ MODIFICATIONS
+
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java
index c06e227d5f..fe43fee038 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java
@@ -132,8 +132,7 @@ public final class HookChangesetBuilder
try
{
copy = DeepCopy.copy(c);
- preProcessorUtil.prepareForReturn(repository, copy,
- !disableEscaping);
+ preProcessorUtil.prepareForReturn(repository, copy);
}
catch (IOException ex)
{
@@ -156,24 +155,6 @@ public final class HookChangesetBuilder
//~--- set methods ----------------------------------------------------------
- /**
- * Disable html escaping for the returned changesets. By default all
- * changesets are html escaped.
- *
- *
- * @param disableEscaping true to disable the html escaping
- *
- * @return {@code this}
- *
- * @since 1.35
- */
- public HookChangesetBuilder setDisableEscaping(boolean disableEscaping)
- {
- this.disableEscaping = disableEscaping;
-
- return this;
- }
-
/**
* Disable the execution of pre processors.
*
@@ -192,9 +173,6 @@ public final class HookChangesetBuilder
//~--- fields ---------------------------------------------------------------
- /** disable escaping */
- private boolean disableEscaping = false;
-
/** disable pre processors marker */
private boolean disablePreProcessors = false;
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java
index 1450cf687a..9c782a781b 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java
@@ -264,7 +264,7 @@ public final class LogCommandBuilder
if (!disablePreProcessors && (cpr != null))
{
- preProcessorUtil.prepareForReturn(repository, cpr, !disableEscaping);
+ preProcessorUtil.prepareForReturn(repository, cpr);
}
return cpr;
@@ -306,24 +306,6 @@ public final class LogCommandBuilder
return this;
}
- /**
- * Disable html escaping for the returned changesets. By default all
- * changesets are html escaped.
- *
- *
- * @param disableEscaping true to disable the html escaping
- *
- * @return {@code this}
- *
- * @since 1.35
- */
- public LogCommandBuilder setDisableEscaping(boolean disableEscaping)
- {
- this.disableEscaping = disableEscaping;
-
- return this;
- }
-
/**
* Disable the execution of pre processors.
*
@@ -545,9 +527,6 @@ public final class LogCommandBuilder
/** repository to query */
private final Repository repository;
- /** disable escaping */
- private boolean disableEscaping = false;
-
/** disable cache */
private boolean disableCache = false;
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java
new file mode 100644
index 0000000000..6459b47cd5
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java
@@ -0,0 +1,110 @@
+package sonia.scm.repository.api;
+
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import sonia.scm.cache.Cache;
+import sonia.scm.repository.Modifications;
+import sonia.scm.repository.PreProcessorUtil;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryCacheKey;
+import sonia.scm.repository.RevisionNotFoundException;
+import sonia.scm.repository.spi.ModificationsCommand;
+import sonia.scm.repository.spi.ModificationsCommandRequest;
+
+import java.io.IOException;
+
+/**
+ * Get the modifications applied to files in a revision.
+ *
+ * Modifications are for example: Add, Update and Delete
+ *
+ * @author Mohamed Karray
+ * @since 2.0
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Accessors(fluent = true)
+public final class ModificationsCommandBuilder {
+ static final String CACHE_NAME = "sonia.cache.cmd.modifications";
+
+ private final ModificationsCommand modificationsCommand;
+
+ private final ModificationsCommandRequest request = new ModificationsCommandRequest();
+
+ private final Repository repository;
+
+ private final Cache cache;
+
+ private final PreProcessorUtil preProcessorUtil;
+
+ @Setter
+ private boolean disableCache = false;
+
+ @Setter
+ private boolean disablePreProcessors = false;
+
+ public ModificationsCommandBuilder revision(String revision){
+ request.setRevision(revision);
+ return this;
+ }
+
+ /**
+ * Reset each parameter to its default value.
+ *
+ *
+ * @return {@code this}
+ */
+ public ModificationsCommandBuilder reset() {
+ request.reset();
+ this.disableCache = false;
+ this.disablePreProcessors = false;
+ return this;
+ }
+
+ public Modifications getModifications() throws IOException, RevisionNotFoundException {
+ Modifications modifications;
+ if (disableCache) {
+ log.info("Get modifications for {} with disabled cache", request);
+ modifications = modificationsCommand.getModifications(request);
+ } else {
+ ModificationsCommandBuilder.CacheKey key = new ModificationsCommandBuilder.CacheKey(repository.getId(), request);
+ if (cache.contains(key)) {
+ modifications = cache.get(key);
+ log.debug("Get modifications for {} from the cache", request);
+ } else {
+ log.info("Get modifications for {} with enabled cache", request);
+ modifications = modificationsCommand.getModifications(request);
+ if (modifications != null) {
+ cache.put(key, modifications);
+ log.debug("Modifications for {} added to the cache with key {}", request, key);
+ }
+ }
+ }
+ if (!disablePreProcessors && (modifications != null)) {
+ preProcessorUtil.prepareForReturn(repository, modifications);
+ }
+ return modifications;
+ }
+
+ @AllArgsConstructor
+ @Getter
+ @Setter
+ @EqualsAndHashCode
+ @ToString
+ class CacheKey implements RepositoryCacheKey {
+ private final String repositoryId;
+ private final ModificationsCommandRequest request;
+
+ @Override
+ public String getRepositoryId() {
+ return repositoryId;
+ }
+ }
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
index 27eed6becd..bdd6e4b320 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
@@ -31,6 +31,7 @@
package sonia.scm.repository.api;
+import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.CacheManager;
@@ -42,6 +43,8 @@ import sonia.scm.repository.spi.RepositoryServiceProvider;
import java.io.Closeable;
import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Stream;
/**
* From the {@link RepositoryService} it is possible to access all commands for
@@ -78,30 +81,32 @@ import java.io.IOException;
* @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
* @since 1.17
*/
+@Slf4j
public final class RepositoryService implements Closeable {
- private CacheManager cacheManager;
- private PreProcessorUtil preProcessorUtil;
- private RepositoryServiceProvider provider;
- private Repository repository;
- private static final Logger logger =
- LoggerFactory.getLogger(RepositoryService.class);
+
+ private static final Logger logger = LoggerFactory.getLogger(RepositoryService.class);
+
+ private final CacheManager cacheManager;
+ private final PreProcessorUtil preProcessorUtil;
+ private final RepositoryServiceProvider provider;
+ private final Repository repository;
+ private final Set protocolProviders;
/**
* Constructs a new {@link RepositoryService}. This constructor should only
* be called from the {@link RepositoryServiceFactory}.
- *
- * @param cacheManager cache manager
+ * @param cacheManager cache manager
* @param provider implementation for {@link RepositoryServiceProvider}
* @param repository the repository
- * @param preProcessorUtil
*/
RepositoryService(CacheManager cacheManager,
- RepositoryServiceProvider provider, Repository repository,
- PreProcessorUtil preProcessorUtil) {
+ RepositoryServiceProvider provider, Repository repository,
+ PreProcessorUtil preProcessorUtil, Set protocolProviders) {
this.cacheManager = cacheManager;
this.provider = provider;
this.repository = repository;
this.preProcessorUtil = preProcessorUtil;
+ this.protocolProviders = protocolProviders;
}
/**
@@ -125,7 +130,7 @@ public final class RepositoryService implements Closeable {
try {
provider.close();
} catch (IOException ex) {
- logger.error("Could not close repository service provider", ex);
+ log.error("Could not close repository service provider", ex);
}
}
@@ -138,7 +143,7 @@ public final class RepositoryService implements Closeable {
*/
public BlameCommandBuilder getBlameCommand() {
logger.debug("create blame command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(),
repository, preProcessorUtil);
@@ -153,7 +158,7 @@ public final class RepositoryService implements Closeable {
*/
public BranchesCommandBuilder getBranchesCommand() {
logger.debug("create branches command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new BranchesCommandBuilder(cacheManager,
provider.getBranchesCommand(), repository);
@@ -168,7 +173,7 @@ public final class RepositoryService implements Closeable {
*/
public BrowseCommandBuilder getBrowseCommand() {
logger.debug("create browse command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(),
repository, preProcessorUtil);
@@ -184,7 +189,7 @@ public final class RepositoryService implements Closeable {
*/
public BundleCommandBuilder getBundleCommand() {
logger.debug("create bundle command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new BundleCommandBuilder(provider.getBundleCommand(), repository);
}
@@ -198,7 +203,7 @@ public final class RepositoryService implements Closeable {
*/
public CatCommandBuilder getCatCommand() {
logger.debug("create cat command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new CatCommandBuilder(provider.getCatCommand());
}
@@ -213,7 +218,7 @@ public final class RepositoryService implements Closeable {
*/
public DiffCommandBuilder getDiffCommand() {
logger.debug("create diff command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new DiffCommandBuilder(provider.getDiffCommand());
}
@@ -229,7 +234,7 @@ public final class RepositoryService implements Closeable {
*/
public IncomingCommandBuilder getIncomingCommand() {
logger.debug("create incoming command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new IncomingCommandBuilder(cacheManager,
provider.getIncomingCommand(), repository, preProcessorUtil);
@@ -244,12 +249,24 @@ public final class RepositoryService implements Closeable {
*/
public LogCommandBuilder getLogCommand() {
logger.debug("create log command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
repository, preProcessorUtil);
}
+ /**
+ * The modification command shows file modifications in a revision.
+ *
+ * @return instance of {@link ModificationsCommandBuilder}
+ * @throws CommandNotSupportedException if the command is not supported
+ * by the implementation of the repository service provider.
+ */
+ public ModificationsCommandBuilder getModificationsCommand() {
+ logger.debug("create modifications command for repository {}", repository.getNamespaceAndName());
+ return new ModificationsCommandBuilder(provider.getModificationsCommand(),repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil);
+ }
+
/**
* The outgoing command show {@link Changeset}s not found in a remote repository.
*
@@ -260,7 +277,7 @@ public final class RepositoryService implements Closeable {
*/
public OutgoingCommandBuilder getOutgoingCommand() {
logger.debug("create outgoing command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new OutgoingCommandBuilder(cacheManager,
provider.getOutgoingCommand(), repository, preProcessorUtil);
@@ -276,7 +293,7 @@ public final class RepositoryService implements Closeable {
*/
public PullCommandBuilder getPullCommand() {
logger.debug("create pull command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new PullCommandBuilder(provider.getPullCommand(), repository);
}
@@ -291,7 +308,7 @@ public final class RepositoryService implements Closeable {
*/
public PushCommandBuilder getPushCommand() {
logger.debug("create push command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new PushCommandBuilder(provider.getPushCommand());
}
@@ -314,7 +331,7 @@ public final class RepositoryService implements Closeable {
*/
public TagsCommandBuilder getTagsCommand() {
logger.debug("create tags command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(),
repository);
@@ -330,7 +347,7 @@ public final class RepositoryService implements Closeable {
*/
public UnbundleCommandBuilder getUnbundleCommand() {
logger.debug("create unbundle command for repository {}",
- repository.getName());
+ repository.getNamespaceAndName());
return new UnbundleCommandBuilder(provider.getUnbundleCommand(),
repository);
@@ -357,5 +374,20 @@ public final class RepositoryService implements Closeable {
return provider.getSupportedFeatures().contains(feature);
}
+ public Stream getSupportedProtocols() {
+ return protocolProviders.stream()
+ .filter(protocolProvider -> protocolProvider.getType().equals(getRepository().getType()))
+ .map(this::createProviderInstanceForRepository);
+ }
+ private T createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) {
+ return protocolProvider.get(repository);
+ }
+
+ public T getProtocol(Class clazz) {
+ return this.getSupportedProtocols()
+ .filter(scmProtocol -> clazz.isAssignableFrom(scmProtocol.getClass()))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(),getRepository().getType())));
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
index 627e57a797..fbb1ee6b58 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
@@ -137,13 +137,15 @@ public final class RepositoryServiceFactory
@Inject
public RepositoryServiceFactory(ScmConfiguration configuration,
CacheManager cacheManager, RepositoryManager repositoryManager,
- Set resolvers, PreProcessorUtil preProcessorUtil)
+ Set resolvers, PreProcessorUtil preProcessorUtil,
+ Set protocolProviders)
{
this.configuration = configuration;
this.cacheManager = cacheManager;
this.repositoryManager = repositoryManager;
this.resolvers = resolvers;
this.preProcessorUtil = preProcessorUtil;
+ this.protocolProviders = protocolProviders;
ScmEventBus.getInstance().register(new CacheClearHook(cacheManager));
}
@@ -208,9 +210,7 @@ public final class RepositoryServiceFactory
if (repository == null)
{
- String msg = "could not find a repository with namespace/name " + namespaceAndName;
-
- throw new RepositoryNotFoundException(msg);
+ throw new RepositoryNotFoundException(namespaceAndName);
}
return create(repository);
@@ -254,7 +254,7 @@ public final class RepositoryServiceFactory
}
service = new RepositoryService(cacheManager, provider, repository,
- preProcessorUtil);
+ preProcessorUtil, protocolProviders);
break;
}
@@ -369,4 +369,6 @@ public final class RepositoryServiceFactory
/** service resolvers */
private final Set resolvers;
+
+ private Set protocolProviders;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java
new file mode 100644
index 0000000000..c987510491
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java
@@ -0,0 +1,19 @@
+package sonia.scm.repository.api;
+
+/**
+ * An ScmProtocol represents a concrete protocol provided by the SCM-Manager instance
+ * to interact with a repository depending on its type. There may be multiple protocols
+ * available for a repository type (eg. http and ssh).
+ */
+public interface ScmProtocol {
+
+ /**
+ * The type of the concrete protocol, eg. "http" or "ssh".
+ */
+ String getType();
+
+ /**
+ * The URL to access the repository providing this protocol.
+ */
+ String getUrl();
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java
new file mode 100644
index 0000000000..597826676d
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java
@@ -0,0 +1,12 @@
+package sonia.scm.repository.api;
+
+import sonia.scm.plugin.ExtensionPoint;
+import sonia.scm.repository.Repository;
+
+@ExtensionPoint(multi = true)
+public interface ScmProtocolProvider {
+
+ String getType();
+
+ T get(Repository repository);
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java
new file mode 100644
index 0000000000..b933abf559
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java
@@ -0,0 +1,38 @@
+package sonia.scm.repository.spi;
+
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.api.ScmProtocol;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URI;
+
+public abstract class HttpScmProtocol implements ScmProtocol {
+
+ private final Repository repository;
+ private final String basePath;
+
+ public HttpScmProtocol(Repository repository, String basePath) {
+ this.repository = repository;
+ this.basePath = basePath;
+ }
+
+ @Override
+ public String getType() {
+ return "http";
+ }
+
+ @Override
+ public String getUrl() {
+ return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", repository.getNamespace(), repository.getName())).toASCIIString();
+ }
+
+ public final void serve(HttpServletRequest request, HttpServletResponse response, ServletConfig config) throws ServletException, IOException {
+ serve(request, response, repository, config);
+ }
+
+ protected abstract void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) throws ServletException, IOException;
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java b/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java
new file mode 100644
index 0000000000..c1b7229036
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java
@@ -0,0 +1,91 @@
+package sonia.scm.repository.spi;
+
+import lombok.extern.slf4j.Slf4j;
+import sonia.scm.api.v2.resources.ScmPathInfoStore;
+import sonia.scm.config.ScmConfiguration;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.api.ScmProtocolProvider;
+
+import javax.inject.Provider;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Optional;
+
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
+
+@Slf4j
+public abstract class InitializingHttpScmProtocolWrapper implements ScmProtocolProvider {
+
+ private final Provider extends ScmProviderHttpServlet> delegateProvider;
+ private final Provider pathInfoStore;
+ private final ScmConfiguration scmConfiguration;
+
+ private volatile boolean isInitialized = false;
+
+
+ protected InitializingHttpScmProtocolWrapper(Provider extends ScmProviderHttpServlet> delegateProvider, Provider pathInfoStore, ScmConfiguration scmConfiguration) {
+ this.delegateProvider = delegateProvider;
+ this.pathInfoStore = pathInfoStore;
+ this.scmConfiguration = scmConfiguration;
+ }
+
+ protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException {
+ httpServlet.init(config);
+ }
+
+ @Override
+ public HttpScmProtocol get(Repository repository) {
+ if (!repository.getType().equals(getType())) {
+ throw new IllegalArgumentException(String.format("cannot handle repository with type %s with protocol for type %s", repository.getType(), getType()));
+ }
+ return new ProtocolWrapper(repository, computeBasePath());
+ }
+
+ private String computeBasePath() {
+ return getPathFromScmPathInfoIfAvailable().orElse(getPathFromConfiguration());
+ }
+
+ private Optional getPathFromScmPathInfoIfAvailable() {
+ try {
+ ScmPathInfoStore scmPathInfoStore = pathInfoStore.get();
+ if (scmPathInfoStore != null && scmPathInfoStore.get() != null) {
+ return of(scmPathInfoStore.get().getRootUri().toASCIIString());
+ }
+ } catch (Exception e) {
+ log.debug("could not get ScmPathInfoStore from context", e);
+ }
+ return empty();
+ }
+
+ private String getPathFromConfiguration() {
+ log.debug("using base path from configuration: {}", scmConfiguration.getBaseUrl());
+ return scmConfiguration.getBaseUrl();
+ }
+
+ private class ProtocolWrapper extends HttpScmProtocol {
+
+ public ProtocolWrapper(Repository repository, String basePath) {
+ super(repository, basePath);
+ }
+
+ @Override
+ protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) throws ServletException, IOException {
+ if (!isInitialized) {
+ synchronized (InitializingHttpScmProtocolWrapper.this) {
+ if (!isInitialized) {
+ ScmProviderHttpServlet httpServlet = delegateProvider.get();
+ initializeServlet(config, httpServlet);
+ isInitialized = true;
+ }
+ }
+ }
+
+ delegateProvider.get().service(request, response, repository);
+ }
+
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java
similarity index 73%
rename from scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java
rename to scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java
index f13f4cbc67..e9b40e8a17 100644
--- a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java
@@ -1,4 +1,4 @@
-/**
+/*
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
@@ -29,32 +29,25 @@
*
*/
+package sonia.scm.repository.spi;
-package sonia.scm.repository;
+import sonia.scm.repository.Modifications;
+import sonia.scm.repository.RevisionNotFoundException;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
+import java.io.IOException;
/**
+ * Command to get the modifications applied to files in a revision.
*
- * @author Sebastian Sdorra
+ * Modifications are for example: Add, Update, Delete
+ *
+ * @author Mohamed Karray
+ * @since 2.0
*/
-public class RepositoryTest
-{
+public interface ModificationsCommand {
- /**
- * Method description
- *
- */
- @Test
- public void testCreateUrl()
- {
- Repository repository = new Repository("123", "hg", "test", "repo");
+ Modifications getModifications(String revision) throws IOException, RevisionNotFoundException;
+
+ Modifications getModifications(ModificationsCommandRequest request) throws IOException, RevisionNotFoundException;
- assertEquals("http://localhost:8080/scm/hg/test/repo",
- repository.createUrl("http://localhost:8080/scm"));
- assertEquals("http://localhost:8080/scm/hg/test/repo",
- repository.createUrl("http://localhost:8080/scm/"));
- }
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java
new file mode 100644
index 0000000000..8a910a3396
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java
@@ -0,0 +1,24 @@
+package sonia.scm.repository.spi;
+
+
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+@ToString
+@EqualsAndHashCode
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class ModificationsCommandRequest implements Resetable {
+ private String revision;
+
+ @Override
+ public void reset() {
+ revision = null;
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
index 976f38fffb..c66c56c0f1 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
@@ -33,20 +33,17 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
import sonia.scm.repository.Feature;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.CommandNotSupportedException;
-//~--- JDK imports ------------------------------------------------------------
-
import java.io.Closeable;
import java.io.IOException;
-
import java.util.Collections;
import java.util.Set;
+//~--- JDK imports ------------------------------------------------------------
+
/**
*
* @author Sebastian Sdorra
@@ -173,6 +170,16 @@ public abstract class RepositoryServiceProvider implements Closeable
throw new CommandNotSupportedException(Command.LOG);
}
+ /**
+ * Get the corresponding {@link ModificationsCommand} implemented from the Plugins
+ *
+ * @return the corresponding {@link ModificationsCommand} implemented from the Plugins
+ * @throws CommandNotSupportedException if there is no Implementation
+ */
+ public ModificationsCommand getModificationsCommand() {
+ throw new CommandNotSupportedException(Command.MODIFICATIONS);
+ }
+
/**
* Method description
*
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java
new file mode 100644
index 0000000000..3a9dad52d6
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java
@@ -0,0 +1,16 @@
+package sonia.scm.repository.spi;
+
+import sonia.scm.repository.Repository;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public interface ScmProviderHttpServlet {
+
+ void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException;
+
+ void init(ServletConfig config) throws ServletException;
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java
new file mode 100644
index 0000000000..c5dd55d277
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java
@@ -0,0 +1,28 @@
+package sonia.scm.repository.spi;
+
+import sonia.scm.repository.Repository;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class ScmProviderHttpServletDecorator implements ScmProviderHttpServlet {
+
+ private final ScmProviderHttpServlet object;
+
+ public ScmProviderHttpServletDecorator(ScmProviderHttpServlet object) {
+ this.object = object;
+ }
+
+ @Override
+ public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
+ object.service(request, response, repository);
+ }
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ object.init(config);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java
new file mode 100644
index 0000000000..531a25e91d
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java
@@ -0,0 +1,15 @@
+package sonia.scm.repository.spi;
+
+import sonia.scm.DecoratorFactory;
+import sonia.scm.plugin.ExtensionPoint;
+
+@ExtensionPoint
+public interface ScmProviderHttpServletDecoratorFactory extends DecoratorFactory {
+ /**
+ * Has to return true if this factory provides a decorator for the given scm type (eg. "git", "hg" or
+ * "svn").
+ * @param type The current scm type this factory can provide a decorator for.
+ * @return true when the provided decorator should be used for the given scm type.
+ */
+ boolean handlesScmType(String type);
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java
new file mode 100644
index 0000000000..3793d4b935
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java
@@ -0,0 +1,33 @@
+package sonia.scm.repository.spi;
+
+import com.google.inject.Inject;
+import sonia.scm.util.Decorators;
+
+import javax.inject.Provider;
+import java.util.List;
+import java.util.Set;
+
+import static java.util.stream.Collectors.toList;
+
+public abstract class ScmProviderHttpServletProvider implements Provider {
+
+ @Inject(optional = true)
+ private Set decoratorFactories;
+
+ private final String type;
+
+ protected ScmProviderHttpServletProvider(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public ScmProviderHttpServlet get() {
+ return Decorators.decorate(getRootServlet(), getDecoratorsForType());
+ }
+
+ private List getDecoratorsForType() {
+ return decoratorFactories.stream().filter(d -> d.handlesScmType(type)).collect(toList());
+ }
+
+ protected abstract ScmProviderHttpServlet getRootServlet();
+}
diff --git a/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java
new file mode 100644
index 0000000000..19c609ba30
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java
@@ -0,0 +1,11 @@
+package sonia.scm.user;
+
+public class ChangePasswordNotAllowedException extends RuntimeException {
+
+ public static final String WRONG_USER_TYPE = "User of type {0} are not allowed to change password";
+
+ public ChangePasswordNotAllowedException(String message) {
+ super(message);
+ }
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java
new file mode 100644
index 0000000000..e06191a8f2
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java
@@ -0,0 +1,10 @@
+package sonia.scm.user;
+
+public class InvalidPasswordException extends RuntimeException {
+
+ public static final String INVALID_MATCHING = "The given Password does not match with the stored one.";
+
+ public InvalidPasswordException(String message) {
+ super(message);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java
index 778e573c14..0d909bec8d 100644
--- a/scm-core/src/main/java/sonia/scm/user/User.java
+++ b/scm-core/src/main/java/sonia/scm/user/User.java
@@ -55,11 +55,10 @@ import java.security.Principal;
*
* @author Sebastian Sdorra
*/
-@StaticPermissions("user")
+@StaticPermissions(value = "user", globalPermissions = {"create", "list"})
@XmlRootElement(name = "users")
@XmlAccessorType(XmlAccessType.FIELD)
-public class
-User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
+public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
{
/** Field description */
@@ -274,6 +273,10 @@ User extends BasicPropertiesAware implements Principal, ModelObject, PermissionO
//J+
}
+ public User changePassword(String password){
+ setPassword(password);
+ return this;
+ }
//~--- get methods ----------------------------------------------------------
/**
diff --git a/scm-core/src/main/java/sonia/scm/user/UserManager.java b/scm-core/src/main/java/sonia/scm/user/UserManager.java
index 0a705c8d60..1f5aee1f19 100644
--- a/scm-core/src/main/java/sonia/scm/user/UserManager.java
+++ b/scm-core/src/main/java/sonia/scm/user/UserManager.java
@@ -38,6 +38,11 @@ package sonia.scm.user;
import sonia.scm.Manager;
import sonia.scm.search.Searchable;
+import java.text.MessageFormat;
+import java.util.function.Consumer;
+
+import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE;
+
/**
* The central class for managing {@link User} objects.
* This class is a singleton and is available via injection.
@@ -68,4 +73,22 @@ public interface UserManager
* @since 1.14
*/
public String getDefaultType();
+
+
+ /**
+ * Only account of the default type "xml" can change their password
+ */
+ default Consumer getUserTypeChecker() {
+ return user -> {
+ if (!isTypeDefault(user)) {
+ throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType()));
+ }
+ };
+ }
+
+ default boolean isTypeDefault(User user) {
+ return getDefaultType().equals(user.getType());
+ }
+
+
}
diff --git a/scm-webapp/src/main/java/sonia/scm/util/Decorators.java b/scm-core/src/main/java/sonia/scm/util/Decorators.java
similarity index 99%
rename from scm-webapp/src/main/java/sonia/scm/util/Decorators.java
rename to scm-core/src/main/java/sonia/scm/util/Decorators.java
index 41c75660a9..6465631d03 100644
--- a/scm-webapp/src/main/java/sonia/scm/util/Decorators.java
+++ b/scm-core/src/main/java/sonia/scm/util/Decorators.java
@@ -37,7 +37,6 @@ package sonia.scm.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.DecoratorFactory;
/**
diff --git a/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
new file mode 100644
index 0000000000..1baecb62af
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
@@ -0,0 +1,40 @@
+package sonia.scm.web;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.util.Map;
+
+public abstract class JsonEnricherBase implements JsonEnricher {
+
+ private final ObjectMapper objectMapper;
+
+ protected JsonEnricherBase(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ }
+
+ protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) {
+ return mediaType.equals(context.getResponseMediaType().toString());
+ }
+
+ protected JsonNode value(Object object) {
+ return objectMapper.convertValue(object, JsonNode.class);
+ }
+
+ protected ObjectNode createObject() {
+ return objectMapper.createObjectNode();
+ }
+
+ protected ObjectNode createObject(Map values) {
+ ObjectNode object = createObject();
+
+ values.forEach((key, value) -> object.set(key, value(value)));
+
+ return object;
+ }
+
+ protected void addPropertyNode(JsonNode parent, String newKey, JsonNode child) {
+ ((ObjectNode) parent).set(newKey, child);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
index ddcce9f4e8..f0711cd1e4 100644
--- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
+++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
@@ -15,12 +15,14 @@ public class VndMediaType {
public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX;
public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION;
+ public static final String INDEX = PREFIX + "index" + SUFFIX;
public static final String USER = PREFIX + "user" + SUFFIX;
public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
+ public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
public static final String TAG = PREFIX + "tag" + SUFFIX;
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
public static final String BRANCH = PREFIX + "branch" + SUFFIX;
@@ -34,6 +36,8 @@ public class VndMediaType {
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
+ @SuppressWarnings("squid:S2068")
+ public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
public static final String ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX;
diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java
index dd3c82e800..328494a626 100644
--- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java
+++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java
@@ -33,39 +33,32 @@
package sonia.scm.web.filter;
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.base.Splitter;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import sonia.scm.ArgumentIsInvalidException;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
+import sonia.scm.repository.spi.ScmProviderHttpServlet;
+import sonia.scm.repository.spi.ScmProviderHttpServletDecorator;
import sonia.scm.security.Role;
import sonia.scm.security.ScmSecurityException;
import sonia.scm.util.HttpUtil;
-import sonia.scm.util.Util;
-import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
-import java.util.Iterator;
-
-//~--- JDK imports ------------------------------------------------------------
/**
* Abstract http filter to check repository permissions.
*
* @author Sebastian Sdorra
*/
-public abstract class PermissionFilter extends HttpFilter
+public abstract class PermissionFilter extends ScmProviderHttpServletDecorator
{
/** the logger for PermissionFilter */
@@ -81,23 +74,14 @@ public abstract class PermissionFilter extends HttpFilter
*
* @since 1.21
*/
- public PermissionFilter(ScmConfiguration configuration)
+ protected PermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate)
{
+ super(delegate);
this.configuration = configuration;
}
//~--- get methods ----------------------------------------------------------
- /**
- * Returns the requested repository.
- *
- *
- * @param request current http request
- *
- * @return requested repository
- */
- protected abstract Repository getRepository(HttpServletRequest request);
-
/**
* Returns true if the current request is a write request.
*
@@ -117,66 +101,38 @@ public abstract class PermissionFilter extends HttpFilter
*
* @param request http request
* @param response http response
- * @param chain filter chain
*
* @throws IOException
* @throws ServletException
*/
@Override
- protected void doFilter(HttpServletRequest request,
- HttpServletResponse response, FilterChain chain)
+ public void service(HttpServletRequest request,
+ HttpServletResponse response, Repository repository)
throws IOException, ServletException
{
Subject subject = SecurityUtils.getSubject();
try
{
- Repository repository = getRepository(request);
+ boolean writeRequest = isWriteRequest(request);
- if (repository != null)
+ if (hasPermission(repository, writeRequest))
{
- boolean writeRequest = isWriteRequest(request);
+ logger.trace("{} access to repository {} for user {} granted",
+ getActionAsString(writeRequest), repository.getName(),
+ getUserName(subject));
- if (hasPermission(repository, writeRequest))
- {
- logger.trace("{} access to repository {} for user {} granted",
- getActionAsString(writeRequest), repository.getName(),
- getUserName(subject));
-
- chain.doFilter(request, response);
- }
- else
- {
- logger.info("{} access to repository {} for user {} denied",
- getActionAsString(writeRequest), repository.getName(),
- getUserName(subject));
-
- sendAccessDenied(request, response, subject);
- }
+ super.service(request, response, repository);
}
else
{
- logger.debug("repository not found");
+ logger.info("{} access to repository {} for user {} denied",
+ getActionAsString(writeRequest), repository.getName(),
+ getUserName(subject));
- response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ sendAccessDenied(request, response, subject);
}
}
- catch (ArgumentIsInvalidException ex)
- {
- if (logger.isTraceEnabled())
- {
- logger.trace(
- "wrong request at ".concat(request.getRequestURI()).concat(
- " send redirect"), ex);
- }
- else if (logger.isWarnEnabled())
- {
- logger.warn("wrong request at {} send redirect",
- request.getRequestURI());
- }
-
- response.sendRedirect(getRepositoryRootHelpUrl(request));
- }
catch (ScmSecurityException | AuthorizationException ex)
{
logger.warn("user " + subject.getPrincipal() + " has not enough permissions", ex);
@@ -217,29 +173,6 @@ public abstract class PermissionFilter extends HttpFilter
HttpUtil.sendUnauthorized(response, configuration.getRealmDescription());
}
- /**
- * Extracts the type of the repositroy from url.
- *
- *
- * @param request http request
- *
- * @return type of repository
- */
- private String extractType(HttpServletRequest request)
- {
- Iterator it = Splitter.on(
- HttpUtil.SEPARATOR_PATH).omitEmptyStrings().split(
- request.getRequestURI()).iterator();
- String type = it.next();
-
- if (Util.isNotEmpty(request.getContextPath()))
- {
- type = it.next();
- }
-
- return type;
- }
-
/**
* Send access denied to the servlet response.
*
@@ -280,25 +213,6 @@ public abstract class PermissionFilter extends HttpFilter
: "read";
}
- /**
- * Returns the repository root help url.
- *
- *
- * @param request current http request
- *
- * @return repository root help url
- */
- private String getRepositoryRootHelpUrl(HttpServletRequest request)
- {
- String type = extractType(request);
- String helpUrl = HttpUtil.getCompleteUrl(request,
- "/api/rest/help/repository-root/");
-
- helpUrl = helpUrl.concat(type).concat(".html");
-
- return helpUrl;
- }
-
/**
* Returns the username from the given subject or anonymous.
*
diff --git a/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java
deleted file mode 100644
index ea0d90c915..0000000000
--- a/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.web.filter;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.base.Throwables;
-import com.google.inject.ProvisionException;
-
-import org.apache.shiro.authz.AuthorizationException;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import sonia.scm.config.ScmConfiguration;
-import sonia.scm.repository.Repository;
-import sonia.scm.repository.RepositoryProvider;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import javax.servlet.http.HttpServletRequest;
-
-/**
- *
- * @author Sebastian Sdorra
- * @since 1.9
- */
-public abstract class ProviderPermissionFilter extends PermissionFilter
-{
-
- /**
- * the logger for ProviderPermissionFilter
- */
- private static final Logger logger =
- LoggerFactory.getLogger(ProviderPermissionFilter.class);
-
- //~--- constructors ---------------------------------------------------------
-
- /**
- * Constructs ...
- *
- *
- * @param configuration
- * @param repositoryProvider
- * @since 1.21
- */
- public ProviderPermissionFilter(ScmConfiguration configuration,
- RepositoryProvider repositoryProvider)
- {
- super(configuration);
- this.repositoryProvider = repositoryProvider;
- }
-
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param request
- *
- * @return
- */
- @Override
- protected Repository getRepository(HttpServletRequest request)
- {
- Repository repository = null;
-
- try
- {
- repository = repositoryProvider.get();
- }
- catch (ProvisionException ex)
- {
- Throwables.propagateIfPossible(ex.getCause(),
- IllegalStateException.class, AuthorizationException.class);
- logger.error("could not get repository from request", ex);
- }
-
- return repository;
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private final RepositoryProvider repositoryProvider;
-}
diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
new file mode 100644
index 0000000000..2ceafc19bb
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
@@ -0,0 +1,74 @@
+package sonia.scm.repository.api;
+
+import org.junit.Test;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.spi.HttpScmProtocol;
+import sonia.scm.repository.spi.RepositoryServiceProvider;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.util.IterableUtil.sizeOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+public class RepositoryServiceTest {
+
+ private final RepositoryServiceProvider provider = mock(RepositoryServiceProvider.class);
+ private final Repository repository = new Repository("", "git", "space", "repo");
+
+ @Test
+ public void shouldReturnMatchingProtocolsFromProvider() {
+ RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
+ Stream supportedProtocols = repositoryService.getSupportedProtocols();
+
+ assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldFindKnownProtocol() {
+ RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
+
+ HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class);
+
+ assertThat(protocol).isNotNull();
+ }
+
+ @Test
+ public void shouldFailForUnknownProtocol() {
+ RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ repositoryService.getProtocol(UnknownScmProtocol.class);
+ });
+ }
+
+ private static class DummyHttpProtocol extends HttpScmProtocol {
+ public DummyHttpProtocol(Repository repository) {
+ super(repository, "");
+ }
+
+ @Override
+ public void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) {
+ }
+ }
+
+ private static class DummyScmProtocolProvider implements ScmProtocolProvider {
+ @Override
+ public String getType() {
+ return "git";
+ }
+
+ @Override
+ public ScmProtocol get(Repository repository) {
+ return new DummyHttpProtocol(repository);
+ }
+ }
+
+ private interface UnknownScmProtocol extends ScmProtocol {}
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java
new file mode 100644
index 0000000000..1fd772fee3
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java
@@ -0,0 +1,40 @@
+package sonia.scm.repository.spi;
+
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import sonia.scm.repository.Repository;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HttpScmProtocolTest {
+
+ @TestFactory
+ Stream shouldCreateCorrectUrlsWithContextPath() {
+ return Stream.of("http://localhost/scm", "http://localhost/scm/")
+ .map(url -> assertResultingUrl(url, "http://localhost/scm/repo/space/name"));
+ }
+
+ @TestFactory
+ Stream shouldCreateCorrectUrlsWithPort() {
+ return Stream.of("http://localhost:8080", "http://localhost:8080/")
+ .map(url -> assertResultingUrl(url, "http://localhost:8080/repo/space/name"));
+ }
+
+ DynamicTest assertResultingUrl(String baseUrl, String expectedUrl) {
+ String actualUrl = createInstanceOfHttpScmProtocol(baseUrl).getUrl();
+ return DynamicTest.dynamicTest(baseUrl + " -> " + expectedUrl, () -> assertThat(actualUrl).isEqualTo(expectedUrl));
+ }
+
+ private HttpScmProtocol createInstanceOfHttpScmProtocol(String baseUrl) {
+ return new HttpScmProtocol(new Repository("", "", "space", "name"), baseUrl) {
+ @Override
+ protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) {
+ }
+ };
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java
new file mode 100644
index 0000000000..8c910f92a9
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java
@@ -0,0 +1,120 @@
+package sonia.scm.repository.spi;
+
+import com.google.inject.ProvisionException;
+import com.google.inject.util.Providers;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.stubbing.OngoingStubbing;
+import sonia.scm.api.v2.resources.ScmPathInfo;
+import sonia.scm.api.v2.resources.ScmPathInfoStore;
+import sonia.scm.config.ScmConfiguration;
+import sonia.scm.repository.Repository;
+
+import javax.inject.Provider;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+public class InitializingHttpScmProtocolWrapperTest {
+
+ private static final Repository REPOSITORY = new Repository("", "git", "space", "name");
+
+ @Mock
+ private ScmProviderHttpServlet delegateServlet;
+ @Mock
+ private ScmPathInfoStore pathInfoStore;
+ @Mock
+ private ScmConfiguration scmConfiguration;
+ private Provider pathInfoStoreProvider;
+
+ @Mock
+ private HttpServletRequest request;
+ @Mock
+ private HttpServletResponse response;
+ @Mock
+ private ServletConfig servletConfig;
+
+ private InitializingHttpScmProtocolWrapper wrapper;
+
+ @Before
+ public void init() {
+ initMocks(this);
+ pathInfoStoreProvider = mock(Provider.class);
+ when(pathInfoStoreProvider.get()).thenReturn(pathInfoStore);
+
+ wrapper = new InitializingHttpScmProtocolWrapper(Providers.of(this.delegateServlet), pathInfoStoreProvider, scmConfiguration) {
+ @Override
+ public String getType() {
+ return "git";
+ }
+ };
+ when(scmConfiguration.getBaseUrl()).thenReturn("http://example.com/scm");
+ }
+
+ @Test
+ public void shouldUsePathFromPathInfo() {
+ mockSetPathInfo();
+
+ HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+ assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl());
+ }
+
+ @Test
+ public void shouldUseConfigurationWhenPathInfoNotSet() {
+ HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+ assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl());
+ }
+
+ @Test
+ public void shouldUseConfigurationWhenNotInRequestScope() {
+ when(pathInfoStoreProvider.get()).thenThrow(new ProvisionException("test"));
+
+ HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+ assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl());
+ }
+
+ @Test
+ public void shouldInitializeAndDelegateRequestThroughFilter() throws ServletException, IOException {
+ HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+ httpScmProtocol.serve(request, response, servletConfig);
+
+ verify(delegateServlet).init(servletConfig);
+ verify(delegateServlet).service(request, response, REPOSITORY);
+ }
+
+ @Test
+ public void shouldInitializeOnlyOnce() throws ServletException, IOException {
+ HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+ httpScmProtocol.serve(request, response, servletConfig);
+ httpScmProtocol.serve(request, response, servletConfig);
+
+ verify(delegateServlet, times(1)).init(servletConfig);
+ verify(delegateServlet, times(2)).service(request, response, REPOSITORY);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldFailForIllegalScmType() {
+ HttpScmProtocol httpScmProtocol = wrapper.get(new Repository("", "other", "space", "name"));
+ }
+
+ private OngoingStubbing mockSetPathInfo() {
+ return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/rest/"));
+ }
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java
new file mode 100644
index 0000000000..43ed4940fa
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java
@@ -0,0 +1,51 @@
+package sonia.scm.web;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.Test;
+
+import javax.ws.rs.core.MediaType;
+
+import static java.util.Collections.singletonMap;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class JsonEnricherBaseTest {
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+ private TestJsonEnricher enricher = new TestJsonEnricher(objectMapper);
+
+ @Test
+ public void testResultHasMediaType() {
+ JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, null);
+
+ assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_JSON, context)).isTrue();
+ assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_XML, context)).isFalse();
+ }
+
+ @Test
+ public void testAppendLink() {
+ ObjectNode root = objectMapper.createObjectNode();
+ ObjectNode links = objectMapper.createObjectNode();
+ root.set("_links", links);
+ JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, root);
+ enricher.enrich(context);
+
+ assertThat(links.get("awesome").get("href").asText()).isEqualTo("/my/awesome/link");
+ }
+
+ private static class TestJsonEnricher extends JsonEnricherBase {
+
+ public TestJsonEnricher(ObjectMapper objectMapper) {
+ super(objectMapper);
+ }
+
+ @Override
+ public void enrich(JsonEnricherContext context) {
+ JsonNode gitConfigRefNode = createObject(singletonMap("href", value("/my/awesome/link")));
+
+ addPropertyNode(context.getResponseEntity().get("_links"), "awesome", gitConfigRefNode);
+ }
+ }
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java b/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java
new file mode 100644
index 0000000000..9fa65d51b8
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java
@@ -0,0 +1,74 @@
+package sonia.scm.web.filter;
+
+import com.github.sdorra.shiro.ShiroRule;
+import com.github.sdorra.shiro.SubjectAware;
+import org.junit.Rule;
+import org.junit.Test;
+import sonia.scm.config.ScmConfiguration;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.spi.ScmProviderHttpServlet;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+@SubjectAware(configuration = "classpath:sonia/scm/shiro.ini")
+public class PermissionFilterTest {
+
+ public static final Repository REPOSITORY = new Repository("1", "git", "space", "name");
+
+ @Rule
+ public final ShiroRule shiroRule = new ShiroRule();
+
+ private final ScmProviderHttpServlet delegateServlet = mock(ScmProviderHttpServlet.class);
+
+ private final PermissionFilter permissionFilter = new PermissionFilter(new ScmConfiguration(), delegateServlet) {
+ @Override
+ protected boolean isWriteRequest(HttpServletRequest request) {
+ return writeRequest;
+ }
+ };
+
+ private final HttpServletRequest request = mock(HttpServletRequest.class);
+ private final HttpServletResponse response = mock(HttpServletResponse.class);
+
+ private boolean writeRequest = false;
+
+ @Test
+ @SubjectAware(username = "reader", password = "secret")
+ public void shouldPassForReaderOnReadRequest() throws IOException, ServletException {
+ writeRequest = false;
+
+ permissionFilter.service(request, response, REPOSITORY);
+
+ verify(delegateServlet).service(request, response, REPOSITORY);
+ }
+
+ @Test
+ @SubjectAware(username = "reader", password = "secret")
+ public void shouldBlockForReaderOnWriteRequest() throws IOException, ServletException {
+ writeRequest = true;
+
+ permissionFilter.service(request, response, REPOSITORY);
+
+ verify(response).sendError(eq(401), anyString());
+ verify(delegateServlet, never()).service(request, response, REPOSITORY);
+ }
+
+ @Test
+ @SubjectAware(username = "writer", password = "secret")
+ public void shouldPassForWriterOnWriteRequest() throws IOException, ServletException {
+ writeRequest = true;
+
+ permissionFilter.service(request, response, REPOSITORY);
+
+ verify(delegateServlet).service(request, response, REPOSITORY);
+ }
+}
diff --git a/scm-core/src/test/resources/sonia/scm/shiro.ini b/scm-core/src/test/resources/sonia/scm/shiro.ini
index e87c81b097..fbdd35ba50 100644
--- a/scm-core/src/test/resources/sonia/scm/shiro.ini
+++ b/scm-core/src/test/resources/sonia/scm/shiro.ini
@@ -1,6 +1,12 @@
[users]
trillian = secret, user
+admin = secret, admin
+writer = secret, repo_write
+reader = secret, repo_read
+unpriv = secret
[roles]
admin = *
-user = something:*
\ No newline at end of file
+user = something:*
+repo_read = "repository:read:1"
+repo_write = "repository:push:1"
diff --git a/scm-it/src/test/java/sonia/scm/it/IndexITCase.java b/scm-it/src/test/java/sonia/scm/it/IndexITCase.java
new file mode 100644
index 0000000000..4a621d962f
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/IndexITCase.java
@@ -0,0 +1,48 @@
+package sonia.scm.it;
+
+import io.restassured.RestAssured;
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+import sonia.scm.it.utils.RestUtil;
+import sonia.scm.web.VndMediaType;
+
+import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
+import static sonia.scm.it.utils.RestUtil.given;
+
+public class IndexITCase {
+
+ @Test
+ public void shouldLinkEverythingForAdmin() {
+ given(VndMediaType.INDEX)
+
+ .when()
+ .get(RestUtil.createResourceUrl(""))
+
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .body(
+ "_links.repositories.href", matchesPattern(".+/repositories/"),
+ "_links.users.href", matchesPattern(".+/users/"),
+ "_links.groups.href", matchesPattern(".+/groups/"),
+ "_links.config.href", matchesPattern(".+/config"),
+ "_links.gitConfig.href", matchesPattern(".+/config/git"),
+ "_links.hgConfig.href", matchesPattern(".+/config/hg"),
+ "_links.svnConfig.href", matchesPattern(".+/config/svn")
+ );
+ }
+
+ @Test
+ public void shouldCreateLoginLinksForAnonymousAccess() {
+ RestAssured.given() // do not specify user credentials
+
+ .when()
+ .get(RestUtil.createResourceUrl(""))
+
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .body(
+ "_links.login.href", matchesPattern(".+/auth/.+")
+ );
+ }
+
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/MeITCase.java b/scm-it/src/test/java/sonia/scm/it/MeITCase.java
new file mode 100644
index 0000000000..64f06765ea
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/MeITCase.java
@@ -0,0 +1,65 @@
+package sonia.scm.it;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import sonia.scm.it.utils.ScmRequests;
+import sonia.scm.it.utils.TestData;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MeITCase {
+
+ @Before
+ public void init() {
+ TestData.cleanup();
+ }
+
+ @Test
+ public void adminShouldChangeOwnPassword() {
+ String newPassword = TestData.USER_SCM_ADMIN + "1";
+ // admin change the own password
+ ScmRequests.start()
+ .given()
+ .url(TestData.getMeUrl())
+ .usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
+ .getMeResource()
+ .assertStatusCode(200)
+ .usingMeResponse()
+ .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
+ .assertPassword(Assert::assertNull)
+ .assertType(s -> assertThat(s).isEqualTo("xml"))
+ .requestChangePassword(TestData.USER_SCM_ADMIN, newPassword)
+ .assertStatusCode(204);
+ // assert password is changed -> login with the new Password than undo changes
+ ScmRequests.start()
+ .given()
+ .url(TestData.getUserUrl(TestData.USER_SCM_ADMIN))
+ .usernameAndPassword(TestData.USER_SCM_ADMIN, newPassword)
+ .getMeResource()
+ .assertStatusCode(200)
+ .usingMeResponse()
+ .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin
+ .requestChangePassword(newPassword, TestData.USER_SCM_ADMIN)
+ .assertStatusCode(204);
+ }
+
+ @Test
+ public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
+ String newUser = "user";
+ String password = "pass";
+ String type = "not XML Type";
+ TestData.createUser(newUser, password, true, type);
+ ScmRequests.start()
+ .given()
+ .url(TestData.getMeUrl())
+ .usernameAndPassword(newUser, password)
+ .getMeResource()
+ .assertStatusCode(200)
+ .usingMeResponse()
+ .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
+ .assertPassword(Assert::assertNull)
+ .assertType(s -> assertThat(s).isEqualTo(type))
+ .assertPasswordLinkDoesNotExists();
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java
index 63d8b46d47..f288d4891c 100644
--- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java
+++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java
@@ -39,6 +39,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
+import sonia.scm.it.utils.RepositoryUtil;
+import sonia.scm.it.utils.TestData;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientException;
@@ -51,11 +53,11 @@ import java.util.Objects;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
-import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile;
-import static sonia.scm.it.RestUtil.given;
-import static sonia.scm.it.ScmTypes.availableScmTypes;
-import static sonia.scm.it.TestData.USER_SCM_ADMIN;
-import static sonia.scm.it.TestData.callRepository;
+import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
+import static sonia.scm.it.utils.RestUtil.given;
+import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
+import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN;
+import static sonia.scm.it.utils.TestData.callRepository;
@RunWith(Parameterized.class)
public class PermissionsITCase {
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 21d4d97b1b..c49a65bea2 100644
--- a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java
+++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java
@@ -42,6 +42,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
+import sonia.scm.it.utils.RepositoryUtil;
+import sonia.scm.it.utils.TestData;
import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.web.VndMediaType;
@@ -53,11 +55,11 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
-import static sonia.scm.it.RegExMatcher.matchesPattern;
-import static sonia.scm.it.RestUtil.createResourceUrl;
-import static sonia.scm.it.RestUtil.given;
-import static sonia.scm.it.ScmTypes.availableScmTypes;
-import static sonia.scm.it.TestData.repositoryJson;
+import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
+import static sonia.scm.it.utils.RestUtil.createResourceUrl;
+import static sonia.scm.it.utils.RestUtil.given;
+import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
+import static sonia.scm.it.utils.TestData.repositoryJson;
@RunWith(Parameterized.class)
public class RepositoriesITCase {
diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
index 399dd26035..3f8832a3f5 100644
--- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
+++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
@@ -3,6 +3,7 @@ package sonia.scm.it;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.apache.http.HttpStatus;
+import org.assertj.core.util.Lists;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
@@ -10,6 +11,9 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import sonia.scm.it.utils.RepositoryUtil;
+import sonia.scm.it.utils.ScmRequests;
+import sonia.scm.it.utils.TestData;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient;
@@ -17,17 +21,20 @@ import sonia.scm.web.VndMediaType;
import java.io.File;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import static java.lang.Thread.sleep;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull;
-import static sonia.scm.it.RestUtil.ADMIN_PASSWORD;
-import static sonia.scm.it.RestUtil.ADMIN_USERNAME;
-import static sonia.scm.it.RestUtil.given;
-import static sonia.scm.it.ScmTypes.availableScmTypes;
+import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
+import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
+import static sonia.scm.it.utils.RestUtil.given;
+import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
@RunWith(Parameterized.class)
public class RepositoryAccessITCase {
@@ -37,6 +44,7 @@ public class RepositoryAccessITCase {
private final String repositoryType;
private File folder;
+ private ScmRequests.AppliedRepositoryRequest repositoryGetRequest;
public RepositoryAccessITCase(String repositoryType) {
this.repositoryType = repositoryType;
@@ -48,9 +56,20 @@ public class RepositoryAccessITCase {
}
@Before
- public void initClient() {
+ public void init() {
TestData.createDefault();
folder = tempFolder.getRoot();
+ repositoryGetRequest = ScmRequests.start()
+ .given()
+ .url(TestData.getDefaultRepositoryUrl(repositoryType))
+ .usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
+ .getRepositoryResource()
+ .assertStatusCode(HttpStatus.SC_OK);
+ ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start()
+ .given()
+ .url(TestData.getMeUrl())
+ .usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
+ .getMeResource();
}
@Test
@@ -153,7 +172,7 @@ public class RepositoryAccessITCase {
.isNotNull()
.contains(String.format("%s/sources/%s", repositoryUrl, changeset.getId()));
- assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.changesets.href"))
+ assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.changeset.href"))
.as("assert single tag changesets link")
.isNotNull()
.contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId()));
@@ -281,5 +300,187 @@ public class RepositoryAccessITCase {
.contains("diff");
}
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void shouldFindFileHistory() throws IOException {
+ RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
+ Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "folder/subfolder/a.txt", "a");
+ repositoryGetRequest
+ .usingRepositoryResponse()
+ .requestSources()
+ .usingSourcesResponse()
+ .requestSelf("folder")
+ .usingSourcesResponse()
+ .requestSelf("subfolder")
+ .usingSourcesResponse()
+ .requestFileHistory("a.txt")
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingChangesetsResponse()
+ .assertChangesets(changesets -> {
+ assertThat(changesets).hasSize(1);
+ assertThat(changesets.get(0)).containsEntry("id", changeset.getId());
+ assertThat(changesets.get(0)).containsEntry("description", changeset.getDescription());
+ }
+ );
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void shouldFindAddedModifications() throws IOException {
+ RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
+ String fileName = "a.txt";
+ Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a");
+ String revision = changeset.getId();
+ repositoryGetRequest
+ .usingRepositoryResponse()
+ .requestChangesets()
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingChangesetsResponse()
+ .requestModifications(revision)
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingModificationsResponse()
+ .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
+ .assertAdded(addedFiles -> assertThat(addedFiles)
+ .hasSize(1)
+ .containsExactly(fileName))
+ .assertRemoved(removedFiles -> assertThat(removedFiles)
+ .hasSize(0))
+ .assertModified(files -> assertThat(files)
+ .hasSize(0));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void shouldFindRemovedModifications() throws IOException {
+ RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
+ String fileName = "a.txt";
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a");
+ Changeset changeset = RepositoryUtil.removeAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName);
+
+ String revision = changeset.getId();
+ repositoryGetRequest
+ .usingRepositoryResponse()
+ .requestChangesets()
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingChangesetsResponse()
+ .requestModifications(revision)
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingModificationsResponse()
+ .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
+ .assertRemoved(removedFiles -> assertThat(removedFiles)
+ .hasSize(1)
+ .containsExactly(fileName))
+ .assertAdded(addedFiles -> assertThat(addedFiles)
+ .hasSize(0))
+ .assertModified(files -> assertThat(files)
+ .hasSize(0));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void shouldFindUpdateModifications() throws IOException {
+ RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
+ String fileName = "a.txt";
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a");
+ Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "new Content");
+
+ String revision = changeset.getId();
+ repositoryGetRequest
+ .usingRepositoryResponse()
+ .requestChangesets()
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingChangesetsResponse()
+ .requestModifications(revision)
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingModificationsResponse()
+ .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
+ .assertModified(modifiedFiles -> assertThat(modifiedFiles)
+ .hasSize(1)
+ .containsExactly(fileName))
+ .assertRemoved(removedFiles -> assertThat(removedFiles)
+ .hasSize(0))
+ .assertAdded(addedFiles -> assertThat(addedFiles)
+ .hasSize(0));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void shouldFindMultipleModifications() throws IOException {
+ RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "b.txt", "b");
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "c.txt", "c");
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "d.txt", "d");
+ Map addedFiles = new HashMap()
+ {{
+ put("a.txt", "bla bla");
+ }};
+ Map modifiedFiles = new HashMap()
+ {{
+ put("b.txt", "new content");
+ }};
+ ArrayList removedFiles = Lists.newArrayList("c.txt", "d.txt");
+ Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
+
+ String revision = changeset.getId();
+ repositoryGetRequest
+ .usingRepositoryResponse()
+ .requestChangesets()
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingChangesetsResponse()
+ .requestModifications(revision)
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingModificationsResponse()
+ .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
+ .assertAdded(a -> assertThat(a)
+ .hasSize(1)
+ .containsExactly("a.txt"))
+ .assertModified(m-> assertThat(m)
+ .hasSize(1)
+ .containsExactly("b.txt"))
+ .assertRemoved(r -> assertThat(r)
+ .hasSize(2)
+ .containsExactly("c.txt", "d.txt"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void svnShouldCreateOneModificationPerFolder() throws IOException {
+ Assume.assumeThat(repositoryType, equalTo("svn"));
+ RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "bbb/bb/b.txt", "b");
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "ccc/cc/c.txt", "c");
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "ddd/dd/d.txt", "d");
+ Map addedFiles = new HashMap()
+ {{
+ put("aaa/aa/a.txt", "bla bla");
+ }};
+ Map modifiedFiles = new HashMap()
+ {{
+ put("bbb/bb/b.txt", "new content");
+ }};
+ ArrayList removedFiles = Lists.newArrayList("ccc/cc/c.txt", "ddd/dd/d.txt");
+ Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
+
+ String revision = changeset.getId();
+ repositoryGetRequest
+ .usingRepositoryResponse()
+ .requestChangesets()
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingChangesetsResponse()
+ .requestModifications(revision)
+ .assertStatusCode(HttpStatus.SC_OK)
+ .usingModificationsResponse()
+ .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
+ .assertAdded(a -> assertThat(a)
+ .hasSize(3)
+ .containsExactly("aaa/aa/a.txt", "aaa", "aaa/aa"))
+ .assertModified(m-> assertThat(m)
+ .hasSize(1)
+ .containsExactly("bbb/bb/b.txt"))
+ .assertRemoved(r -> assertThat(r)
+ .hasSize(2)
+ .containsExactly("ccc/cc/c.txt", "ddd/dd/d.txt"));
+ }
}
diff --git a/scm-it/src/test/java/sonia/scm/it/UserITCase.java b/scm-it/src/test/java/sonia/scm/it/UserITCase.java
new file mode 100644
index 0000000000..33fbe0cc5d
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/UserITCase.java
@@ -0,0 +1,96 @@
+package sonia.scm.it;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import sonia.scm.it.utils.ScmRequests;
+import sonia.scm.it.utils.TestData;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UserITCase {
+
+ @Before
+ public void init(){
+ TestData.cleanup();
+ }
+
+ @Test
+ public void adminShouldChangeOwnPassword() {
+ String newUser = "user";
+ String password = "pass";
+ TestData.createUser(newUser, password, true, "xml");
+ String newPassword = "new_password";
+ // admin change the own password
+ ScmRequests.start()
+ .given()
+ .url(TestData.getUserUrl(newUser))
+ .usernameAndPassword(newUser, password)
+ .getUserResource()
+ .assertStatusCode(200)
+ .usingUserResponse()
+ .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
+ .assertPassword(Assert::assertNull)
+ .requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
+ .assertStatusCode(204);
+ // assert password is changed -> login with the new Password
+ ScmRequests.start()
+ .given()
+ .url(TestData.getUserUrl(newUser))
+ .usernameAndPassword(newUser, newPassword)
+ .getUserResource()
+ .assertStatusCode(200)
+ .usingUserResponse()
+ .assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE))
+ .assertPassword(Assert::assertNull);
+
+ }
+
+ @Test
+ public void adminShouldChangePasswordOfOtherUser() {
+ String newUser = "user";
+ String password = "pass";
+ TestData.createUser(newUser, password, true, "xml");
+ String newPassword = "new_password";
+ // admin change the password of the user
+ ScmRequests.start()
+ .given()
+ .url(TestData.getUserUrl(newUser))// the admin get the user object
+ .usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
+ .getUserResource()
+ .assertStatusCode(200)
+ .usingUserResponse()
+ .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin
+ .assertPassword(Assert::assertNull)
+ .requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
+ .assertStatusCode(204);
+ // assert password is changed
+ ScmRequests.start()
+ .given()
+ .url(TestData.getUserUrl(newUser))
+ .usernameAndPassword(newUser, newPassword)
+ .getUserResource()
+ .assertStatusCode(200);
+
+ }
+
+
+ @Test
+ public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
+ String newUser = "user";
+ String password = "pass";
+ String type = "not XML Type";
+ TestData.createUser(newUser, password, true, type);
+ ScmRequests.start()
+ .given()
+ .url(TestData.getMeUrl())
+ .usernameAndPassword(newUser, password)
+ .getUserResource()
+ .assertStatusCode(200)
+ .usingUserResponse()
+ .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
+ .assertPassword(Assert::assertNull)
+ .assertType(s -> assertThat(s).isEqualTo(type))
+ .assertPasswordLinkDoesNotExists();
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/utils/NullAwareJsonObjectBuilder.java b/scm-it/src/test/java/sonia/scm/it/utils/NullAwareJsonObjectBuilder.java
new file mode 100644
index 0000000000..31a12f1969
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/utils/NullAwareJsonObjectBuilder.java
@@ -0,0 +1,95 @@
+package sonia.scm.it.utils;
+
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public class NullAwareJsonObjectBuilder implements JsonObjectBuilder {
+ public static JsonObjectBuilder wrap(JsonObjectBuilder builder) {
+ if (builder == null) {
+ throw new IllegalArgumentException("Can't wrap nothing.");
+ }
+ return new NullAwareJsonObjectBuilder(builder);
+ }
+
+ private final JsonObjectBuilder builder;
+
+ private NullAwareJsonObjectBuilder(JsonObjectBuilder builder) {
+ this.builder = builder;
+ }
+
+ public JsonObjectBuilder add(String name, JsonValue value) {
+ return builder.add(name, (value == null) ? JsonValue.NULL : value);
+ }
+
+ @Override
+ public JsonObjectBuilder add(String name, String value) {
+ if (value != null){
+ return builder.add(name, value );
+ }else{
+ return builder.addNull(name);
+ }
+ }
+
+ @Override
+ public JsonObjectBuilder add(String name, BigInteger value) {
+ if (value != null){
+ return builder.add(name, value );
+ }else{
+ return builder.addNull(name);
+ }
+ }
+
+ @Override
+ public JsonObjectBuilder add(String name, BigDecimal value) {
+ if (value != null){
+ return builder.add(name, value );
+ }else{
+ return builder.addNull(name);
+ }
+ }
+
+ @Override
+ public JsonObjectBuilder add(String s, int i) {
+ return builder.add(s, i);
+ }
+
+ @Override
+ public JsonObjectBuilder add(String s, long l) {
+ return builder.add(s, l);
+ }
+
+ @Override
+ public JsonObjectBuilder add(String s, double v) {
+ return builder.add(s, v);
+ }
+
+ @Override
+ public JsonObjectBuilder add(String s, boolean b) {
+ return builder.add(s, b);
+ }
+
+ @Override
+ public JsonObjectBuilder addNull(String s) {
+ return builder.addNull(s);
+ }
+
+ @Override
+ public JsonObjectBuilder add(String s, JsonObjectBuilder jsonObjectBuilder) {
+ return builder.add(s, jsonObjectBuilder);
+ }
+
+ @Override
+ public JsonObjectBuilder add(String s, JsonArrayBuilder jsonArrayBuilder) {
+ return builder.add(s, jsonArrayBuilder);
+ }
+
+ @Override
+ public JsonObject build() {
+ return builder.build();
+ }
+
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java b/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java
similarity index 76%
rename from scm-it/src/test/java/sonia/scm/it/RegExMatcher.java
rename to scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java
index e5dc7931d3..8fb9fdf798 100644
--- a/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java
+++ b/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java
@@ -1,4 +1,4 @@
-package sonia.scm.it;
+package sonia.scm.it.utils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
@@ -6,7 +6,7 @@ import org.hamcrest.Matcher;
import java.util.regex.Pattern;
-class RegExMatcher extends BaseMatcher {
+public class RegExMatcher extends BaseMatcher {
public static Matcher matchesPattern(String pattern) {
return new RegExMatcher(pattern);
}
@@ -24,6 +24,6 @@ class RegExMatcher extends BaseMatcher {
@Override
public boolean matches(Object o) {
- return Pattern.compile(pattern).matcher(o.toString()).matches();
+ return o != null && Pattern.compile(pattern).matcher(o.toString()).matches();
}
}
diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java
similarity index 50%
rename from scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java
rename to scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java
index 442856eefa..11db0200f1 100644
--- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java
+++ b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java
@@ -1,4 +1,4 @@
-package sonia.scm.it;
+package sonia.scm.it.utils;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
@@ -14,6 +14,8 @@ import sonia.scm.repository.client.api.RepositoryClientFactory;
import java.io.File;
import java.io.IOException;
+import java.util.List;
+import java.util.Map;
import java.util.UUID;
public class RepositoryUtil {
@@ -22,30 +24,73 @@ public class RepositoryUtil {
private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory();
- static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
+ public static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin");
}
- static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
+ public static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK)
.extract()
- .path("_links.httpProtocol.href");
+ .path("_links.protocol.find{it.name=='http'}.href");
return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder);
}
- static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
+ public static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
String uuid = UUID.randomUUID().toString();
String name = "file-" + uuid + ".uuid";
createAndCommitFile(client, username, name, uuid);
return name;
}
- static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
+ public static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
+ writeAndAddFile(repositoryClient, fileName, content);
+ return commit(repositoryClient, username, "added " + fileName);
+ }
+
+ /**
+ * Bundle multiple File modification in one changeset
+ *
+ * @param repositoryClient
+ * @param username
+ * @param addedFiles map.key: path of the file, value: the file content
+ * @param modifiedFiles map.key: path of the file, value: the file content
+ * @param removedFiles list of file paths to be removed
+ * @return the changeset with all modifications
+ * @throws IOException
+ */
+ public static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map addedFiles, Map modifiedFiles, List removedFiles) throws IOException {
+ for (String fileName : addedFiles.keySet()) {
+ writeAndAddFile(repositoryClient, fileName, addedFiles.get(fileName));
+ }
+ for (String fileName : modifiedFiles.keySet()) {
+ writeAndAddFile(repositoryClient, fileName, modifiedFiles.get(fileName));
+ }
+ for (String fileName : removedFiles) {
+ deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
+ }
+ return commit(repositoryClient, username, "multiple file modifications" );
+ }
+
+ private static File writeAndAddFile(RepositoryClient repositoryClient, String fileName, String content) throws IOException {
File file = new File(repositoryClient.getWorkingCopy(), fileName);
+ Files.createParentDirs(file);
Files.write(content, file, Charsets.UTF_8);
addWithParentDirectories(repositoryClient, file);
- return commit(repositoryClient, username, "added " + fileName);
+ return file;
+ }
+
+ public static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
+ deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
+ return commit(repositoryClient, username, "removed " + fileName);
+ }
+
+ private static void deleteFileAndApplyRemoveCommand(RepositoryClient repositoryClient, String fileName) throws IOException {
+ File file = new File(repositoryClient.getWorkingCopy(), fileName);
+ if (repositoryClient.isCommandSupported(ClientCommand.REMOVE)) {
+ repositoryClient.getRemoveCommand().remove(fileName);
+ }
+ file.delete();
}
private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException {
@@ -53,7 +98,6 @@ public class RepositoryUtil {
String thisName = file.getName();
String path;
if (!repositoryClient.getWorkingCopy().equals(parent)) {
- addWithParentDirectories(repositoryClient, parent);
path = addWithParentDirectories(repositoryClient, parent) + File.separator + thisName;
} else {
path = thisName;
@@ -71,7 +115,7 @@ public class RepositoryUtil {
return changeset;
}
- static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException {
+ public static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException {
if (repositoryClient.isCommandSupported(ClientCommand.TAG)) {
Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN);
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {
diff --git a/scm-it/src/test/java/sonia/scm/it/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java
similarity index 97%
rename from scm-it/src/test/java/sonia/scm/it/RestUtil.java
rename to scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java
index a7409e1995..c8b01a6d72 100644
--- a/scm-it/src/test/java/sonia/scm/it/RestUtil.java
+++ b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java
@@ -1,4 +1,4 @@
-package sonia.scm.it;
+package sonia.scm.it.utils;
import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification;
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
new file mode 100644
index 0000000000..41fd9a1290
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java
@@ -0,0 +1,465 @@
+package sonia.scm.it.utils;
+
+import io.restassured.RestAssured;
+import io.restassured.response.Response;
+import sonia.scm.web.VndMediaType;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static org.hamcrest.Matchers.is;
+import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
+
+
+/**
+ * Encapsulate rest requests of a repository in builder pattern
+ *
+ * A Get Request can be applied with the methods request*()
+ * These methods return a AppliedGet*Request object
+ * This object can be used to apply general assertions over the rest Assured response
+ * In the AppliedGet*Request classes there is a using*Response() method
+ * that return the *Response class containing specific operations related to the specific response
+ * the *Response class contains also the request*() method to apply the next GET request from a link in the response.
+ */
+public class ScmRequests {
+
+ private String url;
+ private String username;
+ private String password;
+
+ public static ScmRequests start() {
+ return new ScmRequests();
+ }
+
+ public Given given() {
+ return new Given();
+ }
+
+
+ /**
+ * Apply a GET Request to the extracted url from the given link
+ *
+ * @param linkPropertyName the property name of link
+ * @param response the response containing the link
+ * @return the response of the GET request using the given link
+ */
+ private Response applyGETRequestFromLink(Response response, String linkPropertyName) {
+ return applyGETRequest(response
+ .then()
+ .extract()
+ .path(linkPropertyName));
+ }
+
+
+ /**
+ * Apply a GET Request to the given url and return the response.
+ *
+ * @param url the url of the GET request
+ * @return the response of the GET request using the given url
+ */
+ private Response applyGETRequest(String url) {
+ return RestAssured.given()
+ .auth().preemptive().basic(username, password)
+ .when()
+ .get(url);
+ }
+
+
+ /**
+ * Apply a PUT Request to the extracted url from the given link
+ *
+ * @param response the response containing the link
+ * @param linkPropertyName the property name of link
+ * @param body
+ * @return the response of the PUT request using the given link
+ */
+ private Response applyPUTRequestFromLink(Response response, String linkPropertyName, String content, String body) {
+ return applyPUTRequest(response
+ .then()
+ .extract()
+ .path(linkPropertyName), content, body);
+ }
+
+
+ /**
+ * Apply a PUT Request to the given url and return the response.
+ *
+ * @param url the url of the PUT request
+ * @param mediaType
+ * @param body
+ * @return the response of the PUT request using the given url
+ */
+ private Response applyPUTRequest(String url, String mediaType, String body) {
+ return RestAssured.given()
+ .auth().preemptive().basic(username, password)
+ .when()
+ .contentType(mediaType)
+ .accept(mediaType)
+ .body(body)
+ .put(url);
+ }
+
+
+ private void setUrl(String url) {
+ this.url = url;
+ }
+
+ private void setUsername(String username) {
+ this.username = username;
+ }
+
+ private void setPassword(String password) {
+ this.password = password;
+ }
+
+ private String getUrl() {
+ return url;
+ }
+
+ private String getUsername() {
+ return username;
+ }
+
+ private String getPassword() {
+ return password;
+ }
+
+ public class Given {
+
+ public GivenUrl url(String url) {
+ setUrl(url);
+ return new GivenUrl();
+ }
+
+ public GivenUrl url(URI url) {
+ setUrl(url.toString());
+ return new GivenUrl();
+ }
+
+ }
+
+ public class GivenWithUrlAndAuth {
+ public AppliedMeRequest getMeResource() {
+ return new AppliedMeRequest(applyGETRequest(url));
+ }
+
+ public AppliedUserRequest getUserResource() {
+ return new AppliedUserRequest(applyGETRequest(url));
+ }
+
+ public AppliedRepositoryRequest getRepositoryResource() {
+ return new AppliedRepositoryRequest(
+ applyGETRequest(url)
+ );
+ }
+ }
+
+ public class AppliedRequest {
+ private Response response;
+
+ public AppliedRequest(Response response) {
+ this.response = response;
+ }
+
+ /**
+ * apply custom assertions to the actual response
+ *
+ * @param consumer consume the response in order to assert the content. the header, the payload etc..
+ * @return the self object
+ */
+ public SELF assertResponse(Consumer consumer) {
+ consumer.accept(response);
+ return (SELF) this;
+ }
+
+ /**
+ * special assertion of the status code
+ *
+ * @param expectedStatusCode the expected status code
+ * @return the self object
+ */
+ public SELF assertStatusCode(int expectedStatusCode) {
+ this.response.then().assertThat().statusCode(expectedStatusCode);
+ return (SELF) this;
+ }
+
+ }
+
+ public class AppliedRepositoryRequest extends AppliedRequest {
+
+ public AppliedRepositoryRequest(Response response) {
+ super(response);
+ }
+
+ public RepositoryResponse usingRepositoryResponse() {
+ return new RepositoryResponse(super.response);
+ }
+ }
+
+ public class RepositoryResponse {
+
+ private Response repositoryResponse;
+
+ public RepositoryResponse(Response repositoryResponse) {
+ this.repositoryResponse = repositoryResponse;
+ }
+
+ public AppliedSourcesRequest requestSources() {
+ return new AppliedSourcesRequest(applyGETRequestFromLink(repositoryResponse, "_links.sources.href"));
+ }
+
+ public AppliedChangesetsRequest requestChangesets() {
+ return new AppliedChangesetsRequest(applyGETRequestFromLink(repositoryResponse, "_links.changesets.href"));
+ }
+ }
+
+ public class AppliedChangesetsRequest extends AppliedRequest {
+
+ public AppliedChangesetsRequest(Response response) {
+ super(response);
+ }
+
+ public ChangesetsResponse usingChangesetsResponse() {
+ return new ChangesetsResponse(super.response);
+ }
+ }
+
+ public class ChangesetsResponse {
+ private Response changesetsResponse;
+
+ public ChangesetsResponse(Response changesetsResponse) {
+ this.changesetsResponse = changesetsResponse;
+ }
+
+ public ChangesetsResponse assertChangesets(Consumer> changesetsConsumer) {
+ List