merge branch m2.0.0-3

This commit is contained in:
Maren Süwer
2018-10-02 14:32:12 +02:00
296 changed files with 7465 additions and 3184 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
# ignore everything except scm-server.tar.gz
**
!scm-server/target/*.tar.gz

View File

@@ -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 distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip

20
Dockerfile Normal file
View File

@@ -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" ]

44
Jenkinsfile vendored
View File

@@ -4,7 +4,7 @@
@Library('github.com/cloudogu/ces-build-lib@59d3e94') @Library('github.com/cloudogu/ces-build-lib@59d3e94')
import com.cloudogu.ces.cesbuildlib.* 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 // Change this as when we go back to default - necessary for proper SonarQube analysis
mainBranch = "2.0.0-m3" mainBranch = "2.0.0-m3"
@@ -12,15 +12,14 @@ node() { // No specific label
properties([ properties([
// Keep only the last 10 build to preserve space // Keep only the last 10 build to preserve space
buildDiscarder(logRotator(numToKeepStr: '10')), buildDiscarder(logRotator(numToKeepStr: '10')),
disableConcurrentBuilds()
]) ])
timeout(activity: true, time: 20, unit: 'MINUTES') {
catchError { catchError {
Maven mvn = setupMavenBuild() Maven mvn = setupMavenBuild()
// Maven build specified it must be 1.8.0-101 or newer
def javaHome = tool 'JDK-1.8.0-101+'
withEnv(["JAVA_HOME=${javaHome}", "PATH=${env.JAVA_HOME}/bin:${env.PATH}"]) {
stage('Checkout') { stage('Checkout') {
checkout scm checkout scm
@@ -46,6 +45,25 @@ node() { // No specific label
currentBuild.result = 'UNSTABLE' 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)
]
}
} }
} }
@@ -57,13 +75,15 @@ node() { // No specific label
mailIfStatusChanged(commitAuthorEmail) mailIfStatusChanged(commitAuthorEmail)
} }
}
String mainBranch String mainBranch
Maven setupMavenBuild() { 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 // Release starts javadoc, which takes very long, so do only for certain branches
mvn.additionalArgs += ' -DperformRelease' mvn.additionalArgs += ' -DperformRelease'
// JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet. // 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 " "-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager "
} else { } else {
mvnArgs += " -Dsonar.branch.name=${env.BRANCH_NAME} " 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 // Avoid exception "The main branch must not have a target" on main branch
mvnArgs += " -Dsonar.branch.target=${mainBranch} " mvnArgs += " -Dsonar.branch.target=${mainBranch} "
} }
@@ -99,6 +119,10 @@ void analyzeWith(Maven mvn) {
} }
} }
boolean isMainBranch() {
return mainBranch.equals(env.BRANCH_NAME)
}
boolean waitForQualityGateWebhookToBeCalled() { boolean waitForQualityGateWebhookToBeCalled() {
boolean isQualityGateSucceeded = true boolean isQualityGateSucceeded = true
timeout(time: 2, unit: 'MINUTES') { // Needed when there is no webhook for example 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}"' 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() { String getCommitAuthorEmail() {
def matcher = getCommitAuthorComplete() =~ "<(.*?)>" def matcher = getCommitAuthorComplete() =~ "<(.*?)>"
matcher ? matcher[0][1] : "" matcher ? matcher[0][1] : ""

View File

@@ -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

View File

@@ -0,0 +1,5 @@
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for SCM-Manager
name: scm-manager
version: 0.1.0

View File

@@ -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 }}

View File

@@ -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 -}}

View File

@@ -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: |
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure id="ScmServer" class="org.eclipse.jetty.server.Server">
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
<!-- increase header size for mercurial -->
<Set name="requestHeaderSize">16384</Set>
<Set name="responseHeaderSize">16384</Set>
{{- if .Values.ingress.enabled -}}
<!--
We have to enable ForwardedRequestCustomizer in order to understand X-Forwarded-xxx headers.
Without the ForwardedRequestCustomizer, scm will possibly generate wrong links
-->
<Call name="addCustomizer">
<Arg><New class="org.eclipse.jetty.server.ForwardedRequestCustomizer"/></Arg>
</Call>
{{- end }}
</New>
<!--
Connectors
-->
<Call name="addConnector">
<Arg>
<New class="org.eclipse.jetty.server.ServerConnector">
<Arg name="server">
<Ref refid="ScmServer" />
</Arg>
<Arg name="factories">
<Array type="org.eclipse.jetty.server.ConnectionFactory">
<Item>
<New class="org.eclipse.jetty.server.HttpConnectionFactory">
<Arg name="config">
<Ref refid="httpConfig" />
</Arg>
</New>
</Item>
</Array>
</Arg>
<Set name="port">
<SystemProperty name="jetty.port" default="8080" />
</Set>
</New>
</Arg>
</Call>
<New id="scm-webapp" class="org.eclipse.jetty.webapp.WebAppContext">
<Set name="contextPath">/scm</Set>
<Set name="war">
<SystemProperty name="basedir" default="."/>/var/webapp/scm-webapp.war</Set>
<!-- disable directory listings -->
<Call name="setInitParameter">
<Arg>org.eclipse.jetty.servlet.Default.dirAllowed</Arg>
<Arg>false</Arg>
</Call>
<Set name="tempDirectory">
<SystemProperty name="basedir" default="."/>/work/scm
</Set>
</New>
<New id="docroot" class="org.eclipse.jetty.webapp.WebAppContext">
<Set name="contextPath">/</Set>
<Set name="baseResource">
<New class="org.eclipse.jetty.util.resource.ResourceCollection">
<Arg>
<Array type="java.lang.String">
<Item>
<SystemProperty name="basedir" default="."/>/var/webapp/docroot</Item>
</Array>
</Arg>
</New>
</Set>
<Set name="tempDirectory">
<SystemProperty name="basedir" default="."/>/work/docroot
</Set>
</New>
<Set name="handler">
<New class="org.eclipse.jetty.server.handler.HandlerCollection">
<Set name="handlers">
<Array type="org.eclipse.jetty.server.Handler">
<Item>
<Ref id="scm-webapp" />
</Item>
<Item>
<Ref id="docroot" />
</Item>
</Array>
</Set>
</New>
</Set>
</Configure>
logging.xml: |
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<--
in a container environment we only need stdout
-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<logger name="sonia.scm" level="INFO" />
<!-- suppress massive gzip logging -->
<logger name="sonia.scm.filter.GZipFilter" level="WARN" />
<logger name="sonia.scm.filter.GZipResponseStream" level="WARN" />
<logger name="sonia.scm.util.ServiceUtil" level="WARN" />
<!-- event bus -->
<logger name="sonia.scm.event.LegmanScmEventBus" level="INFO" />
<!-- shiro -->
<!--
<logger name="org.apache.shiro" level="INFO" />
<logger name="org.apache.shiro.authc.pam.ModularRealmAuthenticator" level="DEBUG" />
-->
<!-- svnkit -->
<!--
<logger name="svnkit" level="WARN" />
<logger name="svnkit.network" level="DEBUG" />
<logger name="svnkit.fsfs" level="WARN" />
-->
<!-- javahg -->
<!--
<logger name="com.aragost.javahg" level="DEBUG" />
-->
<!-- ehcache -->
<!--
<logger name="net.sf.ehcache" level="DEBUG" />
-->
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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 -}}

View File

@@ -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 }}

View File

@@ -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: <storageClass>
## 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: {}

11
pom.xml
View File

@@ -188,15 +188,14 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
<dependency> <dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>ssp-lib</artifactId> <artifactId>ssp-lib</artifactId>
<version>${ssp.version}</version> <version>${ssp.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>ssp-processor</artifactId> <artifactId>ssp-processor</artifactId>
<version>${ssp.version}</version> <version>${ssp.version}</version>
<optional>true</optional> <optional>true</optional>
@@ -430,7 +429,9 @@
<plugin> <plugin>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId> <artifactId>animal-sniffer-maven-plugin</artifactId>
<version>1.17</version> <!-- Note: 1.17 seems to have problems with JDK8. When updating, use > 1.17, if available!
https://github.com/mojohaus/animal-sniffer/issues/53 -->
<version>1.16</version>
<configuration> <configuration>
<signature> <signature>
<groupId>org.codehaus.mojo.signature</groupId> <groupId>org.codehaus.mojo.signature</groupId>
@@ -763,7 +764,7 @@
<jetty.maven.version>9.2.10.v20150310</jetty.maven.version> <jetty.maven.version>9.2.10.v20150310</jetty.maven.version>
<!-- security libraries --> <!-- security libraries -->
<ssp.version>967c8fd521</ssp.version> <ssp.version>1.1.0</ssp.version>
<shiro.version>1.4.0</shiro.version> <shiro.version>1.4.0</shiro.version>
<!-- repostitory libraries --> <!-- repostitory libraries -->

View File

@@ -33,6 +33,13 @@
<version>2.0.0-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- logging --> <!-- logging -->
<dependency> <dependency>
@@ -87,6 +94,12 @@
<artifactId>javax.ws.rs-api</artifactId> <artifactId>javax.ws.rs-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>test</scope>
</dependency>
<!-- json --> <!-- json -->
<dependency> <dependency>
@@ -153,14 +166,13 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
<dependency> <dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>ssp-lib</artifactId> <artifactId>ssp-lib</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>ssp-processor</artifactId> <artifactId>ssp-processor</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>

View File

@@ -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);
}
}

View File

@@ -7,9 +7,4 @@ public class NotFoundException extends Exception {
public NotFoundException() { public NotFoundException() {
} }
public NotFoundException(String message) {
super(message);
}
} }

View File

@@ -3,14 +3,8 @@ package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import java.time.Instant; public abstract class BaseMapper<T, D extends HalRepresentation> implements InstantAttributeMapper {
public abstract class BaseMapper<T, D extends HalRepresentation> {
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract D map(T modelObject); public abstract D map(T modelObject);
protected Instant mapTime(Long epochMilli) {
return epochMilli == null? null: Instant.ofEpochMilli(epochMilli);
}
} }

View File

@@ -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);
}
}

View File

@@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI; import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
@@ -14,7 +13,7 @@ import java.util.Arrays;
* builder for each method. * builder for each method.
* *
* <pre> * <pre>
* LinkBuilder builder = new LinkBuilder(uriInfo, MainResource.class, SubResource.class); * LinkBuilder builder = new LinkBuilder(pathInfo, MainResource.class, SubResource.class);
* Link link = builder * Link link = builder
* .method("sub") * .method("sub")
* .parameters("param") * .parameters("param")
@@ -25,16 +24,16 @@ import java.util.Arrays;
*/ */
@SuppressWarnings("WeakerAccess") // Non-public will result in IllegalAccessError for plugins @SuppressWarnings("WeakerAccess") // Non-public will result in IllegalAccessError for plugins
public class LinkBuilder { public class LinkBuilder {
private final UriInfo uriInfo; private final ScmPathInfo pathInfo;
private final Class[] classes; private final Class[] classes;
private final ImmutableList<Call> calls; private final ImmutableList<Call> calls;
public LinkBuilder(UriInfo uriInfo, Class... classes) { public LinkBuilder(ScmPathInfo pathInfo, Class... classes) {
this(uriInfo, classes, ImmutableList.of()); this(pathInfo, classes, ImmutableList.of());
} }
private LinkBuilder(UriInfo uriInfo, Class[] classes, ImmutableList<Call> calls) { private LinkBuilder(ScmPathInfo pathInfo, Class[] classes, ImmutableList<Call> calls) {
this.uriInfo = uriInfo; this.pathInfo = pathInfo;
this.classes = classes; this.classes = classes;
this.calls = calls; this.calls = calls;
} }
@@ -51,7 +50,7 @@ public class LinkBuilder {
throw new IllegalStateException("not enough methods for all classes"); throw new IllegalStateException("not enough methods for all classes");
} }
URI baseUri = uriInfo.getBaseUri(); URI baseUri = pathInfo.getApiRestUri();
URI relativeUri = createRelativeUri(); URI relativeUri = createRelativeUri();
return baseUri.resolve(relativeUri); return baseUri.resolve(relativeUri);
} }
@@ -61,7 +60,7 @@ public class LinkBuilder {
} }
private LinkBuilder add(String method, String[] parameters) { 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<Call> appendNewCall(String method, String[] parameters) { private ImmutableList<Call> appendNewCall(String method, String[] parameters) {

View File

@@ -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("../..");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions;
@StaticPermissions( @StaticPermissions(
value = "configuration", value = "configuration",
permissions = {"read", "write"}, permissions = {"read", "write"},
globalPermissions = {} globalPermissions = {"list"}
) )
public interface Configuration extends PermissionObject { public interface Configuration extends PermissionObject {
} }

View File

@@ -31,6 +31,8 @@
package sonia.scm.filter; package sonia.scm.filter;
import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
/** /**
* Useful constants for filter implementations. * Useful constants for filter implementations.
* *
@@ -44,26 +46,26 @@ public final class Filters
public static final String PATTERN_ALL = "/*"; public static final String PATTERN_ALL = "/*";
/** Field description */ /** Field description */
public static final String PATTERN_CONFIG = "/api/rest/config*"; public static final String PATTERN_CONFIG = REST_API_PATH + "/config*";
/** Field description */ /** Field description */
public static final String PATTERN_DEBUG = "/debug.html"; public static final String PATTERN_DEBUG = "/debug.html";
/** Field description */ /** Field description */
public static final String PATTERN_GROUPS = "/api/rest/groups*"; public static final String PATTERN_GROUPS = REST_API_PATH + "/groups*";
/** Field description */ /** Field description */
public static final String PATTERN_PLUGINS = "/api/rest/plugins*"; public static final String PATTERN_PLUGINS = REST_API_PATH + "/plugins*";
/** Field description */ /** Field description */
public static final String PATTERN_RESOURCE_REGEX = public static final String PATTERN_RESOURCE_REGEX =
"^/(?:resources|api|plugins|index)[\\./].*(?:html|\\.css|\\.js|\\.xml|\\.json|\\.txt)"; "^/(?:resources|api|plugins|index)[\\./].*(?:html|\\.css|\\.js|\\.xml|\\.json|\\.txt)";
/** Field description */ /** Field description */
public static final String PATTERN_RESTAPI = "/api/rest/*"; public static final String PATTERN_RESTAPI = REST_API_PATH + "/*";
/** Field description */ /** Field description */
public static final String PATTERN_USERS = "/api/rest/users*"; public static final String PATTERN_USERS = REST_API_PATH + "/users*";
/** authentication priority */ /** authentication priority */
public static final int PRIORITY_AUTHENTICATION = 5000; public static final int PRIORITY_AUTHENTICATION = 5000;

View File

@@ -60,7 +60,7 @@ import java.util.List;
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@StaticPermissions("group") @StaticPermissions(value = "group", globalPermissions = {"create", "list"})
@XmlRootElement(name = "groups") @XmlRootElement(name = "groups")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class Group extends BasicPropertiesAware public class Group extends BasicPropertiesAware

View File

@@ -33,9 +33,8 @@ package sonia.scm.plugin;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
import java.net.URL;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import java.net.URL;
/** /**
* The WebResourceLoader is able to load web resources. The resources are loaded * 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 * Returns a {@link URL} for the given path. The method will return null if no
* resources could be found for the given path. * resources could be found for the given path.
* *
* Note: The path is a web path and uses "/" as path separator
*
* @param path resource path * @param path resource path
* *
* @return url object for the given path or null * @return url object for the given path or null
*/ */
public URL getResource(String path); URL getResource(String path);
} }

View File

@@ -41,7 +41,6 @@ import sonia.scm.util.ValidationUtil;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@@ -84,12 +83,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
*/ */
private String id; private String id;
/**
* List of files changed by this changeset
*/
@XmlElement(name = "modifications")
private Modifications modifications;
/** /**
* parent changeset ids * parent changeset ids
*/ */
@@ -137,7 +130,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
&& Objects.equal(parents, other.parents) && Objects.equal(parents, other.parents)
&& Objects.equal(tags, other.tags) && Objects.equal(tags, other.tags)
&& Objects.equal(branches, other.branches) && Objects.equal(branches, other.branches)
&& Objects.equal(modifications, other.modifications)
&& Objects.equal(properties, other.properties); && Objects.equal(properties, other.properties);
//J+ //J+
} }
@@ -152,7 +144,7 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
public int hashCode() public int hashCode()
{ {
return Objects.hashCode(id, date, author, description, parents, tags, 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("branches: ").append(Util.toString(branches)).append("\n");
out.append("tags: ").append(Util.toString(tags)).append("\n"); out.append("tags: ").append(Util.toString(tags)).append("\n");
if (modifications != null)
{
out.append("modifications: \n").append(modifications);
}
return out.toString(); 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. * Return the ids of the parent changesets.
@@ -402,17 +374,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
this.id = id; 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. * Sets the parents of the changeset.
* *

View File

@@ -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<String> escapeList(List<String> values)
{
if (Util.isNotEmpty(values))
{
List<String> newList = Lists.newArrayList();
for (String v : values)
{
newList.add(StringEscapeUtils.escapeHtml(v));
}
values = newList;
}
return values;
}
}

View File

@@ -67,7 +67,8 @@ public class Modifications implements Serializable
* Constructs ... * Constructs ...
* *
*/ */
public Modifications() {} public Modifications() {
}
/** /**
* Constructs ... * Constructs ...
@@ -218,6 +219,10 @@ public class Modifications implements Serializable
return removed; return removed;
} }
public String getRevision() {
return revision;
}
//~--- set methods ---------------------------------------------------------- //~--- set methods ----------------------------------------------------------
/** /**
@@ -253,8 +258,14 @@ public class Modifications implements Serializable
this.removed = removed; this.removed = removed;
} }
public void setRevision(String revision) {
this.revision = revision;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
private String revision;
/** list of added files */ /** list of added files */
@XmlElement(name = "added") @XmlElement(name = "added")
@XmlElementWrapper(name = "added") @XmlElementWrapper(name = "added")

View File

@@ -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<Modifications> {
/**
* Process the given modifications.
*
* @param modifications modifications to process
*/
@Override
void process(Modifications modifications);
}

View File

@@ -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<Modifications> {
/**
* Create a new {@link ModificationsPreProcessor} for the given repository.
*
*
* @param repository repository
*
* @return {@link ModificationsPreProcessor} for the given repository
*/
@Override
ModificationsPreProcessor createPreProcessor(Repository repository);
}

View File

@@ -36,17 +36,15 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject; import com.google.inject.Inject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.util.Util; import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -73,6 +71,8 @@ public class PreProcessorUtil
* @param fileObjectPreProcessorFactorySet * @param fileObjectPreProcessorFactorySet
* @param blameLinePreProcessorSet * @param blameLinePreProcessorSet
* @param blameLinePreProcessorFactorySet * @param blameLinePreProcessorFactorySet
* @param modificationsPreProcessorFactorySet
* @param modificationsPreProcessorSet
*/ */
@Inject @Inject
public PreProcessorUtil(Set<ChangesetPreProcessor> changesetPreProcessorSet, public PreProcessorUtil(Set<ChangesetPreProcessor> changesetPreProcessorSet,
@@ -80,7 +80,9 @@ public class PreProcessorUtil
Set<FileObjectPreProcessor> fileObjectPreProcessorSet, Set<FileObjectPreProcessor> fileObjectPreProcessorSet,
Set<FileObjectPreProcessorFactory> fileObjectPreProcessorFactorySet, Set<FileObjectPreProcessorFactory> fileObjectPreProcessorFactorySet,
Set<BlameLinePreProcessor> blameLinePreProcessorSet, Set<BlameLinePreProcessor> blameLinePreProcessorSet,
Set<BlameLinePreProcessorFactory> blameLinePreProcessorFactorySet) Set<BlameLinePreProcessorFactory> blameLinePreProcessorFactorySet,
Set<ModificationsPreProcessorFactory> modificationsPreProcessorFactorySet,
Set<ModificationsPreProcessor> modificationsPreProcessorSet)
{ {
this.changesetPreProcessorSet = changesetPreProcessorSet; this.changesetPreProcessorSet = changesetPreProcessorSet;
this.changesetPreProcessorFactorySet = changesetPreProcessorFactorySet; this.changesetPreProcessorFactorySet = changesetPreProcessorFactorySet;
@@ -88,6 +90,8 @@ public class PreProcessorUtil
this.fileObjectPreProcessorFactorySet = fileObjectPreProcessorFactorySet; this.fileObjectPreProcessorFactorySet = fileObjectPreProcessorFactorySet;
this.blameLinePreProcessorSet = blameLinePreProcessorSet; this.blameLinePreProcessorSet = blameLinePreProcessorSet;
this.blameLinePreProcessorFactorySet = blameLinePreProcessorFactorySet; this.blameLinePreProcessorFactorySet = blameLinePreProcessorFactorySet;
this.modificationsPreProcessorFactorySet = modificationsPreProcessorFactorySet;
this.modificationsPreProcessorSet = modificationsPreProcessorSet;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -107,14 +111,7 @@ public class PreProcessorUtil
blameLine.getLineNumber(), repository.getName()); blameLine.getLineNumber(), repository.getName());
} }
EscapeUtil.escape(blameLine); handlePreProcess(repository,blameLine,blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
PreProcessorHandler<BlameLine> handler =
new PreProcessorHandler<BlameLine>(blameLinePreProcessorFactorySet,
blameLinePreProcessorSet, repository);
handler.callPreProcessors(blameLine);
handler.callPreProcessorFactories(blameLine);
} }
/** /**
@@ -125,22 +122,6 @@ public class PreProcessorUtil
* @param blameResult * @param blameResult
*/ */
public void prepareForReturn(Repository repository, BlameResult 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()) if (logger.isTraceEnabled())
{ {
@@ -148,17 +129,7 @@ public class PreProcessorUtil
repository.getName()); repository.getName());
} }
if (escape) handlePreProcessForIterable(repository, blameResult.getBlameLines(),blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
{
EscapeUtil.escape(blameResult);
}
PreProcessorHandler<BlameLine> handler =
new PreProcessorHandler<BlameLine>(blameLinePreProcessorFactorySet,
blameLinePreProcessorSet, repository);
handler.callPreProcessors(blameResult.getBlameLines());
handler.callPreProcessorFactories(blameResult.getBlameLines());
} }
/** /**
@@ -170,39 +141,13 @@ public class PreProcessorUtil
*/ */
public void prepareForReturn(Repository repository, Changeset changeset) 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);
} }
/** public void prepareForReturn(Repository repository, Modifications modifications) {
* Method description logger.trace("prepare modifications {} of repository {} for return", modifications, repository.getName());
* handlePreProcess(repository, modifications, modificationsPreProcessorFactorySet, modificationsPreProcessorSet);
*
* @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<Changeset> handler =
new PreProcessorHandler<Changeset>(changesetPreProcessorFactorySet,
changesetPreProcessorSet, repository);
handler.callPreProcessors(changeset);
handler.callPreProcessorFactories(changeset);
} }
/** /**
@@ -213,22 +158,6 @@ public class PreProcessorUtil
* @param result * @param result
*/ */
public void prepareForReturn(Repository repository, BrowserResult 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()) if (logger.isTraceEnabled())
{ {
@@ -236,17 +165,7 @@ public class PreProcessorUtil
repository.getName()); repository.getName());
} }
if (escape) handlePreProcessForIterable(repository, result,fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet);
{
EscapeUtil.escape(result);
}
PreProcessorHandler<FileObject> handler =
new PreProcessorHandler<FileObject>(fileObjectPreProcessorFactorySet,
fileObjectPreProcessorSet, repository);
handler.callPreProcessors(result);
handler.callPreProcessorFactories(result);
} }
/** /**
@@ -255,12 +174,8 @@ public class PreProcessorUtil
* *
* @param repository * @param repository
* @param result * @param result
* @param escape
*
* @since 1.35
*/ */
public void prepareForReturn(Repository repository, public void prepareForReturn(Repository repository, ChangesetPagingResult result)
ChangesetPagingResult result, boolean escape)
{ {
if (logger.isTraceEnabled()) if (logger.isTraceEnabled())
{ {
@@ -268,30 +183,23 @@ public class PreProcessorUtil
repository.getName()); repository.getName());
} }
if (escape) handlePreProcessForIterable(repository,result,changesetPreProcessorFactorySet, changesetPreProcessorSet);
{
EscapeUtil.escape(result);
} }
PreProcessorHandler<Changeset> handler = private <T, F extends PreProcessorFactory<T>, P extends PreProcessor<T>> void handlePreProcess(Repository repository, T processedObject,
new PreProcessorHandler<Changeset>(changesetPreProcessorFactorySet, Collection<F> factories,
changesetPreProcessorSet, repository); Collection<P> preProcessors) {
PreProcessorHandler<T> handler = new PreProcessorHandler<T>(factories, preProcessors, repository);
handler.callPreProcessors(result); handler.callPreProcessors(processedObject);
handler.callPreProcessorFactories(result); handler.callPreProcessorFactories(processedObject);
} }
/** private <T, I extends Iterable<T>, F extends PreProcessorFactory<T>, P extends PreProcessor<T>> void handlePreProcessForIterable(Repository repository, I processedObjects,
* Method description Collection<F> factories,
* Collection<P> preProcessors) {
* PreProcessorHandler<T> handler = new PreProcessorHandler<T>(factories, preProcessors, repository);
* @param repository handler.callPreProcessors(processedObjects);
* @param result handler.callPreProcessorFactories(processedObjects);
*/
public void prepareForReturn(Repository repository,
ChangesetPagingResult result)
{
prepareForReturn(repository, result, true);
} }
//~--- inner classes -------------------------------------------------------- //~--- inner classes --------------------------------------------------------
@@ -454,6 +362,10 @@ public class PreProcessorUtil
/** Field description */ /** Field description */
private final Collection<ChangesetPreProcessor> changesetPreProcessorSet; private final Collection<ChangesetPreProcessor> changesetPreProcessorSet;
private final Collection<ModificationsPreProcessorFactory> modificationsPreProcessorFactorySet;
private final Collection<ModificationsPreProcessor> modificationsPreProcessorSet;
/** Field description */ /** Field description */
private final Collection<FileObjectPreProcessorFactory> fileObjectPreProcessorFactorySet; private final Collection<FileObjectPreProcessorFactory> fileObjectPreProcessorFactorySet;

View File

@@ -40,7 +40,6 @@ import com.google.common.base.Objects;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import sonia.scm.BasicPropertiesAware; import sonia.scm.BasicPropertiesAware;
import sonia.scm.ModelObject; import sonia.scm.ModelObject;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import sonia.scm.util.ValidationUtil; import sonia.scm.util.ValidationUtil;
@@ -349,17 +348,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
// do not copy health check results // 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. * Returns true if the {@link Repository} is the same as the obj argument.
* *

View File

@@ -38,7 +38,6 @@ package sonia.scm.repository;
import sonia.scm.AlreadyExistsException; import sonia.scm.AlreadyExistsException;
import sonia.scm.TypeManager; import sonia.scm.TypeManager;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
@@ -99,29 +98,6 @@ public interface RepositoryManager
*/ */
public Collection<RepositoryType> getConfiguredTypes(); public Collection<RepositoryType> 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 ...). * Returns a {@link RepositoryHandler} by the given type (hg, git, svn ...).
* *

View File

@@ -39,7 +39,6 @@ import sonia.scm.AlreadyExistsException;
import sonia.scm.ManagerDecorator; import sonia.scm.ManagerDecorator;
import sonia.scm.Type; import sonia.scm.Type;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
@@ -120,34 +119,6 @@ public class RepositoryManagerDecorator
return decorated; 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} * {@inheritDoc}
* *

View File

@@ -44,8 +44,8 @@ import sonia.scm.NotFoundException;
public class RepositoryNotFoundException extends NotFoundException public class RepositoryNotFoundException extends NotFoundException
{ {
/** Field description */
private static final long serialVersionUID = -6583078808900520166L; private static final long serialVersionUID = -6583078808900520166L;
private static final String TYPE_REPOSITORY = "repository";
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
@@ -55,10 +55,14 @@ public class RepositoryNotFoundException extends NotFoundException
* *
*/ */
public RepositoryNotFoundException(Repository repository) { public RepositoryNotFoundException(Repository repository) {
super("repository", repository.getName() + "/" + repository.getNamespace()); super(TYPE_REPOSITORY, repository.getName() + "/" + repository.getNamespace());
} }
public RepositoryNotFoundException(String repositoryId) { public RepositoryNotFoundException(String repositoryId) {
super("repository", repositoryId); super(TYPE_REPOSITORY, repositoryId);
}
public RepositoryNotFoundException(NamespaceAndName namespaceAndName) {
super(TYPE_REPOSITORY, namespaceAndName.toString());
} }
} }

View File

@@ -26,35 +26,21 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* *
* http://bitbucket.org/sdorra/scm-manager * http://bitbucket.org/sdorra/scm-manager
*
*/ */
package sonia.scm.repository; package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.inject.throwingproviders.CheckedProvider; import com.google.inject.throwingproviders.CheckedProvider;
import sonia.scm.security.ScmSecurityException;
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 1.10 * @since 1.10
*/ */
public interface RepositoryProvider extends CheckedProvider<Repository> public interface RepositoryProvider extends CheckedProvider<Repository> {
{
/**
* Method description
*
*
* @return
*
* @throws ScmSecurityException
*/
@Override @Override
public Repository get() throws ScmSecurityException; Repository get();
} }

View File

@@ -185,7 +185,7 @@ public final class BlameCommandBuilder
if (!disablePreProcessors && (result != null)) if (!disablePreProcessors && (result != null))
{ {
preProcessorUtil.prepareForReturn(repository, result, !disableEscaping); preProcessorUtil.prepareForReturn(repository, result);
} }
return result; return result;
@@ -210,24 +210,6 @@ public final class BlameCommandBuilder
return this; 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. * Disable the execution of pre processors.
* *
@@ -362,9 +344,6 @@ public final class BlameCommandBuilder
/** the cache */ /** the cache */
private final Cache<CacheKey, BlameResult> cache; private final Cache<CacheKey, BlameResult> cache;
/** disable escaping */
private boolean disableEscaping = false;
/** disable change */ /** disable change */
private boolean disableCache = false; private boolean disableCache = false;

View File

@@ -179,7 +179,7 @@ public final class BrowseCommandBuilder
if (!disablePreProcessors && (result != null)) if (!disablePreProcessors && (result != null))
{ {
preProcessorUtil.prepareForReturn(repository, result, !disableEscaping); preProcessorUtil.prepareForReturn(repository, result);
List<FileObject> fileObjects = result.getFiles(); List<FileObject> fileObjects = result.getFiles();
@@ -212,24 +212,6 @@ public final class BrowseCommandBuilder
return this; 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 * Disabling the last commit means that every call to
* {@link FileObject#getDescription()} and * {@link FileObject#getDescription()} and
@@ -433,9 +415,6 @@ public final class BrowseCommandBuilder
/** cache */ /** cache */
private final Cache<CacheKey, BrowserResult> cache; private final Cache<CacheKey, BrowserResult> cache;
/** disable escaping */
private boolean disableEscaping = false;
/** disables the cache */ /** disables the cache */
private boolean disableCache = false; private boolean disableCache = false;

View File

@@ -61,5 +61,11 @@ public enum Command
/** /**
* @since 1.43 * @since 1.43
*/ */
BUNDLE, UNBUNDLE; BUNDLE, UNBUNDLE,
/**
* @since 2.0
*/
MODIFICATIONS
} }

View File

@@ -132,8 +132,7 @@ public final class HookChangesetBuilder
try try
{ {
copy = DeepCopy.copy(c); copy = DeepCopy.copy(c);
preProcessorUtil.prepareForReturn(repository, copy, preProcessorUtil.prepareForReturn(repository, copy);
!disableEscaping);
} }
catch (IOException ex) catch (IOException ex)
{ {
@@ -156,24 +155,6 @@ public final class HookChangesetBuilder
//~--- set methods ---------------------------------------------------------- //~--- 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. * Disable the execution of pre processors.
* *
@@ -192,9 +173,6 @@ public final class HookChangesetBuilder
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** disable escaping */
private boolean disableEscaping = false;
/** disable pre processors marker */ /** disable pre processors marker */
private boolean disablePreProcessors = false; private boolean disablePreProcessors = false;

View File

@@ -264,7 +264,7 @@ public final class LogCommandBuilder
if (!disablePreProcessors && (cpr != null)) if (!disablePreProcessors && (cpr != null))
{ {
preProcessorUtil.prepareForReturn(repository, cpr, !disableEscaping); preProcessorUtil.prepareForReturn(repository, cpr);
} }
return cpr; return cpr;
@@ -306,24 +306,6 @@ public final class LogCommandBuilder
return this; 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. * Disable the execution of pre processors.
* *
@@ -545,9 +527,6 @@ public final class LogCommandBuilder
/** repository to query */ /** repository to query */
private final Repository repository; private final Repository repository;
/** disable escaping */
private boolean disableEscaping = false;
/** disable cache */ /** disable cache */
private boolean disableCache = false; private boolean disableCache = false;

View File

@@ -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.
* <p>
* 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<ModificationsCommandBuilder.CacheKey, Modifications> 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;
}
}
}

View File

@@ -31,6 +31,7 @@
package sonia.scm.repository.api; package sonia.scm.repository.api;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
@@ -42,6 +43,8 @@ import sonia.scm.repository.spi.RepositoryServiceProvider;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; 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 * 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 * @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
* @since 1.17 * @since 1.17
*/ */
@Slf4j
public final class RepositoryService implements Closeable { public final class RepositoryService implements Closeable {
private CacheManager cacheManager;
private PreProcessorUtil preProcessorUtil; private static final Logger logger = LoggerFactory.getLogger(RepositoryService.class);
private RepositoryServiceProvider provider;
private Repository repository; private final CacheManager cacheManager;
private static final Logger logger = private final PreProcessorUtil preProcessorUtil;
LoggerFactory.getLogger(RepositoryService.class); private final RepositoryServiceProvider provider;
private final Repository repository;
private final Set<ScmProtocolProvider> protocolProviders;
/** /**
* Constructs a new {@link RepositoryService}. This constructor should only * Constructs a new {@link RepositoryService}. This constructor should only
* be called from the {@link RepositoryServiceFactory}. * be called from the {@link RepositoryServiceFactory}.
*
* @param cacheManager cache manager * @param cacheManager cache manager
* @param provider implementation for {@link RepositoryServiceProvider} * @param provider implementation for {@link RepositoryServiceProvider}
* @param repository the repository * @param repository the repository
* @param preProcessorUtil
*/ */
RepositoryService(CacheManager cacheManager, RepositoryService(CacheManager cacheManager,
RepositoryServiceProvider provider, Repository repository, RepositoryServiceProvider provider, Repository repository,
PreProcessorUtil preProcessorUtil) { PreProcessorUtil preProcessorUtil, Set<ScmProtocolProvider> protocolProviders) {
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.provider = provider; this.provider = provider;
this.repository = repository; this.repository = repository;
this.preProcessorUtil = preProcessorUtil; this.preProcessorUtil = preProcessorUtil;
this.protocolProviders = protocolProviders;
} }
/** /**
@@ -125,7 +130,7 @@ public final class RepositoryService implements Closeable {
try { try {
provider.close(); provider.close();
} catch (IOException ex) { } 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() { public BlameCommandBuilder getBlameCommand() {
logger.debug("create blame command for repository {}", logger.debug("create blame command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(), return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(),
repository, preProcessorUtil); repository, preProcessorUtil);
@@ -153,7 +158,7 @@ public final class RepositoryService implements Closeable {
*/ */
public BranchesCommandBuilder getBranchesCommand() { public BranchesCommandBuilder getBranchesCommand() {
logger.debug("create branches command for repository {}", logger.debug("create branches command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new BranchesCommandBuilder(cacheManager, return new BranchesCommandBuilder(cacheManager,
provider.getBranchesCommand(), repository); provider.getBranchesCommand(), repository);
@@ -168,7 +173,7 @@ public final class RepositoryService implements Closeable {
*/ */
public BrowseCommandBuilder getBrowseCommand() { public BrowseCommandBuilder getBrowseCommand() {
logger.debug("create browse command for repository {}", logger.debug("create browse command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(), return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(),
repository, preProcessorUtil); repository, preProcessorUtil);
@@ -184,7 +189,7 @@ public final class RepositoryService implements Closeable {
*/ */
public BundleCommandBuilder getBundleCommand() { public BundleCommandBuilder getBundleCommand() {
logger.debug("create bundle command for repository {}", logger.debug("create bundle command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new BundleCommandBuilder(provider.getBundleCommand(), repository); return new BundleCommandBuilder(provider.getBundleCommand(), repository);
} }
@@ -198,7 +203,7 @@ public final class RepositoryService implements Closeable {
*/ */
public CatCommandBuilder getCatCommand() { public CatCommandBuilder getCatCommand() {
logger.debug("create cat command for repository {}", logger.debug("create cat command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new CatCommandBuilder(provider.getCatCommand()); return new CatCommandBuilder(provider.getCatCommand());
} }
@@ -213,7 +218,7 @@ public final class RepositoryService implements Closeable {
*/ */
public DiffCommandBuilder getDiffCommand() { public DiffCommandBuilder getDiffCommand() {
logger.debug("create diff command for repository {}", logger.debug("create diff command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new DiffCommandBuilder(provider.getDiffCommand()); return new DiffCommandBuilder(provider.getDiffCommand());
} }
@@ -229,7 +234,7 @@ public final class RepositoryService implements Closeable {
*/ */
public IncomingCommandBuilder getIncomingCommand() { public IncomingCommandBuilder getIncomingCommand() {
logger.debug("create incoming command for repository {}", logger.debug("create incoming command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new IncomingCommandBuilder(cacheManager, return new IncomingCommandBuilder(cacheManager,
provider.getIncomingCommand(), repository, preProcessorUtil); provider.getIncomingCommand(), repository, preProcessorUtil);
@@ -244,12 +249,24 @@ public final class RepositoryService implements Closeable {
*/ */
public LogCommandBuilder getLogCommand() { public LogCommandBuilder getLogCommand() {
logger.debug("create log command for repository {}", logger.debug("create log command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new LogCommandBuilder(cacheManager, provider.getLogCommand(), return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
repository, preProcessorUtil); 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. * 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() { public OutgoingCommandBuilder getOutgoingCommand() {
logger.debug("create outgoing command for repository {}", logger.debug("create outgoing command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new OutgoingCommandBuilder(cacheManager, return new OutgoingCommandBuilder(cacheManager,
provider.getOutgoingCommand(), repository, preProcessorUtil); provider.getOutgoingCommand(), repository, preProcessorUtil);
@@ -276,7 +293,7 @@ public final class RepositoryService implements Closeable {
*/ */
public PullCommandBuilder getPullCommand() { public PullCommandBuilder getPullCommand() {
logger.debug("create pull command for repository {}", logger.debug("create pull command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new PullCommandBuilder(provider.getPullCommand(), repository); return new PullCommandBuilder(provider.getPullCommand(), repository);
} }
@@ -291,7 +308,7 @@ public final class RepositoryService implements Closeable {
*/ */
public PushCommandBuilder getPushCommand() { public PushCommandBuilder getPushCommand() {
logger.debug("create push command for repository {}", logger.debug("create push command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new PushCommandBuilder(provider.getPushCommand()); return new PushCommandBuilder(provider.getPushCommand());
} }
@@ -314,7 +331,7 @@ public final class RepositoryService implements Closeable {
*/ */
public TagsCommandBuilder getTagsCommand() { public TagsCommandBuilder getTagsCommand() {
logger.debug("create tags command for repository {}", logger.debug("create tags command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(), return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(),
repository); repository);
@@ -330,7 +347,7 @@ public final class RepositoryService implements Closeable {
*/ */
public UnbundleCommandBuilder getUnbundleCommand() { public UnbundleCommandBuilder getUnbundleCommand() {
logger.debug("create unbundle command for repository {}", logger.debug("create unbundle command for repository {}",
repository.getName()); repository.getNamespaceAndName());
return new UnbundleCommandBuilder(provider.getUnbundleCommand(), return new UnbundleCommandBuilder(provider.getUnbundleCommand(),
repository); repository);
@@ -357,5 +374,20 @@ public final class RepositoryService implements Closeable {
return provider.getSupportedFeatures().contains(feature); return provider.getSupportedFeatures().contains(feature);
} }
public <T extends ScmProtocol> Stream<T> getSupportedProtocols() {
return protocolProviders.stream()
.filter(protocolProvider -> protocolProvider.getType().equals(getRepository().getType()))
.map(this::<T>createProviderInstanceForRepository);
}
private <T extends ScmProtocol> T createProviderInstanceForRepository(ScmProtocolProvider<T> protocolProvider) {
return protocolProvider.get(repository);
}
public <T extends ScmProtocol> T getProtocol(Class<T> clazz) {
return this.<T>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())));
}
} }

View File

@@ -137,13 +137,15 @@ public final class RepositoryServiceFactory
@Inject @Inject
public RepositoryServiceFactory(ScmConfiguration configuration, public RepositoryServiceFactory(ScmConfiguration configuration,
CacheManager cacheManager, RepositoryManager repositoryManager, CacheManager cacheManager, RepositoryManager repositoryManager,
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil) Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
Set<ScmProtocolProvider> protocolProviders)
{ {
this.configuration = configuration; this.configuration = configuration;
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.repositoryManager = repositoryManager; this.repositoryManager = repositoryManager;
this.resolvers = resolvers; this.resolvers = resolvers;
this.preProcessorUtil = preProcessorUtil; this.preProcessorUtil = preProcessorUtil;
this.protocolProviders = protocolProviders;
ScmEventBus.getInstance().register(new CacheClearHook(cacheManager)); ScmEventBus.getInstance().register(new CacheClearHook(cacheManager));
} }
@@ -208,9 +210,7 @@ public final class RepositoryServiceFactory
if (repository == null) if (repository == null)
{ {
String msg = "could not find a repository with namespace/name " + namespaceAndName; throw new RepositoryNotFoundException(namespaceAndName);
throw new RepositoryNotFoundException(msg);
} }
return create(repository); return create(repository);
@@ -254,7 +254,7 @@ public final class RepositoryServiceFactory
} }
service = new RepositoryService(cacheManager, provider, repository, service = new RepositoryService(cacheManager, provider, repository,
preProcessorUtil); preProcessorUtil, protocolProviders);
break; break;
} }
@@ -369,4 +369,6 @@ public final class RepositoryServiceFactory
/** service resolvers */ /** service resolvers */
private final Set<RepositoryServiceResolver> resolvers; private final Set<RepositoryServiceResolver> resolvers;
private Set<ScmProtocolProvider> protocolProviders;
} }

View File

@@ -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();
}

View File

@@ -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<T extends ScmProtocol> {
String getType();
T get(Repository repository);
}

View File

@@ -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;
}

View File

@@ -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<HttpScmProtocol> {
private final Provider<? extends ScmProviderHttpServlet> delegateProvider;
private final Provider<ScmPathInfoStore> pathInfoStore;
private final ScmConfiguration scmConfiguration;
private volatile boolean isInitialized = false;
protected InitializingHttpScmProtocolWrapper(Provider<? extends ScmProviderHttpServlet> delegateProvider, Provider<ScmPathInfoStore> 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<String> 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);
}
}
}

View File

@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2010, Sebastian Sdorra * Copyright (c) 2010, Sebastian Sdorra
* All rights reserved. * 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 java.io.IOException;
import static org.junit.Assert.assertEquals;
/** /**
* Command to get the modifications applied to files in a revision.
* *
* @author Sebastian Sdorra * Modifications are for example: Add, Update, Delete
*/
public class RepositoryTest
{
/**
* Method description
* *
* @author Mohamed Karray
* @since 2.0
*/ */
@Test public interface ModificationsCommand {
public void testCreateUrl()
{ Modifications getModifications(String revision) throws IOException, RevisionNotFoundException;
Repository repository = new Repository("123", "hg", "test", "repo");
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/"));
}
} }

View File

@@ -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;
}
}

View File

@@ -33,20 +33,17 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.Feature; import sonia.scm.repository.Feature;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.CommandNotSupportedException; import sonia.scm.repository.api.CommandNotSupportedException;
//~--- JDK imports ------------------------------------------------------------
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -173,6 +170,16 @@ public abstract class RepositoryServiceProvider implements Closeable
throw new CommandNotSupportedException(Command.LOG); 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 * Method description
* *

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,15 @@
package sonia.scm.repository.spi;
import sonia.scm.DecoratorFactory;
import sonia.scm.plugin.ExtensionPoint;
@ExtensionPoint
public interface ScmProviderHttpServletDecoratorFactory extends DecoratorFactory<ScmProviderHttpServlet> {
/**
* Has to return <code>true</code> 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 <code>true</code> when the provided decorator should be used for the given scm type.
*/
boolean handlesScmType(String type);
}

View File

@@ -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<ScmProviderHttpServlet> {
@Inject(optional = true)
private Set<ScmProviderHttpServletDecoratorFactory> decoratorFactories;
private final String type;
protected ScmProviderHttpServletProvider(String type) {
this.type = type;
}
@Override
public ScmProviderHttpServlet get() {
return Decorators.decorate(getRootServlet(), getDecoratorsForType());
}
private List<ScmProviderHttpServletDecoratorFactory> getDecoratorsForType() {
return decoratorFactories.stream().filter(d -> d.handlesScmType(type)).collect(toList());
}
protected abstract ScmProviderHttpServlet getRootServlet();
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -55,11 +55,10 @@ import java.security.Principal;
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@StaticPermissions("user") @StaticPermissions(value = "user", globalPermissions = {"create", "list"})
@XmlRootElement(name = "users") @XmlRootElement(name = "users")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
{ {
/** Field description */ /** Field description */
@@ -274,6 +273,10 @@ User extends BasicPropertiesAware implements Principal, ModelObject, PermissionO
//J+ //J+
} }
public User changePassword(String password){
setPassword(password);
return this;
}
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
/** /**

View File

@@ -38,6 +38,11 @@ package sonia.scm.user;
import sonia.scm.Manager; import sonia.scm.Manager;
import sonia.scm.search.Searchable; 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. * The central class for managing {@link User} objects.
* This class is a singleton and is available via injection. * This class is a singleton and is available via injection.
@@ -68,4 +73,22 @@ public interface UserManager
* @since 1.14 * @since 1.14
*/ */
public String getDefaultType(); public String getDefaultType();
/**
* Only account of the default type "xml" can change their password
*/
default Consumer<User> 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());
}
} }

View File

@@ -37,7 +37,6 @@ package sonia.scm.util;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.DecoratorFactory; import sonia.scm.DecoratorFactory;
/** /**

View File

@@ -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<String, Object> 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);
}
}

View File

@@ -15,12 +15,14 @@ public class VndMediaType {
public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX; public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX;
public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION; 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 USER = PREFIX + "user" + SUFFIX;
public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + 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 = PREFIX + "tag" + SUFFIX;
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX; public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
public static final String BRANCH = PREFIX + "branch" + 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 REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX; public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + 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 ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX;

View File

@@ -33,39 +33,32 @@
package sonia.scm.web.filter; package sonia.scm.web.filter;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Splitter;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.ArgumentIsInvalidException;
import sonia.scm.SCMContext; import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions; 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.Role;
import sonia.scm.security.ScmSecurityException; import sonia.scm.security.ScmSecurityException;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator;
//~--- JDK imports ------------------------------------------------------------
/** /**
* Abstract http filter to check repository permissions. * Abstract http filter to check repository permissions.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public abstract class PermissionFilter extends HttpFilter public abstract class PermissionFilter extends ScmProviderHttpServletDecorator
{ {
/** the logger for PermissionFilter */ /** the logger for PermissionFilter */
@@ -81,23 +74,14 @@ public abstract class PermissionFilter extends HttpFilter
* *
* @since 1.21 * @since 1.21
*/ */
public PermissionFilter(ScmConfiguration configuration) protected PermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate)
{ {
super(delegate);
this.configuration = configuration; this.configuration = configuration;
} }
//~--- get methods ---------------------------------------------------------- //~--- 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. * Returns true if the current request is a write request.
* *
@@ -117,23 +101,18 @@ public abstract class PermissionFilter extends HttpFilter
* *
* @param request http request * @param request http request
* @param response http response * @param response http response
* @param chain filter chain
* *
* @throws IOException * @throws IOException
* @throws ServletException * @throws ServletException
*/ */
@Override @Override
protected void doFilter(HttpServletRequest request, public void service(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) HttpServletResponse response, Repository repository)
throws IOException, ServletException throws IOException, ServletException
{ {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
try try
{
Repository repository = getRepository(request);
if (repository != null)
{ {
boolean writeRequest = isWriteRequest(request); boolean writeRequest = isWriteRequest(request);
@@ -143,7 +122,7 @@ public abstract class PermissionFilter extends HttpFilter
getActionAsString(writeRequest), repository.getName(), getActionAsString(writeRequest), repository.getName(),
getUserName(subject)); getUserName(subject));
chain.doFilter(request, response); super.service(request, response, repository);
} }
else else
{ {
@@ -154,29 +133,6 @@ public abstract class PermissionFilter extends HttpFilter
sendAccessDenied(request, response, subject); sendAccessDenied(request, response, subject);
} }
} }
else
{
logger.debug("repository not found");
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
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) catch (ScmSecurityException | AuthorizationException ex)
{ {
logger.warn("user " + subject.getPrincipal() + " has not enough permissions", 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()); 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<String> 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. * Send access denied to the servlet response.
* *
@@ -280,25 +213,6 @@ public abstract class PermissionFilter extends HttpFilter
: "read"; : "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. * Returns the username from the given subject or anonymous.
* *

View File

@@ -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;
}

View File

@@ -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<ScmProtocol> 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 {}
}

View File

@@ -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<DynamicTest> shouldCreateCorrectUrlsWithContextPath() {
return Stream.of("http://localhost/scm", "http://localhost/scm/")
.map(url -> assertResultingUrl(url, "http://localhost/scm/repo/space/name"));
}
@TestFactory
Stream<DynamicTest> 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) {
}
};
}
}

View File

@@ -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<ScmPathInfoStore> 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<ScmPathInfo> mockSetPathInfo() {
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/rest/"));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -1,6 +1,12 @@
[users] [users]
trillian = secret, user trillian = secret, user
admin = secret, admin
writer = secret, repo_write
reader = secret, repo_read
unpriv = secret
[roles] [roles]
admin = * admin = *
user = something:* user = something:*
repo_read = "repository:read:1"
repo_write = "repository:push:1"

View File

@@ -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/.+")
);
}
}

View File

@@ -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();
}
}

View File

@@ -39,6 +39,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters; 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.PermissionType;
import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientException; 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.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile; import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
import static sonia.scm.it.TestData.USER_SCM_ADMIN; import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN;
import static sonia.scm.it.TestData.callRepository; import static sonia.scm.it.utils.TestData.callRepository;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class PermissionsITCase { public class PermissionsITCase {

View File

@@ -42,6 +42,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters; 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.repository.client.api.RepositoryClient;
import sonia.scm.web.VndMediaType; 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.is;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static sonia.scm.it.RegExMatcher.matchesPattern; import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
import static sonia.scm.it.RestUtil.createResourceUrl; import static sonia.scm.it.utils.RestUtil.createResourceUrl;
import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
import static sonia.scm.it.TestData.repositoryJson; import static sonia.scm.it.utils.TestData.repositoryJson;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class RepositoriesITCase { public class RepositoriesITCase {

View File

@@ -3,6 +3,7 @@ package sonia.scm.it;
import io.restassured.response.ExtractableResponse; import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response; import io.restassured.response.Response;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.assertj.core.util.Lists;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -10,6 +11,9 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; 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.Changeset;
import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClient;
@@ -17,17 +21,20 @@ import sonia.scm.web.VndMediaType;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import static java.lang.Thread.sleep; import static java.lang.Thread.sleep;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static sonia.scm.it.RestUtil.ADMIN_PASSWORD; import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.RestUtil.ADMIN_USERNAME; import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class RepositoryAccessITCase { public class RepositoryAccessITCase {
@@ -37,6 +44,7 @@ public class RepositoryAccessITCase {
private final String repositoryType; private final String repositoryType;
private File folder; private File folder;
private ScmRequests.AppliedRepositoryRequest repositoryGetRequest;
public RepositoryAccessITCase(String repositoryType) { public RepositoryAccessITCase(String repositoryType) {
this.repositoryType = repositoryType; this.repositoryType = repositoryType;
@@ -48,9 +56,20 @@ public class RepositoryAccessITCase {
} }
@Before @Before
public void initClient() { public void init() {
TestData.createDefault(); TestData.createDefault();
folder = tempFolder.getRoot(); 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 @Test
@@ -153,7 +172,7 @@ public class RepositoryAccessITCase {
.isNotNull() .isNotNull()
.contains(String.format("%s/sources/%s", repositoryUrl, changeset.getId())); .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") .as("assert single tag changesets link")
.isNotNull() .isNotNull()
.contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId())); .contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId()));
@@ -281,5 +300,187 @@ public class RepositoryAccessITCase {
.contains("diff"); .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<String, String> addedFiles = new HashMap<String, String>()
{{
put("a.txt", "bla bla");
}};
Map<String, String> modifiedFiles = new HashMap<String, String>()
{{
put("b.txt", "new content");
}};
ArrayList<String> 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<String, String> addedFiles = new HashMap<String, String>()
{{
put("aaa/aa/a.txt", "bla bla");
}};
Map<String, String> modifiedFiles = new HashMap<String, String>()
{{
put("bbb/bb/b.txt", "new content");
}};
ArrayList<String> 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"));
}
} }

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -1,4 +1,4 @@
package sonia.scm.it; package sonia.scm.it.utils;
import org.hamcrest.BaseMatcher; import org.hamcrest.BaseMatcher;
import org.hamcrest.Description; import org.hamcrest.Description;
@@ -6,7 +6,7 @@ import org.hamcrest.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
class RegExMatcher extends BaseMatcher<String> { public class RegExMatcher extends BaseMatcher<String> {
public static Matcher<String> matchesPattern(String pattern) { public static Matcher<String> matchesPattern(String pattern) {
return new RegExMatcher(pattern); return new RegExMatcher(pattern);
} }
@@ -24,6 +24,6 @@ class RegExMatcher extends BaseMatcher<String> {
@Override @Override
public boolean matches(Object o) { public boolean matches(Object o) {
return Pattern.compile(pattern).matcher(o.toString()).matches(); return o != null && Pattern.compile(pattern).matcher(o.toString()).matches();
} }
} }

View File

@@ -1,4 +1,4 @@
package sonia.scm.it; package sonia.scm.it.utils;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.io.Files; import com.google.common.io.Files;
@@ -14,6 +14,8 @@ import sonia.scm.repository.client.api.RepositoryClientFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class RepositoryUtil { public class RepositoryUtil {
@@ -22,30 +24,73 @@ public class RepositoryUtil {
private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); 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"); 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) String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK)
.extract() .extract()
.path("_links.httpProtocol.href"); .path("_links.protocol.find{it.name=='http'}.href");
return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder); 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 uuid = UUID.randomUUID().toString();
String name = "file-" + uuid + ".uuid"; String name = "file-" + uuid + ".uuid";
createAndCommitFile(client, username, name, uuid); createAndCommitFile(client, username, name, uuid);
return name; 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<String, String> addedFiles, Map<String, String> modifiedFiles, List<String> 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); File file = new File(repositoryClient.getWorkingCopy(), fileName);
Files.createParentDirs(file);
Files.write(content, file, Charsets.UTF_8); Files.write(content, file, Charsets.UTF_8);
addWithParentDirectories(repositoryClient, file); 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 { private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException {
@@ -53,7 +98,6 @@ public class RepositoryUtil {
String thisName = file.getName(); String thisName = file.getName();
String path; String path;
if (!repositoryClient.getWorkingCopy().equals(parent)) { if (!repositoryClient.getWorkingCopy().equals(parent)) {
addWithParentDirectories(repositoryClient, parent);
path = addWithParentDirectories(repositoryClient, parent) + File.separator + thisName; path = addWithParentDirectories(repositoryClient, parent) + File.separator + thisName;
} else { } else {
path = thisName; path = thisName;
@@ -71,7 +115,7 @@ public class RepositoryUtil {
return changeset; 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)) { if (repositoryClient.isCommandSupported(ClientCommand.TAG)) {
Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN); Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN);
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {

View File

@@ -1,4 +1,4 @@
package sonia.scm.it; package sonia.scm.it.utils;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification; import io.restassured.specification.RequestSpecification;

View File

@@ -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
* <p>
* 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 <code>url</code> and return the response.
*
* @param url the url of the GET request
* @return the response of the GET request using the given <code>url</code>
*/
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 <code>url</code> 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 <code>url</code>
*/
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<SELF extends 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<Response> 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<AppliedRepositoryRequest> {
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<AppliedChangesetsRequest> {
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<List<Map>> changesetsConsumer) {
List<Map> changesets = changesetsResponse.then().extract().path("_embedded.changesets");
changesetsConsumer.accept(changesets);
return this;
}
public AppliedDiffRequest requestDiff(String revision) {
return new AppliedDiffRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
}
public AppliedModificationsRequest requestModifications(String revision) {
return new AppliedModificationsRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
}
}
public class AppliedSourcesRequest extends AppliedRequest<AppliedSourcesRequest> {
public AppliedSourcesRequest(Response sourcesResponse) {
super(sourcesResponse);
}
public SourcesResponse usingSourcesResponse() {
return new SourcesResponse(super.response);
}
}
public class SourcesResponse {
private Response sourcesResponse;
public SourcesResponse(Response sourcesResponse) {
this.sourcesResponse = sourcesResponse;
}
public SourcesResponse assertRevision(Consumer<String> assertRevision) {
String revision = sourcesResponse.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
public SourcesResponse assertFiles(Consumer<List> assertFiles) {
List files = sourcesResponse.then().extract().path("files");
assertFiles.accept(files);
return this;
}
public AppliedChangesetsRequest requestFileHistory(String fileName) {
return new AppliedChangesetsRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"));
}
public AppliedSourcesRequest requestSelf(String fileName) {
return new AppliedSourcesRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"));
}
}
public class AppliedDiffRequest extends AppliedRequest<AppliedDiffRequest> {
public AppliedDiffRequest(Response response) {
super(response);
}
}
public class GivenUrl {
public GivenWithUrlAndAuth usernameAndPassword(String username, String password) {
setUsername(username);
setPassword(password);
return new GivenWithUrlAndAuth();
}
}
public class AppliedModificationsRequest extends AppliedRequest<AppliedModificationsRequest> {
public AppliedModificationsRequest(Response response) {
super(response);
}
public ModificationsResponse usingModificationsResponse() {
return new ModificationsResponse(super.response);
}
}
public class ModificationsResponse {
private Response resource;
public ModificationsResponse(Response resource) {
this.resource = resource;
}
public ModificationsResponse assertRevision(Consumer<String> assertRevision) {
String revision = resource.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
public ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
List<String> added = resource.then().extract().path("added");
assertAdded.accept(added);
return this;
}
public ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
List<String> removed = resource.then().extract().path("removed");
assertRemoved.accept(removed);
return this;
}
public ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
List<String> modified = resource.then().extract().path("modified");
assertModified.accept(modified);
return this;
}
}
public class AppliedMeRequest extends AppliedRequest<AppliedMeRequest> {
public AppliedMeRequest(Response response) {
super(response);
}
public MeResponse usingMeResponse() {
return new MeResponse(super.response);
}
}
public class MeResponse extends UserResponse<MeResponse> {
public MeResponse(Response response) {
super(response);
}
public AppliedChangePasswordRequest requestChangePassword(String oldPassword, String newPassword) {
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, "_links.password.href", VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)));
}
}
public class UserResponse<SELF extends UserResponse> extends ModelResponse<SELF> {
public static final String LINKS_PASSWORD_HREF = "_links.password.href";
public UserResponse(Response response) {
super(response);
}
public SELF assertPassword(Consumer<String> assertPassword) {
return super.assertSingleProperty(assertPassword, "password");
}
public SELF assertType(Consumer<String> assertType) {
return assertSingleProperty(assertType, "type");
}
public SELF assertAdmin(Consumer<Boolean> assertAdmin) {
return assertSingleProperty(assertAdmin, "admin");
}
public SELF assertPasswordLinkDoesNotExists() {
return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF);
}
public SELF assertPasswordLinkExists() {
return assertPropertyPathExists(LINKS_PASSWORD_HREF);
}
public AppliedChangePasswordRequest requestChangePassword(String newPassword) {
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(null, newPassword)));
}
}
/**
* encapsulate standard assertions over model properties
*/
public class ModelResponse<SELF extends ModelResponse> {
protected Response response;
public ModelResponse(Response response) {
this.response = response;
}
public <T> SELF assertSingleProperty(Consumer<T> assertSingleProperty, String propertyJsonPath) {
T propertyValue = response.then().extract().path(propertyJsonPath);
assertSingleProperty.accept(propertyValue);
return (SELF) this;
}
public SELF assertPropertyPathExists(String propertyJsonPath) {
response.then().assertThat().body("any { it.containsKey('" + propertyJsonPath + "')}", is(true));
return (SELF) this;
}
public SELF assertPropertyPathDoesNotExists(String propertyJsonPath) {
response.then().assertThat().body("this.any { it.containsKey('" + propertyJsonPath + "')}", is(false));
return (SELF) this;
}
public SELF assertArrayProperty(Consumer<List> assertProperties, String propertyJsonPath) {
List properties = response.then().extract().path(propertyJsonPath);
assertProperties.accept(properties);
return (SELF) this;
}
}
public class AppliedChangePasswordRequest extends AppliedRequest<AppliedChangePasswordRequest> {
public AppliedChangePasswordRequest(Response response) {
super(response);
}
}
public class AppliedUserRequest extends AppliedRequest<AppliedUserRequest> {
public AppliedUserRequest(Response response) {
super(response);
}
public UserResponse usingUserResponse() {
return new UserResponse(super.response);
}
}
}

View File

@@ -1,12 +1,12 @@
package sonia.scm.it; package sonia.scm.it.utils;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
class ScmTypes { public class ScmTypes {
static Collection<String> availableScmTypes() { public static Collection<String> availableScmTypes() {
Collection<String> params = new ArrayList<>(); Collection<String> params = new ArrayList<>();
params.add("git"); params.add("git");

View File

@@ -1,4 +1,4 @@
package sonia.scm.it; package sonia.scm.it.utils;
import io.restassured.response.ValidatableResponse; import io.restassured.response.ValidatableResponse;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
@@ -8,14 +8,16 @@ import sonia.scm.repository.PermissionType;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonObjectBuilder;
import java.net.URI;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static sonia.scm.it.RestUtil.createResourceUrl; import static sonia.scm.it.utils.RestUtil.createResourceUrl;
import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
public class TestData { public class TestData {
@@ -26,6 +28,7 @@ public class TestData {
private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS);
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>(); private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
public static final JsonObjectBuilder JSON_BUILDER = NullAwareJsonObjectBuilder.wrap(Json.createObjectBuilder());
public static void createDefault() { public static void createDefault() {
cleanup(); cleanup();
@@ -44,27 +47,31 @@ public class TestData {
} }
public static void createUser(String username, String password) { public static void createUser(String username, String password) {
createUser(username, password, false, "xml");
}
public static void createUser(String username, String password, boolean isAdmin, String type) {
LOG.info("create user with username: {}", username); LOG.info("create user with username: {}", username);
String admin = isAdmin ? "true" : "false";
given(VndMediaType.USER) given(VndMediaType.USER)
.when() .when()
.content(" {\n" + .content(new StringBuilder()
" \"active\": true,\n" + .append(" {\n")
" \"admin\": false,\n" + .append(" \"active\": true,\n")
" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n" + .append(" \"admin\": ").append(admin).append(",\n")
" \"displayName\": \"" + username + "\",\n" + .append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n")
" \"mail\": \"user1@scm-manager.org\",\n" + .append(" \"displayName\": \"").append(username).append("\",\n")
" \"name\": \"" + username + "\",\n" + .append(" \"mail\": \"user1@scm-manager.org\",\n")
" \"password\": \"" + password + "\",\n" + .append(" \"name\": \"").append(username).append("\",\n")
" \"type\": \"xml\"\n" + .append(" \"password\": \"").append(password).append("\",\n")
" \n" + .append(" \"type\": \"").append(type).append("\"\n")
" }") .append(" }").toString())
.post(createResourceUrl("users")) .post(getUsersUrl())
.then() .then()
.statusCode(HttpStatus.SC_CREATED) .statusCode(HttpStatus.SC_CREATED)
; ;
} }
public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) { public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) {
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl); LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl);
@@ -183,7 +190,7 @@ public class TestData {
} }
public static String repositoryJson(String repositoryType) { public static String repositoryJson(String repositoryType) {
return Json.createObjectBuilder() return JSON_BUILDER
.add("contact", "zaphod.beeblebrox@hitchhiker.com") .add("contact", "zaphod.beeblebrox@hitchhiker.com")
.add("description", "Heart of Gold") .add("description", "Heart of Gold")
.add("name", "HeartOfGold-" + repositoryType) .add("name", "HeartOfGold-" + repositoryType)
@@ -192,6 +199,29 @@ public class TestData {
.build().toString(); .build().toString();
} }
public static URI getMeUrl() {
return RestUtil.createResourceUrl("me/");
}
public static URI getUsersUrl() {
return RestUtil.createResourceUrl("users/");
}
public static URI getUserUrl(String username) {
return getUsersUrl().resolve(username);
}
public static String createPasswordChangeJson(String oldPassword, String newPassword) {
return JSON_BUILDER
.add("oldPassword", oldPassword)
.add("newPassword", newPassword)
.build().toString();
}
public static void main(String[] args) { public static void main(String[] args) {
cleanup(); cleanup();
} }

View File

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.0.7" "@scm-manager/ui-extensions": "^0.0.7"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.13" "@scm-manager/ui-bundler": "^0.0.15"
} }
} }

View File

@@ -0,0 +1,40 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.plugin.Extension;
import sonia.scm.web.JsonEnricherBase;
import sonia.scm.web.JsonEnricherContext;
import javax.inject.Inject;
import javax.inject.Provider;
import static java.util.Collections.singletonMap;
import static sonia.scm.web.VndMediaType.INDEX;
@Extension
public class GitConfigInIndexResource extends JsonEnricherBase {
private final Provider<ScmPathInfoStore> scmPathInfoStore;
@Inject
public GitConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
super(objectMapper);
this.scmPathInfoStore = scmPathInfoStore;
}
@Override
public void enrich(JsonEnricherContext context) {
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
String gitConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class)
.method("get")
.parameters()
.href();
JsonNode gitConfigRefNode = createObject(singletonMap("href", value(gitConfigUrl)));
addPropertyNode(context.getResponseEntity().get("_links"), "gitConfig", gitConfigRefNode);
}
}
}

View File

@@ -18,7 +18,7 @@ import static de.otto.edison.hal.Links.linkingTo;
public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper<GitConfig, GitConfigDto> { public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper<GitConfig, GitConfigDto> {
@Inject @Inject
private UriInfoStore uriInfoStore; private ScmPathInfoStore scmPathInfoStore;
@AfterMapping @AfterMapping
void appendLinks(GitConfig config, @MappingTarget GitConfigDto target) { void appendLinks(GitConfig config, @MappingTarget GitConfigDto target) {
@@ -30,12 +30,12 @@ public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper<GitConfig
} }
private String self() { private String self() {
LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), GitConfigResource.class); LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class);
return linkBuilder.method("get").parameters().href(); return linkBuilder.method("get").parameters().href();
} }
private String update() { private String update() {
LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), GitConfigResource.class); LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class);
return linkBuilder.method("update").parameters().href(); return linkBuilder.method("update").parameters().href();
} }
} }

View File

@@ -37,32 +37,25 @@ package sonia.scm.repository;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.util.Util; import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -224,13 +217,6 @@ public class GitChangesetConverter implements Closeable
changeset.setParents(parentList); changeset.setParents(parentList);
} }
Modifications modifications = createModifications(treeWalk, commit);
if (modifications != null)
{
changeset.setModifications(modifications);
}
Collection<String> tagCollection = tags.get(commit.getId()); Collection<String> tagCollection = tags.get(commit.getId());
if (Util.isNotEmpty(tagCollection)) if (Util.isNotEmpty(tagCollection))
@@ -245,108 +231,7 @@ public class GitChangesetConverter implements Closeable
return changeset; return changeset;
} }
/**
* TODO: copy and rename
*
*
* @param modifications
* @param entry
*/
private void appendModification(Modifications modifications, DiffEntry entry)
{
switch (entry.getChangeType())
{
case ADD :
modifications.getAdded().add(entry.getNewPath());
break;
case MODIFY :
modifications.getModified().add(entry.getNewPath());
break;
case DELETE :
modifications.getRemoved().add(entry.getOldPath());
break;
}
}
/**
* Method description
*
*
* @param treeWalk
* @param commit
*
* @return
*
* @throws IOException
*/
private Modifications createModifications(TreeWalk treeWalk, RevCommit commit)
throws IOException
{
Modifications modifications = null;
treeWalk.reset();
treeWalk.setRecursive(true);
if (commit.getParentCount() > 0)
{
RevCommit parent = commit.getParent(0);
RevTree tree = parent.getTree();
if ((tree == null) && (revWalk != null))
{
revWalk.parseHeaders(parent);
tree = parent.getTree();
}
if (tree != null)
{
treeWalk.addTree(tree);
}
else
{
if (logger.isTraceEnabled())
{
logger.trace("no parent tree at position 0 for commit {}",
commit.getName());
}
treeWalk.addTree(new EmptyTreeIterator());
}
}
else
{
if (logger.isTraceEnabled())
{
logger.trace("no parent available for commit {}", commit.getName());
}
treeWalk.addTree(new EmptyTreeIterator());
}
treeWalk.addTree(commit.getTree());
List<DiffEntry> entries = DiffEntry.scan(treeWalk);
for (DiffEntry e : entries)
{
if (!e.getOldId().equals(e.getNewId()))
{
if (modifications == null)
{
modifications = new Modifications();
}
appendModification(modifications, e);
}
}
return modifications;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------

View File

@@ -0,0 +1,106 @@
package sonia.scm.repository.spi;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Repository;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.List;
@Slf4j
public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand {
protected GitModificationsCommand(GitContext context, Repository repository) {
super(context, repository);
}
private Modifications createModifications(TreeWalk treeWalk, RevCommit commit, RevWalk revWalk, String revision)
throws IOException, UnsupportedModificationTypeException {
treeWalk.reset();
treeWalk.setRecursive(true);
if (commit.getParentCount() > 0) {
RevCommit parent = commit.getParent(0);
RevTree tree = parent.getTree();
if ((tree == null) && (revWalk != null)) {
revWalk.parseHeaders(parent);
tree = parent.getTree();
}
if (tree != null) {
treeWalk.addTree(tree);
} else {
log.trace("no parent tree at position 0 for commit {}", commit.getName());
treeWalk.addTree(new EmptyTreeIterator());
}
} else {
log.trace("no parent available for commit {}", commit.getName());
treeWalk.addTree(new EmptyTreeIterator());
}
treeWalk.addTree(commit.getTree());
List<DiffEntry> entries = DiffEntry.scan(treeWalk);
Modifications modifications = new Modifications();
for (DiffEntry e : entries) {
if (!e.getOldId().equals(e.getNewId())) {
appendModification(modifications, e);
}
}
modifications.setRevision(revision);
return modifications;
}
@Override
public Modifications getModifications(String revision) {
org.eclipse.jgit.lib.Repository gitRepository = null;
RevWalk revWalk = null;
try {
gitRepository = open();
if (!gitRepository.getAllRefs().isEmpty()) {
revWalk = new RevWalk(gitRepository);
ObjectId id = GitUtil.getRevisionId(gitRepository, revision);
RevCommit commit = revWalk.parseCommit(id);
TreeWalk treeWalk = new TreeWalk(gitRepository);
return createModifications(treeWalk, commit, revWalk, revision);
}
} catch (IOException ex) {
log.error("could not open repository", ex);
throw new InternalRepositoryException(ex);
} catch (UnsupportedModificationTypeException ex) {
log.error("Unsupported modification type", ex);
throw new InternalRepositoryException(ex);
} finally {
GitUtil.release(revWalk);
GitUtil.close(gitRepository);
}
return null;
}
@Override
public Modifications getModifications(ModificationsCommandRequest request) {
return getModifications(request.getRevision());
}
private void appendModification(Modifications modifications, DiffEntry entry) throws UnsupportedModificationTypeException {
DiffEntry.ChangeType type = entry.getChangeType();
if (type == DiffEntry.ChangeType.ADD) {
modifications.getAdded().add(entry.getNewPath());
} else if (type == DiffEntry.ChangeType.MODIFY) {
modifications.getModified().add(entry.getNewPath());
} else if (type == DiffEntry.ChangeType.DELETE) {
modifications.getRemoved().add(entry.getOldPath());
} else {
throw new UnsupportedModificationTypeException(MessageFormat.format("The modification type: {0} is not supported.", type));
}
}
}

View File

@@ -33,20 +33,16 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException; import java.io.IOException;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -73,19 +69,10 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
/** public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository) {
* Constructs ...
*
*
* @param handler
* @param repository
*/
public GitRepositoryServiceProvider(GitRepositoryHandler handler,
Repository repository)
{
this.handler = handler; this.handler = handler;
this.repository = repository; this.repository = repository;
context = new GitContext(handler.getDirectory(repository)); this.context = new GitContext(handler.getDirectory(repository));
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -188,6 +175,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitLogCommand(context, repository); return new GitLogCommand(context, repository);
} }
@Override
public ModificationsCommand getModificationsCommand() {
return new GitModificationsCommand(context,repository);
}
/** /**
* Method description * Method description
* *

View File

@@ -35,7 +35,6 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject; import com.google.inject.Inject;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
@@ -45,51 +44,23 @@ import sonia.scm.repository.Repository;
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@Extension @Extension
public class GitRepositoryServiceResolver implements RepositoryServiceResolver public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
{
/** Field description */ private final GitRepositoryHandler handler;
public static final String TYPE = "git";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param handler
*/
@Inject @Inject
public GitRepositoryServiceResolver(GitRepositoryHandler handler) public GitRepositoryServiceResolver(GitRepositoryHandler handler) {
{
this.handler = handler; this.handler = handler;
} }
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param repository
*
* @return
*/
@Override @Override
public GitRepositoryServiceProvider resolve(Repository repository) public GitRepositoryServiceProvider resolve(Repository repository) {
{
GitRepositoryServiceProvider provider = null; GitRepositoryServiceProvider provider = null;
if (TYPE.equalsIgnoreCase(repository.getType())) if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
{
provider = new GitRepositoryServiceProvider(handler, repository); provider = new GitRepositoryServiceProvider(handler, repository);
} }
return provider; return provider;
} }
//~--- fields ---------------------------------------------------------------
/** Field description */
private GitRepositoryHandler handler;
} }

View File

@@ -0,0 +1,9 @@
package sonia.scm.repository.spi;
import sonia.scm.repository.InternalRepositoryException;
public class UnsupportedModificationTypeException extends InternalRepositoryException {
public UnsupportedModificationTypeException(String message) {
super(message);
}
}

View File

@@ -1,69 +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;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import sonia.scm.Priority;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.filter.Filters;
import sonia.scm.filter.WebElement;
import sonia.scm.web.filter.AuthenticationFilter;
import java.util.Set;
/**
* Handles git specific basic authentication.
*
* @author Sebastian Sdorra
*/
@Priority(Filters.PRIORITY_AUTHENTICATION)
@WebElement(value = GitServletModule.PATTERN_GIT)
public class GitBasicAuthenticationFilter extends AuthenticationFilter
{
/**
* Constructs a new instance.
*
* @param configuration scm-manager main configuration
* @param webTokenGenerators web token generators
*/
@Inject
public GitBasicAuthenticationFilter(ScmConfiguration configuration,
Set<WebTokenGenerator> webTokenGenerators)
{
super(configuration, webTokenGenerators);
}
}

View File

@@ -33,38 +33,24 @@
package sonia.scm.web; package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.http.server.GitSmartHttpTools; import org.eclipse.jgit.http.server.GitSmartHttpTools;
import sonia.scm.ClientMessages; import sonia.scm.ClientMessages;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.spi.ScmProviderHttpServlet;
import sonia.scm.web.filter.ProviderPermissionFilter; import sonia.scm.web.filter.PermissionFilter;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import sonia.scm.Priority; import java.io.IOException;
import sonia.scm.filter.Filters;
import sonia.scm.filter.WebElement;
/** /**
* GitPermissionFilter decides if a git request requires write or read privileges. * GitPermissionFilter decides if a git request requires write or read privileges.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@Priority(Filters.PRIORITY_AUTHORIZATION) public class GitPermissionFilter extends PermissionFilter
@WebElement(value = GitServletModule.PATTERN_GIT)
public class GitPermissionFilter extends ProviderPermissionFilter
{ {
private static final String PARAMETER_SERVICE = "service"; private static final String PARAMETER_SERVICE = "service";
@@ -83,11 +69,9 @@ public class GitPermissionFilter extends ProviderPermissionFilter
* Constructs a new instance of the GitPermissionFilter. * Constructs a new instance of the GitPermissionFilter.
* *
* @param configuration scm main configuration * @param configuration scm main configuration
* @param repositoryProvider repository provider
*/ */
@Inject public GitPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) {
public GitPermissionFilter(ScmConfiguration configuration, RepositoryProvider repositoryProvider) { super(configuration, delegate);
super(configuration, repositoryProvider);
} }
@Override @Override
@@ -103,7 +87,7 @@ public class GitPermissionFilter extends ProviderPermissionFilter
} }
@Override @Override
protected boolean isWriteRequest(HttpServletRequest request) { public boolean isWriteRequest(HttpServletRequest request) {
return isReceivePackRequest(request) || return isReceivePackRequest(request) ||
isReceiveServiceRequest(request) || isReceiveServiceRequest(request) ||
isLfsFileUpload(request); isLfsFileUpload(request);

View File

@@ -0,0 +1,30 @@
package sonia.scm.web;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.ScmProviderHttpServlet;
import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory;
import javax.inject.Inject;
@Extension
public class GitPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory {
private final ScmConfiguration configuration;
@Inject
public GitPermissionFilterFactory(ScmConfiguration configuration) {
this.configuration = configuration;
}
@Override
public boolean handlesScmType(String type) {
return GitRepositoryHandler.TYPE_NAME.equals(type);
}
@Override
public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) {
return new GitPermissionFilter(configuration, delegate);
}
}

View File

@@ -125,8 +125,9 @@ public class GitRepositoryResolver implements RepositoryResolver<HttpServletRequ
throw new ServiceNotEnabledException(); throw new ServiceNotEnabledException();
} }
} }
catch (RuntimeException | IOException e) catch (IOException e)
{ {
// REVIEW
throw new RepositoryNotFoundException(repositoryName, e); throw new RepositoryNotFoundException(repositoryName, e);
} }
} }

View File

@@ -278,7 +278,6 @@ public class GitRepositoryViewer
{ {
//J- //J-
ChangesetPagingResult cpr = service.getLogCommand() ChangesetPagingResult cpr = service.getLogCommand()
.setDisableEscaping(true)
.setBranch(name) .setBranch(name)
.setPagingLimit(CHANGESET_PER_BRANCH) .setPagingLimit(CHANGESET_PER_BRANCH)
.getChangesets(); .getChangesets();

View File

@@ -0,0 +1,25 @@
package sonia.scm.web;
import sonia.scm.api.v2.resources.ScmPathInfoStore;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
@Singleton
@Extension
public class GitScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper {
@Inject
public GitScmProtocolProviderWrapper(ScmGitServletProvider servletProvider, Provider<ScmPathInfoStore> uriInfoStore, ScmConfiguration scmConfiguration) {
super(servletProvider, uriInfoStore, scmConfiguration);
}
@Override
public String getType() {
return GitRepositoryHandler.TYPE_NAME;
}
}

Some files were not shown because too many files have changed in this diff Show More