diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..0609164d73 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +# ignore everything except scm-server.tar.gz +** +!scm-server/target/*.tar.gz diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..13c5268429 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "@scm-manager/eslint-config" +} diff --git a/.hgignore b/.hgignore index 6ff185f670..00efb50c1f 100644 --- a/.hgignore +++ b/.hgignore @@ -29,3 +29,8 @@ Desktop DF$ # jrebel rebel.xml \.pyc +# ui +scm-ui/build +scm-ui/coverage +/?node_modules/ + diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000..9cc84ea9b4 Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..8b4bf8dee1 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +# Keep this version number in sync with Jenkinsfile +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..781a3e890b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM openjdk:8u171-alpine3.8 + +ENV SCM_HOME=/var/lib/scm + +RUN set -x \ + && apk add --no-cache mercurial bash \ + && addgroup -S -g 1000 scm \ + && adduser -S -s /bin/false -G scm -h /opt/scm-server -D -H -u 1000 scm \ + && mkdir ${SCM_HOME} \ + && chown scm:scm ${SCM_HOME} + +ADD scm-server/target/scm-server-app.tar.gz /opt +RUN chown -R scm:scm /opt/scm-server + +WORKDIR /opt/scm-server +VOLUME [ "${SCM_HOME}", "/opt/scm-server/var/log" ] +EXPOSE 8080 +USER scm + +ENTRYPOINT [ "/opt/scm-server/bin/scm-server" ] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000000..36135cf4a4 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,171 @@ +#!groovy + +// Keep the version in sync with the one used in pom.xml in order to get correct syntax completion. +@Library('github.com/cloudogu/ces-build-lib@59d3e94') +import com.cloudogu.ces.cesbuildlib.* + +node('docker') { + + // Change this as when we go back to default - necessary for proper SonarQube analysis + mainBranch = '2.0.0-m3' + + properties([ + // Keep only the last 10 build to preserve space + buildDiscarder(logRotator(numToKeepStr: '10')), + disableConcurrentBuilds(), + parameters([ + string(name: 'dockerTag', trim: true, defaultValue: 'latest', description: 'Extra Docker Tag for cloudogu/scm-manager image') + ]) + ]) + + timeout(activity: true, time: 30, unit: 'MINUTES') { + + catchError { + + Maven mvn = setupMavenBuild() + + stage('Checkout') { + checkout scm + } + + stage('Build') { + mvn 'clean install -Pdoc -DskipTests' + } + + stage('Unit Test') { + mvn 'test -Dsonia.scm.test.skip.hg=true -Dmaven.test.failure.ignore=true' + } + + stage('Integration Test') { + mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true' + } + + stage('SonarQube') { + + analyzeWith(mvn) + + if (!waitForQualityGateWebhookToBeCalled()) { + currentBuild.result = 'UNSTABLE' + } + } + + def commitHash = getCommitHash() + def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}" + + if (isMainBranch()) { + + stage('Lifecycle') { + nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build' + } + + stage('Archive') { + archiveArtifacts 'scm-webapp/target/scm-webapp.war' + archiveArtifacts 'scm-server/target/scm-server-app.*' + archiveArtifacts 'scm-webapp/target/scm-webapp-restdocs.zip' + } + + stage('Docker') { + def image = docker.build('cloudogu/scm-manager') + docker.withRegistry('', 'hub.docker.com-cesmarvin') { + image.push(dockerImageTag) + image.push('latest') + if (!'latest'.equals(params.dockerTag)) { + image.push(params.dockerTag) + + def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}" + currentBuild.description = newDockerTag + image.push(newDockerTag) + } + } + } + + stage('Deployment') { + build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [ + string(name: 'changeset', value: commitHash), + string(name: 'imageTag', value: dockerImageTag) + ] + } + } + } + + // Archive Unit and integration test results, if any + junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml' + + // Find maven warnings and visualize in job + warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true + + mailIfStatusChanged(commitAuthorEmail) + } +} + +String mainBranch + +Maven setupMavenBuild() { + // Keep this version number in sync with .mvn/maven-wrapper.properties + Maven mvn = new MavenInDocker(this, '3.5.2-jdk-8') + + if (isMainBranch()) { + // Release starts javadoc, which takes very long, so do only for certain branches + mvn.additionalArgs += ' -DperformRelease' + // JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet. + mvn.additionalArgs += ' -Dmaven.javadoc.failOnError=false' + } + return mvn +} + +void analyzeWith(Maven mvn) { + + withSonarQubeEnv('sonarcloud.io-scm') { + + String mvnArgs = "${env.SONAR_MAVEN_GOAL} " + + "-Dsonar.host.url=${env.SONAR_HOST_URL} " + + "-Dsonar.login=${env.SONAR_AUTH_TOKEN} " + + if (isPullRequest()) { + echo "Analysing SQ in PR mode" + mvnArgs += "-Dsonar.pullrequest.base=${env.CHANGE_TARGET} " + + "-Dsonar.pullrequest.branch=${env.CHANGE_BRANCH} " + + "-Dsonar.pullrequest.key=${env.CHANGE_ID} " + + "-Dsonar.pullrequest.provider=bitbucketcloud " + + "-Dsonar.pullrequest.bitbucketcloud.owner=sdorra " + + "-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager " + + "-Dsonar.cpd.exclusions=**/*StoreFactory.java,**/*UserPassword.js " + } else { + mvnArgs += " -Dsonar.branch.name=${env.BRANCH_NAME} " + if (!isMainBranch()) { + // Avoid exception "The main branch must not have a target" on main branch + mvnArgs += " -Dsonar.branch.target=${mainBranch} " + } + } + mvn "${mvnArgs}" + } +} + +boolean isMainBranch() { + return mainBranch.equals(env.BRANCH_NAME) +} + +boolean waitForQualityGateWebhookToBeCalled() { + boolean isQualityGateSucceeded = true + timeout(time: 5, unit: 'MINUTES') { // Needed when there is no webhook for example + def qGate = waitForQualityGate() + echo "SonarQube Quality Gate status: ${qGate.status}" + if (qGate.status != 'OK') { + isQualityGateSucceeded = false + } + } + return isQualityGateSucceeded +} + +String getCommitAuthorComplete() { + new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{author}"' +} + +String getCommitHash() { + new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{node}"' +} + +String getCommitAuthorEmail() { + def matcher = getCommitAuthorComplete() =~ "<(.*?)>" + matcher ? matcher[0][1] : "" +} diff --git a/deployments/helm/.helmignore b/deployments/helm/.helmignore new file mode 100644 index 0000000000..f0c1319444 --- /dev/null +++ b/deployments/helm/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/deployments/helm/Chart.yaml b/deployments/helm/Chart.yaml new file mode 100644 index 0000000000..c5b5fff4cc --- /dev/null +++ b/deployments/helm/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for SCM-Manager +name: scm-manager +version: 0.1.0 diff --git a/deployments/helm/templates/NOTES.txt b/deployments/helm/templates/NOTES.txt new file mode 100644 index 0000000000..a58c8f124a --- /dev/null +++ b/deployments/helm/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "scm-manager.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ include "scm-manager.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "scm-manager.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ include "scm-manager.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/deployments/helm/templates/_helpers.tpl b/deployments/helm/templates/_helpers.tpl new file mode 100644 index 0000000000..23d4e1b03e --- /dev/null +++ b/deployments/helm/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "scm-manager.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "scm-manager.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "scm-manager.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/deployments/helm/templates/configmap.yaml b/deployments/helm/templates/configmap.yaml new file mode 100644 index 0000000000..1ccb773355 --- /dev/null +++ b/deployments/helm/templates/configmap.yaml @@ -0,0 +1,160 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "scm-manager.fullname" . }} + labels: + app: {{ include "scm-manager.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +data: + server-config.xml: | + + + + + + + 16384 + 16384 + + {{- if .Values.ingress.enabled -}} + + + + + {{- end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /scm + + /var/webapp/scm-webapp.war + + + org.eclipse.jetty.servlet.Default.dirAllowed + false + + + /work/scm + + + + + / + + + + + + /var/webapp/docroot + + + + + + /work/docroot + + + + + + + + + + + + + + + + + + + + + logging.xml: | + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deployments/helm/templates/deployment.yaml b/deployments/helm/templates/deployment.yaml new file mode 100644 index 0000000000..7e19f61e57 --- /dev/null +++ b/deployments/helm/templates/deployment.yaml @@ -0,0 +1,93 @@ +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 + {{- if .Values.plugins }} + - name: install-plugins + image: alpine:3.8 + imagePullPolicy: IfNotPresent + command: ['sh', '/scripts/install-plugins.sh'] + volumeMounts: + - name: data + mountPath: /data + - name: scripts + mountPath: /scripts + {{- end }} + 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" . }} + {{- if .Values.plugins }} + - name: scripts + configMap: + name: {{ include "scm-manager.fullname" . }}-scripts + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/deployments/helm/templates/ingress.yaml b/deployments/helm/templates/ingress.yaml new file mode 100644 index 0000000000..66912c9d96 --- /dev/null +++ b/deployments/helm/templates/ingress.yaml @@ -0,0 +1,38 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "scm-manager.fullname" . -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ include "scm-manager.name" . }} + chart: {{ include "scm-manager.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . | quote }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/deployments/helm/templates/pvc.yaml b/deployments/helm/templates/pvc.yaml new file mode 100644 index 0000000000..0e7d0f6db4 --- /dev/null +++ b/deployments/helm/templates/pvc.yaml @@ -0,0 +1,24 @@ +{{- if .Values.persistence.enabled -}} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ include "scm-manager.fullname" . }} + labels: + app: {{ include "scm-manager.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- if .Values.persistence.storageClass }} +{{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" +{{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" +{{- end }} +{{- end }} +{{- end -}} diff --git a/deployments/helm/templates/scripts.yaml b/deployments/helm/templates/scripts.yaml new file mode 100644 index 0000000000..43a442a8e2 --- /dev/null +++ b/deployments/helm/templates/scripts.yaml @@ -0,0 +1,21 @@ +{{- if .Values.plugins }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "scm-manager.fullname" . }}-scripts + labels: + app: {{ include "scm-manager.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +data: + install-plugins.sh: | + #!/bin/sh + mkdir -p /data/plugins + chown 1000:1000 /data/plugins + {{ range $i, $plugin := .Values.plugins }} + # install plugin {{ $plugin.name }} + wget -O /data/plugins/{{ $plugin.name }}.smp {{ $plugin.url }} + chown 1000:1000 /data/plugins/{{ $plugin.name }}.smp + {{ end }} +{{- end }} diff --git a/deployments/helm/templates/service.yaml b/deployments/helm/templates/service.yaml new file mode 100644 index 0000000000..f3a8207908 --- /dev/null +++ b/deployments/helm/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "scm-manager.fullname" . }} + labels: + app: {{ include "scm-manager.name" . }} + chart: {{ include "scm-manager.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: 8080 + protocol: TCP + name: http + selector: + app: {{ include "scm-manager.name" . }} + release: {{ .Release.Name }} diff --git a/deployments/helm/values.yaml b/deployments/helm/values.yaml new file mode 100644 index 0000000000..0b107a8168 --- /dev/null +++ b/deployments/helm/values.yaml @@ -0,0 +1,69 @@ +# 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 + +# plugins: +# - name: scm-review-plugin +# url: https://oss.cloudogu.com/jenkins/job/scm-manager/job/scm-manager-bitbucket/job/scm-review-plugin/job/develop/lastSuccessfulBuild/artifact/target/scm-review-plugin-2.0.0-SNAPSHOT.smp + +nameOverride: "" +fullnameOverride: "" + +service: + type: LoadBalancer + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + path: / + hosts: + - scm-manager.local + tls: [] + # - secretName: scm-manager-tls + # hosts: + # - scm-manager.local + +## Enable persistence using Persistent Volume Claims +## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ +## +persistence: + enabled: true + ## ghost data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + accessMode: ReadWriteOnce + size: 12Gi + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 2000m + memory: 2048Mi + requests: + cpu: 50m + memory: 256Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/docs/dtd/plugin/2.0.0-01.dtd b/docs/dtd/plugin/2.0.0-01.dtd index eec149f3e8..954a2c2219 100644 --- a/docs/dtd/plugin/2.0.0-01.dtd +++ b/docs/dtd/plugin/2.0.0-01.dtd @@ -29,46 +29,28 @@ - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + @@ -121,4 +103,4 @@ - \ No newline at end of file + diff --git a/docs/logo/favicon_16x16px.ico b/docs/logo/favicon_16x16px.ico new file mode 100644 index 0000000000..3436795fdf Binary files /dev/null and b/docs/logo/favicon_16x16px.ico differ diff --git a/scm-webapp/src/main/webapp/resources/images/favicon.ico b/docs/logo/favicon_16x16px_transparent.ico old mode 100755 new mode 100644 similarity index 100% rename from scm-webapp/src/main/webapp/resources/images/favicon.ico rename to docs/logo/favicon_16x16px_transparent.ico diff --git a/docs/logo/scm-manager_logo.ai b/docs/logo/scm-manager_logo.ai new file mode 100644 index 0000000000..6fe98a15e0 --- /dev/null +++ b/docs/logo/scm-manager_logo.ai @@ -0,0 +1 @@ +%!PS-Adobe-2.0 %%Creator: Adobe Photoshop(TM) Pen Path Export 7.0 %%Title: (scm-manager_logo.ai) %%DocumentNeededResources: procset Adobe_packedarray 2.0 0 %%+ procset Adobe_IllustratorA_AI3 1.0 1 %%ColorUsage: Black&White %%BoundingBox: 0 0 800 275 %%HiResBoundingBox: 0 0 800 275 %AI3_Cropmarks: 0 0 800 275 %%DocumentPreview: None %%EndComments %%BeginProlog %%IncludeResource: procset Adobe_packedarray 2.0 0 Adobe_packedarray /initialize get exec %%IncludeResource: procset Adobe_IllustratorA_AI3 1.0 1 %%EndProlog %%BeginSetup Adobe_IllustratorA_AI3 /initialize get exec n %%EndSetup 0.0 0.0 0.0 1.0 k 0 i 0 J 0 j 1 w 4 M []0 d %%Note: %%Trailer %%EOF \ No newline at end of file diff --git a/docs/logo/scm-manager_logo.jpg b/docs/logo/scm-manager_logo.jpg new file mode 100644 index 0000000000..f518e3f2b2 Binary files /dev/null and b/docs/logo/scm-manager_logo.jpg differ diff --git a/docs/logo/scm-manager_logo.png b/docs/logo/scm-manager_logo.png new file mode 100644 index 0000000000..90a17c7ee4 Binary files /dev/null and b/docs/logo/scm-manager_logo.png differ diff --git a/docs/logo/scm-manager_logo_img.jpg b/docs/logo/scm-manager_logo_img.jpg new file mode 100644 index 0000000000..f2d3b35b66 Binary files /dev/null and b/docs/logo/scm-manager_logo_img.jpg differ diff --git a/docs/logo/scm-manager_logo_img.png b/docs/logo/scm-manager_logo_img.png new file mode 100644 index 0000000000..f349fc5d22 Binary files /dev/null and b/docs/logo/scm-manager_logo_img.png differ diff --git a/docs/logo/scm-manager_logo_img_neg.jpg b/docs/logo/scm-manager_logo_img_neg.jpg new file mode 100644 index 0000000000..1b56b5e627 Binary files /dev/null and b/docs/logo/scm-manager_logo_img_neg.jpg differ diff --git a/docs/logo/scm-manager_logo_img_neg.png b/docs/logo/scm-manager_logo_img_neg.png new file mode 100644 index 0000000000..796a02882c Binary files /dev/null and b/docs/logo/scm-manager_logo_img_neg.png differ diff --git a/docs/logo/scm-manager_logo_neg.jpg b/docs/logo/scm-manager_logo_neg.jpg new file mode 100644 index 0000000000..0d6704d0e3 Binary files /dev/null and b/docs/logo/scm-manager_logo_neg.jpg differ diff --git a/docs/logo/scm-manager_logo_neg.png b/docs/logo/scm-manager_logo_neg.png new file mode 100644 index 0000000000..3eb75aa3ee Binary files /dev/null and b/docs/logo/scm-manager_logo_neg.png differ diff --git a/docs/logo/scm-manager_logo_neg1.jpg b/docs/logo/scm-manager_logo_neg1.jpg new file mode 100644 index 0000000000..d776c6b6e6 Binary files /dev/null and b/docs/logo/scm-manager_logo_neg1.jpg differ diff --git a/docs/logo/scm-manager_logo_neg1.png b/docs/logo/scm-manager_logo_neg1.png new file mode 100644 index 0000000000..817785710b Binary files /dev/null and b/docs/logo/scm-manager_logo_neg1.png differ diff --git a/docs/logo/scm-manager_logo_pos1.jpg b/docs/logo/scm-manager_logo_pos1.jpg new file mode 100644 index 0000000000..fd2533c198 Binary files /dev/null and b/docs/logo/scm-manager_logo_pos1.jpg differ diff --git a/docs/logo/scm-manager_logo_pos1.png b/docs/logo/scm-manager_logo_pos1.png new file mode 100644 index 0000000000..b911417358 Binary files /dev/null and b/docs/logo/scm-manager_logo_pos1.png differ diff --git a/docs/mercurial/clone-empty.md b/docs/mercurial/clone-empty.md new file mode 100644 index 0000000000..44a81de20c --- /dev/null +++ b/docs/mercurial/clone-empty.md @@ -0,0 +1,76 @@ +# Clone empty repository + +```http +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1efk0qxy1dj5v133hev91zwsf4;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). +. +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1rsxj8u1rq9wizawhyyxok2p5;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3D. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=ewyx4m53d8dajjsob6gxobne;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 42. +Server: Jetty(7.6.21.v20160908). + +0000000000000000000000000000000000000000 +; + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1o0hou15jtiywsywutf30qwm8;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). +. +publishing.True +``` diff --git a/docs/mercurial/push-bookmark.md b/docs/mercurial/push-bookmark.md new file mode 100644 index 0000000000..9ed591f9f4 --- /dev/null +++ b/docs/mercurial/push-bookmark.md @@ -0,0 +1,117 @@ +# Push bookmark + +```http +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=7rq9vpp9svfm1sicq7h9vetmv;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). + +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +T 172.17.0.2:8080 -> 172.17.0.1:36576 [AP] +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1553csz4sf7scyvw8mqnqfirn;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 43. +Server: Jetty(7.6.21.v20160908). + +ef5993bb4abb32a0565c347844c6d939fc4f4b98 +;1 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=11xa5u3nrmx8k1nar3sazg6jzh;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1p1uzcvfe1pvzh2buzo658rxw;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1mhlj3ucfzdp6ifmzoua4zwit;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: key=markone&namespace=bookmarks&new=ef5993bb4abb32a0565c347844c6d939fc4f4b98&old=. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 0. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=s4vtagb303dv1xg809wnp7e8z;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 2. +Server: Jetty(7.6.21.v20160908). +. +1 +``` diff --git a/docs/mercurial/push-multiple-branches-to-new.md b/docs/mercurial/push-multiple-branches-to-new.md new file mode 100644 index 0000000000..734c479fef --- /dev/null +++ b/docs/mercurial/push-multiple-branches-to-new.md @@ -0,0 +1,167 @@ +# Push multiple branches to new repository + +```http +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1wu06ykfd4bcv1uv731y4hss2m;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). + +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1rajglvqx222g5nppcq3jdfk0;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 43. +Server: Jetty(7.6.21.v20160908). + +0000000000000000000000000000000000000000 +;0 + +GET /scm/hg/hgtest?cmd=known HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: nodes=c0ceccb3b2f0f5c977ff32b9337519e5f37942c2+187ddf37e237c370514487a0bb1a226f11a780b3+b5914611f84eae14543684b2721eec88b0edac12+8b63a323606f10c86b30465570c2574eb7a3a989. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=a5vykp1f0ga2186l8v3gu6lid;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 4. +Server: Jetty(7.6.21.v20160908). + +0000 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=s8lpwqm4c2nqs9kwcg2ca6vm;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1d2qj3kynxlhvk31oli4kk7vf;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +POST /scm/hg/hgtest?cmd=unbundle HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: heads=686173686564+6768033e216468247bd031a0a2d9876d79818f8f. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 913. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HG10GZx...oh.U......E.1.....2q.<...s.1.YK*e#..b..{....{..%A..... +,\.....Y.XV....Q/J......`Q/.z.{...<.7....r.s.~?.?..5.~`..?..........O.j.0.....Ih.....!@.P... ..a +;!y..cT...]q.8Zg=...<..,.tq.*.........l........';..w^...w...-......Co..Fs.HYg... +9.F#.P......1..;......D.H.9$@.^....r:E..18...H....3..h...-.=.6l......=q .)."Yg..p\...s@.#.H.*....c8&96..2.GjJ.`.J....r...=Q1..@R.3.o{q...|.......yq.k..,cY..:[... ...S.2...VYp..c5..&.SFR.............V.d..o..........,.. A..M....k...0_.LO1..1"4.;...B....5.9.".U.m.e......]\../p..;?C..W9.........n.~o..gW...Q;..$....S..X.CN.5I].H..!.@...U..J...L.lY.../.-...6.:.Q.'...>.e'..<#3........OL}.52ra[..g*Y:Y....w...=..Z\...S.......tz..;..mf...W......&yUN.r.......4...........`..F...nT..U9................_.~..?...BwzUN.r....B. + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=163487i0ayf9s1k2ng9e1azadj;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 102. +Server: Jetty(7.6.21.v20160908). + +1 +adding changesets +adding manifests +adding file changes +added 5 changesets with 3 changes to 3 files + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=a3i712yjss6t1xsxltnssq0tl;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 58. +Server: Jetty(7.6.21.v20160908). + +c0ceccb3b2f0f5c977ff32b9337519e5f37942c2.1 +publishing.True + +POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: key=ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 0. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=g8cavdze42d83knmuasrlg10;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 2. +Server: Jetty(7.6.21.v20160908). +. +1 +``` diff --git a/docs/mercurial/push-multiple-branches.md b/docs/mercurial/push-multiple-branches.md new file mode 100644 index 0000000000..5827cb0ceb --- /dev/null +++ b/docs/mercurial/push-multiple-branches.md @@ -0,0 +1,183 @@ +# Push multiple branches + +```http +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1mvm1rxg8333iib7754ksusxc;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). + +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=58p9y9vcnz5cjs22dtw8mpwk;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 43. +Server: Jetty(7.6.21.v20160908). + +c0ceccb3b2f0f5c977ff32b9337519e5f37942c2 +;0 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=v5wfwj8k4t261dp6808cdouoa;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=3pgqytfhm4za1dco9p41j9yz5;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +GET /scm/hg/hgtest?cmd=branchmap HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). +. + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1tiz6zf7ui54e1j3d4vouxig5m;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 48. +Server: Jetty(7.6.21.v20160908). + +default c0ceccb3b2f0f5c977ff32b9337519e5f37942c2 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1augu4tc71xax1dit20dtxzkez;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +POST /scm/hg/hgtest?cmd=unbundle HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: heads=686173686564+95373ca7cd5371cb6c49bb755ee451d9ec585845. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 746. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HG10GZx...]H.Q...z..r.,.Y..Bw.~..c.Z&...hf.:......e.XK.X,... +,2.E1.B+...(.B"."*..z1.*......M...........93..k|..I..<...h..J_.L.9>.h..@.....op..^.....#....;.*..W....T@....!..dY....jT..A0O6.}..S.2..JPU.O6...aa...rY.VOf9.....7Ukj.&..<...z...j......%}..Jc.8c....k.."9.&".I.P.\..$.At......0..1..g.2.)<..$.. E..dn#....#.Y$3...n...5....J.e.......SNHN.q.MD..4..."I..`PF..?GH1..F..uES..Rl$47.....a........D.1...87.k.t..D..O_.3..6'cN.w.M..|@E.).X!.h*....U.B.X.....h..$.`4... +-..O.:./..oWN.....3...x.L......_[..../..k.R$.x.2..kkv.\2R....4...@.2...1Q..T +..(..m....s.Uo.......{.d.....Y....TYO...S.Pl`a5. ."N$.@...b...qJ.l.).n...1..F.Zy.....&>v;.q.....Jy..X.?.;....>U..|.....d.Y.*.q...NR.3...h.T..x..,.]...p{.^S.S...~..`..q.\j{.oCI.............K.....l9n.s...... + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1e4fnqpncil9z1f7a2pya26nt7;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 102. +Server: Jetty(7.6.21.v20160908). + +1 +adding changesets +adding manifests +adding file changes +added 4 changesets with 2 changes to 2 files + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=f9hvrjssniym1qe33q0u8r2m8;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 101. +Server: Jetty(7.6.21.v20160908). + +b5914611f84eae14543684b2721eec88b0edac12.1 +187ddf37e237c370514487a0bb1a226f11a780b3.1 +publishing.True + +POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: key=ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 0. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=z5lrut6940a650sw6x9bls8a;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 2. +Server: Jetty(7.6.21.v20160908). + +1 +``` diff --git a/docs/mercurial/push-single-changeset.md b/docs/mercurial/push-single-changeset.md new file mode 100644 index 0000000000..499b4c21c3 --- /dev/null +++ b/docs/mercurial/push-single-changeset.md @@ -0,0 +1,147 @@ +# Push single changeset + +```http +GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1. +Accept-Encoding: identity. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=18r2i2jsba46d14ncsmcjdhaem;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 130. +Server: Jetty(7.6.21.v20160908). + +lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + +GET /scm/hg/hgtest?cmd=batch HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: cmds=heads+%3Bknown+nodes%3Dc0ceccb3b2f0f5c977ff32b9337519e5f37942c2. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=1fw0i0c5zpy281gfgha0f26git;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 43. +Server: Jetty(7.6.21.v20160908). + +0000000000000000000000000000000000000000 +;0 + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=dfa46uaqgf39w3jhk857oymu;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 15. +Server: Jetty(7.6.21.v20160908). + +publishing.True + +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=bookmarks. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=2sk1llvrsagg33xgmwyirfpi;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 0. +Server: Jetty(7.6.21.v20160908). + +POST /scm/hg/hgtest?cmd=unbundle HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: heads=686173686564+6768033e216468247bd031a0a2d9876d79818f8f. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 261. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HG10GZx.c``8w.....>|=Y..h.q.....N.......%......Z....&&&.&...YZ.&.&[$.........$.%q..&%..d&.).....%*.....Y.....9z...v\..FF...... +..F..\.z%.%\\.)).) +.P[....D..[un..L).nc..q.m*.H.l#C...eZJ..YJ.Q.qR...e.aJ.EjjJ.AZ..A.Q..E.1.T.'D..C....7s.}..4G........3.S.mL.0.....zk + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=hlucs5utn1ifnpehqmjpt593;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 102. +Server: Jetty(7.6.21.v20160908). + +1 +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files + +T 172.17.0.1:33206 -> 172.17.0.2:8080 [AP] +GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1. +Accept-Encoding: identity. +vary: X-HgArg-1. +x-hgarg-1: namespace=phases. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=15xomlrxl8qja1cj47rjpqda0y;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 58. +Server: Jetty(7.6.21.v20160908). + +c0ceccb3b2f0f5c977ff32b9337519e5f37942c2.1 +publishing.True + +POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1. +Accept-Encoding: identity. +content-type: application/mercurial-0.1. +vary: X-HgArg-1. +x-hgarg-1: key=c0ceccb3b2f0f5c977ff32b9337519e5f37942c2&namespace=phases&new=0&old=1. +accept: application/mercurial-0.1. +authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=. +content-length: 0. +host: localhost:8080. +user-agent: mercurial/proto-1.0 (Mercurial 4.3.1). + +HTTP/1.1 200 OK. +Set-Cookie: JSESSIONID=5zrop5v8e661ipk12tvru525;Path=/scm. +Expires: Thu, 01 Jan 1970 00:00:00 GMT. +Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT. +Content-Type: application/mercurial-0.1. +Content-Length: 2. +Server: Jetty(7.6.21.v20160908). + +1 +``` diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000000..405d16a397 --- /dev/null +++ b/lerna.json @@ -0,0 +1,9 @@ +{ + "packages": [ + "scm-ui/*", + "scm-plugins/*" + ], + "npmClient": "yarn", + "useWorkspaces": true, + "version": "2.0.0-SNAPSHOT" +} diff --git a/mvnw b/mvnw new file mode 100755 index 0000000000..5bf251c077 --- /dev/null +++ b/mvnw @@ -0,0 +1,225 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +echo $MAVEN_PROJECTBASEDIR +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000000..019bd74d76 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,143 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/package.json b/package.json new file mode 100644 index 0000000000..b2f14826c5 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "root", + "private": true, + "workspaces": [ + "scm-ui/*", + "scm-plugins/*" + ], + "scripts": { + "build": "webpack --mode=production --config=scm-ui/ui-scripts/src/webpack.config.js", + "build:dev": "webpack --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js", + "test": "lerna run --scope '@scm-manager/ui-*' test", + "typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck", + "serve": "webpack-dev-server --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js", + "deploy": "ui-scripts publish" + }, + "devDependencies": { + "babel-plugin-reflow": "^0.2.7", + "lerna": "^3.17.0" + }, + "resolutions": { + "babel-core": "7.0.0-bridge.0", + "gitdiff-parser": "https://github.com/scm-manager/gitdiff-parser#6baa7278824ecd17a199d842ca720d0453f68982" + }, + "babel": { + "presets": [ + "@scm-manager/babel-preset" + ] + }, + "jest": { + "preset": "@scm-manager/jest-preset" + }, + "prettier": "@scm-manager/prettier-config", + "dependencies": {} +} diff --git a/pom.xml b/pom.xml index 3ef98ec8d1..910287abf0 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -68,11 +69,12 @@ scm-annotation-processor scm-core scm-test + scm-ui scm-plugins scm-dao-xml scm-webapp scm-server - scm-clients + scm-it @@ -82,15 +84,6 @@ scm-manager release repository http://maven.scm-manager.org/nexus/content/groups/public - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - true - daily - - @@ -112,43 +105,368 @@ + + - junit - junit - ${junit.version} - test + org.junit.jupiter + junit-jupiter-api + + + + org.junit.jupiter + junit-jupiter-params + + + + org.junit.jupiter + junit-jupiter-engine + + + + org.junit.vintage + junit-vintage-engine + + + + org.junit-pioneer + junit-pioneer org.hamcrest hamcrest-core - ${hamcrest.version} test org.hamcrest hamcrest-library - ${hamcrest.version} test org.mockito - mockito-all - ${mokito.version} - test + mockito-core - + + + org.mockito + mockito-junit-jupiter + + + + org.assertj + assertj-core + + + + + + + com.github.sdorra + shiro-unit + 1.0.1 + test + + + + com.github.sdorra + ssp-lib + ${ssp.version} + + + + com.github.sdorra + ssp-processor + ${ssp.version} + true + + + + com.webcohesion.enunciate + enunciate-core-annotations + ${enunciate.version} + + + + org.mapstruct + mapstruct-jdk8 + ${org.mapstruct.version} + + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + provided + + + + de.otto.edison + edison-hal + 2.0.1 + + + + org.projectlombok + lombok + 1.16.18 + provided + + + + + + org.jboss.resteasy + resteasy-jaxrs + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-jaxb-provider + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-jackson2-provider + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-multipart-provider + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-guice + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-servlet-initializer + ${resteasy.version} + + + + javax.ws.rs + javax.ws.rs-api + ${jaxrs.version} + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + org.junit.vintage + junit-vintage-engine + ${junit.version} + test + + + + org.junit-pioneer + junit-pioneer + 0.3.0 + test + + + + org.hamcrest + hamcrest-core + ${hamcrest.version} + test + + + + org.hamcrest + hamcrest-library + ${hamcrest.version} + test + + + + org.mockito + mockito-core + ${mockito.version} + test + + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + + org.assertj + assertj-core + 3.10.0 + test + + + + + + org.apache.httpcomponents + httpclient + 4.5.5 + + + + + + slf4j-api + org.slf4j + ${slf4j.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + + javax.xml.bind + jaxb-api + ${jaxb.version} + + + + com.sun.xml.bind + jaxb-impl + ${jaxb.version} + + + + org.glassfish.jaxb + jaxb-runtime + ${jaxb.version} + + + + javax.activation + activation + 1.1.1 + + + + + + commons-codec + commons-codec + 1.13 + + + + + + + + + + com.github.sdorra + buildfrontend-maven-plugin + 2.5.0 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.1 + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + + org.apache.maven.plugins + maven-assembly-plugin + 2.3 + + + + com.webcohesion.enunciate + enunciate-maven-plugin + ${enunciate.version} + + + + sonia.scm.maven + smp-maven-plugin + 1.0.0-alpha-8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + + + - + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + org.apache.maven.plugins maven-enforcer-plugin - 1.4.1 + 3.0.0-M1 enforce-java @@ -170,17 +488,39 @@ [3.1,) + + + 1.8 + + + module-info + + true + + + org.codehaus.mojo + extra-enforcer-rules + 1.0-beta-7 + + org.codehaus.mojo animal-sniffer-maven-plugin - 1.15 + + 1.16 org.codehaus.mojo.signature @@ -198,6 +538,25 @@ + + com.github.legman + legman-maven-plugin + ${legman.version} + + true + + + + process-classes + + + guava-migration-check + + + + + org.apache.maven.plugins maven-compiler-plugin @@ -215,13 +574,13 @@ see https://blogs.oracle.com/darcy/entry/bootclasspath_older_source --> -Xlint:unchecked,-options + -parameters org.apache.maven.plugins maven-resources-plugin - 2.6 ${project.build.sourceEncoding} @@ -248,7 +607,6 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.9 true ${project.build.sourceEncoding} @@ -261,7 +619,6 @@ http://download.oracle.com/javase/8/docs/api/ http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/ - http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/ https://google.github.io/guice/api-docs/${guice.version}/javadoc http://www.slf4j.org/api/ http://shiro.apache.org/static/current/apidocs/ @@ -290,7 +647,6 @@ org.apache.maven.plugins maven-deploy-plugin - 2.7 @@ -316,13 +672,13 @@ maven-eclipse-plugin 2.6 - + - + org.jacoco jacoco-maven-plugin - 0.7.7.201606060606 + 0.8.1 @@ -338,59 +694,59 @@ - + org.apache.maven.plugins maven-site-plugin - 3.2 - - - - - org.apache.maven.plugins - maven-project-info-reports-plugin - 2.4 - - - - org.apache.maven.plugins - maven-jxr-plugin - 2.3 - - - - org.codehaus.mojo - findbugs-maven-plugin - 2.4.0 - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 2.12 - - - - org.apache.maven.plugins - maven-pmd-plugin - 2.7.1 - - true - ${project.build.sourceEncoding} - ${project.build.javaLevel} - - - - - + 3.7 + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.4 + + + + org.apache.maven.plugins + maven-jxr-plugin + 2.3 + + + + org.codehaus.mojo + findbugs-maven-plugin + 2.4.0 + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.12 + + + + org.apache.maven.plugins + maven-pmd-plugin + 2.7.1 + + ${project.build.sourceEncoding} + ${project.build.javaLevel} + + + + + + @@ -434,18 +790,9 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.8.1 + 3.0.0 - org.jboss.apiviz.APIviz - - org.jboss.apiviz - apiviz - 1.3.2.GA - - - -sourceclasspath ${project.build.outputDirectory} - -nopackagediagram - + false @@ -474,41 +821,62 @@ - 1.10.19 + 2.23.0 1.3 - 4.12 + 5.2.0 - 1.7.22 - 1.1.10 + 1.7.25 + 1.2.3 3.0.1 + + 2.1.1 + 3.6.2.Final + 1.19.4 + 2.11.1 + 2.10.0 4.0 - 1.19.4 - + 2.3.0 + - 1.2.0 - + 1.5.1 + - 9.2.10.v20150310 - 9.2.10.v20150310 + 9.4.22.v20191022 + 9.4.22.v20191022 - 1.0.0-SNAPSHOT - 1.4.0-RC2 + 1.2.0 + 1.4.1 - - v4.5.2.201704071617-r-scm1 - 1.8.15-scm1 + + v5.4.0.201906121030-r-scm2 + 1.9.0-scm3 - 22.0 - 2.2.3 + 26.0-jre + + + 10.16.0 + 1.16.0 1.8 + 1.8 UTF-8 SCM-BSD + 1.2.0.Final + + + + + + **/*StoreFactory.java,**/*UserPassword.js + + ./scm-ui/target/frontend/buildfrontend-node/node-v${nodejs.version}-linux-x64/bin/node + + diff --git a/scm-annotation-processor/pom.xml b/scm-annotation-processor/pom.xml index 622bddd221..fc4419ba41 100644 --- a/scm-annotation-processor/pom.xml +++ b/scm-annotation-processor/pom.xml @@ -8,7 +8,8 @@ scm 2.0.0-SNAPSHOT - + + sonia.scm scm-annotation-processor 2.0.0-SNAPSHOT scm-annotation-processor @@ -26,9 +27,9 @@ - com.sun.jersey - jersey-core - ${jersey.version} + javax.ws.rs + javax.ws.rs-api + ${jaxrs.version} diff --git a/scm-annotation-processor/src/main/java/sonia/scm/annotation/ScmAnnotationProcessor.java b/scm-annotation-processor/src/main/java/sonia/scm/annotation/ScmAnnotationProcessor.java index 6d53276498..38961ccaf8 100644 --- a/scm-annotation-processor/src/main/java/sonia/scm/annotation/ScmAnnotationProcessor.java +++ b/scm-annotation-processor/src/main/java/sonia/scm/annotation/ScmAnnotationProcessor.java @@ -1,9 +1,9 @@ /** * 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 @@ -11,7 +11,7 @@ * 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 @@ -22,13 +22,11 @@ * 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.annotation; //~--- non-JDK imports -------------------------------------------------------- @@ -104,35 +102,28 @@ import javax.xml.transform.stream.StreamResult; import static javax.lang.model.util.ElementFilter.methodsIn; /** - * * @author Sebastian Sdorra */ @SupportedAnnotationTypes("*") @MetaInfServices(Processor.class) -@SuppressWarnings({ "Since16", "Since15" }) +@SuppressWarnings({"Since16", "Since15"}) @SupportedSourceVersion(SourceVersion.RELEASE_8) public final class ScmAnnotationProcessor extends AbstractProcessor { private static final String DESCRIPTOR_MODULE = "META-INF/scm/module.xml"; private static final String DESCRIPTOR_PLUGIN = "META-INF/scm/plugin.xml"; - private static final String EL_MODULE = "module"; - private static final String EMPTY = ""; - private static final String PROPERTY_VALUE = "yes"; - - private static final Set SUBSCRIBE_ANNOTATIONS = ImmutableSet.of(Subscribe.class.getName()); - - private static final Set CLASS_ANNOTATIONS = ImmutableSet.of( - new ClassAnnotation("rest-resource", Path.class), - new ClassAnnotation("rest-provider", Provider.class) - ); - - //~--- methods -------------------------------------------------------------- + private static final Set SUBSCRIBE_ANNOTATIONS = + ImmutableSet.of(Subscribe.class.getName()); + private static final Set CLASS_ANNOTATIONS = + ImmutableSet.of(new ClassAnnotation("rest-resource", Path.class), + new ClassAnnotation("rest-provider", Provider.class)); @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { + public boolean process(Set annotations, + RoundEnvironment roundEnv) { if (!roundEnv.processingOver()) { Set descriptorElements = Sets.newHashSet(); Set subscriberAnnotations = Sets.newHashSet(); @@ -150,10 +141,12 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { } for (ClassAnnotation ca : CLASS_ANNOTATIONS) { - TypeElement annotation = findAnnotation(annotations, ca.annotationClass); + TypeElement annotation = findAnnotation(annotations, + ca.annotationClass); if (annotation != null) { - scanForClassAnnotations(descriptorElements, roundEnv, annotation, ca.elementName); + scanForClassAnnotations(descriptorElements, roundEnv, annotation, + ca.elementName); } } @@ -167,6 +160,7 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { return false; } + private void close(Closeable closeable) { if (closeable != null) { try { @@ -177,13 +171,14 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { } } + private TypeElement findAnnotation(Set annotations, Class annotationClass) { TypeElement annotation = null; - for (TypeElement te : annotations) { - if (te.getQualifiedName().toString().equals(annotationClass.getName())) { - annotation = te; + for (TypeElement typeElement : annotations) { + if (typeElement.getQualifiedName().toString().equals(annotationClass.getName())) { + annotation = typeElement; break; } @@ -192,18 +187,22 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { return annotation; } + private File findDescriptor(Filer filer) throws IOException { - FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, EMPTY, DESCRIPTOR_PLUGIN); + FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, EMPTY, + DESCRIPTOR_PLUGIN); File file = new File(f.toUri()); if (!file.exists()) { - f = filer.getResource(StandardLocation.CLASS_OUTPUT, EMPTY, DESCRIPTOR_MODULE); + f = filer.getResource(StandardLocation.CLASS_OUTPUT, EMPTY, + DESCRIPTOR_MODULE); file = new File(f.toUri()); } return file; } + private Document parseDocument(File file) { Document doc = null; InputStream input = null; @@ -219,8 +218,8 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { doc = builder.newDocument(); doc.appendChild(doc.createElement(EL_MODULE)); } - } - catch (ParserConfigurationException | SAXException | IOException | DOMException ex) { + } catch (ParserConfigurationException | SAXException | IOException + | DOMException ex) { printException("could not parse document", ex); } finally { close(input); @@ -229,6 +228,7 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { return doc; } + private String prepareArrayElement(Object obj) { String v = obj.toString(); @@ -243,6 +243,7 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { return v; } + private void printException(String msg, Throwable throwable) { processingEnv.getMessager().printMessage(Kind.ERROR, msg); @@ -251,11 +252,16 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { processingEnv.getMessager().printMessage(Kind.ERROR, stack); } - private void scanForClassAnnotations(Set descriptorElements, - RoundEnvironment roundEnv, TypeElement annotation, String elementName) { + + private void scanForClassAnnotations( + Set descriptorElements, RoundEnvironment roundEnv, + TypeElement annotation, String elementName) { + Set classes = Sets.newHashSet(); + for (Element e : roundEnv.getElementsAnnotatedWith(annotation)) { - if (e.getKind().isClass() || e.getKind().isInterface()) { + + if (isClassOrInterface(e)) { TypeElement type = (TypeElement) e; String desc = processingEnv.getElementUtils().getDocComment(type); @@ -265,9 +271,7 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { classes.add( new ClassWithAttributes( - type.getQualifiedName().toString(), - desc, - getAttributesFromAnnotation(e, annotation) + type.getQualifiedName().toString(), desc, getAttributesFromAnnotation(e, annotation) ) ); } @@ -276,8 +280,15 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { descriptorElements.add(new ClassSetElement(elementName, classes)); } - private void scanForSubscriberAnnotations(Set descriptorElements, RoundEnvironment roundEnv, - TypeElement annotation) { + + private boolean isClassOrInterface(Element e) { + return e.getKind().isClass() || e.getKind().isInterface(); + } + + + private void scanForSubscriberAnnotations( + Set descriptorElements, RoundEnvironment roundEnv, + TypeElement annotation) { for (Element el : roundEnv.getElementsAnnotatedWith(annotation)) { if (el.getKind() == ElementKind.METHOD) { ExecutableElement ee = (ExecutableElement) el; @@ -305,6 +316,7 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { } } + private void write(Set descriptorElements) { Filer filer = processingEnv.getFiler(); @@ -327,6 +339,7 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { } } + private void writeDocument(Document doc, File file) { Writer writer = null; @@ -346,26 +359,30 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { } } - //~--- get methods ---------------------------------------------------------- - private Map getAttributesFromAnnotation(Element el, TypeElement annotation) { + private Map getAttributesFromAnnotation(Element el, + TypeElement annotation) { Map attributes = Maps.newHashMap(); - for (AnnotationMirror am : el.getAnnotationMirrors()) { - String qn = am.getAnnotationType().asElement().toString(); + for (AnnotationMirror annotationMirror : el.getAnnotationMirrors()) { + String qn = annotationMirror.getAnnotationType().asElement().toString(); if (qn.equals(annotation.toString())) { - for (Entry entry : am.getElementValues().entrySet()) { + for (Entry entry : annotationMirror.getElementValues().entrySet()) { attributes.put(entry.getKey().getSimpleName().toString(), getValue(entry.getValue())); } // add default values - for (ExecutableElement meth : methodsIn(am.getAnnotationType().asElement().getEnclosedElements())) { + for (ExecutableElement meth : methodsIn(annotationMirror.getAnnotationType().asElement().getEnclosedElements())) { String attribute = meth.getSimpleName().toString(); AnnotationValue defaultValue = meth.getDefaultValue(); if (defaultValue != null && !attributes.containsKey(attribute)) { - attributes.put(attribute, getValue(defaultValue)); + String value = getValue(defaultValue); + if (value != null && !value.isEmpty()) { + attributes.put(attribute, value); + } } } } @@ -374,6 +391,7 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { return attributes; } + private String getValue(AnnotationValue v) { String value; Object object = v.getValue(); @@ -388,7 +406,6 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { if (it.hasNext()) { buffer.append(","); } - } value = buffer.toString(); @@ -399,17 +416,17 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { return value; } - //~--- inner classes -------------------------------------------------------- private static final class ClassAnnotation { - private final String elementName; - private final Class annotationClass; + public ClassAnnotation(String elementName, + Class annotationClass) { - ClassAnnotation(String elementName, Class annotationClass) { this.elementName = elementName; this.annotationClass = annotationClass; } + private final Class annotationClass; + private final String elementName; } } diff --git a/scm-annotations/src/main/java/sonia/scm/api/v2/resources/Enrich.java b/scm-annotations/src/main/java/sonia/scm/api/v2/resources/Enrich.java new file mode 100644 index 0000000000..a1269dfc00 --- /dev/null +++ b/scm-annotations/src/main/java/sonia/scm/api/v2/resources/Enrich.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to specify the source of an enricher. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Enrich { + + /** + * Source mapping class. + * + * @return source mapping class + */ + Class value(); +} diff --git a/scm-annotations/src/main/java/sonia/scm/plugin/ExtensionPoint.java b/scm-annotations/src/main/java/sonia/scm/plugin/ExtensionPoint.java index 5417317627..b36a28f993 100644 --- a/scm-annotations/src/main/java/sonia/scm/plugin/ExtensionPoint.java +++ b/scm-annotations/src/main/java/sonia/scm/plugin/ExtensionPoint.java @@ -33,8 +33,6 @@ package sonia.scm.plugin; -//~--- JDK imports ------------------------------------------------------------ - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/scm-annotations/src/main/java/sonia/scm/security/AllowAnonymousAccess.java b/scm-annotations/src/main/java/sonia/scm/security/AllowAnonymousAccess.java new file mode 100644 index 0000000000..b770914d69 --- /dev/null +++ b/scm-annotations/src/main/java/sonia/scm/security/AllowAnonymousAccess.java @@ -0,0 +1,15 @@ +package sonia.scm.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to mark REST resource methods that may be accessed without authentication. + * To mark all methods of a complete class you can annotate the class instead. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AllowAnonymousAccess { +} diff --git a/scm-clients/pom.xml b/scm-clients/pom.xml deleted file mode 100644 index e78be6afa5..0000000000 --- a/scm-clients/pom.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - 4.0.0 - - - sonia.scm - scm - 2.0.0-SNAPSHOT - - - sonia.scm.clients - scm-clients - pom - 2.0.0-SNAPSHOT - scm-clients - - - scm-client-api - scm-client-impl - scm-cli-client - - - - - - - - scm-core - sonia.scm - jar - 2.0.0-SNAPSHOT - - - shiro-core - org.apache.shiro - - - aopalliance - aopalliance - - - guice - com.google.inject - - - guice-multibindings - com.google.inject.extensions - - - guice-servlet - com.google.inject.extensions - - - jersey-core - com.sun.jersey - - - guice-throwingproviders - com.google.inject.extensions - - - commons-lang - commons-lang - - - - - - - diff --git a/scm-clients/scm-cli-client/pom.xml b/scm-clients/scm-cli-client/pom.xml deleted file mode 100644 index 7e32b5c397..0000000000 --- a/scm-clients/scm-cli-client/pom.xml +++ /dev/null @@ -1,218 +0,0 @@ - - - - 4.0.0 - - - scm-clients - sonia.scm.clients - 2.0.0-SNAPSHOT - - - scm-cli-client - 2.0.0-SNAPSHOT - scm-cli-client - - - - - - - javax.servlet - javax.servlet-api - ${servlet.version} - - - - javax.transaction - jta - 1.1 - provided - - - - sonia.scm.clients - scm-client-impl - 2.0.0-SNAPSHOT - - - - args4j - args4j - 2.0.29 - - - - ch.qos.logback - logback-classic - ${logback.version} - - - - org.freemarker - freemarker - 2.3.21 - - - - - - - - - com.mycila.maven-license-plugin - maven-license-plugin - 1.9.0 - -

http://download.scm-manager.org/licenses/mvn-license.txt
- - src/** - **/test/** - - - target/** - .hg/** - **/*.ftl - - true - - - - - org.apache.maven.plugins - maven-assembly-plugin - 2.3 - - - - sonia.scm.cli.App - - - - jar-with-dependencies - - - - - package - - single - - - - - - - - - - - - it - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 2.4 - - - package - - copy - - - - - sonia.scm - scm-webapp - ${project.version} - war - ${project.build.directory}/webapp - scm-webapp.war - - - - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.12 - - - ${project.version} - - - - - integration-test - - integration-test - - - - verify - - verify - - - - - - - org.eclipse.jetty - jetty-maven-plugin - ${jetty.maven.version} - - 8085 - STOP - - - scm.home - target/scm-it - - - file.encoding - UTF-8 - - - - 8081 - - - /scm - - ${project.build.directory}/webapp/scm-webapp.war - 0 - true - - - - start-jetty - pre-integration-test - - deploy-war - - - - stop-jetty - post-integration-test - - stop - - - - - - - - - - - -
diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/App.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/App.java deleted file mode 100644 index 5bf070258e..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/App.java +++ /dev/null @@ -1,339 +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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.LoggerContext; - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import org.kohsuke.args4j.Option; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.cli.cmd.CommandDescriptor; -import sonia.scm.cli.cmd.SubCommand; -import sonia.scm.cli.cmd.SubCommandHandler; -import sonia.scm.cli.cmd.SubCommandOptionHandler; -import sonia.scm.cli.config.ConfigOptionHandler; -import sonia.scm.cli.config.ScmClientConfig; -import sonia.scm.cli.config.ServerConfig; -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintWriter; - -import java.util.ArrayList; -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -public class App -{ - - /** the logger for App */ - private static final Logger logger = LoggerFactory.getLogger(App.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public App() - { - this(System.in, System.out); - } - - /** - * Constructs ... - * - * - * @param input - * @param output - */ - public App(BufferedReader input, PrintWriter output) - { - this.input = input; - this.output = output; - } - - /** - * Constructs ... - * - * - * @param input - * @param output - */ - public App(InputStream input, OutputStream output) - { - this.input = new BufferedReader(new InputStreamReader(input)); - this.output = new PrintWriter(output); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param args - */ - public static void main(String[] args) - { - new App().run(args); - } - - /** - * Method description - * - * - * @param args - */ - protected void run(String[] args) - { - CmdLineParser parser = new CmdLineParser(this); - - try - { - parser.parseArgument(args); - } - catch (CmdLineException ex) - { - - // todo error handling - logger.warn("could not parse commandline", ex); - System.exit(1); - } - - configureLogger(); - loadConfig(); - - I18n i18n = new I18n(); - - if ((args.length == 0) || (subcommand == null) || help) - { - printHelp(parser, i18n); - } - else - { - subcommand.init(input, output, i18n, config); - subcommand.run(arguments); - } - - IOUtil.close(input); - IOUtil.close(output); - } - - /** - * Method description - * - */ - private void configureLogger() - { - LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); - - lc.getLogger(Logger.ROOT_LOGGER_NAME).setLevel(loggingLevel); - } - - /** - * Method description - * - */ - private void loadConfig() - { - if (config == null) - { - config = ScmClientConfig.getInstance().getDefaultConfig(); - } - - if (Util.isNotEmpty(serverUrl)) - { - config.setServerUrl(serverUrl); - } - - if (Util.isNotEmpty(username)) - { - config.setUsername(username); - } - - if (Util.isNotEmpty(password)) - { - config.setPassword(password); - } - } - - /** - * Method description - * - * - * @param parser - * @param i18n - */ - private void printHelp(CmdLineParser parser, I18n i18n) - { - output.println(i18n.getMessage(I18n.USAGE)); - output.println(); - output.append(i18n.getMessage(I18n.OPTIONS)).println(":"); - output.println(); - parser.printUsage(output, i18n.getBundle()); - output.println(); - output.append(i18n.getMessage(I18n.SUBCOMMANDS_TITLE)).println(":"); - output.println(); - - String group = null; - List descList = - SubCommandHandler.getInstance().getDescriptors(); - int length = 0; - - for (CommandDescriptor desc : descList) - { - int l = desc.getName().length(); - - if (l > length) - { - length = l; - } - } - - length += 5; - - for (CommandDescriptor desc : - SubCommandHandler.getInstance().getDescriptors()) - { - if ((group == null) ||!group.equals(desc.getGroup())) - { - output.println(); - group = desc.getGroup(); - output.append(i18n.getMessage(group)).println(":"); - output.println(); - } - - int l = desc.getName().length(); - - output.append(" ").append(desc.getName()); - l = length - l; - - for (int i = 0; i < l; i++) - { - output.append(" "); - } - - output.println(i18n.getMessage(desc.getUsage())); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--config", - usage = "optionConfig", - metaVar = "config", - handler = ConfigOptionHandler.class, - aliases = { "-c" } - ) - private ServerConfig config; - - /** Field description */ - @Option( - name = "--help", - usage = "optionHelpText", - aliases = { "-h" } - ) - private boolean help = false; - - /** Field description */ - @Argument(index = 1, metaVar = "arg") - private List arguments = new ArrayList<>(); - - /** Field description */ - private BufferedReader input; - - /** Field description */ - @Option( - name = "--logging-level", - usage = "optionLoggingLevel", - handler = LoggingLevelOptionHandler.class, - aliases = { "-l" } - ) - private Level loggingLevel = Level.ERROR; - - /** Field description */ - private PrintWriter output; - - /** Field description */ - @Option( - name = "--password", - usage = "optionPassword", - aliases = { "-p" } - ) - private String password; - - /** Field description */ - @Option( - name = "--server", - usage = "optionServerUrl", - aliases = { "-s" } - ) - private String serverUrl; - - /** Field description */ - @Argument( - index = 0, - metaVar = "metaVar_command", - handler = SubCommandOptionHandler.class - ) - private SubCommand subcommand; - - /** Field description */ - @Option( - name = "--user", - usage = "optionUsername", - aliases = { "-u" } - ) - private String username; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/BooleanModifyOptionHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/BooleanModifyOptionHandler.java deleted file mode 100644 index 46d18badb6..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/BooleanModifyOptionHandler.java +++ /dev/null @@ -1,100 +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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import org.kohsuke.args4j.OptionDef; -import org.kohsuke.args4j.spi.OptionHandler; -import org.kohsuke.args4j.spi.Parameters; -import org.kohsuke.args4j.spi.Setter; - -/** - * - * @author Sebastian Sdorra - */ -public class BooleanModifyOptionHandler extends OptionHandler -{ - - /** - * Constructs ... - * - * - * @param parser - * @param option - * @param setter - */ - public BooleanModifyOptionHandler(CmdLineParser parser, OptionDef option, - Setter setter) - { - super(parser, option, setter); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param params - * - * @return - * - * @throws CmdLineException - */ - @Override - public int parseArguments(Parameters params) throws CmdLineException - { - Boolean bool = Boolean.valueOf(params.getParameter(0)); - - setter.addValue(bool); - - return 1; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getDefaultMetaVariable() - { - return I18n.BOOLEAN; - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/I18n.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/I18n.java deleted file mode 100644 index 8147c59b44..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/I18n.java +++ /dev/null @@ -1,138 +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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.MissingResourceException; -import java.util.ResourceBundle; - -/** - * - * @author Sebastian Sdorra - */ -public class I18n -{ - - /** Field description */ - public static final String ERROR = "error"; - - /** Field description */ - public static final String GROUP_NOT_FOUND = "groupNotFound"; - - /** Field description */ - public static final String LEVEL = "level"; - - - public static final String BOOLEAN = "boolean"; - - /** Field description */ - public static final String OPTIONS = "options"; - - /** Field description */ - public static final String REPOSITORY_NOT_FOUND = "repositoryNotFound"; - - /** Field description */ - public static final String RESOURCE_BUNDLE = "sonia.resources.i18n"; - - /** Field description */ - public static final String SUBCOMMANDS_TITLE = "subCommandsTitle"; - - /** Field description */ - public static final String USAGE = "usage"; - - /** Field description */ - public static final String USER_NOT_FOUND = "userNotFound"; - - /** the logger for I18n */ - private static final Logger logger = LoggerFactory.getLogger(I18n.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public I18n() - { - bundle = ResourceBundle.getBundle(RESOURCE_BUNDLE); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public ResourceBundle getBundle() - { - return bundle; - } - - /** - * Method description - * - * - * @param key - * - * @return - */ - public String getMessage(String key) - { - String value = key; - - try - { - value = bundle.getString(key); - } - catch (MissingResourceException ex) - { - logger.warn("could not find resource for key {}", key); - } - - return value; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private ResourceBundle bundle; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/LoggingLevelOptionHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/LoggingLevelOptionHandler.java deleted file mode 100644 index 2622abecfb..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/LoggingLevelOptionHandler.java +++ /dev/null @@ -1,104 +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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import ch.qos.logback.classic.Level; - -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import org.kohsuke.args4j.OptionDef; -import org.kohsuke.args4j.spi.OptionHandler; -import org.kohsuke.args4j.spi.Parameters; -import org.kohsuke.args4j.spi.Setter; - -/** - * - * @author Sebastian Sdorra - */ -public class LoggingLevelOptionHandler extends OptionHandler -{ - - /** - * Constructs ... - * - * - * @param parser - * @param option - * @param setter - */ - public LoggingLevelOptionHandler(CmdLineParser parser, OptionDef option, - Setter setter) - { - super(parser, option, setter); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param params - * - * @return - * - * @throws CmdLineException - */ - @Override - public int parseArguments(Parameters params) throws CmdLineException - { - String value = params.getParameter(0); - Level l = Level.toLevel(value); - - setter.addValue(l); - - return 1; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getDefaultMetaVariable() - { - return I18n.LEVEL; - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/SimpleLocalizable.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/SimpleLocalizable.java deleted file mode 100644 index 33fc3ef0b5..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/SimpleLocalizable.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2014, 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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Localizable; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Locale; - -/** - * TODO create real implementation - * - * @author Sebastian Sdorra - */ -public class SimpleLocalizable implements Localizable -{ - - /** - * Constructs ... - * - * - * @param message - */ - public SimpleLocalizable(String message) - { - this.message = message; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param args - * - * @return - */ - @Override - public String format(Object... args) - { - return message; - } - - /** - * Method description - * - * - * @param locale - * @param args - * - * @return - */ - @Override - public String formatWithLocale(Locale locale, Object... args) - { - return message; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final String message; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/AddMembersSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/AddMembersSubCommand.java deleted file mode 100644 index 2538367fb0..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/AddMembersSubCommand.java +++ /dev/null @@ -1,68 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.group.Group; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "add-members", - usage = "usageAddMember", - group = "group" -) -public class AddMembersSubCommand extends MembersSubCommand -{ - - /** - * Method description - * - * - * @param group - * @param members - */ - @Override - protected void modifyMembers(Group group, List members) - { - group.getMembers().addAll(members); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/AddPermissionSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/AddPermissionSubCommand.java deleted file mode 100644 index 621ebe54de..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/AddPermissionSubCommand.java +++ /dev/null @@ -1,147 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Option; - -import sonia.scm.repository.Permission; -import sonia.scm.repository.PermissionType; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "add-permission", - usage = "usageAddPermission", - group = "repository" -) -public class AddPermissionSubCommand extends PermissionSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public PermissionType getType() - { - return type; - } - - /** - * Method description - * - * - * @return - */ - public boolean isGroup() - { - return group; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param group - */ - public void setGroup(boolean group) - { - this.group = group; - } - - /** - * Method description - * - * - * @param type - */ - public void setType(PermissionType type) - { - this.type = type; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param permissions - */ - @Override - protected void modifyPermissions(List permissions) - { - permissions.add(new Permission(name, type, group)); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--group", - usage = "optionPermissionGroup", - aliases = { "-g" } - ) - private boolean group = false; - - /** Field description */ - @Option( - name = "--name", - usage = "optionPermissionName", - required = true, - aliases = { "-n" } - ) - private String name; - - /** Field description */ - @Option( - name = "--type", - usage = "optionPermissionType", - required = true, - metaVar = "permissiontype", - aliases = { "-t" } - ) - private PermissionType type; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/Command.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/Command.java deleted file mode 100644 index ea6e988dae..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/Command.java +++ /dev/null @@ -1,53 +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.cli.cmd; - -//~--- JDK imports ------------------------------------------------------------ - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * - * @author Sebastian Sdorra - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -public @interface Command -{ - String name() default ""; - String usage() default ""; - String group() default "misc"; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CommandDescriptor.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CommandDescriptor.java deleted file mode 100644 index ce51657ea9..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CommandDescriptor.java +++ /dev/null @@ -1,204 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.util.AssertUtil; -import sonia.scm.util.Util; - -/** - * - * @author Sebastian Sdorra - */ -public class CommandDescriptor implements Comparable -{ - - /** the logger for CommandDescriptor */ - private static final Logger logger = - LoggerFactory.getLogger(CommandDescriptor.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param commandClass - */ - public CommandDescriptor(Class commandClass) - { - AssertUtil.assertIsNotNull(commandClass); - this.commandClass = commandClass; - - Command cmd = commandClass.getAnnotation(Command.class); - - if (cmd != null) - { - this.name = cmd.name(); - this.group = cmd.group(); - this.usage = cmd.usage(); - } - - if (Util.isEmpty(name)) - { - name = commandClass.getSimpleName(); - } - } - - /** - * Constructs ... - * - * - * @param name - * @param usage - * @param commandClass - */ - public CommandDescriptor(String name, String usage, - Class commandClass) - { - this.name = name; - this.usage = usage; - this.commandClass = commandClass; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param desc - * - * @return - */ - @Override - public int compareTo(CommandDescriptor desc) - { - int result = group.compareTo(desc.group); - - if (result == 0) - { - result = name.compareTo(desc.name); - } - - return result; - } - - /** - * Method description - * - * - * @return - */ - public SubCommand createSubCommand() - { - SubCommand command = null; - - try - { - command = commandClass.newInstance(); - command.setCommandName(name); - } - catch (Exception ex) - { - logger.error("could not create SubCommand {}", commandClass.getName()); - } - - return command; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Class getCommandClass() - { - return commandClass; - } - - /** - * Method description - * - * - * @return - */ - public String getGroup() - { - return group; - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public String getUsage() - { - return usage; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Class commandClass; - - /** Field description */ - private String group = "misc"; - - /** Field description */ - private String name; - - /** Field description */ - private String usage = ""; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CreateGroupSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CreateGroupSubCommand.java deleted file mode 100644 index 5fba296532..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CreateGroupSubCommand.java +++ /dev/null @@ -1,212 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Option; - -import sonia.scm.cli.wrapper.GroupWrapper; -import sonia.scm.client.ScmClientSession; -import sonia.scm.group.Group; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "create-group", - usage = "usageCreateGroup", - group = "group" -) -public class CreateGroupSubCommand extends TemplateSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getDescription() - { - return description; - } - - /** - * Method description - * - * - * @return - */ - public List getMembers() - { - return members; - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public String getType() - { - return type; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param description - */ - public void setDescription(String description) - { - this.description = description; - } - - /** - * Method description - * - * - * @param members - */ - public void setMembers(List members) - { - this.members = members; - } - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Method description - * - * - * @param type - */ - public void setType(String type) - { - this.type = type; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - Group group = new Group(); - - group.setName(name); - group.setDescription(description); - group.setType(type); - group.setMembers(members); - - ScmClientSession session = createSession(); - - session.getGroupHandler().create(group); - - Map env = new HashMap<>(); - - env.put("group", new GroupWrapper(group)); - renderTemplate(env, GetGroupSubCommand.TEMPLATE); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--description", - usage = "optionGroupDescription", - aliases = { "-d" } - ) - private String description; - - /** Field description */ - @Option( - name = "--member", - usage = "optionGroupMember", - aliases = { "-m" } - ) - private List members; - - /** Field description */ - @Option( - name = "--name", - usage = "optionGroupName", - required = true, - aliases = { "-n" } - ) - private String name; - - /** Field description */ - @Option( - name = "--type", - usage = "optionGroupType", - aliases = { "-t" } - ) - private String type = "xml"; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CreateRepositorySubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CreateRepositorySubCommand.java deleted file mode 100644 index a0d675e2be..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CreateRepositorySubCommand.java +++ /dev/null @@ -1,220 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Option; - -import sonia.scm.cli.wrapper.RepositoryWrapper; -import sonia.scm.client.ScmClientSession; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "create-repository", - usage = "usageCreateRepository", - group = "repository" -) -public class CreateRepositorySubCommand extends TemplateSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getContact() - { - return contact; - } - - /** - * Method description - * - * - * @return - */ - public String getDescription() - { - return description; - } - - /** - * Method description - * - * - * @return - */ - public String getType() - { - return type; - } - - /** - * Method description - * - * - * @return - */ - public boolean isPublicReadable() - { - return publicReadable; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param contact - */ - public void setContact(String contact) - { - this.contact = contact; - } - - /** - * Method description - * - * - * @param description - */ - public void setDescription(String description) - { - this.description = description; - } - - /** - * Method description - * - * - * @param publicReadable - */ - public void setPublicReadable(boolean publicReadable) - { - this.publicReadable = publicReadable; - } - - /** - * Method description - * - * - * @param type - */ - public void setType(String type) - { - this.type = type; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - Repository repository = new Repository(); - - repository.setName(name); - repository.setType(type); - repository.setContact(contact); - repository.setDescription(description); - - ScmClientSession session = createSession(); - - session.getRepositoryHandler().create(repository); - - Map env = new HashMap<>(); - - env.put("repository", new RepositoryWrapper(config, repository)); - renderTemplate(env, GetRepositorySubCommand.TEMPLATE); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--contact", - usage = "optionRepositoryContact", - aliases = { "-c" } - ) - private String contact; - - /** Field description */ - @Option( - name = "--description", - usage = "optionRepositoryDescription", - aliases = { "-d" } - ) - private String description; - - /** Field description */ - @Option( - name = "--name", - required = true, - usage = "optionRepositoryName", - aliases = { "-n" } - ) - private String name; - - /** Field description */ - @Option( - name = "--public", - usage = "optionRepositoryPublic", - aliases = { "-p" } - ) - private boolean publicReadable; - - /** Field description */ - @Option( - name = "--type", - required = true, - usage = "optionRepositoryType", - aliases = { "-t" } - ) - private String type; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CreateUserSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CreateUserSubCommand.java deleted file mode 100644 index 805c05d83b..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/CreateUserSubCommand.java +++ /dev/null @@ -1,274 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Option; - -import sonia.scm.cli.wrapper.UserWrapper; -import sonia.scm.client.ScmClientSession; -import sonia.scm.user.User; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "create-user", - usage = "usageCreateUser", - group = "user" -) -public class CreateUserSubCommand extends TemplateSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getDisplayName() - { - return displayName; - } - - /** - * Method description - * - * - * @return - */ - public String getMail() - { - return mail; - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public String getPassword() - { - return password; - } - - /** - * Method description - * - * - * @return - */ - public String getType() - { - return type; - } - - /** - * Method description - * - * - * @return - */ - public boolean isAdmin() - { - return admin; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param admin - */ - public void setAdmin(boolean admin) - { - this.admin = admin; - } - - /** - * Method description - * - * - * @param displayName - */ - public void setDisplayName(String displayName) - { - this.displayName = displayName; - } - - /** - * Method description - * - * - * @param mail - */ - public void setMail(String mail) - { - this.mail = mail; - } - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Method description - * - * - * @param password - */ - public void setPassword(String password) - { - this.password = password; - } - - /** - * Method description - * - * - * @param type - */ - public void setType(String type) - { - this.type = type; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - User user = new User(); - - user.setName(name); - user.setAdmin(admin); - user.setDisplayName(displayName); - user.setPassword(password); - user.setMail(mail); - user.setType(type); - - ScmClientSession session = createSession(); - - session.getUserHandler().create(user); - - Map env = new HashMap<>(); - - env.put("user", new UserWrapper(user)); - renderTemplate(env, GetUserSubCommand.TEMPLATE); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--admin", - usage = "optionUserAdmin", - aliases = { "-a" } - ) - private boolean admin = false; - - /** Field description */ - @Option( - name = "--display-name", - usage = "optionUserDisplayName", - required = true, - aliases = { "-d" } - ) - private String displayName; - - /** Field description */ - @Option( - name = "--mail", - usage = "optionUserMail", - aliases = { "-m" } - ) - private String mail; - - /** Field description */ - @Option( - name = "--name", - usage = "optionUserName", - required = true, - aliases = { "-n" } - ) - private String name; - - /** Field description */ - @Option( - name = "--password", - usage = "optionUserPassword", - aliases = { "-p" } - ) - private String password; - - /** Field description */ - @Option( - name = "--type", - usage = "optionUserType", - aliases = { "-t" } - ) - private String type = "xml"; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteConfigSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteConfigSubCommand.java deleted file mode 100644 index 93c2ad253e..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteConfigSubCommand.java +++ /dev/null @@ -1,61 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.cli.config.ScmClientConfig; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "delete-config", - usage = "usageDeleteConfig", - group = "config" -) -public class DeleteConfigSubCommand extends SubCommand -{ - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientConfig.getInstance().delete(); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteGroupSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteGroupSubCommand.java deleted file mode 100644 index 345cf5baec..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteGroupSubCommand.java +++ /dev/null @@ -1,101 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; - -import sonia.scm.client.ScmClientSession; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "delete-group", - usage = "usageDeleteGroup", - group = "group" -) -public class DeleteGroupSubCommand extends SubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - - session.getGroupHandler().delete(name); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Argument( - usage = "optionGroupName", - metaVar = "groupname", - required = true - ) - private String name; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteMembersSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteMembersSubCommand.java deleted file mode 100644 index 449fab5181..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteMembersSubCommand.java +++ /dev/null @@ -1,68 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.group.Group; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "delete-members", - usage = "usageDeleteMembers", - group = "group" -) -public class DeleteMembersSubCommand extends MembersSubCommand -{ - - /** - * Method description - * - * - * @param group - * @param members - */ - @Override - protected void modifyMembers(Group group, List members) - { - group.getMembers().removeAll(members); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeletePermissionSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeletePermissionSubCommand.java deleted file mode 100644 index 2b482c0f4e..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeletePermissionSubCommand.java +++ /dev/null @@ -1,105 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Option; -import sonia.scm.repository.Permission; - -import java.util.List; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "delete-permission", - usage = "usageDeletePermission", - group = "repository" -) -public class DeletePermissionSubCommand extends PermissionSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param permissions - */ - @Override - protected void modifyPermissions(List permissions) - { - permissions.removeIf(p -> name.equals(p.getName())); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--name", - usage = "optionPermissionName", - required = true, - aliases = { "-n" } - ) - private String name; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteRepositorySubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteRepositorySubCommand.java deleted file mode 100644 index 742eb72f80..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteRepositorySubCommand.java +++ /dev/null @@ -1,101 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; - -import sonia.scm.client.ScmClientSession; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "delete-repository", - usage = "usageDeleteRepository", - group = "repository" -) -public class DeleteRepositorySubCommand extends SubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return id; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param id - */ - public void setId(String id) - { - this.id = id; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - - session.getRepositoryHandler().delete(id); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Argument( - usage = "optionRepositoryId", - metaVar = "repositoryid", - required = true - ) - private String id; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteUserSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteUserSubCommand.java deleted file mode 100644 index c254a2e7e3..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/DeleteUserSubCommand.java +++ /dev/null @@ -1,101 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; - -import sonia.scm.client.ScmClientSession; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "delete-user", - usage = "usageDeleteUser", - group = "user" -) -public class DeleteUserSubCommand extends SubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - - session.getUserHandler().delete(name); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Argument( - usage = "optionUserName", - metaVar = "username", - required = true - ) - private String name; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/EncryptSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/EncryptSubCommand.java deleted file mode 100644 index af76698062..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/EncryptSubCommand.java +++ /dev/null @@ -1,98 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; - -/** - * - * @author Sebastian Sdorra - * @since 1.41 - */ -@Command( - name = "encrypt", - usage = "usageEncrypt", - group = "security" -) -public class EncryptSubCommand extends SubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getValue() - { - return value; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param value - */ - public void setValue(String value) - { - this.value = value; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - String enc = createSession().getSecurityHandler().encrypt(value); - - output.println(enc); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Argument( - usage = "optionEncryptValue", - metaVar = "value", - required = true - ) - private String value; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GenerateKeySubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GenerateKeySubCommand.java deleted file mode 100644 index b8658391b0..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GenerateKeySubCommand.java +++ /dev/null @@ -1,55 +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.cli.cmd; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "generate-key", - usage = "usageGenerateKey", - group = "security" -) -public class GenerateKeySubCommand extends SubCommand -{ - - /** - * Method description - * - */ - @Override - protected void run() - { - output.println(createSession().getSecurityHandler().generateKey()); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GetGroupSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GetGroupSubCommand.java deleted file mode 100644 index 9bb02a4703..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GetGroupSubCommand.java +++ /dev/null @@ -1,125 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; - -import sonia.scm.cli.I18n; -import sonia.scm.cli.wrapper.GroupWrapper; -import sonia.scm.client.ScmClientSession; -import sonia.scm.group.Group; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "get-group", - usage = "usageGetGroup", - group = "group" -) -public class GetGroupSubCommand extends TemplateSubCommand -{ - - /** Field description */ - public static final String TEMPLATE = "/sonia/resources/get-group.ftl"; - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - Group group = session.getGroupHandler().get(name); - - if (group != null) - { - Map env = new HashMap<>(); - - env.put("group", new GroupWrapper(group)); - renderTemplate(env, TEMPLATE); - } - else - { - output.println(i18n.getMessage(I18n.GROUP_NOT_FOUND)); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Argument( - usage = "optionGroupName", - metaVar = "groupname", - required = true - ) - private String name; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GetRepositorySubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GetRepositorySubCommand.java deleted file mode 100644 index 5dd27db525..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GetRepositorySubCommand.java +++ /dev/null @@ -1,140 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; - -import sonia.scm.cli.I18n; -import sonia.scm.cli.wrapper.RepositoryWrapper; -import sonia.scm.client.ScmClientSession; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "get-repository", - usage = "usageGetRepository", - group = "repository" -) -public class GetRepositorySubCommand extends TemplateSubCommand -{ - - /** Field description */ - public static final String TEMPLATE = "/sonia/resources/get-repository.ftl"; - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return id; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param id - */ - public void setId(String id) - { - this.id = id; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - - Repository repository; - - int index = id.indexOf("/"); - - if (index > 0) - { - String type = id.substring(0, index); - String name = id.substring(index + 1); - - repository = session.getRepositoryHandler().get(type, name); - } - else - { - repository = session.getRepositoryHandler().get(id); - } - - if (repository != null) - { - Map env = new HashMap<>(); - - env.put("repository", new RepositoryWrapper(config, repository)); - renderTemplate(env, TEMPLATE); - } - else - { - output.println(i18n.getMessage(I18n.REPOSITORY_NOT_FOUND)); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Argument( - usage = "optionRepositoryIdOrTypeAndName", - metaVar = "repositoryid", - required = true - ) - private String id; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GetUserSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GetUserSubCommand.java deleted file mode 100644 index 2356d6323a..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/GetUserSubCommand.java +++ /dev/null @@ -1,125 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; - -import sonia.scm.cli.I18n; -import sonia.scm.cli.wrapper.UserWrapper; -import sonia.scm.client.ScmClientSession; -import sonia.scm.user.User; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "get-user", - usage = "usageGetUser", - group = "user" -) -public class GetUserSubCommand extends TemplateSubCommand -{ - - /** Field description */ - public static final String TEMPLATE = "/sonia/resources/get-user.ftl"; - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - User user = session.getUserHandler().get(name); - - if (user != null) - { - Map env = new HashMap<>(); - - env.put("user", new UserWrapper(user)); - renderTemplate(env, TEMPLATE); - } - else - { - output.println(i18n.getMessage(I18n.USER_NOT_FOUND)); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Argument( - usage = "optionUserName", - metaVar = "username", - required = true - ) - private String name; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportBundleSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportBundleSubCommand.java deleted file mode 100644 index d746df0828..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportBundleSubCommand.java +++ /dev/null @@ -1,186 +0,0 @@ -/** -* Copyright (c) 2014, 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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.io.Files; - -import org.kohsuke.args4j.Option; - -import sonia.scm.ConfigurationException; -import sonia.scm.client.ImportBundleRequest; -import sonia.scm.client.ScmClientSession; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; - -/** - * - * @author Sebastian Sdorra - * @since 1.43 - */ -@Command( - name = "import-from-bundle", - usage = "usageImportBundle", - group = "repository" -) -public class ImportBundleSubCommand extends ImportSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public File getBundle() - { - return bundle; - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public boolean isCompressed() - { - return compressed; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param bundle - */ - public void setBundle(File bundle) - { - this.bundle = bundle; - } - - /** - * Method description - * - * - * @param compressed - */ - public void setCompressed(boolean compressed) - { - this.compressed = compressed; - } - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - if (!bundle.exists()) - { - throw new ConfigurationException("could not find bundle"); - } - else - { - ScmClientSession session = createSession(); - - ImportBundleRequest req = new ImportBundleRequest(getType(), name, - Files.asByteSource(bundle)); - - req.setCompressed(compressed); - - Repository repository = - session.getRepositoryHandler().importFromBundle(req); - - printImportedRepository(repository); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--bundle", - required = true, - usage = "optionRepositoryBundle", - aliases = { "-b" } - ) - private File bundle; - - /** Field description */ - @Option( - name = "--compressed", - usage = "optionRepositoryBundleCompressed", - aliases = { "-c" } - ) - private boolean compressed = false; - - /** Field description */ - @Option( - name = "--name", - required = true, - usage = "optionRepositoryName", - aliases = { "-n" } - ) - private String name; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportDirectorySubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportDirectorySubCommand.java deleted file mode 100644 index 788eef4fae..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportDirectorySubCommand.java +++ /dev/null @@ -1,82 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.collect.Maps; - -import sonia.scm.client.ImportResultWrapper; -import sonia.scm.client.ScmClientSession; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - * @since 1.43 - */ -@Command( - name = "import-from-directory", - usage = "usageImportDirectory", - group = "repository" -) -public class ImportDirectorySubCommand extends ImportSubCommand -{ - - /** Field description */ - public static final String TEMPLATE = - "/sonia/resources/import-from-directory.ftl"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - ImportResultWrapper wrapper = - session.getRepositoryHandler().importFromDirectory(getType()); - Map env = Maps.newHashMap(); - - env.put("importedDirectories", wrapper.getImportedDirectories()); - env.put("failedDirectories", wrapper.getFailedDirectories()); - renderTemplate(env, TEMPLATE); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportSubCommand.java deleted file mode 100644 index 7c8efe704d..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportSubCommand.java +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright (c) 2014, 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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.collect.Maps; - -import org.kohsuke.args4j.Argument; - -import sonia.scm.cli.wrapper.RepositoryWrapper; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - * @since 1.43 - */ -public abstract class ImportSubCommand extends TemplateSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getType() - { - return type; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param type - */ - public void setType(String type) - { - this.type = type; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - */ - protected void printImportedRepository(Repository repository) - { - Map env = Maps.newHashMap(); - - env.put("repository", new RepositoryWrapper(config, repository)); - renderTemplate(env, GetRepositorySubCommand.TEMPLATE); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Argument( - usage = "optionRepositoryType", - metaVar = "repositorytype", - required = true - ) - private String type; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportUrlSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportUrlSubCommand.java deleted file mode 100644 index 7b1ac86237..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ImportUrlSubCommand.java +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright (c) 2014, 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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - - -import org.kohsuke.args4j.Option; -import sonia.scm.client.ImportUrlRequest; -import sonia.scm.client.ScmClientSession; -import sonia.scm.repository.Repository; - -import java.net.URL; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "import-from-url", - usage = "usageImportUrl", - group = "repository" -) -public class ImportUrlSubCommand extends ImportSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public URL getUrl() - { - return url; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Method description - * - * - * @param url - */ - public void setUrl(URL url) - { - this.url = url; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - - ImportUrlRequest request = new ImportUrlRequest(getType(), name, - url.toExternalForm()); - Repository repository = - session.getRepositoryHandler().importFromUrl(request); - - printImportedRepository(repository); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--name", - required = true, - usage = "optionRepositoryName", - aliases = { "-n" } - ) - private String name; - - /** Field description */ - @Option( - name = "--url", - required = true, - usage = "optionRemoteRepositoryUrl", - aliases = { "-r" } - ) - private URL url; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ListGroupsSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ListGroupsSubCommand.java deleted file mode 100644 index 80b9428f6b..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ListGroupsSubCommand.java +++ /dev/null @@ -1,79 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.cli.wrapper.WrapperUtil; -import sonia.scm.client.ScmClientSession; -import sonia.scm.group.Group; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "list-groups", - usage = "usageListGroups", - group = "group" -) -public class ListGroupsSubCommand extends TemplateSubCommand -{ - - /** Field description */ - public static final String TEMPLATE = "/sonia/resources/list-groups.ftl"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - List groups = session.getGroupHandler().getAll(); - Map env = new HashMap<>(); - - env.put("groups", WrapperUtil.wrapGroups(groups)); - renderTemplate(env, TEMPLATE); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ListRepositoriesSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ListRepositoriesSubCommand.java deleted file mode 100644 index f876d2a878..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ListRepositoriesSubCommand.java +++ /dev/null @@ -1,80 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.cli.wrapper.WrapperUtil; -import sonia.scm.client.ScmClientSession; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "list-repositories", - usage = "usageListRepositories", - group = "repository" -) -public class ListRepositoriesSubCommand extends TemplateSubCommand -{ - - /** Field description */ - public static final String TEMPLATE = - "/sonia/resources/list-repositories.ftl"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - List repositories = session.getRepositoryHandler().getAll(); - Map env = new HashMap<>(); - - env.put("repositories", WrapperUtil.wrapRepositories(config, repositories)); - renderTemplate(env, TEMPLATE); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ListUsersSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ListUsersSubCommand.java deleted file mode 100644 index 9f17f59160..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ListUsersSubCommand.java +++ /dev/null @@ -1,79 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.cli.wrapper.WrapperUtil; -import sonia.scm.client.ScmClientSession; -import sonia.scm.user.User; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "list-users", - usage = "usageListUsers", - group = "user" -) -public class ListUsersSubCommand extends TemplateSubCommand -{ - - /** Field description */ - public static final String TEMPLATE = "/sonia/resources/list-users.ftl"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - List users = session.getUserHandler().getAll(); - Map env = new HashMap<>(); - - env.put("users", WrapperUtil.wrapUsers(users)); - renderTemplate(env, TEMPLATE); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/MembersSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/MembersSubCommand.java deleted file mode 100644 index 490a239cd3..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/MembersSubCommand.java +++ /dev/null @@ -1,164 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; - -import sonia.scm.cli.I18n; -import sonia.scm.cli.wrapper.GroupWrapper; -import sonia.scm.client.GroupClientHandler; -import sonia.scm.client.ScmClientSession; -import sonia.scm.group.Group; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class MembersSubCommand extends TemplateSubCommand -{ - - /** - * Method description - * - * - * @param group - * @param members - */ - protected abstract void modifyMembers(Group group, List members); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public List getMembers() - { - return members; - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param members - */ - public void setMembers(List members) - { - this.members = members; - } - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - GroupClientHandler handler = session.getGroupHandler(); - Group group = handler.get(name); - - if (group != null) - { - modifyMembers(group, members); - handler.modify(group); - - Map env = new HashMap<>(); - - env.put("group", new GroupWrapper(group)); - renderTemplate(env, GetGroupSubCommand.TEMPLATE); - } - else - { - output.println(i18n.getMessage(I18n.GROUP_NOT_FOUND)); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--member", - usage = "optionGroupMember", - required = true, - aliases = { "-m" } - ) - private List members; - - /** Field description */ - @Argument( - usage = "optionGroupName", - metaVar = "groupname", - required = true - ) - private String name; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ModifyGroupSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ModifyGroupSubCommand.java deleted file mode 100644 index 6cb151e22d..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ModifyGroupSubCommand.java +++ /dev/null @@ -1,139 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; - -import sonia.scm.cli.I18n; -import sonia.scm.cli.wrapper.GroupWrapper; -import sonia.scm.client.GroupClientHandler; -import sonia.scm.client.ScmClientSession; -import sonia.scm.group.Group; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "modify-group", - usage = "usageModifyGroup", - group = "group" -) -public class ModifyGroupSubCommand extends TemplateSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getDescription() - { - return description; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param description - */ - public void setDescription(String description) - { - this.description = description; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - GroupClientHandler handler = session.getGroupHandler(); - Group group = handler.get(name); - - if (group != null) - { - if (Util.isNotEmpty(description)) - { - group.setDescription(description); - } - - handler.modify(group); - - Map env = new HashMap<>(); - - env.put("group", new GroupWrapper(group)); - renderTemplate(env, GetGroupSubCommand.TEMPLATE); - } - else - { - output.println(i18n.getMessage(I18n.GROUP_NOT_FOUND)); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--description", - usage = "optionGroupDescription", - aliases = { "-d" } - ) - private String description; - - /** Field description */ - @Argument( - usage = "optionGroupName", - metaVar = "groupname", - required = true - ) - private String name; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ModifyRepositorySubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ModifyRepositorySubCommand.java deleted file mode 100644 index b0d64242d0..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ModifyRepositorySubCommand.java +++ /dev/null @@ -1,269 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; - -import sonia.scm.cli.BooleanModifyOptionHandler; -import sonia.scm.cli.I18n; -import sonia.scm.cli.wrapper.RepositoryWrapper; -import sonia.scm.client.RepositoryClientHandler; -import sonia.scm.client.ScmClientSession; -import sonia.scm.repository.Repository; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "modify-repository", - usage = "usageModifyRepository", - group = "repository" -) -public class ModifyRepositorySubCommand extends TemplateSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public Boolean getArchvied() - { - return archvied; - } - - /** - * Method description - * - * - * @return - */ - public String getContact() - { - return contact; - } - - /** - * Method description - * - * - * @return - */ - public String getDescription() - { - return description; - } - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return id; - } - - /** - * Method description - * - * - * @return - */ - public Boolean getPublicReadable() - { - return publicReadable; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param archvied - */ - public void setArchvied(Boolean archvied) - { - this.archvied = archvied; - } - - /** - * Method description - * - * - * @param contact - */ - public void setContact(String contact) - { - this.contact = contact; - } - - /** - * Method description - * - * - * @param description - */ - public void setDescription(String description) - { - this.description = description; - } - - /** - * Method description - * - * - * @param id - */ - public void setId(String id) - { - this.id = id; - } - - /** - * Method description - * - * - * @param publicReadable - */ - public void setPublicReadable(Boolean publicReadable) - { - this.publicReadable = publicReadable; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - RepositoryClientHandler handler = session.getRepositoryHandler(); - Repository repository = handler.get(id); - - if (repository != null) - { - if (Util.isNotEmpty(contact)) - { - repository.setContact(contact); - } - - if (Util.isNotEmpty(description)) - { - repository.setDescription(description); - } - - if (archvied != null) - { - repository.setArchived(archvied); - } - - if (publicReadable != null) - { - repository.setPublicReadable(publicReadable); - } - - handler.modify(repository); - - Map env = new HashMap<>(); - - env.put("repository", new RepositoryWrapper(config, repository)); - renderTemplate(env, GetRepositorySubCommand.TEMPLATE); - } - else - { - output.println(i18n.getMessage(I18n.REPOSITORY_NOT_FOUND)); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--archived", - usage = "optionRepositoryArchive", - aliases = { "-a" }, - handler = BooleanModifyOptionHandler.class - ) - private Boolean archvied; - - /** Field description */ - @Option( - name = "--contact", - usage = "optionRepositoryContact", - aliases = { "-c" } - ) - private String contact; - - /** Field description */ - @Option( - name = "--description", - usage = "optionRepositoryDescription", - aliases = { "-d" } - ) - private String description; - - /** Field description */ - @Argument( - usage = "optionRepositoryId", - metaVar = "repositoryid", - required = true - ) - private String id; - - /** Field description */ - @Option( - name = "--public", - usage = "optionRepositoryPublic", - aliases = { "-p" }, - handler = BooleanModifyOptionHandler.class - ) - private Boolean publicReadable; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ModifyUserSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ModifyUserSubCommand.java deleted file mode 100644 index 63a25d5d07..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ModifyUserSubCommand.java +++ /dev/null @@ -1,231 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; - -import sonia.scm.cli.I18n; -import sonia.scm.cli.wrapper.UserWrapper; -import sonia.scm.client.ScmClientSession; -import sonia.scm.client.UserClientHandler; -import sonia.scm.user.User; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "modify-user", - usage = "usageModifyUser", - group = "user" -) -public class ModifyUserSubCommand extends TemplateSubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getDisplayName() - { - return displayName; - } - - /** - * Method description - * - * - * @return - */ - public String getMail() - { - return mail; - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public String getPassword() - { - return password; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param displayName - */ - public void setDisplayName(String displayName) - { - this.displayName = displayName; - } - - /** - * Method description - * - * - * @param mail - */ - public void setMail(String mail) - { - this.mail = mail; - } - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Method description - * - * - * @param password - */ - public void setPassword(String password) - { - this.password = password; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - UserClientHandler handler = session.getUserHandler(); - User user = handler.get(name); - - if (user != null) - { - if (Util.isNotEmpty(displayName)) - { - user.setDisplayName(displayName); - } - - if (Util.isNotEmpty(mail)) - { - user.setMail(mail); - } - - if (Util.isNotEmpty(password)) - { - user.setPassword(password); - } - - handler.modify(user); - - Map env = new HashMap<>(); - - env.put("user", new UserWrapper(user)); - renderTemplate(env, GetUserSubCommand.TEMPLATE); - } - else - { - output.println(i18n.getMessage(I18n.USER_NOT_FOUND)); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option( - name = "--display-name", - usage = "optionUserDisplayName", - aliases = { "-d" } - ) - private String displayName; - - /** Field description */ - @Option( - name = "--mail", - usage = "optionUserMail", - aliases = { "-m" } - ) - private String mail; - - /** Field description */ - @Argument( - usage = "optionUserName", - metaVar = "username", - required = true - ) - private String name; - - /** Field description */ - @Option( - name = "--password", - usage = "optionUserPassword", - aliases = { "-p" } - ) - private String password; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/PermissionSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/PermissionSubCommand.java deleted file mode 100644 index d40c50c38a..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/PermissionSubCommand.java +++ /dev/null @@ -1,141 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.Argument; - -import sonia.scm.cli.I18n; -import sonia.scm.cli.wrapper.RepositoryWrapper; -import sonia.scm.client.RepositoryClientHandler; -import sonia.scm.client.ScmClientSession; -import sonia.scm.repository.Permission; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class PermissionSubCommand extends TemplateSubCommand -{ - - /** - * Method description - * - * - * @param permissions - */ - protected abstract void modifyPermissions(List permissions); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return id; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param id - */ - public void setId(String id) - { - this.id = id; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - ScmClientSession session = createSession(); - RepositoryClientHandler handler = session.getRepositoryHandler(); - Repository repository = handler.get(id); - - if (repository != null) - { - List permissions = repository.getPermissions(); - - if (permissions == null) - { - permissions = new ArrayList<>(); - } - - modifyPermissions(permissions); - repository.setPermissions(permissions); - handler.modify(repository); - - Map env = new HashMap<>(); - - env.put("repository", new RepositoryWrapper(config, repository)); - renderTemplate(env, GetRepositorySubCommand.TEMPLATE); - } - else - { - output.println(i18n.getMessage(I18n.REPOSITORY_NOT_FOUND)); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Argument( - usage = "optionRepositoryId", - metaVar = "repositoryid", - required = true - ) - private String id; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ServerVersionSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ServerVersionSubCommand.java deleted file mode 100644 index e5c60f0779..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/ServerVersionSubCommand.java +++ /dev/null @@ -1,67 +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.cli.cmd; - -import sonia.scm.ScmState; -import sonia.scm.client.ScmClientSession; -import sonia.scm.util.Util; - -/** - * - * @author Sebastian Sdorra - * @version 1.9 - */ -@Command( - name = "server-version", - usage = "usageServerVersion", - group = "misc" -) -public class ServerVersionSubCommand extends SubCommand -{ - - @Override - protected void run() - { - ScmClientSession session = createSession(); - ScmState state = session.getState(); - String version = null; - if ( state != null ){ - version = state.getVersion(); - - } - if ( Util.isEmpty(version) ){ - version = VersionSubCommand.DEFAULT_VERSION; - } - - output.append("scm-manager version: ").println( version ); - } - -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/StoreConfigSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/StoreConfigSubCommand.java deleted file mode 100644 index 3fcd06e479..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/StoreConfigSubCommand.java +++ /dev/null @@ -1,62 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.cli.config.ScmClientConfig; - -/** - * - * @author Sebastian Sdorra - */ -@Command( - name = "store-config", - usage = "usageStoreConfig", - group = "config" -) -public class StoreConfigSubCommand extends SubCommand -{ - - /** - * Method description - * - */ - @Override - protected void run() - { - output.println("store config"); - ScmClientConfig.getInstance().store(); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/SubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/SubCommand.java deleted file mode 100644 index 09dcf344ef..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/SubCommand.java +++ /dev/null @@ -1,235 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import org.kohsuke.args4j.Option; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.cli.I18n; -import sonia.scm.cli.config.ServerConfig; -import sonia.scm.client.ScmClient; -import sonia.scm.client.ScmClientSession; -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.BufferedReader; -import java.io.PrintWriter; - -import java.util.Collection; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class SubCommand -{ - - /** the logger for SubCommand */ - private static final Logger logger = - LoggerFactory.getLogger(SubCommand.class); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - protected abstract void run(); - - /** - * Method description - * - * - * @param input - * @param output - * @param i18n - * @param config - */ - public void init(BufferedReader input, PrintWriter output, I18n i18n, - ServerConfig config) - { - this.input = input; - this.output = output; - this.i18n = i18n; - this.config = config; - } - - /** - * Method description - * - * - * @param args - */ - public void run(Collection args) - { - CmdLineParser parser = new CmdLineParser(this); - - try - { - parser.parseArgument(args); - - if (help) - { - parser.printUsage(output, i18n.getBundle()); - System.exit(1); - } - else - { - try - { - run(); - } - finally - { - IOUtil.close(session); - } - } - } - catch (CmdLineException ex) - { - if (logger.isWarnEnabled()) - { - logger.warn("could not parse comannd line", ex); - } - - if (!help) - { - output.append(i18n.getMessage(I18n.ERROR)).append(": "); - output.println(ex.getMessage()); - output.println(); - } - - printHelp(parser); - } - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getCommandName() - { - return commandName; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param name - */ - public void setCommandName(String name) - { - this.commandName = name; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - protected ScmClientSession createSession() - { - if (Util.isNotEmpty(config.getUsername()) - && Util.isNotEmpty(config.getPassword())) - { - session = ScmClient.createSession(config.getServerUrl(), - config.getUsername(), - config.getPassword()); - } - else - { - session = ScmClient.createSession(config.getServerUrl()); - } - - return session; - } - - /** - * Method description - * - * - * @param parser - */ - protected void printHelp(CmdLineParser parser) - { - parser.printUsage(output, i18n.getBundle()); - System.exit(1); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - protected ServerConfig config; - - /** Field description */ - protected I18n i18n; - - /** Field description */ - protected BufferedReader input; - - /** Field description */ - protected PrintWriter output; - - /** Field description */ - private String commandName; - - /** Field description */ - @Option( - name = "--help", - usage = "optionHelpText", - aliases = { "-h" } - ) - private boolean help = false; - - /** Field description */ - private ScmClientSession session; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/SubCommandHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/SubCommandHandler.java deleted file mode 100644 index 36f39d470c..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/SubCommandHandler.java +++ /dev/null @@ -1,234 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.ConfigurationException; -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -import java.net.URL; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -public class SubCommandHandler -{ - - /** Field description */ - public static final String RESOURCE_SERVICES = - "META-INF/services/".concat(SubCommand.class.getName()); - - /** Field description */ - private static volatile SubCommandHandler instance; - - /** the logger for SubCommandOptionHandler */ - private static final Logger logger = - LoggerFactory.getLogger(SubCommandOptionHandler.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - private SubCommandHandler() - { - subCommands = new HashMap<>(); - loadSubCommands(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public static SubCommandHandler getInstance() - { - if (instance == null) - { - synchronized (SubCommandHandler.class) - { - if (instance == null) - { - instance = new SubCommandHandler(); - } - } - } - - return instance; - } - - /** - * Method description - * - * - * @param name - * - * @return - */ - public CommandDescriptor getDescriptor(String name) - { - return subCommands.get(name); - } - - /** - * Method description - * - * - * @return - */ - public List getDescriptors() - { - List descs = - new ArrayList<>(subCommands.values()); - - Collections.sort(descs); - - return descs; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param url - */ - private void loadSubCommand(URL url) - { - BufferedReader reader = null; - - try - { - reader = new BufferedReader(new InputStreamReader(url.openStream())); - - String line = reader.readLine(); - - while (line != null) - { - parseLine(line); - line = reader.readLine(); - } - } - catch (IOException ex) - { - logger.error("could not load commands"); - } - finally - { - IOUtil.close(reader); - } - } - - /** - * Method description - * - */ - private void loadSubCommands() - { - try - { - Enumeration enm = - SubCommandHandler.class.getClassLoader().getResources( - RESOURCE_SERVICES); - - while (enm.hasMoreElements()) - { - URL url = enm.nextElement(); - - loadSubCommand(url); - } - } - catch (IOException ex) - { - throw new ConfigurationException("could not load SubComamnds", ex); - } - } - - /** - * Method description - * - * - * @param line - */ - @SuppressWarnings("unchecked") - private void parseLine(String line) - { - line = line.trim(); - - if (Util.isNotEmpty(line) &&!line.startsWith("#")) - { - try - { - Class clazz = - (Class) Class.forName(line); - CommandDescriptor desc = new CommandDescriptor(clazz); - - subCommands.put(desc.getName(), desc); - } - catch (ClassNotFoundException ex) - { - logger.warn("could not found command class {}", line); - } - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Map subCommands; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/SubCommandOptionHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/SubCommandOptionHandler.java deleted file mode 100644 index 39957c2158..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/SubCommandOptionHandler.java +++ /dev/null @@ -1,115 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import org.kohsuke.args4j.OptionDef; -import org.kohsuke.args4j.spi.OptionHandler; -import org.kohsuke.args4j.spi.Parameters; -import org.kohsuke.args4j.spi.Setter; -import sonia.scm.cli.SimpleLocalizable; - -/** - * - * @author Sebastian Sdorra - */ -public class SubCommandOptionHandler extends OptionHandler -{ - - /** - * Constructs ... - * - * - * @param parser - * @param option - * @param setter - */ - public SubCommandOptionHandler(CmdLineParser parser, OptionDef option, - Setter setter) - { - super(parser, option, setter); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param parameters - * - * @return - * - * @throws CmdLineException - */ - @Override - public int parseArguments(Parameters parameters) throws CmdLineException - { - String name = parameters.getParameter(0); - CommandDescriptor desc = - SubCommandHandler.getInstance().getDescriptor(name); - - if (desc != null) - { - owner.stopOptionParsing(); - setter.addValue(desc.createSubCommand()); - } - else - { - String msg = "command ".concat(name).concat(" not found"); - - throw new CmdLineException(owner, new SimpleLocalizable(msg)); - } - - return 1; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getDefaultMetaVariable() - { - return "command"; - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/TemplateSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/TemplateSubCommand.java deleted file mode 100644 index b9d985b3b9..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/TemplateSubCommand.java +++ /dev/null @@ -1,165 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateException; - -import org.kohsuke.args4j.Option; - -import sonia.scm.ConfigurationException; -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringReader; - -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class TemplateSubCommand extends SubCommand -{ - - /** - * Method description - * - * - * @return - */ - public String getTemplate() - { - return template; - } - - /** - * Method description - * - * - * @return - */ - public File getTemplateFile() - { - return templateFile; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param template - */ - public void setTemplate(String template) - { - this.template = template; - } - - /** - * Method description - * - * - * @param templateFile - */ - public void setTemplateFile(File templateFile) - { - this.templateFile = templateFile; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param env - * @param defaultTemplate - */ - protected void renderTemplate(Map env, String defaultTemplate) - { - Configuration configuration = new Configuration(Configuration.VERSION_2_3_20); - Reader reader = null; - - try - { - if ((templateFile != null) && templateFile.exists()) - { - reader = new FileReader(templateFile); - } - else if (Util.isNotEmpty(template)) - { - reader = new StringReader(template); - } - else - { - reader = new InputStreamReader( - TemplateSubCommand.class.getResourceAsStream(defaultTemplate)); - } - - Template tpl = new Template("default-template", reader, configuration); - - tpl.process(env, output); - } - catch (TemplateException | IOException ex) - { - throw new ConfigurationException("could not render template", ex); - } - finally - { - IOUtil.close(reader); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Option(name = "--template", usage = "optionTemplate") - private String template; - - /** Field description */ - @Option(name = "--template-file", usage = "optionTemplateFile") - private File templateFile; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/VersionSubCommand.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/VersionSubCommand.java deleted file mode 100644 index 285739f2c2..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/cmd/VersionSubCommand.java +++ /dev/null @@ -1,133 +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.cli.cmd; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; -import java.io.InputStream; - -import java.util.Properties; - -/** - * - * @author Sebastian Sdorra - * @since 1.9 - */ -@Command( - name = "version", - usage = "usageVersion", - group = "misc" -) -public class VersionSubCommand extends SubCommand -{ - - /** Default version {@link String} */ - public static final String DEFAULT_VERSION = "unknown"; - - /** Path to the maven properties file of the scm-core artifact */ - public static final String MAVEN_PROPERTIES = - "/META-INF/maven/sonia.scm.clients/scm-cli-client/pom.properties"; - - /** Maven property for the version of the artifact */ - public static final String MAVEN_PROPERTY_VERSION = "version"; - - /** the logger for VersionSubCommand */ - private static final Logger logger = - LoggerFactory.getLogger(VersionSubCommand.class); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - protected void run() - { - String version = getVersion(); - - output.append("scm-cli-client version: ").println(version); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - private String getVersion() - { - String version = null; - InputStream stream = null; - - try - { - stream = VersionSubCommand.class.getResourceAsStream(MAVEN_PROPERTIES); - - if (stream != null) - { - Properties properties = new Properties(); - - properties.load(stream); - version = properties.getProperty(MAVEN_PROPERTY_VERSION); - } - } - catch (IOException ex) - { - logger.warn("could not parse maven.properties", ex); - } - finally - { - IOUtil.close(stream); - } - - if (Util.isEmpty(version)) - { - version = DEFAULT_VERSION; - } - - return version; - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigOptionHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigOptionHandler.java deleted file mode 100644 index 15e030b8bc..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigOptionHandler.java +++ /dev/null @@ -1,102 +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.cli.config; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import org.kohsuke.args4j.OptionDef; -import org.kohsuke.args4j.spi.OptionHandler; -import org.kohsuke.args4j.spi.Parameters; -import org.kohsuke.args4j.spi.Setter; - -/** - * - * @author Sebastian Sdorra - */ -public class ConfigOptionHandler extends OptionHandler -{ - - /** - * Constructs ... - * - * - * @param parser - * @param option - * @param setter - */ - public ConfigOptionHandler(CmdLineParser parser, OptionDef option, - Setter setter) - { - super(parser, option, setter); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param parameters - * - * @return - * - * @throws CmdLineException - */ - @Override - public int parseArguments(Parameters parameters) throws CmdLineException - { - String name = parameters.getParameter(0); - ServerConfig config = ScmClientConfig.getInstance().getConfig(name); - - setter.addValue(config); - - return 1; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getDefaultMetaVariable() - { - return "metaVar_config"; - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfig.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfig.java deleted file mode 100644 index 68c50731b7..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfig.java +++ /dev/null @@ -1,183 +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.cli.config; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.xml.bind.annotation.*; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "client-config") -@XmlAccessorType(XmlAccessType.FIELD) -public class ScmClientConfig -{ - - /** Field description */ - public static final String DEFAULT_NAME = "default"; - - /** Field description */ - private static volatile ScmClientConfig instance; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - private ScmClientConfig() - { - this.serverConfigMap = new HashMap<>(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public static ScmClientConfig getInstance() - { - if (instance == null) - { - synchronized (ScmClientConfig.class) - { - if (instance == null) - { - instance = load(); - } - } - } - - return instance; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - private static ScmClientConfig load() - { - ScmClientConfigFileHandler fileHandler = new ScmClientConfigFileHandler(); - ScmClientConfig config = fileHandler.read(); - - if (config == null) - { - config = new ScmClientConfig(); - } - - config.setFileHandler(fileHandler); - - return config; - } - - /** - * Method description - * - */ - public void delete() - { - fileHandler.delete(); - } - - /** - * Method description - * - */ - public void store() - { - fileHandler.write(this); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param name - * - * @return - */ - public ServerConfig getConfig(String name) - { - return serverConfigMap.computeIfAbsent(name, k -> new ServerConfig()); - } - - /** - * Method description - * - * - * @return - */ - public ServerConfig getDefaultConfig() - { - return getConfig(DEFAULT_NAME); - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param fileHandler - */ - private void setFileHandler(ScmClientConfigFileHandler fileHandler) - { - this.fileHandler = fileHandler; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @XmlTransient - private ScmClientConfigFileHandler fileHandler; - - /** Field description */ - @XmlElement(name = "server-config") - @XmlJavaTypeAdapter(XmlConfigAdapter.class) - private Map serverConfigMap; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java deleted file mode 100644 index 5cc2c46978..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java +++ /dev/null @@ -1,306 +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.cli.config; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - -import java.util.UUID; -import java.util.prefs.Preferences; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.PBEParameterSpec; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Marshaller; -import javax.xml.bind.Unmarshaller; - -/** - * - * @author Sebastian Sdorra - */ -public class ScmClientConfigFileHandler -{ - - /** Field description */ - public static final String DEFAULT_CONFIG_NAME = ".scm-cli-config.enc.xml"; - - /** Field description */ - public static final String ENV_CONFIG_FILE = "SCM_CLI_CONFIG"; - - /** Field description */ - public static final String PREF_SECRET_KEY = "scm.client.key"; - - /** Field description */ - public static final String SALT = "AE16347F"; - - /** Field description */ - public static final int SPEC_ITERATION = 12; - - /** Field description */ - private static final String CIPHER_NAME = "PBEWithMD5AndDES"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public ScmClientConfigFileHandler() - { - prefs = Preferences.userNodeForPackage(ScmClientConfigFileHandler.class); - key = prefs.get(PREF_SECRET_KEY, null); - - if (Util.isEmpty(key)) - { - key = createNewKey(); - prefs.put(PREF_SECRET_KEY, key); - } - - try - { - context = JAXBContext.newInstance(ScmClientConfig.class); - } - catch (JAXBException ex) - { - throw new ScmConfigException( - "could not create JAXBContext for ScmClientConfig", ex); - } - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - public void delete() - { - File configFile = getConfigFile(); - - if (configFile.exists() &&!configFile.delete()) - { - throw new ScmConfigException("could not delete config file"); - } - - prefs.remove(PREF_SECRET_KEY); - } - - /** - * Method description - * - * - * @return - */ - public ScmClientConfig read() - { - ScmClientConfig config = null; - File configFile = getConfigFile(); - - if (configFile.exists()) - { - InputStream input = null; - - try - { - Cipher c = createCipher(Cipher.DECRYPT_MODE); - - input = new CipherInputStream(new FileInputStream(configFile), c); - - Unmarshaller um = context.createUnmarshaller(); - - config = (ScmClientConfig) um.unmarshal(input); - } - catch (Exception ex) - { - throw new ScmConfigException("could not read config file", ex); - } - finally - { - IOUtil.close(input); - } - } - - return config; - } - - /** - * Method description - * - * - * @param config - */ - public void write(ScmClientConfig config) - { - File configFile = getConfigFile(); - OutputStream output = null; - - try - { - Cipher c = createCipher(Cipher.ENCRYPT_MODE); - - output = new CipherOutputStream(new FileOutputStream(configFile), c); - - Marshaller m = context.createMarshaller(); - - m.marshal(config, output); - } - catch (Exception ex) - { - throw new ScmConfigException("could not write config file", ex); - } - finally - { - IOUtil.close(output); - } - } - - /** - * Method description - * - * - * @param mode - * - * @return - * - * - * @throws InvalidAlgorithmParameterException - * @throws InvalidKeyException - * @throws InvalidKeySpecException - * @throws NoSuchAlgorithmException - * @throws NoSuchPaddingException - */ - private Cipher createCipher(int mode) - throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidKeySpecException, InvalidKeyException, - InvalidAlgorithmParameterException - { - SecretKey sk = createSecretKey(); - Cipher cipher = Cipher.getInstance(CIPHER_NAME); - PBEParameterSpec spec = new PBEParameterSpec(SALT.getBytes(), - SPEC_ITERATION); - - cipher.init(mode, sk, spec); - - return cipher; - } - - /** - * Method description - * - * - * @return - */ - private String createNewKey() - { - return UUID.randomUUID().toString(); - } - - /** - * Method description - * - * - * @return - * - * @throws InvalidKeySpecException - * @throws NoSuchAlgorithmException - */ - private SecretKey createSecretKey() - throws NoSuchAlgorithmException, InvalidKeySpecException - { - PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray()); - SecretKeyFactory factory = SecretKeyFactory.getInstance(CIPHER_NAME); - - return factory.generateSecret(keySpec); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - private File getConfigFile() - { - File configFile = null; - String configPath = System.getenv(ENV_CONFIG_FILE); - - if (Util.isEmpty(configPath)) - { - configFile = new File(System.getProperty("user.home"), - DEFAULT_CONFIG_NAME); - } - else - { - configFile = new File(configPath); - } - - return configFile; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private JAXBContext context; - - /** Field description */ - private String key; - - /** Field description */ - private Preferences prefs; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmConfigException.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmConfigException.java deleted file mode 100644 index 33fbef093e..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmConfigException.java +++ /dev/null @@ -1,90 +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.cli.config; - -/** - * - * @author Sebastian Sdorra - */ -public class ScmConfigException extends RuntimeException -{ - - /** Field description */ - private static final long serialVersionUID = -4226165375815233654L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public ScmConfigException() - { - super(); - } - - /** - * Constructs ... - * - * - * @param message - */ - public ScmConfigException(String message) - { - super(message); - } - - /** - * Constructs ... - * - * - * @param cause - */ - public ScmConfigException(Throwable cause) - { - super(cause); - } - - /** - * Constructs ... - * - * - * @param message - * @param cause - */ - public ScmConfigException(String message, Throwable cause) - { - super(message, cause); - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ServerConfig.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ServerConfig.java deleted file mode 100644 index c7fb4108c5..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ServerConfig.java +++ /dev/null @@ -1,162 +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.cli.config; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.Validateable; - -/** - * - * @author Sebastian Sdorra - */ -public class ServerConfig implements Validateable -{ - - /** - * Constructs ... - * - */ - public ServerConfig() {} - - /** - * Constructs ... - * - * - * @param serverUrl - * @param username - * @param password - */ - public ServerConfig(String serverUrl, String username, String password) - { - this.serverUrl = serverUrl; - this.username = username; - this.password = password; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getPassword() - { - return password; - } - - /** - * Method description - * - * - * @return - */ - public String getServerUrl() - { - return serverUrl; - } - - /** - * Method description - * - * - * @return - */ - public String getUsername() - { - return username; - } - - /** - * Method description - * - * - * @return - */ - @Override - public boolean isValid() - { - - // TODO - return true; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param password - */ - public void setPassword(String password) - { - this.password = password; - } - - /** - * Method description - * - * - * @param serverUrl - */ - public void setServerUrl(String serverUrl) - { - this.serverUrl = serverUrl; - } - - /** - * Method description - * - * - * @param username - */ - public void setUsername(String username) - { - this.username = username; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String password; - - /** Field description */ - private String serverUrl; - - /** Field description */ - private String username; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/XmlConfigAdapter.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/XmlConfigAdapter.java deleted file mode 100644 index c7471cf601..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/XmlConfigAdapter.java +++ /dev/null @@ -1,98 +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.cli.config; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import javax.xml.bind.annotation.adapters.XmlAdapter; - -/** - * - * @author Sebastian Sdorra - */ -public class XmlConfigAdapter - extends XmlAdapter> -{ - - /** - * Method description - * - * - * @param map - * - * @return - * - * @throws Exception - */ - @Override - public XmlConfigSet marshal(Map map) throws Exception - { - Set set = new HashSet<>(); - - for (Map.Entry e : map.entrySet()) - { - set.add(new XmlConfigElement(e.getKey(), e.getValue())); - } - - return new XmlConfigSet(set); - } - - /** - * Method description - * - * - * @param set - * - * @return - * - * @throws Exception - */ - @Override - public Map unmarshal(XmlConfigSet set) throws Exception - { - Map map = new HashMap<>(); - - for (XmlConfigElement e : set) - { - map.put(e.getName(), e.getConfig()); - } - - return map; - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/XmlConfigElement.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/XmlConfigElement.java deleted file mode 100644 index f4e3eb2c04..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/XmlConfigElement.java +++ /dev/null @@ -1,127 +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.cli.config; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "server") -@XmlAccessorType(XmlAccessType.FIELD) -public class XmlConfigElement -{ - - /** - * Constructs ... - * - */ - public XmlConfigElement() {} - - /** - * Constructs ... - * - * - * @param name - * @param config - */ - public XmlConfigElement(String name, ServerConfig config) - { - this.name = name; - this.config = config; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public ServerConfig getConfig() - { - return config; - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param config - */ - public void setConfig(ServerConfig config) - { - this.config = config; - } - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "server-config") - private ServerConfig config; - - /** Field description */ - private String name; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/XmlConfigSet.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/XmlConfigSet.java deleted file mode 100644 index 9bd3370775..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/XmlConfigSet.java +++ /dev/null @@ -1,117 +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.cli.config; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Iterator; -import java.util.Set; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "server-config") -@XmlAccessorType(XmlAccessType.FIELD) -public class XmlConfigSet implements Iterable -{ - - /** - * Constructs ... - * - */ - public XmlConfigSet() {} - - /** - * Constructs ... - * - * - * @param configSet - */ - public XmlConfigSet(Set configSet) - { - this.configSet = configSet; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public Iterator iterator() - { - return configSet.iterator(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Set getConfigSet() - { - return configSet; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param configSet - */ - public void setConfigSet(Set configSet) - { - this.configSet = configSet; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "server") - private Set configSet; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/AbstractWrapper.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/AbstractWrapper.java deleted file mode 100644 index 0e72aec28e..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/AbstractWrapper.java +++ /dev/null @@ -1,66 +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.cli.wrapper; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Date; - -/** - * - * @author Sebastian Sdorra - */ -public class AbstractWrapper -{ - - /** - * Method description - * - * - * @param value - * - * @return - */ - protected Date getDate(Long value) - { - Date date = null; - - if (value != null) - { - date = new Date(value); - } - - return date; - } -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/GroupWrapper.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/GroupWrapper.java deleted file mode 100644 index d26225b5f7..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/GroupWrapper.java +++ /dev/null @@ -1,146 +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.cli.wrapper; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.group.Group; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Date; -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -public class GroupWrapper extends AbstractWrapper -{ - - /** - * Constructs ... - * - * - * @param group - */ - public GroupWrapper(Group group) - { - this.group = group; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Date getCreationDate() - { - return getDate(group.getCreationDate()); - } - - /** - * Method description - * - * - * @return - */ - public String getDescription() - { - return group.getDescription(); - } - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return group.getId(); - } - - /** - * Method description - * - * - * @return - */ - public Date getLastModified() - { - return getDate(group.getLastModified()); - } - - /** - * Method description - * - * - * @return - */ - public List getMembers() - { - return group.getMembers(); - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return group.getName(); - } - - /** - * Method description - * - * - * @return - */ - public String getType() - { - return group.getType(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Group group; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/RepositoryWrapper.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/RepositoryWrapper.java deleted file mode 100644 index 216afde624..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/RepositoryWrapper.java +++ /dev/null @@ -1,210 +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.cli.wrapper; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.cli.config.ServerConfig; -import sonia.scm.repository.Permission; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Date; -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -public class RepositoryWrapper extends AbstractWrapper -{ - - /** - * Constructs ... - * - * - * @param config - * @param repository - */ - public RepositoryWrapper(ServerConfig config, Repository repository) - { - this(config.getServerUrl(), repository); - } - - /** - * Constructs ... - * - * - * - * @param baseUrl - * @param repository - */ - public RepositoryWrapper(String baseUrl, Repository repository) - { - this.baseUrl = baseUrl; - this.repository = repository; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getContact() - { - return repository.getContact(); - } - - /** - * Method description - * - * - * @return - */ - public Date getCreationDate() - { - return getDate(repository.getCreationDate()); - } - - /** - * Method description - * - * - * @return - */ - public String getDescription() - { - return repository.getDescription(); - } - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return repository.getId(); - } - - /** - * Method description - * - * - * @return - */ - public Date getLastModified() - { - return getDate(repository.getLastModified()); - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return repository.getName(); - } - - /** - * Method description - * - * - * @return - */ - public List getPermissions() - { - return repository.getPermissions(); - } - - /** - * Method description - * - * - * @return - */ - public String getType() - { - return repository.getType(); - } - - /** - * Method description - * - * - * @return - */ - public String getUrl() - { - return repository.createUrl(baseUrl); - } - - /** - * Method description - * - * - * @return - */ - public boolean isArchived() - { - return repository.isArchived(); - } - - /** - * Method description - * - * - * @return - */ - public boolean isPublicReadable() - { - return repository.isPublicReadable(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String baseUrl; - - /** Field description */ - private Repository repository; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/UserWrapper.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/UserWrapper.java deleted file mode 100644 index 55ccc6681c..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/UserWrapper.java +++ /dev/null @@ -1,156 +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.cli.wrapper; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.user.User; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Date; - -/** - * - * @author Sebastian Sdorra - */ -public class UserWrapper extends AbstractWrapper -{ - - /** - * Constructs ... - * - * - * @param user - */ - public UserWrapper(User user) - { - this.user = user; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Date getCreationDate() - { - return getDate(user.getCreationDate()); - } - - /** - * Method description - * - * - * @return - */ - public String getDisplayName() - { - return user.getDisplayName(); - } - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return user.getId(); - } - - /** - * Method description - * - * - * @return - */ - public Date getLastModified() - { - return getDate(user.getLastModified()); - } - - /** - * Method description - * - * - * @return - */ - public String getMail() - { - return user.getMail(); - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return user.getName(); - } - - /** - * Method description - * - * - * @return - */ - public String getType() - { - return user.getType(); - } - - /** - * Method description - * - * - * @return - */ - public boolean isAdmin() - { - return user.isAdmin(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private User user; -} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/WrapperUtil.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/WrapperUtil.java deleted file mode 100644 index 466cf55019..0000000000 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/wrapper/WrapperUtil.java +++ /dev/null @@ -1,127 +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.cli.wrapper; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.cli.config.ServerConfig; -import sonia.scm.group.Group; -import sonia.scm.repository.Repository; -import sonia.scm.user.User; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -public final class WrapperUtil -{ - - /** - * Constructs ... - * - */ - private WrapperUtil() {} - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param groups - * - * @return - */ - public static List wrapGroups(Collection groups) - { - List wrappers = new ArrayList<>(); - - for (Group g : groups) - { - wrappers.add(new GroupWrapper(g)); - } - - return wrappers; - } - - /** - * Method description - * - * - * - * - * @param config - * @param repositories - * - * @return - */ - public static List wrapRepositories(ServerConfig config, - Collection repositories) - { - List wrappers = new ArrayList<>(); - - for (Repository r : repositories) - { - wrappers.add(new RepositoryWrapper(config.getServerUrl(), r)); - } - - return wrappers; - } - - /** - * Method description - * - * - * @param users - * - * @return - */ - public static List wrapUsers(Collection users) - { - List wrappers = new ArrayList<>(); - - for (User u : users) - { - wrappers.add(new UserWrapper(u)); - } - - return wrappers; - } -} diff --git a/scm-clients/scm-cli-client/src/main/resources/META-INF/services/sonia.scm.cli.cmd.SubCommand b/scm-clients/scm-cli-client/src/main/resources/META-INF/services/sonia.scm.cli.cmd.SubCommand deleted file mode 100644 index 91f2345948..0000000000 --- a/scm-clients/scm-cli-client/src/main/resources/META-INF/services/sonia.scm.cli.cmd.SubCommand +++ /dev/null @@ -1,72 +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 -# - -# config -sonia.scm.cli.cmd.StoreConfigSubCommand -sonia.scm.cli.cmd.DeleteConfigSubCommand - -# repository -sonia.scm.cli.cmd.CreateRepositorySubCommand -sonia.scm.cli.cmd.ModifyRepositorySubCommand -sonia.scm.cli.cmd.GetRepositorySubCommand -sonia.scm.cli.cmd.ListRepositoriesSubCommand -sonia.scm.cli.cmd.DeleteRepositorySubCommand -sonia.scm.cli.cmd.ImportDirectorySubCommand -sonia.scm.cli.cmd.ImportUrlSubCommand -sonia.scm.cli.cmd.ImportBundleSubCommand - -# permission -sonia.scm.cli.cmd.AddPermissionSubCommand -sonia.scm.cli.cmd.DeletePermissionSubCommand - -# user -sonia.scm.cli.cmd.ListUsersSubCommand -sonia.scm.cli.cmd.GetUserSubCommand -sonia.scm.cli.cmd.CreateUserSubCommand -sonia.scm.cli.cmd.DeleteUserSubCommand -sonia.scm.cli.cmd.ModifyUserSubCommand - -# group -sonia.scm.cli.cmd.ListGroupsSubCommand -sonia.scm.cli.cmd.GetGroupSubCommand -sonia.scm.cli.cmd.CreateGroupSubCommand -sonia.scm.cli.cmd.DeleteGroupSubCommand -sonia.scm.cli.cmd.ModifyGroupSubCommand - -# member -sonia.scm.cli.cmd.AddMembersSubCommand -sonia.scm.cli.cmd.DeleteMembersSubCommand - -# security -sonia.scm.cli.cmd.EncryptSubCommand -sonia.scm.cli.cmd.GenerateKeySubCommand - -# misc -sonia.scm.cli.cmd.VersionSubCommand -sonia.scm.cli.cmd.ServerVersionSubCommand diff --git a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/get-group.ftl b/scm-clients/scm-cli-client/src/main/resources/sonia/resources/get-group.ftl deleted file mode 100644 index 6126b6d81f..0000000000 --- a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/get-group.ftl +++ /dev/null @@ -1,11 +0,0 @@ -Name: ${group.name} -Type: ${group.type} -Description: ${group.description!""} -Creation-Date: <#if group.creationDate??>${group.creationDate?string("yyyy-MM-dd HH:mm:ss")} -Last-Modified: <#if group.lastModified??>${group.lastModified?string("yyyy-MM-dd HH:mm:ss")} -Members: -<#if group.members??> -<#list group.members as member> - ${member} - - diff --git a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/get-repository.ftl b/scm-clients/scm-cli-client/src/main/resources/sonia/resources/get-repository.ftl deleted file mode 100644 index 7ac5d4c38f..0000000000 --- a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/get-repository.ftl +++ /dev/null @@ -1,16 +0,0 @@ -ID: ${repository.id} -Name: ${repository.name} -Type: ${repository.type} -E-Mail: ${repository.contact!""} -Description: ${repository.description!""} -Public: ${repository.publicReadable?string} -Archived: ${repository.archived?string} -Creation-Date: <#if repository.creationDate??>${repository.creationDate?string("yyyy-MM-dd HH:mm:ss")} -Last-Modified: <#if repository.lastModified??>${repository.lastModified?string("yyyy-MM-dd HH:mm:ss")} -URL: ${repository.url} -Permissions: -<#if repository.permissions??> -<#list repository.permissions as permission> - ${permission.type} - ${permission.name} (Group: ${permission.groupPermission?string}) - - diff --git a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/get-user.ftl b/scm-clients/scm-cli-client/src/main/resources/sonia/resources/get-user.ftl deleted file mode 100644 index 45f08e56d1..0000000000 --- a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/get-user.ftl +++ /dev/null @@ -1,8 +0,0 @@ -Name: ${user.name} -Display Name: ${user.displayName} -Type: ${user.type} -E-Mail: ${user.mail!""} -Active: ${user.admin?string} -Administrator: ${user.admin?string} -Creation-Date: <#if user.creationDate??>${user.creationDate?string("yyyy-MM-dd HH:mm:ss")} -Last-Modified: <#if user.lastModified??>${user.lastModified?string("yyyy-MM-dd HH:mm:ss")} diff --git a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/i18n.properties b/scm-clients/scm-cli-client/src/main/resources/sonia/resources/i18n.properties deleted file mode 100644 index 9d6afd3dda..0000000000 --- a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/i18n.properties +++ /dev/null @@ -1,131 +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 -# -# - -VAL = value -FILE = file -error = Error - -subCommandsTitle = list of commands - -optionConfig = Configuration name -optionServerUrl = SCM-Manager URL -optionUsername = Username -optionPassword = Password -optionHelpText = Shows this help -optionLoggingLevel = Logging level (DEBUG, INFO, WARN, ERROR) -optionTemplate = Template -optionTemplateFile = Template file -optionRepositoryId = Repository Id -optionRepositoryIdOrTypeAndName = Repository Id or type/name -optionRepositoryName = Repository name -optionRepositoryType = Repository type -optionRepositoryContact = Repository contact -optionRepositoryDescription = Repository description -optionRepositoryPublic = Repository public readable -optionRepositoryArchive = Repository archived -optionRemoteRepositoryUrl = Remote repository url -optionRepositoryBundle = Import repository from a bundle file (e.g. svn dump) -optionRepositoryBundleCompressed = Indicates that the bundle is gzip compressed - -optionPermissionGroup = Group -optionPermissionName = Group or user name -optionPermissionType = Permission type (READ,WRITE or OWNER) - -optionUserName = Username -optionUserDisplayName = Display name -optionUserMail = E-Mail address -optionUserPassword = Password -optionUserType = Type -optionUserAdmin = Administrator - -optionGroupName = Name of the group -optionGroupDescription = Description -optionGroupType = Type -optionGroupMember = Member - -optionEncryptValue = value to encrypt - -repositoryNotFound = The repository is not available -userNotFound = The user could not be found -groupNotFoun = The group could not be found - -config = configname -arg = subcommand arguments -command = subcommand -permissiontype = value -groupname = groupname -repositoryid = repositoryid -username = username -repositorytype = type - -config = Configuration -misc = Miscellaneous -repository = Repository -group = Group -user = User -security = Security -level = Logging-Level -boolean = true or false -URL = url -bundle = file - -options = Options -usage = scm-cli-client [options] command [command options] - -# usages -usageAddMember = Add members to a existing group -usageAddPermission = Add permission to a existing repository -usageCreateGroup = Create a new group -usageCreateRepository = Create a new repository -usageCreateUser = Create a new user -usageDeleteConfig = Delete all stored configurations -usageDeleteGroup = Delete a group -usageDeleteMembers = Delete members of a group -usageDeletePermission = Delete a permission of a repository -usageDeleteRepository = Delete a repository -usageDeleteUser = Delete a user -usageGetGroup = Print a group -usageGetRepository = Print a repository -usageGetUser = Print a user -usageListGroups= Print a list of all groups -usageListUsers= Print a list of all users -usageListRepositories= Print a list of all repositories -usageModifyGroup = Modify a group -usageModifyUser = Modify a user -usageModifyRepository = Modify a repository -usageStoreConfig = Stores the current configuration -usageVersion = Show the version of scm-cli-client -usageServerVersion = Show the version of the scm-manager -usageImportDirectory = Import repositories from repository directory -usageImportUrl = Import repository from remote url - -usageEncrypt = Encrypts the given value -usageGenerateKey = Generates a unique key \ No newline at end of file diff --git a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/import-from-directory.ftl b/scm-clients/scm-cli-client/src/main/resources/sonia/resources/import-from-directory.ftl deleted file mode 100644 index 33cf9e083e..0000000000 --- a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/import-from-directory.ftl +++ /dev/null @@ -1,9 +0,0 @@ -Imported repositories: -<#list importedDirectories as imported> -- ${imported} - - -Repositories failed to import: -<#list failedDirectories as failed> -- ${failed} - \ No newline at end of file diff --git a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/list-groups.ftl b/scm-clients/scm-cli-client/src/main/resources/sonia/resources/list-groups.ftl deleted file mode 100644 index b25340f8f1..0000000000 --- a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/list-groups.ftl +++ /dev/null @@ -1,14 +0,0 @@ -<#list groups as group> -Name: ${group.name} -Type: ${group.type} -Description: ${group.description!""} -Creation-Date: <#if group.creationDate??>${group.creationDate?string("yyyy-MM-dd HH:mm:ss")} -Last-Modified: <#if group.lastModified??>${group.lastModified?string("yyyy-MM-dd HH:mm:ss")} -Members: -<#if group.members??> -<#list group.members as member> - ${member} - - - - \ No newline at end of file diff --git a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/list-repositories.ftl b/scm-clients/scm-cli-client/src/main/resources/sonia/resources/list-repositories.ftl deleted file mode 100644 index c78c51b505..0000000000 --- a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/list-repositories.ftl +++ /dev/null @@ -1,19 +0,0 @@ -<#list repositories as repository> -ID: ${repository.id} -Name: ${repository.name} -Type: ${repository.type} -E-Mail: ${repository.contact!""} -Description: ${repository.description!""} -Public: ${repository.publicReadable?string} -Archived: ${repository.archived?string} -Creation-Date: <#if repository.creationDate??>${repository.creationDate?string("yyyy-MM-dd HH:mm:ss")} -Last-Modified: <#if repository.lastModified??>${repository.lastModified?string("yyyy-MM-dd HH:mm:ss")} -URL: ${repository.url} -Permissions: -<#if repository.permissions??> -<#list repository.permissions as permission> - ${permission.type} - ${permission.name} (Group: ${permission.groupPermission?string}) - - - - diff --git a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/list-users.ftl b/scm-clients/scm-cli-client/src/main/resources/sonia/resources/list-users.ftl deleted file mode 100644 index eab8106c3c..0000000000 --- a/scm-clients/scm-cli-client/src/main/resources/sonia/resources/list-users.ftl +++ /dev/null @@ -1,11 +0,0 @@ -<#list users as user> -Name: ${user.name} -Display Name: ${user.displayName} -Type: ${user.type} -E-Mail: ${user.mail!""} -Active: ${user.admin?string} -Administrator: ${user.admin?string} -Creation-Date: <#if user.creationDate??>${user.creationDate?string("yyyy-MM-dd HH:mm:ss")} -Last-Modified: <#if user.lastModified??>${user.lastModified?string("yyyy-MM-dd HH:mm:ss")} - - diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/AbstractITCaseBase.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/AbstractITCaseBase.java deleted file mode 100644 index 5184ea37ed..0000000000 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/AbstractITCaseBase.java +++ /dev/null @@ -1,129 +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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.collect.Lists; -import com.google.common.io.Closeables; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import java.util.Arrays; -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class AbstractITCaseBase -{ - - /** - * Method description - * - * - * @param cmd - * - * @return - * - * @throws IOException - */ - protected String execute(String... cmd) throws IOException - { - String result = null; - ByteArrayInputStream inputStream = null; - ByteArrayOutputStream outputStream = null; - - try - { - inputStream = new ByteArrayInputStream(new byte[0]); - outputStream = new ByteArrayOutputStream(); - - App app = new App(inputStream, outputStream); - - app.run(cmd); - - outputStream.flush(); - result = outputStream.toString().trim(); - } - finally - { - Closeables.close(inputStream, true); - Closeables.close(outputStream, true); - } - - return result; - } - - /** - * Method description - * - * - * @param cmd - * - * @return - * - * @throws IOException - */ - protected String executeServer(String... cmd) throws IOException - { - List cmdList = Lists.newArrayList(); - - cmdList.add("--server"); - cmdList.add("http://localhost:8081/scm"); - cmdList.add("--user"); - cmdList.add("scmadmin"); - cmdList.add("--password"); - cmdList.add("scmadmin"); - cmdList.addAll(Arrays.asList(cmd)); - - return execute(cmdList.toArray(new String[cmdList.size()])); - } - - /** - * Method description - * - * - * @param values - * - * @return - */ - protected String prepareForTest(String values) - { - return values.replaceAll("\\n", ";").replaceAll("\\s+", ""); - } -} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/RepositoriesITCase.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/RepositoriesITCase.java deleted file mode 100644 index d09e912157..0000000000 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/RepositoriesITCase.java +++ /dev/null @@ -1,86 +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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * - * @author Sebastian Sdorra - */ -public class RepositoriesITCase extends AbstractITCaseBase -{ - - /** - * Method description - * - * - * @throws IOException - */ - @Test - public void listRepositoriesTest() throws IOException - { - executeServer("create-repository", "--type", "git", "--name", "hobbo", - "--description", "Test Repo"); - - String repositories = prepareForTest(executeServer("list-repositories")); - - assertThat(repositories, containsString("Name:hobbo;")); - assertThat(repositories, containsString("Description:TestRepo;")); - assertThat(repositories, containsString("Type:git;")); - - Pattern pattern = Pattern.compile("ID:([^;]+);"); - Matcher matcher = pattern.matcher(repositories); - - if (!matcher.find()) - { - fail("could not extract id of repository"); - } - - String id = matcher.group(1); - - executeServer("delete-repository", id); - } -} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/ServerVersionITCase.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/ServerVersionITCase.java deleted file mode 100644 index 7e424587fb..0000000000 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/ServerVersionITCase.java +++ /dev/null @@ -1,70 +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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assume.assumeTrue; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -/** - * - * @author Sebastian Sdorra - */ -public class ServerVersionITCase extends AbstractITCaseBase -{ - - /** - * Method description - * - * - * @throws IOException - */ - @Test - public void testServerVersion() throws IOException - { - String systemVersion = System.getProperty("scm.version"); - - assumeTrue("skip test, because system property scm.version is not set", - systemVersion != null); - - String version = executeServer("server-version"); - - assertEquals("scm-manager version: ".concat(systemVersion), version); - } -} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/UsersITCase.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/UsersITCase.java deleted file mode 100644 index e5b29457a9..0000000000 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/UsersITCase.java +++ /dev/null @@ -1,72 +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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -/** - * - * @author Sebastian Sdorra - */ -public class UsersITCase extends AbstractITCaseBase -{ - - /** - * Method description - * - * - * @throws IOException - */ - @Test - public void listUsersTest() throws IOException - { - String users = prepareForTest(executeServer("list-users")); - - assertThat(users, containsString("Name:scmadmin;")); - assertThat(users, containsString("DisplayName:SCMAdministrator;")); - assertThat(users, containsString("Type:xml;")); - assertThat(users, containsString("E-Mail:scm-admin@scm-manager.org;")); - assertThat(users, containsString("Active:true;")); - assertThat(users, containsString("Administrator:true;")); - System.out.println(users); - } -} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/VersionITCase.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/VersionITCase.java deleted file mode 100644 index 3d9563c34e..0000000000 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/VersionITCase.java +++ /dev/null @@ -1,65 +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.cli; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -/** - * - * @author Sebastian Sdorra - */ -public class VersionITCase extends AbstractITCaseBase -{ - - /** - * Method description - * - * - * @throws IOException - */ - @Test - public void testVersion() throws IOException - { - // maven properties are not available during it tests - String version = execute("version"); - - assertEquals("scm-cli-client version: unknown", version); - } -} diff --git a/scm-clients/scm-client-api/pom.xml b/scm-clients/scm-client-api/pom.xml deleted file mode 100644 index ce9561d2e5..0000000000 --- a/scm-clients/scm-client-api/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - 4.0.0 - - - sonia.scm.clients - scm-clients - 2.0.0-SNAPSHOT - - - scm-client-api - jar - 2.0.0-SNAPSHOT - scm-client-api - - - - - - - javax.servlet - javax.servlet-api - ${servlet.version} - provided - - - - javax.transaction - jta - 1.1 - provided - - - - - diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ClientChangesetHandler.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ClientChangesetHandler.java deleted file mode 100644 index 2207d2c6b9..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ClientChangesetHandler.java +++ /dev/null @@ -1,81 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.Changeset; -import sonia.scm.repository.ChangesetPagingResult; - -/** - * - * @author Sebastian Sdorra - * @since 1.8 - */ -public interface ClientChangesetHandler -{ - - /** - * Method description - * - * - * @param revision - * - * @return - * - * @since 1.12 - */ - public Changeset getChangeset(String revision); - - /** - * Method description - * - * - * @param start - * @param limit - * - * @return - */ - public ChangesetPagingResult getChangesets(int start, int limit); - - /** - * @param path - * @param revision - * @param start - * @param limit - * @return - */ - public ChangesetPagingResult getChangesets(String path, String revision, - int start, int limit); -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ClientHandler.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ClientHandler.java deleted file mode 100644 index b178ab3680..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ClientHandler.java +++ /dev/null @@ -1,103 +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.client; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - * - * @param - */ -public interface ClientHandler -{ - - /** - * Method description - * - * - * - * @param item - */ - public void create(T item); - - /** - * Method description - * - * - * @param id - */ - public void delete(String id); - - /** - * Method description - * - * - * - * @param item - */ - public void delete(T item); - - /** - * Method description - * - * - * - * @param item - */ - public void modify(T item); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param id - * - * @return - */ - public T get(String id); - - /** - * Method description - * - * - * @return - */ - public List getAll(); -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ClientRepositoryBrowser.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ClientRepositoryBrowser.java deleted file mode 100644 index 5c1f753e63..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ClientRepositoryBrowser.java +++ /dev/null @@ -1,100 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.BlameLine; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; -import java.io.InputStream; - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - * @since 1.8 - */ -public interface ClientRepositoryBrowser -{ - - /** - * Method description - * - * - * @param revision - * @param path - * - * @return - */ - public List getBlameLines(String revision, String path); - - /** - * Method description - * - * - * @param revision - * @param path - * - * @return - * - * @throws IOException - */ - public InputStream getContent(String revision, String path) - throws IOException; - - /** - * Method description - * - * - * @param revision - * @param path - * - * @return - */ - public List getFiles(String revision, String path); - - /** - * Method description - * - * - * @param revision - * - * @return - */ - public List getFiles(String revision); -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/FileObjectWrapper.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/FileObjectWrapper.java deleted file mode 100644 index ddbeef69a3..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/FileObjectWrapper.java +++ /dev/null @@ -1,193 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.BlameLine; -import sonia.scm.repository.FileObject; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; -import java.io.InputStream; - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - * @since 1.8 - */ -public class FileObjectWrapper -{ - - /** - * Constructs ... - * - * - * - * @param repositoryBrowser - * @param revision - * @param file - */ - public FileObjectWrapper(ClientRepositoryBrowser repositoryBrowser, - String revision, FileObject file) - { - this.repositoryBrowser = repositoryBrowser; - this.revision = revision; - this.file = file; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public List getBlameLines() - { - return repositoryBrowser.getBlameLines(revision, getPath()); - } - - /** - * Method description - * - * - * @return - */ - public List getChildren() - { - List children = null; - - if (isDirectory()) - { - children = repositoryBrowser.getFiles(revision, getPath()); - } - - return children; - } - - /** - * Method description - * - * - * @return - * - * @throws IOException - */ - public InputStream getContent() throws IOException - { - return repositoryBrowser.getContent(revision, getPath()); - } - - /** - * Method description - * - * - * @return - */ - public String getDescription() - { - return file.getDescription(); - } - - /** - * Method description - * - * - * @return - */ - public Long getLastModified() - { - return file.getLastModified(); - } - - /** - * Method description - * - * - * @return - */ - public long getLength() - { - return file.getLength(); - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return file.getName(); - } - - /** - * Method description - * - * - * @return - */ - public String getPath() - { - return file.getPath(); - } - - /** - * Method description - * - * - * @return - */ - public boolean isDirectory() - { - return file.isDirectory(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private FileObject file; - - /** Field description */ - private ClientRepositoryBrowser repositoryBrowser; - - /** Field description */ - private String revision; -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/GroupClientHandler.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/GroupClientHandler.java deleted file mode 100644 index 93fe52cc8d..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/GroupClientHandler.java +++ /dev/null @@ -1,44 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.group.Group; - -/** - * - * @author Sebastian Sdorra - */ -public interface GroupClientHandler extends ClientHandler {} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ImportBundleRequest.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ImportBundleRequest.java deleted file mode 100644 index c6c9e71016..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ImportBundleRequest.java +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) 2014, 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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.io.ByteSource; - -/** - * - * @author Sebastian Sdorra - * @since 1.43 - */ -public class ImportBundleRequest -{ - - /** - * Constructs ... - * - */ - ImportBundleRequest() {} - - /** - * Constructs ... - * - * - * @param type - * @param name - * @param bundle - */ - public ImportBundleRequest(String type, String name, ByteSource bundle) - { - this.type = type; - this.name = name; - this.bundle = bundle; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public ByteSource getBundle() - { - return bundle; - } - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public String getType() - { - return type; - } - - /** - * Method description - * - * - * @return - */ - public boolean isCompressed() - { - return compressed; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param compressed - */ - public void setCompressed(boolean compressed) - { - this.compressed = compressed; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private ByteSource bundle; - - /** Field description */ - private boolean compressed = false; - - /** Field description */ - private String name; - - /** Field description */ - private String type; -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ImportResultWrapper.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ImportResultWrapper.java deleted file mode 100644 index 1417e3b888..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ImportResultWrapper.java +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Copyright (c) 2014, 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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Function; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; - -import sonia.scm.repository.ImportResult; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - * @since 1.43 - */ -public class ImportResultWrapper -{ - - /** - * Constructs ... - * - * - * @param client - * @param type - * @param result - */ - public ImportResultWrapper(RepositoryClientHandler client, String type, - ImportResult result) - { - this.client = client; - this.type = type; - this.result = result; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public List getFailedDirectories() - { - List directories = result.getFailedDirectories(); - - if (directories == null) - { - directories = ImmutableList.of(); - } - - return directories; - } - - /** - * Method description - * - * - * @return - */ - public List getImportedDirectories() - { - List directories = result.getImportedDirectories(); - - if (directories == null) - { - directories = ImmutableList.of(); - } - - return directories; - } - - /** - * Method description - * - * - * @return - */ - public Iterable getImportedRepositories() - { - return Iterables.transform(getImportedDirectories(), - new RepositoryResolver(client, type)); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 14/11/29 - * @author Enter your name here... - */ - private static class RepositoryResolver - implements Function - { - - /** - * Constructs ... - * - * - * @param clientHandler - * @param type - */ - public RepositoryResolver(RepositoryClientHandler clientHandler, - String type) - { - this.clientHandler = clientHandler; - this.type = type; - } - - //~--- methods ------------------------------------------------------------ - - /** - * Method description - * - * - * @param name - * - * @return - */ - @Override - public Repository apply(String name) - { - return clientHandler.get(type, type); - } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private final RepositoryClientHandler clientHandler; - - /** Field description */ - private final String type; - } - - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final RepositoryClientHandler client; - - /** Field description */ - private final ImportResult result; - - /** Field description */ - private final String type; -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ImportUrlRequest.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ImportUrlRequest.java deleted file mode 100644 index 66969f4a86..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ImportUrlRequest.java +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) 2014, 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.client; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlTransient; - -/** - * - * @author Sebastian Sdorra - * @since 1.43 - */ -@XmlRootElement(name = "import") -@XmlAccessorType(XmlAccessType.FIELD) -public class ImportUrlRequest -{ - - /** - * Constructs ... - * - */ - ImportUrlRequest() {} - - /** - * Constructs ... - * - * - * @param type - * @param name - * @param url - */ - public ImportUrlRequest(String type, String name, String url) - { - this.type = type; - this.name = name; - this.url = url; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public String getType() - { - return type; - } - - /** - * Method description - * - * - * @return - */ - public String getUrl() - { - return url; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String name; - - /** Field description */ - @XmlTransient - private String type; - - /** Field description */ - private String url; -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/RepositoryClientHandler.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/RepositoryClientHandler.java deleted file mode 100644 index 9c0060aaff..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/RepositoryClientHandler.java +++ /dev/null @@ -1,149 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.NotSupportedFeatuerException; -import sonia.scm.Type; -import sonia.scm.repository.Repository; -import sonia.scm.repository.Tags; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collection; - -/** - * - * @author Sebastian Sdorra - */ -public interface RepositoryClientHandler extends ClientHandler -{ - - /** - * Method description - * - * - * @param type - * - * @return - * - * @since 1.43 - */ - public ImportResultWrapper importFromDirectory(String type); - - /** - * Method description - * - * - * @param request - * - * @return - * - * @since 1.43 - */ - public Repository importFromBundle(ImportBundleRequest request); - - /** - * Method description - * - * - * @param request - * @return - * - * @since 1.43 - */ - public Repository importFromUrl(ImportUrlRequest request); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * - * @return - * @since 1.11 - */ - public Repository get(String type, String name); - - /** - * Method description - * - * - * @param repository - * - * @return - * @since 1.8 - * - * @throws NotSupportedFeatuerException - */ - public ClientChangesetHandler getChangesetHandler(Repository repository) - throws NotSupportedFeatuerException; - - /** - * Method description - * - * - * @param repository - * - * @return - * @since 1.8 - * - * @throws NotSupportedFeatuerException - */ - public ClientRepositoryBrowser getRepositoryBrowser(Repository repository) - throws NotSupportedFeatuerException; - - /** - * Method description - * - * - * @return - */ - public Collection getRepositoryTypes(); - - /** - * Returns all tags of the given repository. - * - * - * @param repository repository - * - * @return all tags of the given repository - * @since 1.18 - */ - public Tags getTags(Repository repository); -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClient.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClient.java deleted file mode 100644 index c57e4992bb..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClient.java +++ /dev/null @@ -1,135 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.util.ServiceUtil; - -/** - * - * @author Sebastian Sdorra - */ -public final class ScmClient -{ - - /** Field description */ - private static volatile ScmClientProvider provider = null; - - /** the logger for ScmClient */ - private static final Logger logger = LoggerFactory.getLogger(ScmClient.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - private ScmClient() {} - - //~--- methods -------------------------------------------------------------- - - /** - * Creates an ScmClientSession for the given user - * - * - * @param url - * @param username - * @param password - * - * @return - * - * @throws ScmClientException - */ - public static ScmClientSession createSession(String url, String username, - String password) - throws ScmClientException - { - return getProvider().createSession(url, username, password); - } - - /** - * Creates an anonymous ScmClientSession - * - * - * @param url - * - * @return - * - * @throws ScmClientException - */ - public static ScmClientSession createSession(String url) - throws ScmClientException - { - return getProvider().createSession(url, null, null); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - * - */ - private static ScmClientProvider getProvider() - { - if (provider == null) - { - synchronized (ScmClientProvider.class) - { - if (provider == null) - { - provider = ServiceUtil.getService(ScmClientProvider.class); - } - } - } - - if (provider == null) - { - throw new ScmClientException("could not find a ScmClientProvider"); - } - else if (logger.isInfoEnabled()) - { - logger.info("create ScmClient with provider {}", - provider.getClass().getName()); - } - - return provider; - } -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClientException.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClientException.java deleted file mode 100644 index 2a15a277f7..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClientException.java +++ /dev/null @@ -1,174 +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.client; - -/** - * - * @author Sebastian Sdorra - */ -public class ScmClientException extends RuntimeException -{ - - /** Field description */ - public static final int SC_FORBIDDEN = 403; - - /** Field description */ - public static final int SC_NOTFOUND = 404; - - /** Field description */ - public static final int SC_UNAUTHORIZED = 401; - - /** Field description */ - public static final int SC_UNKNOWN = -1; - - /** Field description */ - private static final long serialVersionUID = -2302107106896701393L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param statusCode - */ - public ScmClientException(int statusCode) - { - this.statusCode = statusCode; - } - - /** - * Constructs ... - * - * - * @param message - */ - public ScmClientException(String message) - { - super(message); - } - - /** - * Constructs ... - * - * - * @param cause - */ - public ScmClientException(Throwable cause) - { - super(cause); - } - - /** - * Constructs ... - * - * - * - * @param statusCode - * @param message - */ - public ScmClientException(int statusCode, String message) - { - super(message); - this.statusCode = statusCode; - } - - /** - * Constructs ... - * - * - * @param message - * @param cause - */ - public ScmClientException(String message, Throwable cause) - { - super(message, cause); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getContent() - { - return content; - } - - /** - * Method description - * - * - * @return - */ - public int getStatusCode() - { - return statusCode; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param content - */ - public void setContent(String content) - { - this.content = content; - } - - /** - * Method description - * - * - * @param statusCode - */ - public void setStatusCode(int statusCode) - { - this.statusCode = statusCode; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String content; - - /** Field description */ - private int statusCode = SC_UNKNOWN; -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClientProvider.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClientProvider.java deleted file mode 100644 index 1a03696400..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClientProvider.java +++ /dev/null @@ -1,56 +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.client; - -/** - * - * @author Sebastian Sdorra - */ -public interface ScmClientProvider -{ - - /** - * Method description - * - * - * @param url - * @param username - * @param password - * - * @return - * - */ - public ScmClientSession createSession(String url, String username, - String password); -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClientSession.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClientSession.java deleted file mode 100644 index 7f58a94852..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmClientSession.java +++ /dev/null @@ -1,99 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.ScmState; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Closeable; - -/** - * - * @author Sebastian Sdorra - */ -public interface ScmClientSession extends Closeable -{ - - /** - * Method description - * - */ - @Override - public void close(); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public GroupClientHandler getGroupHandler(); - - /** - * Method description - * - * - * @return - */ - public RepositoryClientHandler getRepositoryHandler(); - - /** - * Method description - * - * - * @return - */ - public SecurityClientHandler getSecurityHandler(); - - /** - * Method description - * - * - * @return - */ - public ScmState getState(); - - /** - * Method description - * - * - * @return - */ - public UserClientHandler getUserHandler(); -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmForbiddenException.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmForbiddenException.java deleted file mode 100644 index d029d654a8..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmForbiddenException.java +++ /dev/null @@ -1,67 +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.client; - -/** - * - * @author Sebastian Sdorra - */ -public class ScmForbiddenException extends ScmClientException -{ - - /** Field description */ - private static final long serialVersionUID = 3937346624508458660L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public ScmForbiddenException() - { - super(SC_FORBIDDEN); - } - - /** - * Constructs ... - * - * - * @param message - */ - public ScmForbiddenException(String message) - { - super(SC_FORBIDDEN, message); - } -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmNotFoundException.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmNotFoundException.java deleted file mode 100644 index ae0835e8cb..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmNotFoundException.java +++ /dev/null @@ -1,67 +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.client; - -/** - * - * @author Sebastian Sdorra - */ -public class ScmNotFoundException extends ScmClientException -{ - - /** Field description */ - private static final long serialVersionUID = -7015126639998723954L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public ScmNotFoundException() - { - super(SC_NOTFOUND); - } - - /** - * Constructs ... - * - * - * @param message - */ - public ScmNotFoundException(String message) - { - super(SC_NOTFOUND, message); - } -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmUnauthorizedException.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmUnauthorizedException.java deleted file mode 100644 index fc561117e1..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/ScmUnauthorizedException.java +++ /dev/null @@ -1,67 +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.client; - -/** - * - * @author Sebastian Sdorra - */ -public class ScmUnauthorizedException extends ScmClientException -{ - - /** Field description */ - private static final long serialVersionUID = 4398907424134588809L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public ScmUnauthorizedException() - { - super(SC_UNAUTHORIZED); - } - - /** - * Constructs ... - * - * - * @param message - */ - public ScmUnauthorizedException(String message) - { - super(SC_UNAUTHORIZED, message); - } -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/SecurityClientHandler.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/SecurityClientHandler.java deleted file mode 100644 index a0131a7f1c..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/SecurityClientHandler.java +++ /dev/null @@ -1,59 +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.client; - -/** - * - * @author Sebastian Sdorra - * @since 1.41 - */ -public interface SecurityClientHandler -{ - - /** - * Method description - * - * - * @param value - * - * @return - */ - public String encrypt(String value); - - /** - * Method description - * - * - * @return - */ - public String generateKey(); -} diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/UserClientHandler.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/UserClientHandler.java deleted file mode 100644 index 59192ba5a7..0000000000 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/UserClientHandler.java +++ /dev/null @@ -1,44 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.user.User; - -/** - * - * @author Sebastian Sdorra - */ -public interface UserClientHandler extends ClientHandler {} diff --git a/scm-clients/scm-client-impl/pom.xml b/scm-clients/scm-client-impl/pom.xml deleted file mode 100644 index 84548301e6..0000000000 --- a/scm-clients/scm-client-impl/pom.xml +++ /dev/null @@ -1,226 +0,0 @@ - - - - 4.0.0 - - - sonia.scm.clients - scm-clients - 2.0.0-SNAPSHOT - - - scm-client-impl - jar - 2.0.0-SNAPSHOT - scm-client-impl - - - - - - - javax.servlet - javax.servlet-api - ${servlet.version} - provided - - - - javax.transaction - jta - 1.1 - provided - - - - sonia.scm.clients - scm-client-api - 2.0.0-SNAPSHOT - - - - com.sun.jersey - jersey-client - ${jersey.version} - - - - com.sun.jersey.contribs - jersey-multipart - ${jersey.version} - - - - - - org.slf4j - jul-to-slf4j - ${slf4j.version} - test - - - - ch.qos.logback - logback-classic - ${logback.version} - test - - - - sonia.scm - scm-test - 2.0.0-SNAPSHOT - test - - - - - - - - - - Sonatype - Sonatype Release - http://oss.sonatype.org/content/repositories/releases - - - - maven.scm-manager.org - scm-manager release repository - http://maven.scm-manager.org/nexus/content/groups/public - - - - - - - - - org.apache.maven.plugins - maven-assembly-plugin - 2.3 - - - jar-with-dependencies - - - - - package - - single - - - - - - - - - - - - it - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 2.4 - - - package - - copy - - - - - sonia.scm - scm-webapp - ${project.version} - war - ${project.build.directory}/webapp - scm-webapp.war - - - - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.12 - - - integration-test - - integration-test - - - - verify - - verify - - - - - - - org.eclipse.jetty - jetty-maven-plugin - ${jetty.maven.version} - - 8085 - STOP - - - scm.home - target/scm-it - - - file.encoding - UTF-8 - - - - 8081 - - - /scm - - ${project.build.directory}/webapp/scm-webapp.war - 0 - true - - - - start-jetty - pre-integration-test - - deploy-war - - - - stop-jetty - post-integration-test - - stop - - - - - - - - - - - - diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/AbstractClientHandler.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/AbstractClientHandler.java deleted file mode 100644 index 0e29456adc..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/AbstractClientHandler.java +++ /dev/null @@ -1,346 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.ModelObject; -import sonia.scm.url.UrlProvider; -import sonia.scm.util.AssertUtil; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.GenericType; -import com.sun.jersey.api.client.WebResource; - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - * - * @param - */ -public abstract class AbstractClientHandler - implements ClientHandler -{ - - /** the logger for AbstractClientHandler */ - private static final Logger logger = - LoggerFactory.getLogger(AbstractClientHandler.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param session - * @param itemClass - */ - public AbstractClientHandler(JerseyClientSession session, Class itemClass) - { - this.session = session; - this.itemClass = itemClass; - this.client = session.getClient(); - this.urlProvider = session.getUrlProvider(); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @return - */ - protected abstract GenericType> createGenericListType(); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param itemId - * - * @return - */ - protected abstract String getItemUrl(String itemId); - - /** - * Method description - * - * - * @return - */ - protected abstract String getItemsUrl(); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param item - */ - @Override - public void create(T item) - { - AssertUtil.assertIsNotNull(item); - - WebResource resource = client.resource(getItemsUrl()); - ClientResponse response = null; - - try - { - response = resource.post(ClientResponse.class, item); - ClientUtil.checkResponse(response, 201); - - String url = response.getHeaders().get("Location").get(0); - - AssertUtil.assertIsNotEmpty(url); - - T newItem = getItemByUrl(url); - - AssertUtil.assertIsNotNull(newItem); - postCreate(response, item, newItem); - } - finally - { - ClientUtil.close(response); - } - } - - /** - * Method description - * - * - * @param id - */ - @Override - public void delete(String id) - { - AssertUtil.assertIsNotEmpty(id); - - WebResource resource = client.resource(getItemUrl(id)); - ClientResponse response = null; - - try - { - response = resource.delete(ClientResponse.class); - ClientUtil.checkResponse(response, 204); - } - finally - { - ClientUtil.close(response); - } - } - - /** - * Method description - * - * - * @param item - */ - @Override - public void delete(T item) - { - AssertUtil.assertIsNotNull(item); - delete(item.getId()); - } - - /** - * Method description - * - * - * @param item - */ - @Override - public void modify(T item) - { - AssertUtil.assertIsNotNull(item); - - String id = item.getId(); - - AssertUtil.assertIsNotEmpty(id); - - WebResource resource = client.resource(getItemUrl(id)); - ClientResponse response = null; - - try - { - response = resource.put(ClientResponse.class, item); - ClientUtil.checkResponse(response, 204); - postModify(response, item); - } - finally - { - ClientUtil.close(response); - } - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param id - * - * @return - */ - @Override - public T get(String id) - { - return getItemByUrl(getItemUrl(id)); - } - - /** - * Method description - * - * - * @return - */ - @Override - public List getAll() - { - List items = null; - String url = getItemsUrl(); - - if (logger.isDebugEnabled()) - { - logger.debug("fetch all items of {} from url", itemClass.getSimpleName(), - url); - } - - WebResource resource = client.resource(url); - ClientResponse response = null; - - try - { - response = resource.get(ClientResponse.class); - ClientUtil.checkResponse(response, 200); - items = response.getEntity(createGenericListType()); - } - finally - { - ClientUtil.close(response); - } - - return items; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param response - * @param item - * @param newItem - */ - protected void postCreate(ClientResponse response, T item, T newItem) {} - - /** - * Method description - * - * - * @param response - * @param item - */ - protected void postModify(ClientResponse response, T item) {} - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param url - * - * @return - */ - protected T getItemByUrl(String url) - { - if (logger.isDebugEnabled()) - { - logger.debug("fetch item {} from url {}", itemClass.getSimpleName(), url); - } - - T item = null; - WebResource resource = client.resource(url); - ClientResponse response = null; - - try - { - response = resource.get(ClientResponse.class); - - int sc = response.getStatus(); - - if (sc != ScmClientException.SC_NOTFOUND) - { - ClientUtil.checkResponse(response, 200); - item = response.getEntity(itemClass); - } - } - finally - { - ClientUtil.close(response); - } - - return item; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - protected Client client; - - /** Field description */ - protected JerseyClientSession session; - - /** Field description */ - protected UrlProvider urlProvider; - - /** Field description */ - private Class itemClass; -} diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/ClientUtil.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/ClientUtil.java deleted file mode 100644 index e297405026..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/ClientUtil.java +++ /dev/null @@ -1,238 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.WebResource; -import com.sun.jersey.api.client.filter.LoggingFilter; - -/** - * - * @author Sebastian Sdorra - */ -public final class ClientUtil -{ - - /** the logger for ClientUtil */ - private static final Logger logger = - LoggerFactory.getLogger(ClientUtil.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - private ClientUtil() {} - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param exception - * @param response - */ - public static void appendContent(ScmClientException exception, - ClientResponse response) - { - try - { - exception.setContent(response.getEntity(String.class)); - } - catch (Exception ex) - { - logger.warn("could not read content", ex); - } - } - - /** - * Method description - * - * - * @param response - * @param expectedStatusCode - */ - public static void checkResponse(ClientResponse response, - int expectedStatusCode) - { - int sc = response.getStatus(); - - if (sc != expectedStatusCode) - { - if (logger.isWarnEnabled()) - { - logger.warn("response code {} expected, but {} returned", - expectedStatusCode, sc); - } - - sendException(response, sc); - } - } - - /** - * Method description - * - * - * @param response - * - */ - public static void checkResponse(ClientResponse response) - { - int sc = response.getStatus(); - - if (sc >= 300) - { - if (logger.isWarnEnabled()) - { - logger.warn("request failed, response code {} returned", sc); - } - - sendException(response, sc); - } - } - - /** - * Method description - * - * - * @param response - */ - public static void close(ClientResponse response) - { - if (response != null) - { - response.close(); - } - } - - /** - * Method description - * - * - * @param client - * @param url - * - * @return - */ - public static WebResource createResource(Client client, String url) - { - return createResource(client, url, false); - } - - /** - * Method description - * - * - * @param client - * @param url - * @param enableLogging - * - * @return - */ - public static WebResource createResource(Client client, String url, - boolean enableLogging) - { - WebResource resource = client.resource(url); - - if (enableLogging) - { - resource.addFilter(new LoggingFilter()); - } - - return resource; - } - - /** - * Method description - * - * - * @param response - * @param sc - */ - public static void sendException(ClientResponse response, int sc) - { - ScmClientException exception = null; - - switch (sc) - { - case ScmClientException.SC_UNAUTHORIZED : - exception = new ScmUnauthorizedException(); - - break; - - case ScmClientException.SC_FORBIDDEN : - exception = new ScmForbiddenException(); - - break; - - case ScmClientException.SC_NOTFOUND : - exception = new ScmNotFoundException(); - - break; - - default : - exception = new ScmClientException(sc); - appendContent(exception, response); - } - - throw exception; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param response - * - * @return - */ - public static boolean isSuccessfull(ClientResponse response) - { - int status = response.getStatus(); - - return (status > 200) && (status < 300); - } -} diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientChangesetHandler.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientChangesetHandler.java deleted file mode 100644 index 8cc8abb60a..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientChangesetHandler.java +++ /dev/null @@ -1,191 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.Changeset; -import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.WebResource; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyClientChangesetHandler implements ClientChangesetHandler -{ - - /** - * Constructs ... - * - * - * @param session - * @param repository - */ - public JerseyClientChangesetHandler(JerseyClientSession session, - Repository repository) - { - this.session = session; - this.repository = repository; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param revision - * - * @return - */ - @Override - public Changeset getChangeset(String revision) - { - Changeset changeset = null; - String url = - session.getUrlProvider().getRepositoryUrlProvider().getChangesetUrl( - repository.getId(), revision); - WebResource resource = session.getClient().resource(url); - ClientResponse response = null; - - try - { - response = resource.get(ClientResponse.class); - - if (response.getStatus() != ScmClientException.SC_NOTFOUND) - { - ClientUtil.checkResponse(response, 200); - changeset = response.getEntity(Changeset.class); - } - } - finally - { - ClientUtil.close(response); - } - - return changeset; - } - - /** - * Method description - * - * - * @param start - * @param limit - * - * @return - */ - @Override - public ChangesetPagingResult getChangesets(int start, int limit) - { - ChangesetPagingResult result = null; - String url = - session.getUrlProvider().getRepositoryUrlProvider().getChangesetUrl( - repository.getId(), start, limit); - WebResource resource = session.getClient().resource(url); - ClientResponse response = null; - - try - { - response = resource.get(ClientResponse.class); - - if (response.getStatus() != ScmClientException.SC_NOTFOUND) - { - ClientUtil.checkResponse(response, 200); - result = response.getEntity(ChangesetPagingResult.class); - } - } - finally - { - ClientUtil.close(response); - } - - return result; - } - - /** - * Method description - * - * - * - * @param path - * @param revision - * @param start - * @param limit - * - * @return - */ - @Override - public ChangesetPagingResult getChangesets(String path, String revision, - int start, int limit) - { - ChangesetPagingResult result = null; - String url = - session.getUrlProvider().getRepositoryUrlProvider().getChangesetUrl( - repository.getId(), path, revision, start, limit); - WebResource resource = session.getClient().resource(url); - ClientResponse response = null; - - try - { - response = resource.get(ClientResponse.class); - - if (response.getStatus() != ScmClientException.SC_NOTFOUND) - { - ClientUtil.checkResponse(response, 200); - result = response.getEntity(ChangesetPagingResult.class); - } - } - finally - { - ClientUtil.close(response); - } - - return result; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Repository repository; - - /** Field description */ - private JerseyClientSession session; -} diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientProvider.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientProvider.java deleted file mode 100644 index eb9825d470..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientProvider.java +++ /dev/null @@ -1,282 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Strings; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.ScmState; -import sonia.scm.url.UrlProvider; -import sonia.scm.url.UrlProviderFactory; -import sonia.scm.util.AssertUtil; -import sonia.scm.util.HttpUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.ClientHandlerException; -import com.sun.jersey.api.client.ClientRequest; -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.WebResource; -import com.sun.jersey.api.client.config.DefaultClientConfig; -import com.sun.jersey.api.client.filter.ClientFilter; -import com.sun.jersey.core.util.MultivaluedMapImpl; -import com.sun.jersey.multipart.impl.MultiPartWriter; - -import javax.ws.rs.core.MultivaluedMap; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyClientProvider implements ScmClientProvider -{ - - /** the logger for JerseyClientProvider */ - private static final Logger logger = - LoggerFactory.getLogger(JerseyClientProvider.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public JerseyClientProvider() {} - - /** - * Constructs ... - * - * - * @param enableLogging - */ - public JerseyClientProvider(boolean enableLogging) - { - this.enableLogging = enableLogging; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param url - * @param username - * @param password - * - * @return - * - */ - @Override - public JerseyClientSession createSession(String url, String username, - String password) - { - AssertUtil.assertIsNotEmpty(url); - - String user = "anonymous"; - - if (Util.isNotEmpty(username)) - { - user = username; - } - - logger.info("create new session for {} with username {}", url, user); - - UrlProvider urlProvider = UrlProviderFactory.createUrlProvider(url, - UrlProviderFactory.TYPE_RESTAPI_XML); - - Client client = - Client.create(new DefaultClientConfig(MultiPartWriter.class)); - - boolean loginAttempt = isLoginAttempt(username, password); - ClientResponse response; - - if (loginAttempt) - { - response = login(urlProvider, client, username, password); - } - else - { - response = state(urlProvider, client); - } - - ClientUtil.checkResponse(response); - - ScmState state = response.getEntity(ScmState.class); - - if (!state.isSuccess()) - { - logger.warn("server returned state failed"); - - throw new ScmClientException("create ScmClientSession failed"); - } - - logger.info("create session successfully for user {}", user); - - if (loginAttempt) - { - appendAuthenticationFilter(client, state); - } - - return new JerseyClientSession(client, urlProvider, state); - } - - private void appendAuthenticationFilter(Client client, ScmState state) - { - String token = state.getToken(); - - if (Strings.isNullOrEmpty(token)) - { - throw new ScmClientException( - "scm-manager does not return a bearer token"); - } - - // authentication for further requests - client.addFilter(new AuthenticationFilter(token)); - } - - private ClientResponse login(UrlProvider urlProvider, Client client, - String username, String password) - { - String authUrl = urlProvider.getAuthenticationUrl(); - - if (logger.isDebugEnabled()) - { - logger.debug("try login at {}", authUrl); - } - - WebResource resource = ClientUtil.createResource(client, authUrl, - enableLogging); - - if (logger.isDebugEnabled()) - { - logger.debug("try login for {}", username); - } - - MultivaluedMap formData = new MultivaluedMapImpl(); - - formData.add("username", username); - formData.add("password", password); - formData.add("grant_type", "password"); - - return resource.type("application/x-www-form-urlencoded").post( - ClientResponse.class, formData); - } - - private ClientResponse state(UrlProvider urlProvider, Client client) - { - String stateUrl = urlProvider.getStateUrl(); - - if (logger.isDebugEnabled()) - { - logger.debug("retrive state from {}", stateUrl); - } - - WebResource resource = ClientUtil.createResource(client, stateUrl, - enableLogging); - - if (logger.isDebugEnabled()) - { - logger.debug("try anonymous login"); - } - - return resource.get(ClientResponse.class); - } - - //~--- get methods ---------------------------------------------------------- - - private boolean isLoginAttempt(String username, String password) - { - return Util.isNotEmpty(username) && Util.isNotEmpty(password); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Authentication filter - */ - private class AuthenticationFilter extends ClientFilter - { - - /** - * Constructs ... - * - * - * @param bearerToken - */ - public AuthenticationFilter(String bearerToken) - { - this.bearerToken = bearerToken; - } - - //~--- methods ------------------------------------------------------------ - - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws ClientHandlerException - */ - @Override - public ClientResponse handle(ClientRequest request) - throws ClientHandlerException - { - request.getHeaders().putSingle(HttpUtil.HEADER_AUTHORIZATION, - HttpUtil.AUTHORIZATION_SCHEME_BEARER.concat(" ").concat(bearerToken)); - - return getNext().handle(request); - } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private final String bearerToken; - } - - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private boolean enableLogging = false; -} diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientRepositoryBrowser.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientRepositoryBrowser.java deleted file mode 100644 index 1b68cdc63d..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientRepositoryBrowser.java +++ /dev/null @@ -1,232 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.BlameLine; -import sonia.scm.repository.BlameResult; -import sonia.scm.repository.BrowserResult; -import sonia.scm.repository.FileObject; -import sonia.scm.repository.Repository; -import sonia.scm.util.AssertUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.WebResource; - -import java.io.IOException; -import java.io.InputStream; - -import java.util.ArrayList; -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyClientRepositoryBrowser implements ClientRepositoryBrowser -{ - - /** - * Constructs ... - * - * - * @param session - * @param repository - */ - public JerseyClientRepositoryBrowser(JerseyClientSession session, - Repository repository) - { - this.session = session; - this.repository = repository; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param revision - * @param path - * - * @return - */ - @Override - public List getBlameLines(String revision, String path) - { - List blameLines = null; - String url = - session.getUrlProvider().getRepositoryUrlProvider().getBlameUrl( - repository.getId(), path, revision); - WebResource resource = session.getClient().resource(url); - ClientResponse response = null; - - try - { - response = resource.get(ClientResponse.class); - - if (response.getStatus() != ScmClientException.SC_NOTFOUND) - { - ClientUtil.checkResponse(response, 200); - - BlameResult result = response.getEntity(BlameResult.class); - - AssertUtil.assertIsNotNull(result); - blameLines = result.getBlameLines(); - } - } - finally - { - ClientUtil.close(response); - } - - return blameLines; - } - - /** - * Method description - * - * - * @param revision - * @param path - * - * @return - * - * @throws IOException - */ - @Override - public InputStream getContent(String revision, String path) throws IOException - { - InputStream input = null; - String url = - session.getUrlProvider().getRepositoryUrlProvider().getContentUrl( - repository.getId(), path, revision); - WebResource resource = session.getClient().resource(url); - ClientResponse response = null; - - try - { - response = resource.get(ClientResponse.class); - - if (response.getStatus() != ScmClientException.SC_NOTFOUND) - { - ClientUtil.checkResponse(response, 200); - input = response.getEntityInputStream(); - } - } - finally - { - ClientUtil.close(response); - } - - return input; - } - - /** - * Method description - * - * - * @param revision - * @param path - * - * @return - */ - @Override - public List getFiles(String revision, String path) - { - List files = null; - String url = - session.getUrlProvider().getRepositoryUrlProvider().getBrowseUrl( - repository.getId(), path, revision); - WebResource resource = session.getClient().resource(url); - ClientResponse response = null; - - try - { - response = resource.get(ClientResponse.class); - - if (response.getStatus() != ScmClientException.SC_NOTFOUND) - { - ClientUtil.checkResponse(response, 200); - - BrowserResult result = response.getEntity(BrowserResult.class); - - AssertUtil.assertIsNotNull(result); - files = new ArrayList<>(); - - List foList = result.getFiles(); - - if (Util.isNotEmpty(foList)) - { - for (FileObject fo : foList) - { - files.add(new FileObjectWrapper(this, revision, fo)); - } - } - } - } - finally - { - ClientUtil.close(response); - } - - return files; - } - - /** - * Method description - * - * - * @param revision - * - * @return - */ - @Override - public List getFiles(String revision) - { - return getFiles(revision, Util.EMPTY_STRING); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Repository repository; - - /** Field description */ - private JerseyClientSession session; -} diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientSession.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientSession.java deleted file mode 100644 index 5e0531be4e..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyClientSession.java +++ /dev/null @@ -1,189 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.ScmState; -import sonia.scm.url.UrlProvider; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.Client; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyClientSession implements ScmClientSession -{ - - /** the logger for JerseyClientSession */ - private static final Logger logger = - LoggerFactory.getLogger(JerseyClientSession.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param client - * @param urlProvider - * @param state - */ - public JerseyClientSession(Client client, UrlProvider urlProvider, - ScmState state) - { - this.client = client; - this.urlProvider = urlProvider; - this.state = state; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - */ - @Override - public void close() - { - if (logger.isInfoEnabled()) - { - logger.info("close client session"); - } - - client.destroy(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Client getClient() - { - return client; - } - - /** - * Method description - * - * - * @return - */ - @Override - public GroupClientHandler getGroupHandler() - { - return new JerseyGroupClientHandler(this); - } - - /** - * Method description - * - * - * @return - */ - @Override - public RepositoryClientHandler getRepositoryHandler() - { - return new JerseyRepositoryClientHandler(this); - } - - /** - * Method description - * - * - * @return - */ - @Override - public SecurityClientHandler getSecurityHandler() - { - return new JerseySecurityClientHandler(this); - } - - /** - * Method description - * - * - * @return - */ - @Override - public ScmState getState() - { - return state; - } - - /** - * Method description - * - * - * @return - */ - public UrlProvider getUrlProvider() - { - return urlProvider; - } - - /** - * Method description - * - * - * @return - */ - @Override - public UserClientHandler getUserHandler() - { - return new JerseyUserClientHandler(this); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final Client client; - - /** Field description */ - private final ScmState state; - - /** Field description */ - private final UrlProvider urlProvider; -} diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyGroupClientHandler.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyGroupClientHandler.java deleted file mode 100644 index 0c22798279..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyGroupClientHandler.java +++ /dev/null @@ -1,122 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.group.Group; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.GenericType; - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyGroupClientHandler extends AbstractClientHandler - implements GroupClientHandler -{ - - /** - * Constructs ... - * - * - * @param session - */ - public JerseyGroupClientHandler(JerseyClientSession session) - { - super(session, Group.class); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - protected GenericType> createGenericListType() - { - return new GenericType>() {} - ; - } - - /** - * Method description - * - * - * @param response - * @param item - * @param newItem - */ - @Override - protected void postCreate(ClientResponse response, Group item, Group newItem) - { - newItem.copyProperties(item); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param itemId - * - * @return - */ - @Override - protected String getItemUrl(String itemId) - { - return urlProvider.getGroupUrlProvider().getDetailUrl(itemId); - } - - /** - * Method description - * - * - * @return - */ - @Override - protected String getItemsUrl() - { - return urlProvider.getGroupUrlProvider().getAllUrl(); - } -} diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyRepositoryClientHandler.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyRepositoryClientHandler.java deleted file mode 100644 index ff611c9aa2..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyRepositoryClientHandler.java +++ /dev/null @@ -1,386 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Strings; - -import sonia.scm.NotSupportedFeatuerException; -import sonia.scm.Type; -import sonia.scm.repository.ImportResult; -import sonia.scm.repository.Repository; -import sonia.scm.repository.Tags; -import sonia.scm.util.HttpUtil; -import sonia.scm.util.IOUtil; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.GenericType; -import com.sun.jersey.api.client.WebResource; -import com.sun.jersey.multipart.FormDataMultiPart; -import com.sun.jersey.multipart.file.StreamDataBodyPart; - -import java.io.IOException; -import java.io.InputStream; - -import java.util.Collection; -import java.util.List; - -import javax.ws.rs.core.MediaType; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyRepositoryClientHandler - extends AbstractClientHandler implements RepositoryClientHandler -{ - - /** Field description */ - private static final String IMPORT_TYPE_BUNDLE = "bundle"; - - /** Field description */ - private static final String IMPORT_TYPE_DIRECTORY = "directory"; - - /** Field description */ - private static final String IMPORT_TYPE_URL = "url"; - - /** Field description */ - private static final String PARAM_BUNDLE = "bundle"; - - /** Field description */ - private static final String PARAM_COMPRESSED = "compressed"; - - /** Field description */ - private static final String PARAM_NAME = "name"; - - /** Field description */ - private static final String URL_IMPORT = "import/repositories/"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param session - */ - public JerseyRepositoryClientHandler(JerseyClientSession session) - { - super(session, Repository.class); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * - * @return - */ - @Override - public Repository importFromBundle(ImportBundleRequest request) - { - WebResource r = client.resource(getImportUrl(request.getType(), - IMPORT_TYPE_BUNDLE)).queryParam(PARAM_COMPRESSED, - Boolean.toString(request.isCompressed())); - Repository repository = null; - InputStream stream = null; - - try - { - stream = request.getBundle().openStream(); - - FormDataMultiPart form = new FormDataMultiPart(); - - form.field(PARAM_NAME, request.getName()); - form.bodyPart(new StreamDataBodyPart(PARAM_BUNDLE, stream)); - - ClientResponse response = - r.type(MediaType.MULTIPART_FORM_DATA).post(ClientResponse.class, form); - - ClientUtil.checkResponse(response); - - String location = - response.getHeaders().getFirst(HttpUtil.HEADER_LOCATION); - - if (Strings.isNullOrEmpty(location)) - { - throw new ScmClientException("no location header found after import"); - } - - repository = getItemByUrl(location); - } - catch (IOException ex) - { - throw new ScmClientException("could not import bundle", ex); - } - finally - { - IOUtil.close(stream); - } - - return repository; - } - - /** - * Method description - * - * - * @param type - * - * @return - */ - @Override - public ImportResultWrapper importFromDirectory(String type) - { - WebResource r = client.resource(getImportUrl(type, IMPORT_TYPE_DIRECTORY)); - ClientResponse response = r.post(ClientResponse.class); - - ClientUtil.checkResponse(response); - - return new ImportResultWrapper(this, type, - response.getEntity(ImportResult.class)); - } - - /** - * Method description - * - * - * @param request - * - * @return - */ - @Override - public Repository importFromUrl(ImportUrlRequest request) - { - WebResource r = client.resource(getImportUrl(request.getType(), - IMPORT_TYPE_URL)); - ClientResponse response = r.post(ClientResponse.class, request); - - ClientUtil.checkResponse(response); - - String location = response.getHeaders().getFirst(HttpUtil.HEADER_LOCATION); - - if (Strings.isNullOrEmpty(location)) - { - throw new ScmClientException("no location header found after import"); - } - - return getItemByUrl(location); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * - * @return - */ - @Override - public Repository get(String type, String name) - { - String url = urlProvider.getRepositoryUrlProvider().getDetailUrl(type, - name); - - return getItemByUrl(url); - } - - /** - * Method description - * - * - * @param repository - * - * @return - * - * @throws NotSupportedFeatuerException - */ - @Override - public ClientChangesetHandler getChangesetHandler(Repository repository) - throws NotSupportedFeatuerException - { - return new JerseyClientChangesetHandler(session, repository); - } - - /** - * Method description - * - * - * @param repository - * - * @return - * - */ - @Override - public JerseyClientRepositoryBrowser getRepositoryBrowser( - Repository repository) - { - return new JerseyClientRepositoryBrowser(session, repository); - } - - /** - * Method description - * - * - * @return - */ - @Override - public Collection getRepositoryTypes() - { - return session.getState().getRepositoryTypes(); - } - - /** - * Method description - * - * - * @param repository - * - * @return - */ - @Override - public Tags getTags(Repository repository) - { - Tags tags = null; - String url = session.getUrlProvider().getRepositoryUrlProvider().getTagsUrl( - repository.getId()); - WebResource resource = session.getClient().resource(url); - ClientResponse response = null; - - try - { - response = resource.get(ClientResponse.class); - - if (response.getStatus() != ScmClientException.SC_NOTFOUND) - { - ClientUtil.checkResponse(response, 200); - tags = response.getEntity(Tags.class); - } - } - finally - { - ClientUtil.close(response); - } - - return tags; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - protected GenericType> createGenericListType() - { - return new GenericType>() {} - ; - } - - /** - * Method description - * - * - * @param response - * @param repository - * @param newRepository - */ - @Override - protected void postCreate(ClientResponse response, Repository repository, - Repository newRepository) - { - newRepository.copyProperties(repository); - - // copyProperties does not copy the repository id - repository.setId(newRepository.getId()); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param itemId - * - * @return - */ - @Override - protected String getItemUrl(String itemId) - { - return urlProvider.getRepositoryUrlProvider().getDetailUrl(itemId); - } - - /** - * Method description - * - * - * @return - */ - @Override - protected String getItemsUrl() - { - return urlProvider.getRepositoryUrlProvider().getAllUrl(); - } - - /** - * Method description - * - * - * @param type - * @param importType - * - * @return - */ - private String getImportUrl(String type, String importType) - { - StringBuilder buffer = new StringBuilder(URL_IMPORT); - - buffer.append(type).append(HttpUtil.SEPARATOR_PATH).append(importType); - - return HttpUtil.append(urlProvider.getBaseUrl(), buffer.toString()); - } -} diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseySecurityClientHandler.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseySecurityClientHandler.java deleted file mode 100644 index 5f24752078..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseySecurityClientHandler.java +++ /dev/null @@ -1,90 +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.client; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseySecurityClientHandler implements SecurityClientHandler -{ - - /** - * Constructs ... - * - * - * @param session - */ - JerseySecurityClientHandler(JerseyClientSession session) - { - this.session = session; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param value - * - * @return - */ - @Override - public String encrypt(String value) - { - String url = - session.getUrlProvider().getSecurityUrlProvider().getEncryptUrl(); - - return session.getClient().resource(url).post(String.class, value); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String generateKey() - { - String url = - session.getUrlProvider().getSecurityUrlProvider().getGenerateKeyUrl(); - - return session.getClient().resource(url).get(String.class); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final JerseyClientSession session; -} diff --git a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyUserClientHandler.java b/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyUserClientHandler.java deleted file mode 100644 index a353434379..0000000000 --- a/scm-clients/scm-client-impl/src/main/java/sonia/scm/client/JerseyUserClientHandler.java +++ /dev/null @@ -1,122 +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.client; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.user.User; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.GenericType; - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyUserClientHandler extends AbstractClientHandler - implements UserClientHandler -{ - - /** - * Constructs ... - * - * - * @param session - */ - public JerseyUserClientHandler(JerseyClientSession session) - { - super(session, User.class); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - protected GenericType> createGenericListType() - { - return new GenericType>() {} - ; - } - - /** - * Method description - * - * - * @param response - * @param item - * @param newItem - */ - @Override - protected void postCreate(ClientResponse response, User item, User newItem) - { - newItem.copyProperties(item); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param itemId - * - * @return - */ - @Override - protected String getItemUrl(String itemId) - { - return urlProvider.getUserUrlProvider().getDetailUrl(itemId); - } - - /** - * Method description - * - * - * @return - */ - @Override - protected String getItemsUrl() - { - return urlProvider.getUserUrlProvider().getAllUrl(); - } -} diff --git a/scm-clients/scm-client-impl/src/main/resources/META-INF/services/sonia.scm.client.ScmClientProvider b/scm-clients/scm-client-impl/src/main/resources/META-INF/services/sonia.scm.client.ScmClientProvider deleted file mode 100644 index ac345b3480..0000000000 --- a/scm-clients/scm-client-impl/src/main/resources/META-INF/services/sonia.scm.client.ScmClientProvider +++ /dev/null @@ -1 +0,0 @@ -sonia.scm.client.JerseyClientProvider \ No newline at end of file diff --git a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/AbstractClientHandlerTestBase.java b/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/AbstractClientHandlerTestBase.java deleted file mode 100644 index fe4547ecb8..0000000000 --- a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/AbstractClientHandlerTestBase.java +++ /dev/null @@ -1,295 +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.client.it; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import sonia.scm.ModelObject; -import sonia.scm.client.ClientHandler; -import sonia.scm.client.JerseyClientSession; -import sonia.scm.client.ScmForbiddenException; -import sonia.scm.client.ScmUnauthorizedException; - -import static org.junit.Assert.*; - -import static sonia.scm.client.it.ClientTestUtil.*; - -/** - * - * @author Sebastian Sdorra - * - * @param - */ -public abstract class AbstractClientHandlerTestBase -{ - - /** - * Method description - * - * - * @param session - * - * @return - */ - protected abstract ClientHandler createHandler( - JerseyClientSession session); - - /** - * Method description - * - * - * @return - */ - protected abstract ModifyTest createModifyTest(); - - /** - * Method description - * - * - * @param number - * - * @return - */ - protected abstract T createTestData(int number); - - /** - * Method description - * - * - */ - @Test - public void testCreate() - { - JerseyClientSession session = createAdminSession(); - T item = createTestData(1); - ClientHandler handler = createHandler(session); - - handler.create(item); - assertIsValid(item); - - String id = item.getId(); - T o = handler.get(id); - - assertNotNull(o); - assertEquals(item.getId(), o.getId()); - session.close(); - } - - /** - * Method description - * - * - */ - @Test - public void testDelete() - { - JerseyClientSession session = createAdminSession(); - T item = createTestData(2); - ClientHandler handler = createHandler(session); - - handler.create(item); - assertIsValid(item); - - String id = item.getId(); - - handler.delete(item); - - T o = handler.get(id); - - assertNull(o); - } - - /** - * Method description - * - * - */ - @Test(expected = ScmUnauthorizedException.class) - public void testDisabledCreateAnonymous() - { - JerseyClientSession session = createAnonymousSession(); - T item = createTestData(3); - - createHandler(session).create(item); - session.close(); - } - - /** - * Method description - * - * - */ - @Test - public void testEnabledCreateAnonymous() - { - setAnonymousAccess(true); - - JerseyClientSession session = createAnonymousSession(); - T item = createTestData(3); - boolean forbidden = false; - - try - { - createHandler(session).create(item); - } - catch (ScmForbiddenException ex) - { - forbidden = true; - } - - assertTrue(forbidden); - session.close(); - setAnonymousAccess(false); - } - - /** - * Method description - * - */ - @Test - public void testEnabledModifyAnonymous() - { - setAnonymousAccess(true); - - JerseyClientSession session = createAdminSession(); - T item = createTestData(4); - - createHandler(session).create(item); - assertIsValid(item); - session.close(); - session = createAnonymousSession(); - - ModifyTest mt = createModifyTest(); - - mt.modify(item); - - boolean notfound = false; - - try - { - createHandler(session).modify(item); - } - catch (ScmForbiddenException ex) - { - notfound = true; - } - - setAnonymousAccess(false); - session.close(); - session = createAdminSession(); - createHandler(session).delete(item); - session.close(); - assertTrue(notfound); - } - - /** - * Method description - * - */ - @Test - public void testModify() - { - long start = System.currentTimeMillis(); - JerseyClientSession session = createAdminSession(); - T item = createTestData(4); - ClientHandler handler = createHandler(session); - - handler.create(item); - assertIsValid(item); - item = handler.get(item.getId()); - - ModifyTest mt = createModifyTest(); - - mt.modify(item); - handler.modify(item); - item = handler.get(item.getId()); - assertIsValid(item); - assertTrue(mt.isCorrectModified(item)); - assertNotNull(item.getLastModified()); - assertTrue(item.getLastModified() > start); - assertTrue(item.getLastModified() < System.currentTimeMillis()); - handler.delete(item); - session.close(); - } - - /** - * Method description - * - * - * @param item - */ - protected void assertIsValid(T item) - { - assertNotNull(item); - assertNotNull(item.getId()); - assertTrue(item.getId().length() > 0); - } - - //~--- inner interfaces ----------------------------------------------------- - - /** - * Interface description - * - * - * @param - * - * @version Enter version here..., 11/05/13 - * @author Enter your name here... - */ - protected interface ModifyTest - { - - /** - * Method description - * - * - * @param item - */ - public void modify(T item); - - //~--- get methods -------------------------------------------------------- - - /** - * Method description - * - * - * @param item - * - * @return - */ - public boolean isCorrectModified(T item); - } -} diff --git a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/ClientTestUtil.java b/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/ClientTestUtil.java deleted file mode 100644 index c567ff0e86..0000000000 --- a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/ClientTestUtil.java +++ /dev/null @@ -1,144 +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.client.it; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.client.ClientUtil; -import sonia.scm.client.JerseyClientProvider; -import sonia.scm.client.JerseyClientSession; -import sonia.scm.config.ScmConfiguration; -import sonia.scm.url.UrlProvider; - -//~--- JDK imports ------------------------------------------------------------ - -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.WebResource; - -/** - * - * @author Sebastian Sdorra - */ -public final class ClientTestUtil -{ - - /** Field description */ - public static final String ADMIN_PASSWORD = "scmadmin"; - - /** Field description */ - public static final String ADMIN_USERNAME = "scmadmin"; - - /** Field description */ - public static final String REPOSITORY_TYPE = "git"; - - /** Field description */ - public static final String URL_BASE = "http://localhost:8081/scm"; - - /** Field description */ - public static final boolean REQUEST_LOGGING = false; - - private ClientTestUtil() - { - } - - - - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - * - */ - public static JerseyClientSession createAdminSession() - { - return createSession(ADMIN_USERNAME, ADMIN_PASSWORD); - } - - /** - * Method description - * - * - * @return - * - */ - public static JerseyClientSession createAnonymousSession() - { - return createSession(null, null); - } - - /** - * Method description - * - * - * @param username - * @param password - * - * @return - * - */ - public static JerseyClientSession createSession(String username, - String password) - { - JerseyClientProvider provider = new JerseyClientProvider(REQUEST_LOGGING); - - return provider.createSession(URL_BASE, username, password); - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param access - * - */ - public static void setAnonymousAccess(boolean access) - { - JerseyClientSession adminSession = createAdminSession(); - UrlProvider up = adminSession.getUrlProvider(); - Client client = adminSession.getClient(); - WebResource resource = ClientUtil.createResource(client, up.getConfigUrl(), - REQUEST_LOGGING); - ScmConfiguration config = resource.get(ScmConfiguration.class); - - config.setAnonymousAccessEnabled(access); - resource.post(config); - adminSession.close(); - } -} diff --git a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyClientProviderITCase.java b/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyClientProviderITCase.java deleted file mode 100644 index 3aab675b0b..0000000000 --- a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyClientProviderITCase.java +++ /dev/null @@ -1,138 +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.client.it; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import sonia.scm.client.JerseyClientSession; -import sonia.scm.client.ScmClientException; -import sonia.scm.client.ScmUnauthorizedException; - -import static org.junit.Assert.*; - -import static sonia.scm.client.it.ClientTestUtil.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyClientProviderITCase -{ - - /** - * Method description - * - * - * - */ - @Test(expected = ScmUnauthorizedException.class) - public void createSessionAnonymousFailedTest() - { - createAnonymousSession().close(); - } - - /** - * Method description - * - * - * - */ - @Test - public void createSessionAnonymousTest() - { - - // enable anonymous access - setAnonymousAccess(true); - - // test anonymous access - createAnonymousSession().close(); - - // disable anonymous access - setAnonymousAccess(false); - } - - /** - * Method description - * - * - * - */ - @Test - public void createSessionTest() - { - JerseyClientSession session = createAdminSession(); - - assertNotNull(session); - assertNotNull(session.getState()); - assertNotNull(session.getState().getUser()); - assertEquals(session.getState().getUser().getName(), "scmadmin"); - session.close(); - } - - /** - * Method description - * - * - * - * @throws IOException - * @throws ScmClientException - */ - @Test(expected = ScmUnauthorizedException.class) - public void createSessionWithUnkownUserTest() - throws ScmClientException, IOException - { - createSession("dent", "dent123").close(); - } - - /** - * Method description - * - * - * - * @throws IOException - * @throws ScmClientException - */ - @Test(expected = ScmUnauthorizedException.class) - public void createSessionWithWrongPasswordTest() - throws ScmClientException, IOException - { - createSession("scmadmin", "ka123").close(); - } -} diff --git a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyGroupClientHandlerITCase.java b/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyGroupClientHandlerITCase.java deleted file mode 100644 index 0650564741..0000000000 --- a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyGroupClientHandlerITCase.java +++ /dev/null @@ -1,102 +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.client.it; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.client.ClientHandler; -import sonia.scm.client.JerseyClientSession; -import sonia.scm.client.it.AbstractClientHandlerTestBase.ModifyTest; -import sonia.scm.group.Group; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyGroupClientHandlerITCase - extends AbstractClientHandlerTestBase -{ - - /** - * Method description - * - * - * @param session - * - * @return - */ - @Override - protected ClientHandler createHandler(JerseyClientSession session) - { - return session.getGroupHandler(); - } - - /** - * Method description - * - * - * @return - */ - @Override - protected ModifyTest createModifyTest() - { - return new ModifyTest() - { - @Override - public void modify(Group item) - { - item.setDescription("Modified Description"); - } - @Override - public boolean isCorrectModified(Group item) - { - return "Modified Description".equals(item.getDescription()); - } - }; - } - - /** - * Method description - * - * - * @param number - * - * @return - */ - @Override - protected Group createTestData(int number) - { - return new Group("xml", "group-" + number); - } -} diff --git a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyRepositoryClientHandlerITCase.java b/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyRepositoryClientHandlerITCase.java deleted file mode 100644 index d960d57392..0000000000 --- a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyRepositoryClientHandlerITCase.java +++ /dev/null @@ -1,149 +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.client.it; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.client.ClientHandler; -import sonia.scm.client.JerseyClientSession; -import sonia.scm.client.it.AbstractClientHandlerTestBase.ModifyTest; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryTestData; - -import static org.junit.Assert.*; - -import static sonia.scm.client.it.ClientTestUtil.*; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyRepositoryClientHandlerITCase - extends AbstractClientHandlerTestBase -{ - - /** - * Method description - * - * - * @param item - */ - @Override - protected void assertIsValid(Repository item) - { - super.assertIsValid(item); - assertNotNull(item.getCreationDate()); - assertTrue(item.getCreationDate() > 0); - assertTrue(item.getCreationDate() < System.currentTimeMillis()); - } - - /** - * Method description - * - * - * @param session - * - * @return - */ - @Override - protected ClientHandler createHandler(JerseyClientSession session) - { - return session.getRepositoryHandler(); - } - - /** - * Method description - * - * - * @return - */ - @Override - protected ModifyTest createModifyTest() - { - return new ModifyTest() - { - @Override - public void modify(Repository item) - { - item.setDescription("Modified description"); - } - @Override - public boolean isCorrectModified(Repository item) - { - return "Modified description".equals(item.getDescription()); - } - }; - } - - /** - * Method description - * - * - * @param number - * - * @return - */ - @Override - protected Repository createTestData(int number) - { - Repository repository = null; - - switch (number) - { - case 1 : - repository = RepositoryTestData.createHeartOfGold(REPOSITORY_TYPE); - - break; - - case 2 : - repository = RepositoryTestData.createHappyVerticalPeopleTransporter( - REPOSITORY_TYPE); - - break; - - case 3 : - repository = RepositoryTestData.create42Puzzle(REPOSITORY_TYPE); - - break; - - case 4 : - repository = RepositoryTestData.createRestaurantAtTheEndOfTheUniverse( - REPOSITORY_TYPE); - - break; - } - - return repository; - } -} diff --git a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyUserClientHandlerITCase.java b/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyUserClientHandlerITCase.java deleted file mode 100644 index d84deabb42..0000000000 --- a/scm-clients/scm-client-impl/src/test/java/sonia/scm/client/it/JerseyUserClientHandlerITCase.java +++ /dev/null @@ -1,128 +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.client.it; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.client.ClientHandler; -import sonia.scm.client.JerseyClientSession; -import sonia.scm.client.it.AbstractClientHandlerTestBase.ModifyTest; -import sonia.scm.user.User; -import sonia.scm.user.UserTestData; - -/** - * - * @author Sebastian Sdorra - */ -public class JerseyUserClientHandlerITCase - extends AbstractClientHandlerTestBase -{ - - /** - * Method description - * - * - * @param session - * - * @return - */ - @Override - protected ClientHandler createHandler(JerseyClientSession session) - { - return session.getUserHandler(); - } - - /** - * Method description - * - * - * @return - */ - @Override - protected ModifyTest createModifyTest() - { - return new ModifyTest() - { - @Override - public void modify(User item) - { - item.setDisplayName("Modified DisplayName"); - } - @Override - public boolean isCorrectModified(User item) - { - return "Modified DisplayName".equals(item.getDisplayName()); - } - }; - } - - /** - * Method description - * - * - * @param number - * - * @return - */ - @Override - protected User createTestData(int number) - { - User user = null; - - switch (number) - { - case 1 : - user = UserTestData.createAdams(); - - break; - - case 2 : - user = UserTestData.createDent(); - - break; - - case 3 : - user = UserTestData.createMarvin(); - - break; - - case 4 : - user = UserTestData.createPerfect(); - - break; - } - - return user; - } -} diff --git a/scm-clients/scm-client-impl/src/test/resources/logback.xml b/scm-clients/scm-client-impl/src/test/resources/logback.xml deleted file mode 100644 index 26effea73e..0000000000 --- a/scm-clients/scm-client-impl/src/test/resources/logback.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - \ No newline at end of file diff --git a/scm-core/pom.xml b/scm-core/pom.xml index 3b88e2fa9a..8437b256b2 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -9,6 +9,7 @@ 2.0.0-SNAPSHOT + sonia.scm scm-core 2.0.0-SNAPSHOT scm-core @@ -23,15 +24,22 @@ ${servlet.version} provided - + - + sonia.scm scm-annotations 2.0.0-SNAPSHOT - + + + org.projectlombok + lombok + provided + + + @@ -39,7 +47,13 @@ org.slf4j ${slf4j.version} - + + + jcl-over-slf4j + org.slf4j + ${slf4j.version} + + @@ -67,83 +81,155 @@ guice-servlet ${guice.version} - + com.google.inject.extensions guice-throwingproviders ${guice.version} - + - com.sun.jersey - jersey-core - ${jersey.version} + javax.ws.rs + javax.ws.rs-api + provided - + + + org.jboss.resteasy + resteasy-jaxrs + test + + + + + + com.fasterxml.jackson.core + jackson-core + + + + com.fasterxml.jackson.core + jackson-databind + + + + com.fasterxml.jackson.core + jackson-annotations + + + + + de.otto.edison + edison-hal + + + + + org.mapstruct + mapstruct-jdk8 + + + + org.mapstruct + mapstruct-processor + provided + + + + + com.webcohesion.enunciate + enunciate-core-annotations + + - + com.github.legman core ${legman.version} - + + + + + javax.xml.bind + jaxb-api + + + + com.sun.xml.bind + jaxb-impl + + + + org.glassfish.jaxb + jaxb-runtime + + + + javax.activation + activation + + - + com.google.guava guava ${guava.version} - + commons-lang commons-lang 2.6 - + - + sonia.scm scm-annotation-processor 2.0.0-SNAPSHOT provided - + com.github.sdorra ssp-lib - ${ssp.version} com.github.sdorra ssp-processor - ${ssp.version} true - + com.github.sdorra shiro-unit - 1.0.0 test + + org.hibernate + hibernate-validator + 5.3.6.Final + compile + - + - + org.apache.maven.plugins maven-javadoc-plugin - 2.9 + 3.0.0 true ${project.build.sourceEncoding} @@ -162,15 +248,28 @@ http://download.oracle.com/javase/6/docs/api/ http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/ - http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/ https://google.github.io/guice/api-docs/${guice.version}/javadoc http://www.slf4j.org/api/ http://shiro.apache.org/static/${shiro.version}/apidocs/ + org.jboss.apiviz.APIviz + + org.jboss.apiviz + apiviz + 1.3.2.GA + + + + -sourceclasspath ${project.build.outputDirectory} + + + -nopackagediagram + + - + - + diff --git a/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java new file mode 100644 index 0000000000..3ca8335346 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java @@ -0,0 +1,34 @@ +package sonia.scm; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.joining; + +public class AlreadyExistsException extends ExceptionWithContext { + + private static final String CODE = "FtR7UznKU1"; + + public AlreadyExistsException(ModelObject object) { + this(singletonList(new ContextEntry(object.getClass(), object.getId()))); + } + + public static AlreadyExistsException alreadyExists(ContextEntry.ContextBuilder builder) { + return new AlreadyExistsException(builder.build()); + } + + private AlreadyExistsException(List context) { + super(context, createMessage(context)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(List context) { + return context.stream() + .map(c -> c.getType().toLowerCase() + " with id " + c.getId()) + .collect(joining(" in ", "", " already exists")); + } +} diff --git a/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java b/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java deleted file mode 100644 index 727e9a8160..0000000000 --- a/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm; - -/** - * - * @author Sebastian Sdorra - * @since 1.17 - */ -public class ArgumentIsInvalidException extends IllegalStateException -{ - - /** - * Constructs ... - * - */ - public ArgumentIsInvalidException() - { - super(); - } - - /** - * Constructs ... - * - * - * @param s - */ - public ArgumentIsInvalidException(String s) - { - super(s); - } - - /** - * Constructs ... - * - * - * @param cause - */ - public ArgumentIsInvalidException(Throwable cause) - { - super(cause); - } - - /** - * Constructs ... - * - * - * @param message - * @param cause - */ - public ArgumentIsInvalidException(String message, Throwable cause) - { - super(message, cause); - } -} diff --git a/scm-core/src/main/java/sonia/scm/BadRequestException.java b/scm-core/src/main/java/sonia/scm/BadRequestException.java new file mode 100644 index 0000000000..3290e77521 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/BadRequestException.java @@ -0,0 +1,13 @@ +package sonia.scm; + +import java.util.List; + +public abstract class BadRequestException extends ExceptionWithContext { + public BadRequestException(List context, String message) { + super(context, message); + } + + public BadRequestException(List context, String message, Exception cause) { + super(context, message, cause); + } +} diff --git a/scm-core/src/main/java/sonia/scm/BasicContextProvider.java b/scm-core/src/main/java/sonia/scm/BasicContextProvider.java index f6507fc453..f4ed04c3f5 100644 --- a/scm-core/src/main/java/sonia/scm/BasicContextProvider.java +++ b/scm-core/src/main/java/sonia/scm/BasicContextProvider.java @@ -35,6 +35,7 @@ package sonia.scm; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import sonia.scm.util.Util; //~--- JDK imports ------------------------------------------------------------ @@ -43,6 +44,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.util.Locale; import java.util.Properties; @@ -105,19 +107,24 @@ public class BasicContextProvider implements SCMContextProvider } } + @VisibleForTesting + BasicContextProvider(File baseDirectory, String version, Stage stage) { + this.baseDirectory = baseDirectory; + this.version = version; + this.stage = stage; + } + //~--- methods -------------------------------------------------------------- - /** - * {@inheritDoc} - */ - @Override - public void close() throws IOException {} - /** - * {@inheritDoc} - */ @Override - public void init() {} + public Path resolve(Path path) { + if (path.isAbsolute()) { + return path; + } + + return baseDirectory.toPath().resolve(path); + } //~--- get methods ---------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/BasicPropertiesAware.java b/scm-core/src/main/java/sonia/scm/BasicPropertiesAware.java index f0b7772a77..cfd4284fae 100644 --- a/scm-core/src/main/java/sonia/scm/BasicPropertiesAware.java +++ b/scm-core/src/main/java/sonia/scm/BasicPropertiesAware.java @@ -37,18 +37,15 @@ package sonia.scm; import com.google.common.base.Objects; import com.google.common.collect.Maps; - import sonia.scm.xml.XmlMapStringAdapter; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -import java.util.Map; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.io.Serializable; +import java.util.Map; + +//~--- JDK imports ------------------------------------------------------------ /** * Default implementation of {@link PropertiesAware} interface. diff --git a/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java b/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java new file mode 100644 index 0000000000..990d05181b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java @@ -0,0 +1,35 @@ +package sonia.scm; + +import java.util.Collections; +import java.util.List; + +import static java.util.stream.Collectors.joining; + +public class ConcurrentModificationException extends ExceptionWithContext { + + private static final String CODE = "2wR7UzpPG1"; + + public ConcurrentModificationException(Class type, String id) { + this(Collections.singletonList(new ContextEntry(type, id))); + } + + public ConcurrentModificationException(String type, String id) { + this(Collections.singletonList(new ContextEntry(type, id))); + } + + public ConcurrentModificationException(List context) { + super(context, createMessage(context)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(List context) { + return context.stream() + .map(c -> c.getType().toLowerCase() + " with id " + c.getId()) + .collect(joining(" in ", "", " has been modified concurrently")); + } +} + diff --git a/scm-core/src/main/java/sonia/scm/ContextEntry.java b/scm-core/src/main/java/sonia/scm/ContextEntry.java new file mode 100644 index 0000000000..bbd2fadc5d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ContextEntry.java @@ -0,0 +1,84 @@ +package sonia.scm; + +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.util.AssertUtil; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class ContextEntry { + private final String type; + private final String id; + + ContextEntry(Class type, String id) { + this(type.getSimpleName(), id); + } + + ContextEntry(String type, String id) { + AssertUtil.assertIsNotEmpty(type); + AssertUtil.assertIsNotEmpty(id); + this.type = type; + this.id = id; + } + + public String getType() { + return type; + } + + public String getId() { + return id; + } + + + public static class ContextBuilder { + private final List context = new LinkedList<>(); + + public static List noContext() { + return new ContextBuilder().build(); + } + + public static List only(String type, String id) { + return new ContextBuilder().in(type, id).build(); + } + + public static ContextBuilder entity(Repository repository) { + return new ContextBuilder().in(repository.getNamespaceAndName()); + } + + public static ContextBuilder entity(NamespaceAndName namespaceAndName) { + return new ContextBuilder().in(Repository.class, namespaceAndName.logString()); + } + + public static ContextBuilder entity(Class type, String id) { + return new ContextBuilder().in(type, id); + } + + public static ContextBuilder entity(String type, String id) { + return new ContextBuilder().in(type, id); + } + + public ContextBuilder in(Repository repository) { + return in(repository.getNamespaceAndName()); + } + + public ContextBuilder in(NamespaceAndName namespaceAndName) { + return this.in(Repository.class, namespaceAndName.logString()); + } + + public ContextBuilder in(Class type, String id) { + context.add(new ContextEntry(type, id)); + return this; + } + + public ContextBuilder in(String type, String id) { + context.add(new ContextEntry(type, id)); + return this; + } + + public List build() { + return Collections.unmodifiableList(context); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/DisplayManager.java b/scm-core/src/main/java/sonia/scm/DisplayManager.java new file mode 100644 index 0000000000..72113a483d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/DisplayManager.java @@ -0,0 +1,19 @@ +package sonia.scm; + +import java.util.Collection; +import java.util.Optional; + +public interface DisplayManager { + + int DEFAULT_LIMIT = 5; + + /** + * Returns a {@link Collection} of filtered objects + * + * @param filter the searched string + * @return filtered object from the store + */ + Collection autocomplete(String filter); + + Optional get(String id); +} diff --git a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java new file mode 100644 index 0000000000..8702567880 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java @@ -0,0 +1,28 @@ +package sonia.scm; + +import java.util.List; + +import static java.util.Collections.unmodifiableList; + +public abstract class ExceptionWithContext extends RuntimeException { + + private static final long serialVersionUID = 4327413456580409224L; + + private final List context; + + public ExceptionWithContext(List context, String message) { + super(message); + this.context = context; + } + + public ExceptionWithContext(List context, String message, Exception cause) { + super(message, cause); + this.context = context; + } + + public List getContext() { + return unmodifiableList(context); + } + + public abstract String getCode(); +} diff --git a/scm-core/src/main/java/sonia/scm/FeatureNotSupportedException.java b/scm-core/src/main/java/sonia/scm/FeatureNotSupportedException.java new file mode 100644 index 0000000000..2d64af4318 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/FeatureNotSupportedException.java @@ -0,0 +1,63 @@ +/** + * 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; + +import java.util.Collections; + +/** + * + * @author Sebastian Sdorra + * @version 1.6 + */ +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class FeatureNotSupportedException extends BadRequestException { + + private static final long serialVersionUID = 256498734456613496L; + + private static final String CODE = "9SR8G0kmU1"; + + public FeatureNotSupportedException(String feature) + { + super(Collections.emptyList(),createMessage(feature)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(String feature) { + return "feature " + feature + " is not supported by this repository"; + } +} diff --git a/scm-core/src/main/java/sonia/scm/GenericDAO.java b/scm-core/src/main/java/sonia/scm/GenericDAO.java index b63a96f733..003c73806c 100644 --- a/scm-core/src/main/java/sonia/scm/GenericDAO.java +++ b/scm-core/src/main/java/sonia/scm/GenericDAO.java @@ -114,4 +114,5 @@ public interface GenericDAO * @return all items */ public Collection getAll(); + } diff --git a/scm-core/src/main/java/sonia/scm/Handler.java b/scm-core/src/main/java/sonia/scm/Handler.java index 67ec039fba..a4e927ed7b 100644 --- a/scm-core/src/main/java/sonia/scm/Handler.java +++ b/scm-core/src/main/java/sonia/scm/Handler.java @@ -39,11 +39,8 @@ package sonia.scm; * @author Sebastian Sdorra * * @param a typed object - * @param */ -public interface Handler - extends HandlerBase -{ +public interface Handler extends HandlerBase { /** * Returns the type object of the handler. @@ -51,7 +48,7 @@ public interface Handler * * @return type object of the handler */ - public Type getType(); + Type getType(); /** * Returns true if the hanlder is configured. @@ -59,5 +56,5 @@ public interface Handler * * @return true if the hanlder is configured */ - public boolean isConfigured(); + boolean isConfigured(); } diff --git a/scm-core/src/main/java/sonia/scm/HandlerBase.java b/scm-core/src/main/java/sonia/scm/HandlerBase.java index 0baea8d929..d960cc6107 100644 --- a/scm-core/src/main/java/sonia/scm/HandlerBase.java +++ b/scm-core/src/main/java/sonia/scm/HandlerBase.java @@ -36,7 +36,6 @@ package sonia.scm; //~--- JDK imports ------------------------------------------------------------ import java.io.Closeable; -import java.io.IOException; /** * The base class of all handlers. @@ -44,42 +43,31 @@ import java.io.IOException; * @author Sebastian Sdorra * * @param type object of the handler - * @param exception type of the handler */ -public interface HandlerBase +public interface HandlerBase extends Initable, Closeable { /** * Persists a new object. * - * - * @param object to store - * - * @throws E - * @throws IOException + * @return The persisted object. */ - public void create(T object) throws E, IOException; + T create(T object); /** * Removes a persistent object. * * * @param object to delete - * - * @throws E - * @throws IOException */ - public void delete(T object) throws E, IOException; + void delete(T object); /** * Modifies a persistent object. * * * @param object to modify - * - * @throws E - * @throws IOException */ - public void modify(T object) throws E, IOException; + void modify(T object); } diff --git a/scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java b/scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java new file mode 100644 index 0000000000..63b9d0d4fc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java @@ -0,0 +1,21 @@ +package sonia.scm; + +import java.util.Collections; + +public class IllegalIdentifierChangeException extends BadRequestException { + + private static final String CODE = "thbsUFokjk"; + + public IllegalIdentifierChangeException(ContextEntry.ContextBuilder context, String message) { + super(context.build(), message); + } + + public IllegalIdentifierChangeException(String message) { + super(Collections.emptyList(), message); + } + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 600ab268ca..6e0b72295c 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -33,12 +33,9 @@ package sonia.scm; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import java.util.Collection; import java.util.Comparator; +import java.util.function.Predicate; /** * Base interface for all manager classes. @@ -46,22 +43,21 @@ import java.util.Comparator; * @author Sebastian Sdorra * * @param type of the model object - * @param type of the exception */ -public interface Manager - extends HandlerBase, LastModifiedAware +public interface Manager + extends HandlerBase, LastModifiedAware { + /** * Reloads a object from store and overwrites all changes. * * * @param object to refresh * - * @throws E - * @throws IOException + * @throws NotFoundException */ - public void refresh(T object) throws E, IOException; + void refresh(T object); //~--- get methods ---------------------------------------------------------- @@ -73,7 +69,7 @@ public interface Manager * * @return object with the given id */ - public T get(String id); + T get(String id); /** * Returns a {@link java.util.Collection} of all objects in the store. @@ -81,17 +77,18 @@ public interface Manager * * @return all object in the store */ - public Collection getAll(); + Collection getAll(); /** * Returns all object of the store sorted by the given {@link java.util.Comparator} * * + * @param filter to filter the returned objects * @param comparator to sort the returned objects * @since 1.4 * @return all object of the store sorted by the given {@link java.util.Comparator} */ - public Collection getAll(Comparator comparator); + Collection getAll(Predicate filter, Comparator comparator); /** * Returns objects from the store which are starts at the given start @@ -102,14 +99,14 @@ public interface Manager * @param limit parameter * * @since 1.4 - * @return objects from the store which are starts at the given + * @return objects from the store which are starts at the given * start parameter */ - public Collection getAll(int start, int limit); + Collection getAll(int start, int limit); /** * Returns objects from the store which are starts at the given start - * parameter sorted by the given {@link java.util.Comparator}. + * parameter sorted by the given {@link java.util.Comparator}. * The objects returned are limited by the limit parameter. * * @@ -118,8 +115,30 @@ public interface Manager * @param limit parameter * * @since 1.4 - * @return objects from the store which are starts at the given + * @return objects from the store which are starts at the given * start parameter */ - public Collection getAll(Comparator comparator, int start, int limit); + Collection getAll(Comparator comparator, int start, int limit); + + /** + * Returns objects from the store divided into pages with the given page + * size for the given page number (zero based) and sorted by the given + * {@link java.util.Comparator}. + *

This default implementation reads all items, first, so you might want to adapt this + * whenever reading is expensive!

+ * + * @param filter to filter returned objects + * @param comparator to sort the returned objects + * @param pageNumber the number of the page to be returned (zero based) + * @param pageSize the size of the pages + * + * @since 2.0 + * @return {@link PageResult} with the objects from the store for the requested + * page. If the requested page number exceeds the existing pages, an + * empty page result is returned. + */ + default PageResult getPage(Predicate filter, Comparator comparator, int pageNumber, int pageSize) { + return PageResult.createPage(getAll(filter, comparator), pageNumber, pageSize); + } + } diff --git a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java index af0215202c..0c46366a56 100644 --- a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java @@ -35,9 +35,9 @@ package sonia.scm; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; - import java.util.Collection; import java.util.Comparator; +import java.util.function.Predicate; /** * Basic decorator for manager classes. @@ -46,11 +46,8 @@ import java.util.Comparator; * @since 1.23 * * @param model type - * @param exception type */ -public class ManagerDecorator - implements Manager -{ +public class ManagerDecorator implements Manager { /** * Constructs a new ManagerDecorator. @@ -58,125 +55,78 @@ public class ManagerDecorator * * @param decorated manager implementation */ - public ManagerDecorator(Manager decorated) + public ManagerDecorator(Manager decorated) { this.decorated = decorated; } - //~--- methods -------------------------------------------------------------- - - /** - * {@inheritDoc} - */ @Override public void close() throws IOException { decorated.close(); } - /** - * {@inheritDoc} - */ @Override - public void create(T object) throws E, IOException - { - decorated.create(object); + public T create(T object) { + return decorated.create(object); } - /** - * {@inheritDoc} - */ @Override - public void delete(T object) throws E, IOException - { + public void delete(T object){ decorated.delete(object); } - /** - * {@inheritDoc} - */ @Override public void init(SCMContextProvider context) { decorated.init(context); } - /** - * {@inheritDoc} - */ @Override - public void modify(T object) throws E, IOException - { + public void modify(T object){ decorated.modify(object); } - /** - * {@inheritDoc} - */ @Override - public void refresh(T object) throws E, IOException - { + public void refresh(T object){ decorated.refresh(object); } - //~--- get methods ---------------------------------------------------------- - - /** - * {@inheritDoc} - */ @Override public T get(String id) { return decorated.get(id); } - /** - * {@inheritDoc} - */ @Override public Collection getAll() { return decorated.getAll(); } - /** - * {@inheritDoc} - */ @Override - public Collection getAll(Comparator comparator) + public Collection getAll(Predicate filter, Comparator comparator) { - return decorated.getAll(comparator); + return decorated.getAll(filter, comparator); } - /** - * {@inheritDoc} - */ @Override public Collection getAll(int start, int limit) { return decorated.getAll(start, limit); } - /** - * {@inheritDoc} - */ @Override public Collection getAll(Comparator comparator, int start, int limit) { return decorated.getAll(comparator, start, limit); } - /** - * {@inheritDoc} - */ @Override public Long getLastModified() { return decorated.getLastModified(); } - //~--- fields --------------------------------------------------------------- - - /** manager implementation */ - private Manager decorated; + private Manager decorated; } diff --git a/scm-core/src/main/java/sonia/scm/ModelObject.java b/scm-core/src/main/java/sonia/scm/ModelObject.java index 76ba021e67..cca9608ceb 100644 --- a/scm-core/src/main/java/sonia/scm/ModelObject.java +++ b/scm-core/src/main/java/sonia/scm/ModelObject.java @@ -53,5 +53,11 @@ public interface ModelObject * * @return unique id */ - public String getId(); + String getId(); + + void setLastModified(Long timestamp); + + Long getCreationDate(); + + void setCreationDate(Long timestamp); } diff --git a/scm-core/src/main/java/sonia/scm/NoChangesMadeException.java b/scm-core/src/main/java/sonia/scm/NoChangesMadeException.java new file mode 100644 index 0000000000..9bf0363398 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/NoChangesMadeException.java @@ -0,0 +1,18 @@ +package sonia.scm; + +import sonia.scm.repository.Repository; + +public class NoChangesMadeException extends BadRequestException { + public NoChangesMadeException(Repository repository, String branch) { + super(ContextEntry.ContextBuilder.entity(repository).build(), "no changes detected to branch " + branch); + } + + public NoChangesMadeException(Repository repository) { + super(ContextEntry.ContextBuilder.entity(repository).build(), "no changes detected"); + } + + @Override + public String getCode() { + return "40RaYIeeR1"; + } +} diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java new file mode 100644 index 0000000000..9c478da855 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/NotFoundException.java @@ -0,0 +1,40 @@ +package sonia.scm; + +import java.util.Collections; +import java.util.List; + +import static java.util.stream.Collectors.joining; + +public class NotFoundException extends ExceptionWithContext { + + private static final long serialVersionUID = 1710455380886499111L; + + private static final String CODE = "AGR7UzkhA1"; + + public NotFoundException(Class type, String id) { + this(Collections.singletonList(new ContextEntry(type, id))); + } + + public NotFoundException(String type, String id) { + this(Collections.singletonList(new ContextEntry(type, id))); + } + + public static NotFoundException notFound(ContextEntry.ContextBuilder contextBuilder) { + return new NotFoundException(contextBuilder.build()); + } + + private NotFoundException(List context) { + super(context, createMessage(context)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(List context) { + return context.stream() + .map(c -> c.getType().toLowerCase() + " with id " + c.getId()) + .collect(joining(" in ", "could not find ", "")); + } +} diff --git a/scm-core/src/main/java/sonia/scm/NotSupportedFeatuerException.java b/scm-core/src/main/java/sonia/scm/NotSupportedFeatuerException.java deleted file mode 100644 index e6fea6cfa7..0000000000 --- a/scm-core/src/main/java/sonia/scm/NotSupportedFeatuerException.java +++ /dev/null @@ -1,65 +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 - * @version 1.6 - */ -public class NotSupportedFeatuerException extends Exception -{ - - /** Field description */ - private static final long serialVersionUID = 256498734456613496L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public NotSupportedFeatuerException() {} - - /** - * Constructs ... - * - * - * @param message - */ - public NotSupportedFeatuerException(String message) - { - super(message); - } -} diff --git a/scm-core/src/main/java/sonia/scm/PageResult.java b/scm-core/src/main/java/sonia/scm/PageResult.java new file mode 100644 index 0000000000..983a4e5dd1 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/PageResult.java @@ -0,0 +1,45 @@ +package sonia.scm; + +import java.util.Collection; +import java.util.Collections; + +import static com.google.common.base.Preconditions.checkArgument; +import static sonia.scm.util.Util.createSubCollection; + +/** + * This represents the result of a page request. Contains the results for + * the page and the overall count of all elements. + */ +public class PageResult { + + private final Collection entities; + private final int overallCount; + + public static PageResult createPage(Collection allEntities, int pageNumber, int pageSize) { + checkArgument(pageSize > 0, "pageSize must be at least 1"); + checkArgument(pageNumber >= 0, "pageNumber must be non-negative"); + + Collection pagedEntities = createSubCollection(allEntities, pageNumber * pageSize, pageSize); + + return new PageResult<>(pagedEntities, allEntities.size()); + } + + public PageResult(Collection entities, int overallCount) { + this.entities = entities; + this.overallCount = overallCount; + } + + /** + * The entities for the current page. + */ + public Collection getEntities() { + return Collections.unmodifiableCollection(entities); + } + + /** + * The overall count of all available elements. + */ + public int getOverallCount() { + return overallCount; + } +} diff --git a/scm-core/src/main/java/sonia/scm/ReducedModelObject.java b/scm-core/src/main/java/sonia/scm/ReducedModelObject.java new file mode 100644 index 0000000000..b8db8c3ee0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ReducedModelObject.java @@ -0,0 +1,15 @@ +package sonia.scm; + + +/** + * This is a reduced form of a model object. + * It can be used as search result to avoid returning the whole object properties. + * + * @author Mohamed Karray + */ +public interface ReducedModelObject { + + String getId(); + + String getDisplayName(); +} diff --git a/scm-core/src/main/java/sonia/scm/SCMContext.java b/scm-core/src/main/java/sonia/scm/SCMContext.java index f2c1af81d0..f5367625bb 100644 --- a/scm-core/src/main/java/sonia/scm/SCMContext.java +++ b/scm-core/src/main/java/sonia/scm/SCMContext.java @@ -39,7 +39,7 @@ import sonia.scm.user.User; import sonia.scm.util.ServiceUtil; /** - * The SCMConext searches a implementation of {@link SCMContextProvider} and + * The SCMContext searches a implementation of {@link SCMContextProvider} and * holds a singleton instance of this implementation. * * @author Sebastian Sdorra @@ -51,7 +51,7 @@ public final class SCMContext public static final String DEFAULT_PACKAGE = "sonia.scm"; /** Name of the anonymous user */ - public static final String USER_ANONYMOUS = "anonymous"; + public static final String USER_ANONYMOUS = "_anonymous"; /** * the anonymous user @@ -59,7 +59,7 @@ public final class SCMContext */ public static final User ANONYMOUS = new User(USER_ANONYMOUS, "SCM Anonymous", - "scm-anonymous@scm-manager.com"); + "scm-anonymous@scm-manager.org"); /** Singleton instance of {@link SCMContextProvider} */ private static volatile SCMContextProvider provider; @@ -94,8 +94,6 @@ public final class SCMContext { provider = new BasicContextProvider(); } - - provider.init(); } } } diff --git a/scm-core/src/main/java/sonia/scm/SCMContextProvider.java b/scm-core/src/main/java/sonia/scm/SCMContextProvider.java index 18328403fe..dac03baed0 100644 --- a/scm-core/src/main/java/sonia/scm/SCMContextProvider.java +++ b/scm-core/src/main/java/sonia/scm/SCMContextProvider.java @@ -37,6 +37,7 @@ package sonia.scm; import java.io.Closeable; import java.io.File; +import java.nio.file.Path; /** * The main class for retrieving the home and the version of the SCM-Manager. @@ -45,25 +46,25 @@ import java.io.File; * * @author Sebastian Sdorra */ -public interface SCMContextProvider extends Closeable -{ - - /** - * Initializes the {@link SCMContextProvider}. - * This method is called when the SCM manager is started. - * - */ - public void init(); - - //~--- get methods ---------------------------------------------------------- - +public interface SCMContextProvider { /** * Returns the base directory of the SCM-Manager. * * * @return base directory of the SCM-Manager */ - public File getBaseDirectory(); + File getBaseDirectory(); + + /** + * Resolves the given path against the base directory. + * + * @param path path to resolve + * + * @return absolute resolved path + * + * @since 2.0.0 + */ + Path resolve(Path path); /** * Returns the current stage of SCM-Manager. @@ -72,7 +73,7 @@ public interface SCMContextProvider extends Closeable * @return stage of SCM-Manager * @since 1.12 */ - public Stage getStage(); + Stage getStage(); /** * Returns a exception which is occurred on context startup. @@ -82,7 +83,7 @@ public interface SCMContextProvider extends Closeable * @return startup exception of null * @since 1.14 */ - public Throwable getStartupError(); + Throwable getStartupError(); /** * Returns the version of the SCM-Manager. @@ -90,5 +91,5 @@ public interface SCMContextProvider extends Closeable * * @return version of the SCM-Manager */ - public String getVersion(); + String getVersion(); } diff --git a/scm-core/src/main/java/sonia/scm/ScmClientConfig.java b/scm-core/src/main/java/sonia/scm/ScmClientConfig.java deleted file mode 100644 index 40b7d36aaa..0000000000 --- a/scm-core/src/main/java/sonia/scm/ScmClientConfig.java +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.config.ScmConfiguration; - -/** - * Configuration object for a SCM-Manager - * client (WebInterface, RestClient, ...). - * - * @author Sebastian Sdorra - */ -public class ScmClientConfig -{ - - /** - * Constructs {@link ScmClientConfig} object - * - */ - public ScmClientConfig() {} - - /** - * Constructs {@link ScmClientConfig} object - * - * - * @param configuration SCM-Manager main configuration - * @since 1.14 - */ - public ScmClientConfig(ScmConfiguration configuration) - { - this.dateFormat = configuration.getDateFormat(); - this.disableGroupingGrid = configuration.isDisableGroupingGrid(); - this.enableRepositoryArchive = configuration.isEnableRepositoryArchive(); - } - - /** - * Constructs {@link ScmClientConfig} object - * - * - * @param dateFormat - */ - public ScmClientConfig(String dateFormat) - { - this.dateFormat = dateFormat; - } - - /** - * Constructs {@link ScmClientConfig} object - * - * @since 1.9 - * - * @param dateFormat - * @param disableGroupingGrid true to disable repository grouping - */ - public ScmClientConfig(String dateFormat, boolean disableGroupingGrid) - { - this.dateFormat = dateFormat; - this.disableGroupingGrid = disableGroupingGrid; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns the date format for the user interface. This format is a - * JavaScript date format. - * - * @see Date Format - * @return JavaScript date format - */ - public String getDateFormat() - { - return dateFormat; - } - - /** - * Returns true if the grouping of repositories is disabled. - * - * @since 1.9 - * - * @return true if the grouping of repositories is disabled - */ - public boolean isDisableGroupingGrid() - { - return disableGroupingGrid; - } - - /** - * Returns true if the repository archive is disabled. - * - * - * @return true if the repository archive is disabled - * @since 1.14 - */ - public boolean isEnableRepositoryArchive() - { - return enableRepositoryArchive; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Setter for the date format - * - * - * - * @param dateFormat - JavaScript date format - */ - public void setDateFormat(String dateFormat) - { - this.dateFormat = dateFormat; - } - - /** - * Enables or disables the grouping of repositories. - * - * @since 1.9 - * - * - * @param disableGroupingGrid - */ - public void setDisableGroupingGrid(boolean disableGroupingGrid) - { - this.disableGroupingGrid = disableGroupingGrid; - } - - /** - * Enable or disable the repository archive. Default is disabled. - * - * - * @param enableRepositoryArchive true to disable the repository archive - * @since 1.14 - */ - public void setEnableRepositoryArchive(boolean enableRepositoryArchive) - { - this.enableRepositoryArchive = enableRepositoryArchive; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String dateFormat; - - /** Field description */ - private boolean enableRepositoryArchive = true; - - /** Field description */ - private boolean disableGroupingGrid = true; -} diff --git a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java new file mode 100644 index 0000000000..a28812eb8a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java @@ -0,0 +1,136 @@ +package sonia.scm; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; + +import static java.util.Collections.unmodifiableCollection; + +/** + * Use this exception to handle invalid input values that cannot be handled using + * JEE bean validation. + * Use the {@link Builder} to conditionally create a new exception: + *
+ * Builder
+ *   .doThrow()
+ *   .violation("name or alias must not be empty if not anonymous", "myParameter", "name")
+ *   .violation("name or alias must not be empty if not anonymous", "myParameter", "alias")
+ *   .when(myParameter.getName() == null && myParameter.getAlias() == null && !myParameter.isAnonymous())
+ *   .andThrow()
+ *   .violation("name must be empty if anonymous", "myParameter", "name")
+ *   .when(myParameter.getName() != null && myParameter.isAnonymous());
+ * 
+ * Mind that using this way you do not have to use if-else constructs. + */ +public class ScmConstraintViolationException extends RuntimeException implements Serializable { + + private static final long serialVersionUID = 6904534307450229887L; + + private final Collection violations; + + private final String furtherInformation; + + private ScmConstraintViolationException(Collection violations, String furtherInformation) { + this.violations = violations; + this.furtherInformation = furtherInformation; + } + + /** + * The violations that caused this exception. + */ + public Collection getViolations() { + return unmodifiableCollection(violations); + } + + /** + * An optional URL for more informations about this constraint violation. + */ + public String getUrl() { + return furtherInformation; + } + + /** + * Builder to conditionally create constraint violations. + */ + public static class Builder { + private final Collection violations = new ArrayList<>(); + private String furtherInformation; + + /** + * Use this to create a new builder instance. + */ + public static Builder doThrow() { + return new Builder(); + } + + /** + * Resets this builder to check for further violations. + * @return this builder instance. + */ + public Builder andThrow() { + this.violations.clear(); + this.furtherInformation = null; + return this; + } + + /** + * Describes the violation with a custom message and the affected property. When more than one property is affected, + * you can call this method multiple times. + * @param message The message describing the violation. + * @param pathElements The affected property denoted by the path to reach this property, + * eg. "someParameter", "complexProperty", "attribute" + * @return this builder instance. + */ + public Builder violation(String message, String... pathElements) { + this.violations.add(new ScmConstraintViolation(message, pathElements)); + return this; + } + + /** + * Use this to specify a URL with further information about this violation and hints how to solve this. + * This is optional. + * @return this builder instance. + */ + public Builder withFurtherInformation(String furtherInformation) { + this.furtherInformation = furtherInformation; + return this; + } + + /** + * When the given condition is true, a exception will be thrown. Otherwise this simply resets this + * builder and does nothing else. + * @param condition The condition that indicates a violation of this constraint. + * @return this builder instance. + */ + public Builder when(boolean condition) { + if (condition && !this.violations.isEmpty()) { + throw new ScmConstraintViolationException(violations, furtherInformation); + } + return andThrow(); + } + } + + /** + * A single constraint violation. + */ + public static class ScmConstraintViolation implements Serializable { + + private static final long serialVersionUID = -6900317468157084538L; + + private final String message; + private final String path; + + private ScmConstraintViolation(String message, String... pathElements) { + this.message = message; + this.path = String.join(".", pathElements); + } + + public String getMessage() { + return message; + } + + public String getPropertyPath() { + return path; + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/ScmState.java b/scm-core/src/main/java/sonia/scm/ScmState.java deleted file mode 100644 index 5610a40c08..0000000000 --- a/scm-core/src/main/java/sonia/scm/ScmState.java +++ /dev/null @@ -1,254 +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; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.security.PermissionDescriptor; -import sonia.scm.user.User; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collection; -import java.util.List; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * This class represents the current state of the SCM-Manager. - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "state") -@XmlAccessorType(XmlAccessType.FIELD) -public final class ScmState -{ - - /** - * Constructs {@link ScmState} object. - * This constructor is required by JAXB. - * - */ - ScmState() {} - - /** - * Constructs {@link ScmState} object. - * - * - * @param version scm-manager version - * @param user current user - * @param groups groups of the current user - * @param token authentication token - * @param repositoryTypes available repository types - * @param defaultUserType default user type - * @param clientConfig client configuration - * @param assignedPermission assigned permissions - * @param availablePermissions list of available permissions - * - * @since 2.0.0 - */ - public ScmState(String version, User user, Collection groups, - String token, Collection repositoryTypes, String defaultUserType, - ScmClientConfig clientConfig, List assignedPermission, - List availablePermissions) - { - this.version = version; - this.user = user; - this.groups = groups; - this.token = token; - this.repositoryTypes = repositoryTypes; - this.clientConfig = clientConfig; - this.defaultUserType = defaultUserType; - this.assignedPermissions = assignedPermission; - this.availablePermissions = availablePermissions; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Return a list of assigned permissions. - * - * - * @return list of assigned permissions - * @since 1.31 - */ - public List getAssignedPermissions() - { - return assignedPermissions; - } - - /** - * Returns a list of available global permissions. - * - * - * @return available global permissions - * @since 1.31 - */ - public List getAvailablePermissions() - { - return availablePermissions; - } - - /** - * Returns configuration for SCM-Manager clients. - * - * - * @return configuration for SCM-Manager clients - */ - public ScmClientConfig getClientConfig() - { - return clientConfig; - } - - /** - * Returns the default user type - * - * - * @return default user type - * - * @since 1.14 - */ - public String getDefaultUserType() - { - return defaultUserType; - } - - /** - * Returns a {@link java.util.Collection} of groups names which are associated - * to the current user. - * - * - * @return a {@link java.util.Collection} of groups names - */ - public Collection getGroups() - { - return groups; - } - - /** - * Returns all available repository types. - * - * - * @return all available repository types - */ - public Collection getRepositoryTypes() - { - return repositoryTypes; - } - - /** - * Returns authentication token or {@code null}. - * - * - * @return authentication token or {@code null} - * - * @since 2.0.0 - */ - public String getToken() - { - return token; - } - - /** - * Returns the current logged in user. - * - * - * @return current logged in user - */ - public User getUser() - { - return user; - } - - /** - * Returns the version of the SCM-Manager. - * - * - * @return version of the SCM-Manager - */ - public String getVersion() - { - return version; - } - - /** - * Returns true if the request was successful. - * This method is required by extjs. - * - * @return true if the request was successful - */ - public boolean isSuccess() - { - return success; - } - - //~--- fields --------------------------------------------------------------- - - /** marker for extjs */ - private final boolean success = true; - - /** authentication token */ - private String token; - - /** Field description */ - private List assignedPermissions; - - /** - * Avaliable global permission - * @since 1.31 - */ - private List availablePermissions; - - /** Field description */ - private ScmClientConfig clientConfig; - - /** Field description */ - private String defaultUserType; - - /** Field description */ - private Collection groups; - - /** Field description */ - @XmlElement(name = "repositoryTypes") - private Collection repositoryTypes; - - /** Field description */ - private User user; - - /** Field description */ - private String version; -} diff --git a/scm-core/src/main/java/sonia/scm/ScmStateFactory.java b/scm-core/src/main/java/sonia/scm/ScmStateFactory.java deleted file mode 100644 index 41502a6850..0000000000 --- a/scm-core/src/main/java/sonia/scm/ScmStateFactory.java +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Copyright (c) 2014, 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; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.collect.ImmutableList; - -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.Subject; - -import sonia.scm.config.ScmConfiguration; -import sonia.scm.group.GroupNames; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.security.AuthorizationCollector; -import sonia.scm.security.PermissionDescriptor; -import sonia.scm.security.Role; -import sonia.scm.security.SecuritySystem; -import sonia.scm.user.User; -import sonia.scm.user.UserManager; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import javax.inject.Inject; - -/** - * Factory to create {@link ScmState}. - * - * @author Sebastian Sdorra - * @since 2.0.0 - */ -public final class ScmStateFactory -{ - - /** - * Constructs a new {@link ScmStateFactory}. - * - * - * @param contextProvider context provider - * @param configuration configuration - * @param repositoryManger repository manager - * @param userManager user manager - * @param securitySystem security system - * @param authorizationCollector authorization collector - */ - @Inject - public ScmStateFactory(SCMContextProvider contextProvider, - ScmConfiguration configuration, RepositoryManager repositoryManger, - UserManager userManager, SecuritySystem securitySystem, - AuthorizationCollector authorizationCollector) - { - this.contextProvider = contextProvider; - this.configuration = configuration; - this.repositoryManger = repositoryManger; - this.userManager = userManager; - this.securitySystem = securitySystem; - this.authorizationCollector = authorizationCollector; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Returns anonymous state. - * - * - * @return anonymous state - */ - @SuppressWarnings("unchecked") - public ScmState createAnonymousState() - { - return createState(SCMContext.ANONYMOUS, Collections.EMPTY_LIST, null, - Collections.EMPTY_LIST, Collections.EMPTY_LIST); - } - - /** - * Creates an state from the given subject. - * - * - * @param subject subject - * - * @return state from subject - */ - public ScmState createState(Subject subject) - { - return createState(subject, null); - } - - /** - * Creates an state from the given subject and authentication token. - * - * - * @param subject subject - * @param token authentication token - * - * @return state from subject and authentication token - */ - @SuppressWarnings("unchecked") - public ScmState createState(Subject subject, String token) - { - PrincipalCollection collection = subject.getPrincipals(); - User user = collection.oneByType(User.class); - GroupNames groups = collection.oneByType(GroupNames.class); - - List ap = Collections.EMPTY_LIST; - - if (subject.hasRole(Role.ADMIN)) - { - ap = securitySystem.getAvailablePermissions(); - } - - List permissions = - ImmutableList.copyOf( - authorizationCollector.collect().getStringPermissions()); - - return createState(user, groups.getCollection(), token, permissions, ap); - } - - private ScmState createState(User user, Collection groups, - String token, List assignedPermissions, - List availablePermissions) - { - User u = user.clone(); - - // do not return password on authentication - u.setPassword(null); - - return new ScmState(contextProvider.getVersion(), u, groups, token, - repositoryManger.getConfiguredTypes(), userManager.getDefaultType(), - new ScmClientConfig(configuration), assignedPermissions, - availablePermissions); - } - - //~--- fields --------------------------------------------------------------- - - /** authorization collector */ - private final AuthorizationCollector authorizationCollector; - - /** configuration */ - private final ScmConfiguration configuration; - - /** context provider */ - private final SCMContextProvider contextProvider; - - /** repository manager */ - private final RepositoryManager repositoryManger; - - /** security system */ - private final SecuritySystem securitySystem; - - /** user manager */ - private final UserManager userManager; -} diff --git a/scm-core/src/main/java/sonia/scm/TransformFilter.java b/scm-core/src/main/java/sonia/scm/TransformFilter.java index e872ab384f..fa798fe36e 100644 --- a/scm-core/src/main/java/sonia/scm/TransformFilter.java +++ b/scm-core/src/main/java/sonia/scm/TransformFilter.java @@ -39,8 +39,10 @@ package sonia.scm; * @author Sebastian Sdorra * * @param type of objects to transform + * @param result type of the transformation */ -public interface TransformFilter +@FunctionalInterface +public interface TransformFilter { /** @@ -52,5 +54,5 @@ public interface TransformFilter * * @return tranformed object */ - public T accept(T item); + R accept(T item); } diff --git a/scm-core/src/main/java/sonia/scm/TypeManager.java b/scm-core/src/main/java/sonia/scm/TypeManager.java index 52a43e5593..6f13cb82d8 100644 --- a/scm-core/src/main/java/sonia/scm/TypeManager.java +++ b/scm-core/src/main/java/sonia/scm/TypeManager.java @@ -44,10 +44,8 @@ import java.util.Collection; * * @param type of the model object * @param type of the handler - * @param type of the exception */ -public interface TypeManager, - E extends Exception> extends Manager +public interface TypeManager> extends Manager { /** @@ -58,7 +56,7 @@ public interface TypeManager, * * @return the handler for given type */ - public H getHandler(String type); + H getHandler(String type); /** * Returns a {@link java.util.Collection} of all @@ -66,5 +64,5 @@ public interface TypeManager, * * @return all available types */ - public Collection getTypes(); + Collection getTypes(); } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseDtoMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseDtoMapper.java new file mode 100644 index 0000000000..8d67822c1b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseDtoMapper.java @@ -0,0 +1,12 @@ +package sonia.scm.api.v2.resources; + +import java.time.Instant; + +class BaseDtoMapper { + Long mapDate(Instant value) { + if (value != null) { + return value.toEpochMilli(); + } + return null; + } +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java new file mode 100644 index 0000000000..eba8173de1 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java @@ -0,0 +1,10 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import org.mapstruct.Mapping; + +public abstract class BaseMapper extends HalAppenderMapper implements InstantAttributeMapper { + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract D map(T modelObject); +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java new file mode 100644 index 0000000000..6759f5cb8c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.Instant; + +@Getter +@Setter +@NoArgsConstructor +public class ChangesetDto extends HalRepresentation { + + /** + * The changeset identification string + */ + private String id; + + /** + * The author of the changeset + */ + private PersonDto author; + + /** + * The date when the changeset was committed + */ + private Instant date; + + /** + * The text of the changeset description + */ + private String description; + + public ChangesetDto(Links links, Embedded embedded) { + super(links, embedded); + } +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java new file mode 100644 index 0000000000..cd7d7ecebe --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java @@ -0,0 +1,14 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Context; +import org.mapstruct.Mapping; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +public interface ChangesetToChangesetDtoMapper { + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + ChangesetDto map(Changeset changeset, @Context Repository repository); + + +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/CollectionToDtoMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/CollectionToDtoMapper.java new file mode 100644 index 0000000000..523629ce3b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/CollectionToDtoMapper.java @@ -0,0 +1,32 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Links.linkingTo; + +public abstract class CollectionToDtoMapper { + + private final String collectionName; + private final BaseMapper mapper; + + protected CollectionToDtoMapper(String collectionName, BaseMapper mapper) { + this.collectionName = collectionName; + this.mapper = mapper; + } + + public HalRepresentation map(Collection collection) { + List dtos = collection.stream().map(mapper::map).collect(Collectors.toList()); + return new HalRepresentation( + linkingTo().self(createSelfLink()).build(), + embeddedBuilder().with(collectionName, dtos).build() + ); + } + + protected abstract String createSelfLink(); + +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java new file mode 100644 index 0000000000..b313f68af8 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java @@ -0,0 +1,66 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; + +import java.util.List; + +/** + * The {@link HalAppender} can be used within an {@link HalEnricher} to append hateoas links to a json response. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public interface HalAppender { + + /** + * Appends one link to the json response. + * + * @param rel name of relation + * @param href link uri + */ + void appendLink(String rel, String href); + + /** + * Returns a builder which is able to append an array of links to the resource. + * + * @param rel name of link relation + * @return multi link builder + */ + LinkArrayBuilder linkArrayBuilder(String rel); + + /** + * Appends one embedded object to the json response. + * + * @param rel name of relation + * @param embeddedItem embedded object + */ + void appendEmbedded(String rel, HalRepresentation embeddedItem); + + /** + * Appends a list of embedded objects to the json response. + * + * @param rel name of relation + * @param embeddedItems embedded objects + */ + void appendEmbedded(String rel, List embeddedItems); + + /** + * Builder for link arrays. + */ + interface LinkArrayBuilder { + + /** + * Append a link to the array. + * + * @param name name of link + * @param href link target + * @return {@code this} + */ + LinkArrayBuilder append(String name, String href); + + /** + * Builds the array and appends the it to the json response. + */ + void build(); + } +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppenderMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppenderMapper.java new file mode 100644 index 0000000000..dd49b765bc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppenderMapper.java @@ -0,0 +1,36 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.annotations.VisibleForTesting; + +import javax.inject.Inject; + +public class HalAppenderMapper { + + @Inject + private HalEnricherRegistry registry; + + @VisibleForTesting + void setRegistry(HalEnricherRegistry registry) { + this.registry = registry; + } + + protected void applyEnrichers(HalAppender appender, Object source, Object... contextEntries) { + // null check is only their to not break existing tests + if (registry != null) { + + Object[] ctx = new Object[contextEntries.length + 1]; + ctx[0] = source; + for (int i = 0; i < contextEntries.length; i++) { + ctx[i + 1] = contextEntries[i]; + } + + HalEnricherContext context = HalEnricherContext.of(ctx); + + Iterable enrichers = registry.allByType(source.getClass()); + for (HalEnricher enricher : enrichers) { + enricher.enrich(context, appender); + } + } + } + +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricher.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricher.java new file mode 100644 index 0000000000..647a1cf74e --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricher.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.plugin.ExtensionPoint; + +/** + * A {@link HalEnricher} can be used to append hal specific attributes, such as links, to the json response. + * To register an enricher use the {@link Enrich} annotation or the {@link HalEnricherRegistry} which is available + * via injection. + * + * Warning: enrichers are always registered as singletons. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +@ExtensionPoint +@FunctionalInterface +public interface HalEnricher { + + /** + * Enriches the response with hal specific attributes. + * + * @param context contains the source for the json mapping and related objects + * @param appender can be used to append links or embedded objects to the json response + */ + void enrich(HalEnricherContext context, HalAppender appender); +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherContext.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherContext.java new file mode 100644 index 0000000000..36128087b8 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherContext.java @@ -0,0 +1,72 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; + +/** + * Context object for the {@link HalEnricher}. The context holds the source object for the json and all related + * objects, which can be useful for the enrichment. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class HalEnricherContext { + + private final Map instanceMap; + + private HalEnricherContext(Map instanceMap) { + this.instanceMap = instanceMap; + } + + /** + * Creates a context with the given entries + * + * @param instances entries of the context + * + * @return context of given entries + */ + public static HalEnricherContext of(Object... instances) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Object instance : instances) { + builder.put(instance.getClass(), instance); + } + return new HalEnricherContext(builder.build()); + } + + /** + * Returns the registered object from the context. The method will return an empty optional, if no object with the + * given type was registered. + * + * @param type type of instance + * @param type of instance + * @return optional instance + */ + public Optional oneByType(Class type) { + Object instance = instanceMap.get(type); + if (instance != null) { + return Optional.of(type.cast(instance)); + } + return Optional.empty(); + } + + /** + * Returns the registered object from the context, but throws an {@link NoSuchElementException} if the type was not + * registered. + * + * @param type type of instance + * @param type of instance + * @return instance + */ + public T oneRequireByType(Class type) { + Optional instance = oneByType(type); + if (instance.isPresent()) { + return instance.get(); + } else { + throw new NoSuchElementException("No instance for given type present"); + } + } + +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherRegistry.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherRegistry.java new file mode 100644 index 0000000000..3fadbfa388 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherRegistry.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import sonia.scm.plugin.Extension; + +import javax.inject.Singleton; + +/** + * The {@link HalEnricherRegistry} is responsible for binding {@link HalEnricher} instances to their source types. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +@Extension +@Singleton +public final class HalEnricherRegistry { + + private final Multimap enrichers = HashMultimap.create(); + + /** + * Registers a new {@link HalEnricher} for the given source type. + * + * @param sourceType type of json mapping source + * @param enricher link enricher instance + */ + public void register(Class sourceType, HalEnricher enricher) { + enrichers.put(sourceType, enricher); + } + + /** + * Returns all registered {@link HalEnricher} for the given type. + * + * @param sourceType type of json mapping source + * @return all registered enrichers + */ + public Iterable allByType(Class sourceType) { + return enrichers.get(sourceType); + } +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java new file mode 100644 index 0000000000..346ce83816 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java @@ -0,0 +1,10 @@ +package sonia.scm.api.v2.resources; + +/** + * The {@link Index} object can be used to register a {@link HalEnricher} for the index resource. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class Index { +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/InstantAttributeMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/InstantAttributeMapper.java new file mode 100644 index 0000000000..468bdfc137 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/InstantAttributeMapper.java @@ -0,0 +1,9 @@ +package sonia.scm.api.v2.resources; + +import java.time.Instant; + +public interface InstantAttributeMapper { + default Instant mapTime(Long epochMilli) { + return epochMilli == null? null: Instant.ofEpochMilli(epochMilli); + } +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java new file mode 100644 index 0000000000..0797134c9f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java @@ -0,0 +1,114 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.collect.ImmutableList; + +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.util.Arrays; + +/** + * This class is used to create links for JAX-RS resources. Create a new instance specifying all resource classes used + * to process the request. Then for each of these classes call builder.method(...).parameters(...) for each + * of these classes consecutively. The builder itself is immutable, so that each instance is reusable and you get a new + * builder for each method. + * + *
+ * LinkBuilder builder = new LinkBuilder(pathInfo, MainResource.class, SubResource.class);
+ * Link link = builder
+ *     .method("sub")
+ *     .parameters("param")
+ *     .method("x")
+ *     .parameters("param_1", "param_2")
+ *     .create();
+ * 
+ */ +@SuppressWarnings("WeakerAccess") // Non-public will result in IllegalAccessError for plugins +public class LinkBuilder { + private final ScmPathInfo pathInfo; + private final Class[] classes; + private final ImmutableList calls; + + public LinkBuilder(ScmPathInfo pathInfo, Class... classes) { + this(pathInfo, classes, ImmutableList.of()); + } + + private LinkBuilder(ScmPathInfo pathInfo, Class[] classes, ImmutableList calls) { + this.pathInfo = pathInfo; + this.classes = classes; + this.calls = calls; + } + + public Parameters method(String method) { + if (calls.size() >= classes.length) { + throw new IllegalStateException("no more classes for methods"); + } + return new Parameters(method); + } + + public URI create() { + if (calls.size() < classes.length) { + throw new IllegalStateException("not enough methods for all classes"); + } + + URI baseUri = pathInfo.getApiRestUri(); + URI relativeUri = createRelativeUri(); + return baseUri.resolve(relativeUri); + } + + public String href() { + return create().toString(); + } + + private LinkBuilder add(String method, String[] parameters) { + return new LinkBuilder(pathInfo, classes, appendNewCall(method, parameters)); + } + + private ImmutableList appendNewCall(String method, String[] parameters) { + return ImmutableList. builder().addAll(calls).add(createNewCall(method, parameters)).build(); + } + + private Call createNewCall(String method, String[] parameters) { + return new Call(LinkBuilder.this.classes[calls.size()], method, parameters); + } + + private URI createRelativeUri() { + UriBuilder uriBuilder = userUriBuilder(); + calls.forEach(call -> uriBuilder.path(call.clazz, call.method)); + String[] concatenatedParameters = calls + .stream() + .map(call -> call.parameters) + .flatMap(Arrays::stream) + .toArray(String[]::new); + return uriBuilder.build((Object[]) concatenatedParameters); + } + + private UriBuilder userUriBuilder() { + return UriBuilder.fromResource(classes[0]); + } + + public class Parameters { + + private final String method; + + private Parameters(String method) { + this.method = method; + } + + public LinkBuilder parameters(String... parameters) { + return LinkBuilder.this.add(method, parameters); + } + } + + private static class Call { + private final Class clazz; + private final String method; + + private final String[] parameters; + + private Call(Class clazz, String method, String[] parameters) { + this.clazz = clazz; + this.method = method; + this.parameters = parameters; + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java new file mode 100644 index 0000000000..84fe2ddf7b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java @@ -0,0 +1,12 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.plugin.ExtensionPoint; + +import java.net.URI; +import java.util.Optional; + +@ExtensionPoint(multi = false) +@FunctionalInterface +public interface LogoutRedirection { + Optional afterLogoutRedirectTo(); +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java new file mode 100644 index 0000000000..a027a78d79 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java @@ -0,0 +1,10 @@ +package sonia.scm.api.v2.resources; + +/** + * The {@link Me} object can be used to register a {@link HalEnricher} for the me resource. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class Me { +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java new file mode 100644 index 0000000000..0661d6a4ef --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java @@ -0,0 +1,14 @@ +package sonia.scm.api.v2.resources; + +import lombok.Getter; +import lombok.Setter; +import org.hibernate.validator.constraints.NotEmpty; + +@Getter @Setter +public class MergeCommandDto { + + @NotEmpty + private String sourceRevision; + @NotEmpty + private String targetRevision; +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/PersonDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/PersonDto.java new file mode 100644 index 0000000000..ce35b507c8 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/PersonDto.java @@ -0,0 +1,22 @@ +package sonia.scm.api.v2.resources; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class PersonDto { + + /** + * mail address of the person + */ + private String mail; + + /** + * name of the person + */ + private String name; + +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java new file mode 100644 index 0000000000..496c87b440 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java @@ -0,0 +1,14 @@ +package sonia.scm.api.v2.resources; + +import java.net.URI; + +public interface ScmPathInfo { + + String REST_API_PATH = "/api"; + + URI getApiRestUri(); + + default URI getRootUri() { + return getApiRestUri().resolve(".."); + } +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java new file mode 100644 index 0000000000..c88bd4a2b5 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java @@ -0,0 +1,18 @@ +package sonia.scm.api.v2.resources; + +public class ScmPathInfoStore { + + private ScmPathInfo pathInfo; + + public ScmPathInfo get() { + return pathInfo; + } + + public void set(ScmPathInfo info) { + if (this.pathInfo != null) { + throw new IllegalStateException("UriInfo already set"); + } + this.pathInfo = info; + } + +} diff --git a/scm-core/src/main/java/sonia/scm/cache/CacheStatistics.java b/scm-core/src/main/java/sonia/scm/cache/CacheStatistics.java index 3507763820..cc791ff91f 100644 --- a/scm-core/src/main/java/sonia/scm/cache/CacheStatistics.java +++ b/scm-core/src/main/java/sonia/scm/cache/CacheStatistics.java @@ -103,10 +103,10 @@ public final class CacheStatistics { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("hitCount", hitCount) - .add("missCount", missCount) - .toString(); + .add("name", name) + .add("hitCount", hitCount) + .add("missCount", missCount) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/config/Configuration.java b/scm-core/src/main/java/sonia/scm/config/Configuration.java new file mode 100644 index 0000000000..4019925c27 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/config/Configuration.java @@ -0,0 +1,29 @@ +package sonia.scm.config; + +import com.github.sdorra.ssp.PermissionObject; +import com.github.sdorra.ssp.StaticPermissions; + +/** + * Base for all kinds of configurations. + * + * Allows for permission like + * + *
    + *
  • "configuration:read:global",
  • + *
  • "configuration:write:svn",
  • + *
  • "configuration:*:git",
  • + *
  • "configuration:*"
  • + *
+ * + *
+ * + * And for permission checks like {@code ConfigurationPermissions.read(configurationObject).check();} + */ +@StaticPermissions( + value = "configuration", + permissions = {"read", "write"}, + globalPermissions = {"list"}, + custom = true, customGlobal = true +) +public interface Configuration extends PermissionObject { +} diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index a136bb0b36..edf842f94e 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,39 +24,33 @@ * 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.config; -//~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.Sets; import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.event.ScmEventBus; import sonia.scm.util.HttpUtil; import sonia.scm.xml.XmlSetStringAdapter; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; - -import java.util.Set; -import java.util.concurrent.TimeUnit; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.io.File; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +//~--- JDK imports ------------------------------------------------------------ /** * The main configuration object for SCM-Manager. @@ -64,41 +58,139 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; * * @author Sebastian Sdorra */ + @Singleton @XmlRootElement(name = "scm-config") @XmlAccessorType(XmlAccessType.FIELD) -public class ScmConfiguration -{ +public class ScmConfiguration implements Configuration { - /** Default JavaScript date format */ + /** + * Default JavaScript date format + */ public static final String DEFAULT_DATEFORMAT = "YYYY-MM-DD HH:mm:ss"; - /** Default plugin url */ + /** + * Default plugin url + */ public static final String DEFAULT_PLUGINURL = - "http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false"; + "https://plugin-center-api.scm-manager.org/api/v1/plugins/{version}?os={os}&arch={arch}"; - /** Default plugin url from version 1.0 */ + /** + * Default url for login information (plugin and feature tips on the login page). + */ + public static final String DEFAULT_LOGIN_INFO_URL = "https://login-info.scm-manager.org/api/v1/login-info"; + + /** + * Default plugin url from version 1.0 + */ public static final String OLD_PLUGINURL = "http://plugins.scm-manager.org/plugins.xml.gz"; - /** Path to the configuration file */ + /** + * Path to the configuration file + */ public static final String PATH = "config".concat(File.separator).concat("config.xml"); - /** the logger for ScmConfiguration */ - private static final Logger logger = - LoggerFactory.getLogger(ScmConfiguration.class); + /** + * the logger for ScmConfiguration + */ + private static final Logger logger = LoggerFactory.getLogger(ScmConfiguration.class); + + @SuppressWarnings("WeakerAccess") // This might be needed for permission checking + public static final String PERMISSION = "global"; + + @XmlElement(name = "base-url") + private String baseUrl; + + + @XmlElement(name = "force-base-url") + private boolean forceBaseUrl; + + /** + * Maximum allowed login attempts. + * + * @since 1.34 + */ + @XmlElement(name = "login-attempt-limit") + private int loginAttemptLimit = -1; + + /** + * glob patterns for urls which are excluded from proxy + */ + @XmlElement(name = "proxy-excludes") + @XmlJavaTypeAdapter(XmlSetStringAdapter.class) + private Set proxyExcludes; + + + private String proxyPassword; + + + private int proxyPort = 8080; + + + private String proxyServer = "proxy.mydomain.com"; + + + private String proxyUser; + + /** + * Skip failed authenticators. + * + * @since 1.36 + */ + @XmlElement(name = "skip-failed-authenticators") + private boolean skipFailedAuthenticators = false; + + + @XmlElement(name = "plugin-url") + private String pluginUrl = DEFAULT_PLUGINURL; + + /** + * Login attempt timeout. + * + * @since 1.34 + */ + @XmlElement(name = "login-attempt-limit-timeout") + private long loginAttemptLimitTimeout = TimeUnit.MINUTES.toSeconds(5l); + + + private boolean enableProxy = false; + + /** + * Authentication realm for basic authentication. + */ + private String realmDescription = HttpUtil.AUTHENTICATION_REALM; + private boolean disableGroupingGrid = false; + /** + * JavaScript date format from moment.js + * + * @see http://momentjs.com/docs/#/parsing/ + */ + private String dateFormat = DEFAULT_DATEFORMAT; + private boolean anonymousAccessEnabled = false; + + /** + * Enables xsrf cookie protection. + * + * @since 1.47 + */ + @XmlElement(name = "xsrf-protection") + private boolean enabledXsrfProtection = true; + + @XmlElement(name = "namespace-strategy") + private String namespaceStrategy = "UsernameNamespaceStrategy"; + + @XmlElement(name = "login-info-url") + private String loginInfoUrl = DEFAULT_LOGIN_INFO_URL; - //~--- methods -------------------------------------------------------------- /** * Calls the {@link sonia.scm.ConfigChangedListener#configChanged(Object)} * method of all registered listeners. */ - public void fireChangeEvent() - { - if (logger.isDebugEnabled()) - { + public void fireChangeEvent() { + if (logger.isDebugEnabled()) { logger.debug("fire config changed event"); } @@ -109,18 +201,13 @@ public class ScmConfiguration /** * Load all properties from another {@link ScmConfiguration} object. * - * - * * @param other */ - public void load(ScmConfiguration other) - { + public void load(ScmConfiguration other) { this.realmDescription = other.realmDescription; this.dateFormat = other.dateFormat; this.pluginUrl = other.pluginUrl; this.anonymousAccessEnabled = other.anonymousAccessEnabled; - this.adminUsers = other.adminUsers; - this.adminGroups = other.adminGroups; this.enableProxy = other.enableProxy; this.proxyPort = other.proxyPort; this.proxyServer = other.proxyServer; @@ -130,46 +217,22 @@ public class ScmConfiguration this.forceBaseUrl = other.forceBaseUrl; this.baseUrl = other.baseUrl; this.disableGroupingGrid = other.disableGroupingGrid; - this.enableRepositoryArchive = other.enableRepositoryArchive; this.skipFailedAuthenticators = other.skipFailedAuthenticators; this.loginAttemptLimit = other.loginAttemptLimit; this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout; this.enabledXsrfProtection = other.enabledXsrfProtection; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns a set of admin group names. - * - * - * @return set of admin group names - */ - public Set getAdminGroups() - { - return adminGroups; - } - - /** - * Returns a set of admin user names. - * - * - * @return set of admin user names - */ - public Set getAdminUsers() - { - return adminUsers; + this.namespaceStrategy = other.namespaceStrategy; + this.loginInfoUrl = other.loginInfoUrl; } /** * Returns the complete base url of the scm-manager including the context path. * For example http://localhost:8080/scm * - * @since 1.5 * @return complete base url of the scm-manager + * @since 1.5 */ - public String getBaseUrl() - { + public String getBaseUrl() { return baseUrl; } @@ -177,23 +240,14 @@ public class ScmConfiguration * Returns the date format for the user interface. This format is a * JavaScript date format, from the library moment.js. * - * @see http://momentjs.com/docs/#/parsing/ * @return moment.js date format + * @see http://momentjs.com/docs/#/parsing/ */ - public String getDateFormat() - { + public String getDateFormat() { return dateFormat; } - /** - * Returns maximum allowed login attempts. - * - * @return maximum allowed login attempts - * - * @since 1.34 - */ - public int getLoginAttemptLimit() - { + public int getLoginAttemptLimit() { return loginAttemptLimit; } @@ -202,11 +256,9 @@ public class ScmConfiguration * because of too many failed login attempts. * * @return login attempt timeout in seconds - * * @since 1.34 */ - public long getLoginAttemptLimitTimeout() - { + public long getLoginAttemptLimitTimeout() { return loginAttemptLimitTimeout; } @@ -222,8 +274,7 @@ public class ScmConfiguration * * @return the complete plugin url. */ - public String getPluginUrl() - { + public String getPluginUrl() { return pluginUrl; } @@ -231,289 +282,122 @@ public class ScmConfiguration * Returns a set of glob patterns for urls which should excluded from * proxy settings. * - * * @return set of glob patterns * @since 1.23 */ - public Set getProxyExcludes() - { - if (proxyExcludes == null) - { + public Set getProxyExcludes() { + if (proxyExcludes == null) { proxyExcludes = Sets.newHashSet(); } return proxyExcludes; } - /** - * Method description - * - * - * @return - * @since 1.7 - */ - public String getProxyPassword() - { + public String getProxyPassword() { return proxyPassword; } - /** - * Returns the proxy port. - * - * - * @return proxy port - */ - public int getProxyPort() - { + public int getProxyPort() { return proxyPort; } /** * Returns the servername or ip of the proxyserver. * - * * @return servername or ip of the proxyserver */ - public String getProxyServer() - { + public String getProxyServer() { return proxyServer; } - /** - * Method description - * - * - * @return - * @since 1.7 - */ - public String getProxyUser() - { + public String getProxyUser() { return proxyUser; } - /** - * Returns the realm description. - * - * - * @return realm description - * @since 1.36 - */ - public String getRealmDescription() - { + public String getRealmDescription() { return realmDescription; } - - /** - * Returns true if the anonymous access to the SCM-Manager is enabled. - * - * - * @return true if the anonymous access to the SCM-Manager is enabled - */ - public boolean isAnonymousAccessEnabled() - { + public boolean isAnonymousAccessEnabled() { return anonymousAccessEnabled; } - /** - * Method description - * - * @since 1.9 - * @return - */ - public boolean isDisableGroupingGrid() - { + public boolean isDisableGroupingGrid() { return disableGroupingGrid; } /** * Returns {@code true} if the cookie xsrf protection is enabled. - * - * @see Issue 793 + * * @return {@code true} if the cookie xsrf protection is enabled - * + * @see Issue 793 * @since 1.47 */ - public boolean isEnabledXsrfProtection() - { + public boolean isEnabledXsrfProtection() { return enabledXsrfProtection; } - /** - * Returns true if proxy is enabled. - * - * - * @return true if proxy is enabled - */ - public boolean isEnableProxy() - { + public boolean isEnableProxy() { return enableProxy; } - /** - * Returns true if the repository archive is enabled. - * - * - * @return true if the repository archive is enabled - * @since 1.14 - */ - public boolean isEnableRepositoryArchive() - { - return enableRepositoryArchive; - } - - /** - * Returns true if force base url is enabled. - * - * @since 1.5 - * @return true if force base url is enabled - */ - public boolean isForceBaseUrl() - { + public boolean isForceBaseUrl() { return forceBaseUrl; } - /** - * Returns true if the login attempt limit is enabled. - * - * - * @return true if login attempt limit is enabled - * - * @since 1.37 - */ - public boolean isLoginAttemptLimitEnabled() - { + public boolean isLoginAttemptLimitEnabled() { return loginAttemptLimit > 0; } + public String getNamespaceStrategy() { + return namespaceStrategy; + } + + public String getLoginInfoUrl() { + return loginInfoUrl; + } + /** * Returns true if failed authenticators are skipped. * - * * @return true if failed authenticators are skipped - * * @since 1.36 */ - public boolean isSkipFailedAuthenticators() - { + public boolean isSkipFailedAuthenticators() { return skipFailedAuthenticators; } - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param adminGroups - */ - public void setAdminGroups(Set adminGroups) - { - this.adminGroups = adminGroups; - } - - /** - * Method description - * - * - * @param adminUsers - */ - public void setAdminUsers(Set adminUsers) - { - this.adminUsers = adminUsers; - } - - /** - * Method description - * - * - * @param anonymousAccessEnabled - */ - public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) - { + public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) { this.anonymousAccessEnabled = anonymousAccessEnabled; } - /** - * Method description - * - * - * @param baseUrl - * @since 1.5 - */ - public void setBaseUrl(String baseUrl) - { + public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } - /** - * Sets the date format for the ui. - * - * - * @param dateFormat date format for ui - */ - public void setDateFormat(String dateFormat) - { + public void setDateFormat(String dateFormat) { this.dateFormat = dateFormat; } - /** - * Method description - * - * @since 1.9 - * - * @param disableGroupingGrid - */ - public void setDisableGroupingGrid(boolean disableGroupingGrid) - { + public void setDisableGroupingGrid(boolean disableGroupingGrid) { this.disableGroupingGrid = disableGroupingGrid; } - /** - * Method description - * - * - * @param enableProxy - */ - public void setEnableProxy(boolean enableProxy) - { + public void setEnableProxy(boolean enableProxy) { this.enableProxy = enableProxy; } - /** - * Enable or disable the repository archive. Default is disabled. - * - * - * @param enableRepositoryArchive true to disable the repository archive - * @since 1.14 - */ - public void setEnableRepositoryArchive(boolean enableRepositoryArchive) - { - this.enableRepositoryArchive = enableRepositoryArchive; - } - - /** - * Method description - * - * - * @param forceBaseUrl - * @since 1.5 - */ - public void setForceBaseUrl(boolean forceBaseUrl) - { + public void setForceBaseUrl(boolean forceBaseUrl) { this.forceBaseUrl = forceBaseUrl; } /** * Set maximum allowed login attempts. * - * * @param loginAttemptLimit login attempt limit - * * @since 1.34 */ - public void setLoginAttemptLimit(int loginAttemptLimit) - { + public void setLoginAttemptLimit(int loginAttemptLimit) { this.loginAttemptLimit = loginAttemptLimit; } @@ -522,22 +406,13 @@ public class ScmConfiguration * because of too many failed login attempts. * * @param loginAttemptLimitTimeout login attempt timeout in seconds - * * @since 1.34 */ - public void setLoginAttemptLimitTimeout(long loginAttemptLimitTimeout) - { + public void setLoginAttemptLimitTimeout(long loginAttemptLimitTimeout) { this.loginAttemptLimitTimeout = loginAttemptLimitTimeout; } - /** - * Method description - * - * - * @param pluginUrl - */ - public void setPluginUrl(String pluginUrl) - { + public void setPluginUrl(String pluginUrl) { this.pluginUrl = pluginUrl; } @@ -545,194 +420,68 @@ public class ScmConfiguration * Set glob patterns for urls which are should be excluded from proxy * settings. * - * * @param proxyExcludes glob patterns * @since 1.23 */ - public void setProxyExcludes(Set proxyExcludes) - { + public void setProxyExcludes(Set proxyExcludes) { this.proxyExcludes = proxyExcludes; } - /** - * Method description - * - * - * @param proxyPassword - * @since 1.7 - */ - public void setProxyPassword(String proxyPassword) - { + public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; } - /** - * Method description - * - * - * @param proxyPort - */ - public void setProxyPort(int proxyPort) - { + public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; } - /** - * Method description - * - * - * @param proxyServer - */ - public void setProxyServer(String proxyServer) - { + public void setProxyServer(String proxyServer) { this.proxyServer = proxyServer; } - /** - * Method description - * - * - * @param proxyUser - * @since 1.7 - */ - public void setProxyUser(String proxyUser) - { + public void setProxyUser(String proxyUser) { this.proxyUser = proxyUser; } - /** - * Sets the realm description. - * - * - * @param realmDescription - * @since 1.36 - */ - public void setRealmDescription(String realmDescription) - { + public void setRealmDescription(String realmDescription) { this.realmDescription = realmDescription; } /** - * If set to true the authentication chain is not stopped, if an + * If set to true the authentication chain is not stopped, if an * authenticator finds the user but fails to authenticate the user. * * @param skipFailedAuthenticators true to skip failed authenticators - * * @since 1.36 */ - public void setSkipFailedAuthenticators(boolean skipFailedAuthenticators) - { + public void setSkipFailedAuthenticators(boolean skipFailedAuthenticators) { this.skipFailedAuthenticators = skipFailedAuthenticators; } /** * Set {@code true} to enable xsrf cookie protection. - * + * * @param enabledXsrfProtection {@code true} to enable xsrf protection * @see Issue 793 - * * @since 1.47 */ - public void setEnabledXsrfProtection(boolean enabledXsrfProtection) - { + public void setEnabledXsrfProtection(boolean enabledXsrfProtection) { this.enabledXsrfProtection = enabledXsrfProtection; } - //~--- fields --------------------------------------------------------------- + public void setNamespaceStrategy(String namespaceStrategy) { + this.namespaceStrategy = namespaceStrategy; + } - /** Field description */ - @XmlElement(name = "admin-groups") - @XmlJavaTypeAdapter(XmlSetStringAdapter.class) - private Set adminGroups; + public void setLoginInfoUrl(String loginInfoUrl) { + this.loginInfoUrl = loginInfoUrl; + } - /** Field description */ - @XmlElement(name = "admin-users") - @XmlJavaTypeAdapter(XmlSetStringAdapter.class) - private Set adminUsers; - - /** Field description */ - @XmlElement(name = "base-url") - private String baseUrl; - - /** Field description */ - @XmlElement(name = "force-base-url") - private boolean forceBaseUrl; - - /** - * Maximum allowed login attempts. - * - * @since 1.34 - */ - @XmlElement(name = "login-attempt-limit") - private int loginAttemptLimit = -1; - - /** glob patterns for urls which are excluded from proxy */ - @XmlElement(name = "proxy-excludes") - @XmlJavaTypeAdapter(XmlSetStringAdapter.class) - private Set proxyExcludes; - - /** Field description */ - private String proxyPassword; - - /** Field description */ - private int proxyPort = 8080; - - /** Field description */ - private String proxyServer = "proxy.mydomain.com"; - - /** Field description */ - private String proxyUser; - - /** - * Skip failed authenticators. - * - * @since 1.36 - */ - @XmlElement(name = "skip-failed-authenticators") - private boolean skipFailedAuthenticators = false; - - /** Field description */ - @XmlElement(name = "plugin-url") - private String pluginUrl = DEFAULT_PLUGINURL; - - /** - * Login attempt timeout. - * - * @since 1.34 - */ - @XmlElement(name = "login-attempt-limit-timeout") - private long loginAttemptLimitTimeout = TimeUnit.MINUTES.toSeconds(5L); - - /** Field description */ - private boolean enableProxy = false; - - /** - * - * Authentication realm for basic authentication. - * - */ - private String realmDescription = HttpUtil.AUTHENTICATION_REALM; - - /** Field description */ - private boolean enableRepositoryArchive = false; - - /** Field description */ - private boolean disableGroupingGrid = false; - - /** - * JavaScript date format from moment.js - * @see http://momentjs.com/docs/#/parsing/ - */ - private String dateFormat = DEFAULT_DATEFORMAT; - - /** Field description */ - private boolean anonymousAccessEnabled = false; - - /** - * Enables xsrf cookie protection. - * - * @since 1.47 - */ - @XmlElement(name = "xsrf-protection") - private boolean enabledXsrfProtection = true; + @Override + // Only for permission checks, don't serialize to XML + @XmlTransient + public String getId() { + // Don't change this without migrating SCM permission configuration! + return PERMISSION; + } } diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java b/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java new file mode 100644 index 0000000000..5cbda6d1e1 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java @@ -0,0 +1,33 @@ +package sonia.scm.config; + +import com.github.legman.Subscribe; +import com.google.inject.Inject; +import sonia.scm.EagerSingleton; +import sonia.scm.SCMContext; +import sonia.scm.plugin.Extension; +import sonia.scm.user.UserManager; + +@Extension +@EagerSingleton +public class ScmConfigurationChangedListener { + + private UserManager userManager; + + @Inject + public ScmConfigurationChangedListener(UserManager userManager) { + this.userManager = userManager; + } + + @Subscribe + public void handleEvent(ScmConfigurationChangedEvent event) { + createAnonymousUserIfRequired(event); + } + + private void createAnonymousUserIfRequired(ScmConfigurationChangedEvent event) { + if (event.getConfiguration().isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS)) { + userManager.create(SCMContext.ANONYMOUS); + } + } +} + + diff --git a/scm-core/src/main/java/sonia/scm/event/AbstractHandlerEvent.java b/scm-core/src/main/java/sonia/scm/event/AbstractHandlerEvent.java index f828af9635..1582247f35 100644 --- a/scm-core/src/main/java/sonia/scm/event/AbstractHandlerEvent.java +++ b/scm-core/src/main/java/sonia/scm/event/AbstractHandlerEvent.java @@ -35,7 +35,6 @@ package sonia.scm.event; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; - import sonia.scm.HandlerEventType; /** @@ -129,10 +128,10 @@ public class AbstractHandlerEvent implements HandlerEvent { //J- return MoreObjects.toStringHelper(this) - .add("eventType", eventType) - .add("item", item) - .add("oldItem", oldItem) - .toString(); + .add("eventType", eventType) + .add("item", item) + .add("oldItem", oldItem) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/filter/Filters.java b/scm-core/src/main/java/sonia/scm/filter/Filters.java index b6a45811bc..d3ac708296 100644 --- a/scm-core/src/main/java/sonia/scm/filter/Filters.java +++ b/scm-core/src/main/java/sonia/scm/filter/Filters.java @@ -31,6 +31,8 @@ package sonia.scm.filter; +import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; + /** * Useful constants for filter implementations. * @@ -43,27 +45,11 @@ public final class Filters /** Field description */ public static final String PATTERN_ALL = "/*"; - /** Field description */ - public static final String PATTERN_CONFIG = "/api/rest/config*"; - /** Field description */ public static final String PATTERN_DEBUG = "/debug.html"; /** Field description */ - public static final String PATTERN_GROUPS = "/api/rest/groups*"; - - /** Field description */ - public static final String PATTERN_PLUGINS = "/api/rest/plugins*"; - - /** Field description */ - public static final String PATTERN_RESOURCE_REGEX = - "^/(?:resources|api|plugins|index)[\\./].*(?:html|\\.css|\\.js|\\.xml|\\.json|\\.txt)"; - - /** Field description */ - public static final String PATTERN_RESTAPI = "/api/rest/*"; - - /** Field description */ - public static final String PATTERN_USERS = "/api/rest/users*"; + public static final String PATTERN_RESTAPI = REST_API_PATH + "/*"; /** authentication priority */ public static final int PRIORITY_AUTHENTICATION = 5000; diff --git a/scm-core/src/main/java/sonia/scm/filter/GZipFilter.java b/scm-core/src/main/java/sonia/scm/filter/GZipFilter.java deleted file mode 100644 index 49fa8ebebf..0000000000 --- a/scm-core/src/main/java/sonia/scm/filter/GZipFilter.java +++ /dev/null @@ -1,125 +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.filter; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.Priority; -import sonia.scm.util.WebUtil; -import sonia.scm.web.filter.HttpFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * Filter for gzip encoding. - * - * @author Sebastian Sdorra - * @since 1.15 - */ -@Priority(Filters.PRIORITY_PRE_BASEURL) -@WebElement(value = Filters.PATTERN_RESOURCE_REGEX, regex = true) -public class GZipFilter extends HttpFilter -{ - - /** - * the logger for GZipFilter - */ - private static final Logger logger = - LoggerFactory.getLogger(GZipFilter.class); - - //~--- get methods ---------------------------------------------------------- - - /** - * Return the configuration for the gzip filter. - * - * - * @return gzip filter configuration - */ - public GZipFilterConfig getConfig() - { - return config; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Encodes the response, if the request has support for gzip encoding. - * - * - * @param request http request - * @param response http response - * @param chain filter chain - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void doFilter(HttpServletRequest request, - HttpServletResponse response, FilterChain chain) - throws IOException, ServletException - { - if (WebUtil.isGzipSupported(request)) - { - if (logger.isTraceEnabled()) - { - logger.trace("compress output with gzip"); - } - - GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response, - config); - - chain.doFilter(request, wrappedResponse); - wrappedResponse.finishResponse(); - } - else - { - chain.doFilter(request, response); - } - } - - //~--- fields --------------------------------------------------------------- - - /** gzip filter configuration */ - private GZipFilterConfig config = new GZipFilterConfig(); -} diff --git a/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java b/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java new file mode 100644 index 0000000000..ef0ec8a5ef --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java @@ -0,0 +1,59 @@ +package sonia.scm.filter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.ext.WriterInterceptor; +import javax.ws.rs.ext.WriterInterceptorContext; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Locale; +import java.util.zip.GZIPOutputStream; + +@javax.ws.rs.ext.Provider +public class GZipResponseFilter implements WriterInterceptor { + + private static final Logger LOG = LoggerFactory.getLogger(GZipResponseFilter.class); + + private final Provider requestProvider; + + @Inject + public GZipResponseFilter(Provider requestProvider) { + this.requestProvider = requestProvider; + } + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + if (isGZipSupported()) { + LOG.trace("compress output with gzip"); + encodeWithGZip(context); + } else { + context.proceed(); + } + } + + private void encodeWithGZip(WriterInterceptorContext context) throws IOException { + context.getHeaders().remove(HttpHeaders.CONTENT_LENGTH); + context.getHeaders().add(HttpHeaders.CONTENT_ENCODING, "gzip"); + + OutputStream outputStream = context.getOutputStream(); + GZIPOutputStream compressedOutputStream = new GZIPOutputStream(outputStream); + context.setOutputStream(compressedOutputStream); + try { + context.proceed(); + } finally { + compressedOutputStream.finish(); + context.setOutputStream(outputStream); + } + } + + private boolean isGZipSupported() { + Object encoding = requestProvider.get().getHeader(HttpHeaders.ACCEPT_ENCODING); + return encoding != null && encoding.toString().toLowerCase(Locale.ENGLISH).contains("gzip"); + } +} diff --git a/scm-core/src/main/java/sonia/scm/group/DisplayGroup.java b/scm-core/src/main/java/sonia/scm/group/DisplayGroup.java new file mode 100644 index 0000000000..a16a3046e9 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/DisplayGroup.java @@ -0,0 +1,26 @@ +package sonia.scm.group; + +import sonia.scm.ReducedModelObject; + +public class DisplayGroup implements ReducedModelObject { + + private final String id; + private final String displayName; + + public static DisplayGroup from(Group group) { + return new DisplayGroup(group.getId(), group.getDescription()); + } + + private DisplayGroup(String id, String displayName) { + this.id = id; + this.displayName = displayName; + } + + public String getId() { + return id; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/scm-core/src/main/java/sonia/scm/group/Group.java b/scm-core/src/main/java/sonia/scm/group/Group.java index cb1e4e9cda..8673804185 100644 --- a/scm-core/src/main/java/sonia/scm/group/Group.java +++ b/scm-core/src/main/java/sonia/scm/group/Group.java @@ -42,6 +42,7 @@ import com.google.common.base.Objects; import com.google.common.collect.Lists; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; +import sonia.scm.ReducedModelObject; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; @@ -49,23 +50,27 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; import java.util.Arrays; -import java.util.Iterator; +import java.util.Collections; import java.util.List; //~--- JDK imports ------------------------------------------------------------ /** * Organizes users into a group for easier permissions management. - * + * * TODO for 2.0: Use a set instead of a list for members * * @author Sebastian Sdorra */ -@StaticPermissions("group") +@StaticPermissions( + value = "group", + globalPermissions = {"create", "list", "autocomplete"}, + custom = true, customGlobal = true +) @XmlRootElement(name = "groups") @XmlAccessorType(XmlAccessType.FIELD) public class Group extends BasicPropertiesAware - implements ModelObject, Iterable, PermissionObject + implements ModelObject, PermissionObject, ReducedModelObject { /** Field description */ @@ -191,6 +196,7 @@ public class Group extends BasicPropertiesAware group.setMembers(members); group.setType(type); group.setDescription(description); + group.setExternal(external); } /** @@ -220,6 +226,7 @@ public class Group extends BasicPropertiesAware && Objects.equal(description, other.description) && Objects.equal(members, other.members) && Objects.equal(type, other.type) + && Objects.equal(external, other.external) && Objects.equal(creationDate, other.creationDate) && Objects.equal(lastModified, other.lastModified) && Objects.equal(properties, other.properties); @@ -238,18 +245,6 @@ public class Group extends BasicPropertiesAware lastModified, properties); } - /** - * Returns a {@link java.util.Iterator} for the members of this {@link Group}. - * - * - * @return a {@link java.util.Iterator} for the members of this {@link Group} - */ - @Override - public Iterator iterator() - { - return getMembers().iterator(); - } - /** * Remove the given member from this group. * @@ -274,14 +269,15 @@ public class Group extends BasicPropertiesAware { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("description", description) - .add("members", members) - .add("type", type) - .add("creationDate", creationDate) - .add("lastModified", lastModified) - .add("properties", properties) - .toString(); + .add("name", name) + .add("description", description) + .add("members", members) + .add("type", type) + .add("external", external) + .add("creationDate", creationDate) + .add("lastModified", lastModified) + .add("properties", properties) + .toString(); //J+ } @@ -322,6 +318,11 @@ public class Group extends BasicPropertiesAware return name; } + @Override + public String getDisplayName() { + return description; + } + /** * Returns a timestamp of the last modified date of this group. * @@ -342,8 +343,9 @@ public class Group extends BasicPropertiesAware */ public List getMembers() { - if (members == null) - { + if (external) { + return Collections.emptyList(); + } else if (members == null) { members = Lists.newArrayList(); } @@ -373,6 +375,15 @@ public class Group extends BasicPropertiesAware return type; } + /** + * Returns {@code true} if the members of the groups managed external of scm-manager. + * + * @return {@code true} if the group is an external group + */ + public boolean isExternal() { + return external; + } + /** * Returns true if the member is a member of this group. * @@ -466,8 +477,21 @@ public class Group extends BasicPropertiesAware this.type = type; } + /** + * {@code true} to mark the group as external. + * + * @param {@code true} for a external group + */ + public void setExternal(boolean external) + { + this.external = external; + } + //~--- fields --------------------------------------------------------------- + /** external group */ + private boolean external = false; + /** timestamp of the creation date of this group */ private Long creationDate; diff --git a/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java deleted file mode 100644 index 8389900098..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/GroupAlreadyExistsException.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.group; - -/** - * This {@link Exception} is thrown when trying to create a group - * that already exists. - * - * @author Sebastian Sdorra - */ -public class GroupAlreadyExistsException extends GroupException -{ - - /** Field description */ - private static final long serialVersionUID = 4042878550219750430L; - - /** - * Constructs a new instance. - * - * @param message exception message - */ - public GroupAlreadyExistsException(String message) { - super(message); - } -} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupCollector.java b/scm-core/src/main/java/sonia/scm/group/GroupCollector.java new file mode 100644 index 0000000000..4546db1bc4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupCollector.java @@ -0,0 +1,10 @@ +package sonia.scm.group; + +import java.util.Set; + +public interface GroupCollector { + + String AUTHENTICATED = "_authenticated"; + + Set collect(String principal); +} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupDisplayManager.java b/scm-core/src/main/java/sonia/scm/group/GroupDisplayManager.java new file mode 100644 index 0000000000..d9188e9402 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupDisplayManager.java @@ -0,0 +1,6 @@ +package sonia.scm.group; + +import sonia.scm.DisplayManager; + +public interface GroupDisplayManager extends DisplayManager { +} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupException.java b/scm-core/src/main/java/sonia/scm/group/GroupException.java deleted file mode 100644 index e0dc799e92..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/GroupException.java +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.group; - -/** - * General {@link Exception} for group errors. - * - * @author Sebastian Sdorra - */ -public class GroupException extends Exception -{ - - /** Field description */ - private static final long serialVersionUID = 5191341492098994225L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a {@link GroupException} object. - * - */ - public GroupException() - { - super(); - } - - /** - * Constructs a {@link GroupException} object. - * - * - * @param message for the exception - */ - public GroupException(String message) - { - super(message); - } - - /** - * Constructs a {@link GroupException} object. - * - * - * @param cause of the exception - */ - public GroupException(Throwable cause) - { - super(cause); - } - - /** - * Constructs a {@link GroupException} object. - * - * - * @param message of the exception - * @param cause of the exception - */ - public GroupException(String message, Throwable cause) - { - super(message, cause); - } -} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupManager.java b/scm-core/src/main/java/sonia/scm/group/GroupManager.java index 428559edef..288196894d 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupManager.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupManager.java @@ -38,10 +38,10 @@ package sonia.scm.group; import sonia.scm.Manager; import sonia.scm.search.Searchable; -//~--- JDK imports ------------------------------------------------------------ - import java.util.Collection; +//~--- JDK imports ------------------------------------------------------------ + /** * The central class for managing {@link Group}s. * This class is a singleton and is available via injection. @@ -49,7 +49,7 @@ import java.util.Collection; * @author Sebastian Sdorra */ public interface GroupManager - extends Manager, Searchable + extends Manager, Searchable { /** diff --git a/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java b/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java index 955e218b43..e2367d863c 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java @@ -38,10 +38,10 @@ package sonia.scm.group; import sonia.scm.ManagerDecorator; import sonia.scm.search.SearchRequest; -//~--- JDK imports ------------------------------------------------------------ - import java.util.Collection; +//~--- JDK imports ------------------------------------------------------------ + /** * Decorator for {@link GroupManager}. * @@ -49,7 +49,7 @@ import java.util.Collection; * @since 1.23 */ public class GroupManagerDecorator - extends ManagerDecorator implements GroupManager + extends ManagerDecorator implements GroupManager { /** diff --git a/scm-core/src/main/java/sonia/scm/group/GroupNames.java b/scm-core/src/main/java/sonia/scm/group/GroupNames.java deleted file mode 100644 index 43ca462d32..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/GroupNames.java +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.group; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Joiner; -import com.google.common.base.Objects; -import com.google.common.collect.Lists; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; - -/** - * This class represents all associated groups for a user. - * - * @author Sebastian Sdorra - * @since 1.21 - */ -public final class GroupNames implements Serializable, Iterable -{ - - /** - * Group for all authenticated users - * @since 1.31 - */ - public static final String AUTHENTICATED = "_authenticated"; - - /** Field description */ - private static final long serialVersionUID = 8615685985213897947L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - @SuppressWarnings("unchecked") - public GroupNames() - { - this.collection = Collections.EMPTY_LIST; - } - - /** - * Constructs ... - * - * - * @param collection - */ - public GroupNames(Collection collection) - { - this.collection = Collections.unmodifiableCollection(collection); - } - - /** - * Constructs ... - * - * - * @param groupName - * @param groupNames - */ - public GroupNames(String groupName, String... groupNames) - { - this.collection = Lists.asList(groupName, groupNames); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param groupName - * - * @return - */ - public boolean contains(String groupName) - { - return collection.contains(groupName); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final GroupNames other = (GroupNames) obj; - - return Objects.equal(collection, other.collection); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() - { - return Objects.hashCode(collection); - } - - /** - * Method description - * - * - * @return - */ - @Override - public Iterator iterator() - { - return collection.iterator(); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - return Joiner.on(", ").join(collection); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Collection getCollection() - { - return collection; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final Collection collection; -} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java b/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java deleted file mode 100644 index f4b9934128..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/GroupNotFoundException.java +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm.group; - -/** - * The GroupNotFoundException is thrown e.g. from the - * modify method of the {@link GroupManager}, if the group does not exists. - * - * @author Sebastian Sdorra - * - * @since 1.28 - */ -public class GroupNotFoundException extends GroupException -{ - - /** Field description */ - private static final long serialVersionUID = -1617037899954718001L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new GroupNotFoundException. - * - */ - public GroupNotFoundException() {} - - /** - * Constructs a new GroupNotFoundException. - * - * - * @param message message for the exception - */ - public GroupNotFoundException(String message) - { - super(message); - } - - /** - * Constructs a new GroupNotFoundException. - * - * - * @param throwable root cause - */ - public GroupNotFoundException(Throwable throwable) - { - super(throwable); - } - - /** - * Constructs a new GroupNotFoundException. - * - * - * @param message message for the exception - * @param throwable root cause - */ - public GroupNotFoundException(String message, Throwable throwable) - { - super(message, throwable); - } -} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupResolver.java b/scm-core/src/main/java/sonia/scm/group/GroupResolver.java new file mode 100644 index 0000000000..5aba63c93b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupResolver.java @@ -0,0 +1,10 @@ +package sonia.scm.group; + +import sonia.scm.plugin.ExtensionPoint; + +import java.util.Set; + +@ExtensionPoint +public interface GroupResolver { + Set resolve(String principal); +} diff --git a/scm-core/src/main/java/sonia/scm/i18n/I18nMessages.java b/scm-core/src/main/java/sonia/scm/i18n/I18nMessages.java index 9c4a9353be..0af1cef895 100644 --- a/scm-core/src/main/java/sonia/scm/i18n/I18nMessages.java +++ b/scm-core/src/main/java/sonia/scm/i18n/I18nMessages.java @@ -36,16 +36,13 @@ package sonia.scm.i18n; import com.google.common.base.Objects; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; - import sonia.scm.util.ClassLoaders; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Field; - import java.util.Locale; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * The I18nMessages class instantiates a class and initializes all {@link String} diff --git a/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java b/scm-core/src/main/java/sonia/scm/lifecycle/RestartEvent.java similarity index 90% rename from scm-core/src/main/java/sonia/scm/boot/RestartEvent.java rename to scm-core/src/main/java/sonia/scm/lifecycle/RestartEvent.java index 244bc8f03c..1978cb9d7c 100644 --- a/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java +++ b/scm-core/src/main/java/sonia/scm/lifecycle/RestartEvent.java @@ -29,18 +29,20 @@ -package sonia.scm.boot; +package sonia.scm.lifecycle; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.Stage; import sonia.scm.event.Event; /** * This event can be used to force a restart of the webapp context. The restart * event is useful during plugin development, because we don't have to restart - * the whole server, to see our changes. The restart event can only be used in - * stage {@link Stage#DEVELOPMENT}. + * the whole server, to see our changes. The restart event could also be used + * to install or upgrade plugins. + * + * But the restart event should be used carefully, because the whole context + * will be restarted and that process could take some time. * * @author Sebastian Sdorra * @since 2.0.0 diff --git a/scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java b/scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java new file mode 100644 index 0000000000..acc37ce8ee --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java @@ -0,0 +1,7 @@ +package sonia.scm.migration; + +import java.util.Collection; + +public interface MigrationDAO { + Collection getAll(); +} diff --git a/scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java b/scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java new file mode 100644 index 0000000000..19f5d8ba3a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java @@ -0,0 +1,38 @@ +package sonia.scm.migration; + +public class MigrationInfo { + + private final String id; + private final String protocol; + private final String originalRepositoryName; + private final String namespace; + private final String name; + + public MigrationInfo(String id, String protocol, String originalRepositoryName, String namespace, String name) { + this.id = id; + this.protocol = protocol; + this.originalRepositoryName = originalRepositoryName; + this.namespace = namespace; + this.name = name; + } + + public String getId() { + return id; + } + + public String getProtocol() { + return protocol; + } + + public String getOriginalRepositoryName() { + return originalRepositoryName; + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } +} diff --git a/scm-core/src/main/java/sonia/scm/migration/UpdateException.java b/scm-core/src/main/java/sonia/scm/migration/UpdateException.java new file mode 100644 index 0000000000..946db59536 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/UpdateException.java @@ -0,0 +1,17 @@ +package sonia.scm.migration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UpdateException extends RuntimeException { + private static Logger LOG = LoggerFactory.getLogger(UpdateException.class); + + public UpdateException(String message) { + super(message); + } + + public UpdateException(String message, Throwable cause) { + super(message, cause); + LOG.error(message, cause); + } +} diff --git a/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java new file mode 100644 index 0000000000..ed5f60a630 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java @@ -0,0 +1,82 @@ +package sonia.scm.migration; + +import sonia.scm.plugin.ExtensionPoint; +import sonia.scm.version.Version; + +/** + * This is the main interface for data migration/update. Using this interface, SCM-Manager provides the possibility to + * change data structures between versions for a given type of data. + *

The data type can be an arbitrary string, but it is considered a best practice to use a qualified name, for + * example + *

    + *
  • com.example.myPlugin.configuration
  • for data in plugins, or + *
  • com.cloudogu.scm.repository
  • for core data structures. + *
+ *

+ *

The version is unrelated to other versions and therefore can be chosen freely, so that a data type can be updated + * without in various ways independent of other data types or the official version of the plugin or the core. + * A coordination between different data types and their versions is only necessary, when update steps of different data + * types rely on each other. If a update step of data type A has to run before another step for data type + * B, the version number of the second step has to be greater in regards to {@link Version#compareTo(Version)}. + *

+ *

The algorithm looks something like this:
+ * Whenever the SCM-Manager starts, + *

    + *
  • it creates a so called bootstrap guice context, that contains + *
      + *
    • a {@link sonia.scm.security.KeyGenerator},
    • + *
    • the {@link sonia.scm.repository.RepositoryLocationResolver},
    • + *
    • the {@link sonia.scm.io.FileSystem},
    • + *
    • the {@link sonia.scm.security.CipherHandler},
    • + *
    • a {@link sonia.scm.store.ConfigurationStoreFactory},
    • + *
    • a {@link sonia.scm.store.ConfigurationEntryStoreFactory},
    • + *
    • a {@link sonia.scm.store.DataStoreFactory},
    • + *
    • a {@link sonia.scm.store.BlobStoreFactory}, and
    • + *
    • the {@link sonia.scm.plugin.PluginLoader}.
    • + *
    + * Mind, that there are no DAOs, Managers or the like available at this time! + *
  • + *
  • It then checks whether there are instances of this interface that have not run before, that is either + *
      + *
    • their version number given by {@link #getTargetVersion()} is bigger than the last recorded target version of an + * executed update step for the data type given by {@link #getAffectedDataType()}, or + *
    • + *
    • there is no version number known for the given data type. + *
    • + *
    + * These are the relevant update steps. + *
  • + *
  • These relevant update steps are then sorted ascending by their target version given by + * {@link #getTargetVersion()}. + *
  • + *
  • Finally, these sorted steps are executed one after another calling {@link #doUpdate()} of each step, updating the + * version for the data type accordingly. + *
  • + *
  • If all works well, SCM-Manager then creates the runtime guice context by loading all further modules.
  • + *
  • If any of the update steps fails, the whole process is interrupted and SCM-Manager will not start up and will + * not record the version number of this update step. + *
  • + *
+ *

+ */ +@ExtensionPoint +public interface UpdateStep { + /** + * Implement this to update the data to the new version. If any {@link Exception} is thrown, SCM-Manager will not + * start up. + */ + void doUpdate() throws Exception; + + /** + * Declares the new version of the data type given by {@link #getAffectedDataType()}. A update step will only be + * executed, when this version is bigger than the last recorded version for its data type according to + * {@link Version#compareTo(Version)} + */ + Version getTargetVersion(); + + /** + * Declares the data type this update step will take care of. This should be a qualified name, like + * com.example.myPlugin.configuration. + */ + String getAffectedDataType(); +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/AvailablePlugin.java b/scm-core/src/main/java/sonia/scm/plugin/AvailablePlugin.java new file mode 100644 index 0000000000..c05e970e50 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/AvailablePlugin.java @@ -0,0 +1,32 @@ +package sonia.scm.plugin; + +import com.google.common.base.Preconditions; + +public class AvailablePlugin implements Plugin { + + private final AvailablePluginDescriptor pluginDescriptor; + private final boolean pending; + + public AvailablePlugin(AvailablePluginDescriptor pluginDescriptor) { + this(pluginDescriptor, false); + } + + private AvailablePlugin(AvailablePluginDescriptor pluginDescriptor, boolean pending) { + this.pluginDescriptor = pluginDescriptor; + this.pending = pending; + } + + @Override + public AvailablePluginDescriptor getDescriptor() { + return pluginDescriptor; + } + + public boolean isPending() { + return pending; + } + + AvailablePlugin install() { + Preconditions.checkState(!pending, "installation is already pending"); + return new AvailablePlugin(pluginDescriptor, true); + } +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java new file mode 100644 index 0000000000..1c164a0d81 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java @@ -0,0 +1,47 @@ +package sonia.scm.plugin; + +import java.util.Optional; +import java.util.Set; + +/** + * @since 2.0.0 + */ +public class AvailablePluginDescriptor implements PluginDescriptor { + + private final PluginInformation information; + private final PluginCondition condition; + private final Set dependencies; + private final String url; + private final String checksum; + + public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set dependencies, String url, String checksum) { + this.information = information; + this.condition = condition; + this.dependencies = dependencies; + this.url = url; + this.checksum = checksum; + } + + public String getUrl() { + return url; + } + + public Optional getChecksum() { + return Optional.ofNullable(checksum); + } + + @Override + public PluginInformation getInformation() { + return information; + } + + @Override + public PluginCondition getCondition() { + return condition; + } + + @Override + public Set getDependencies() { + return dependencies; + } +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/ClassElement.java b/scm-core/src/main/java/sonia/scm/plugin/ClassElement.java index 1b43a41ad6..813a5a850f 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/ClassElement.java +++ b/scm-core/src/main/java/sonia/scm/plugin/ClassElement.java @@ -36,10 +36,10 @@ package sonia.scm.plugin; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -//~--- JDK imports ------------------------------------------------------------ - import javax.xml.bind.annotation.XmlElement; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -119,9 +119,9 @@ public final class ClassElement { //J- return MoreObjects.toStringHelper(this) - .add("clazz", clazz) - .add("description", description) - .toString(); + .add("clazz", clazz) + .add("description", description) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/plugin/ExtensionPointElement.java b/scm-core/src/main/java/sonia/scm/plugin/ExtensionPointElement.java index eda91d4e6f..7f6f1efe07 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/ExtensionPointElement.java +++ b/scm-core/src/main/java/sonia/scm/plugin/ExtensionPointElement.java @@ -36,13 +36,13 @@ package sonia.scm.plugin; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -//~--- JDK imports ------------------------------------------------------------ - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -131,11 +131,11 @@ public final class ExtensionPointElement { //J- return MoreObjects.toStringHelper(this) - .add("class", clazz) - .add("description", description) - .add("multiple", multiple) - .add("autoBind", autoBind) - .toString(); + .add("class", clazz) + .add("description", description) + .add("multiple", multiple) + .add("autoBind", autoBind) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java new file mode 100644 index 0000000000..09f05e2c99 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.plugin; + +//~--- JDK imports ------------------------------------------------------------ + +import java.nio.file.Path; + +/** + * Wrapper for a {@link InstalledPluginDescriptor}. The wrapper holds the directory, + * {@link ClassLoader} and {@link WebResourceLoader} of a plugin. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class InstalledPlugin implements Plugin +{ + + public static final String UNINSTALL_MARKER_FILENAME = "uninstall"; + + /** + * Constructs a new plugin wrapper. + * @param descriptor wrapped plugin + * @param classLoader plugin class loader + * @param webResourceLoader web resource loader + * @param directory plugin directory + * @param core marked as core or not + */ + public InstalledPlugin(InstalledPluginDescriptor descriptor, ClassLoader classLoader, + WebResourceLoader webResourceLoader, Path directory, boolean core) + { + this.descriptor = descriptor; + this.classLoader = classLoader; + this.webResourceLoader = webResourceLoader; + this.directory = directory; + this.core = core; + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns plugin class loader. + * + * + * @return plugin class loader + */ + public ClassLoader getClassLoader() + { + return classLoader; + } + + /** + * Returns plugin directory. + * + * + * @return plugin directory + */ + public Path getDirectory() + { + return directory; + } + + /** + * Returns the id of the plugin. + * + * + * @return id of plugin + */ + public String getId() + { + return descriptor.getInformation().getId(); + } + + /** + * Returns the plugin descriptor. + * + * + * @return plugin descriptor + */ + @Override + public InstalledPluginDescriptor getDescriptor() + { + return descriptor; + } + + /** + * Returns the {@link WebResourceLoader} for this plugin. + * + * + * @return web resource loader + */ + public WebResourceLoader getWebResourceLoader() + { + return webResourceLoader; + } + + public boolean isCore() { + return core; + } + + public boolean isMarkedForUninstall() { + return markedForUninstall; + } + + public void setMarkedForUninstall(boolean markedForUninstall) { + this.markedForUninstall = markedForUninstall; + } + + public boolean isUninstallable() { + return uninstallable; + } + + public void setUninstallable(boolean uninstallable) { + this.uninstallable = uninstallable; + } + +//~--- fields --------------------------------------------------------------- + + /** plugin class loader */ + private final ClassLoader classLoader; + + /** plugin directory */ + private final Path directory; + + /** plugin */ + private final InstalledPluginDescriptor descriptor; + + /** plugin web resource loader */ + private final WebResourceLoader webResourceLoader; + + private final boolean core; + + private boolean markedForUninstall = false; + private boolean uninstallable = false; +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java new file mode 100644 index 0000000000..88ae4c5dff --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.plugin; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Set; + +//~--- JDK imports ------------------------------------------------------------ + +/** + * + * @author Sebastian Sdorra + */ +@XmlRootElement(name = "plugin") +@XmlAccessorType(XmlAccessType.FIELD) +public final class InstalledPluginDescriptor extends ScmModule implements PluginDescriptor +{ + + /** + * Constructs ... + * + */ + InstalledPluginDescriptor() {} + + /** + * Constructs ... + * + * + * @param scmVersion + * @param information + * @param resources + * @param condition + * @param childFirstClassLoader + * @param dependencies + */ + public InstalledPluginDescriptor(int scmVersion, PluginInformation information, + PluginResources resources, PluginCondition condition, + boolean childFirstClassLoader, Set dependencies) + { + this.scmVersion = scmVersion; + this.information = information; + this.resources = resources; + this.condition = condition; + this.childFirstClassLoader = childFirstClassLoader; + this.dependencies = dependencies; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param obj + * + * @return + */ + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + final InstalledPluginDescriptor other = (InstalledPluginDescriptor) obj; + + return Objects.equal(scmVersion, other.scmVersion) + && Objects.equal(condition, other.condition) + && Objects.equal(information, other.information) + && Objects.equal(resources, other.resources) + && Objects.equal(childFirstClassLoader, other.childFirstClassLoader) + && Objects.equal(dependencies, other.dependencies); + } + + /** + * Method description + * + * + * @return + */ + @Override + public int hashCode() + { + return Objects.hashCode(scmVersion, condition, information, resources, + childFirstClassLoader, dependencies); + } + + /** + * Method description + * + * + * @return + */ + @Override + public String toString() + { + //J- + return MoreObjects.toStringHelper(this) + .add("scmVersion", scmVersion) + .add("condition", condition) + .add("information", information) + .add("resources", resources) + .add("childFirstClassLoader", childFirstClassLoader) + .add("dependencies", dependencies) + .toString(); + //J+ + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @return + */ + @Override + public PluginCondition getCondition() + { + return condition; + } + + /** + * Method description + * + * + * @return + * + * @since 2.0.0 + */ + @Override + public Set getDependencies() + { + if (dependencies == null) + { + dependencies = ImmutableSet.of(); + } + + return dependencies; + } + + /** + * Method description + * + * + * @return + */ + @Override + public PluginInformation getInformation() + { + return information; + } + + /** + * Method description + * + * + * @return + */ + public PluginResources getResources() + { + return resources; + } + + /** + * Method description + * + * + * @return + */ + public int getScmVersion() + { + return scmVersion; + } + + /** + * Method description + * + * + * @return + */ + public boolean isChildFirstClassLoader() + { + return childFirstClassLoader; + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + @XmlElement(name = "child-first-classloader") + private boolean childFirstClassLoader; + + /** Field description */ + @XmlElement(name = "conditions") + private PluginCondition condition; + + /** Field description */ + @XmlElement(name = "dependency") + @XmlElementWrapper(name = "dependencies") + private Set dependencies; + + /** Field description */ + @XmlElement(name = "information") + private PluginInformation information; + + /** Field description */ + private PluginResources resources; + + /** Field description */ + @XmlElement(name = "scm-version") + private int scmVersion = 1; +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/Plugin.java b/scm-core/src/main/java/sonia/scm/plugin/Plugin.java index 438ca2184c..8b440f8ab9 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/Plugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/Plugin.java @@ -1,251 +1,6 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - package sonia.scm.plugin; -//~--- non-JDK imports -------------------------------------------------------- +public interface Plugin { -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; -import com.google.common.collect.ImmutableSet; - -import javax.xml.bind.annotation.*; -import java.util.Set; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement -@XmlAccessorType(XmlAccessType.FIELD) -public final class Plugin extends ScmModule -{ - - /** - * Constructs ... - * - */ - Plugin() {} - - /** - * Constructs ... - * - * - * @param scmVersion - * @param information - * @param resources - * @param condition - * @param childFirstClassLoader - * @param dependencies - */ - public Plugin(int scmVersion, PluginInformation information, - PluginResources resources, PluginCondition condition, - boolean childFirstClassLoader, Set dependencies) - { - this.scmVersion = scmVersion; - this.information = information; - this.resources = resources; - this.condition = condition; - this.childFirstClassLoader = childFirstClassLoader; - this.dependencies = dependencies; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param obj - * - * @return - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final Plugin other = (Plugin) obj; - - return Objects.equal(scmVersion, other.scmVersion) - && Objects.equal(condition, other.condition) - && Objects.equal(information, other.information) - && Objects.equal(resources, other.resources) - && Objects.equal(childFirstClassLoader, other.childFirstClassLoader) - && Objects.equal(dependencies, other.dependencies); - } - - /** - * Method description - * - * - * @return - */ - @Override - public int hashCode() - { - return Objects.hashCode(scmVersion, condition, information, resources, - childFirstClassLoader, dependencies); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - //J- - return MoreObjects.toStringHelper(this) - .add("scmVersion", scmVersion) - .add("condition", condition) - .add("information", information) - .add("resources", resources) - .add("childFirstClassLoader", childFirstClassLoader) - .add("dependencies", dependencies) - .toString(); - //J+ - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public PluginCondition getCondition() - { - return condition; - } - - /** - * Method description - * - * - * @return - * - * @since 2.0.0 - */ - public Set getDependencies() - { - if (dependencies == null) - { - dependencies = ImmutableSet.of(); - } - - return dependencies; - } - - /** - * Method description - * - * - * @return - */ - public PluginInformation getInformation() - { - return information; - } - - /** - * Method description - * - * - * @return - */ - public PluginResources getResources() - { - return resources; - } - - /** - * Method description - * - * - * @return - */ - public int getScmVersion() - { - return scmVersion; - } - - /** - * Method description - * - * - * @return - */ - public boolean isChildFirstClassLoader() - { - return childFirstClassLoader; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "child-first-classloader") - private boolean childFirstClassLoader; - - /** Field description */ - @XmlElement(name = "conditions") - private PluginCondition condition; - - /** Field description */ - @XmlElement(name = "dependency") - @XmlElementWrapper(name = "dependencies") - private Set dependencies; - - /** Field description */ - private PluginInformation information; - - /** Field description */ - private PluginResources resources; - - /** Field description */ - @XmlElement(name = "scm-version") - private int scmVersion = 1; + PluginDescriptor getDescriptor(); } diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginCenter.java b/scm-core/src/main/java/sonia/scm/plugin/PluginCenter.java deleted file mode 100644 index e4de29eb26..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginCenter.java +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -import java.util.HashSet; -import java.util.Set; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "plugin-center") -@XmlAccessorType(XmlAccessType.FIELD) -public class PluginCenter implements Serializable -{ - - /** Field description */ - private static final long serialVersionUID = -6414175308610267397L; - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Set getPlugins() - { - return plugins; - } - - /** - * Method description - * - * - * @return - */ - public Set getRepositories() - { - return repositories; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param plugins - */ - public void setPlugins(Set plugins) - { - this.plugins = plugins; - } - - /** - * Method description - * - * - * @param repositories - */ - public void setRepositories(Set repositories) - { - this.repositories = repositories; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "plugin") - @XmlElementWrapper(name = "plugins") - private Set plugins = new HashSet<>(); - - /** Field description */ - @XmlElement(name = "repository") - @XmlElementWrapper(name = "repositories") - private Set repositories = new HashSet<>(); -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginCondition.java b/scm-core/src/main/java/sonia/scm/plugin/PluginCondition.java index fb03b66351..e70821d0df 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginCondition.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginCondition.java @@ -43,7 +43,11 @@ import sonia.scm.util.SystemUtil; import sonia.scm.util.Util; import sonia.scm.version.Version; -import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -102,7 +106,7 @@ public class PluginCondition implements Cloneable, Serializable if (Util.isNotEmpty(os)) { - clone.setOs(new ArrayList<>(os)); + clone.setOs(new ArrayList(os)); } return clone; @@ -159,10 +163,10 @@ public class PluginCondition implements Cloneable, Serializable { //J- return MoreObjects.toStringHelper(this) - .add("arch", arch) - .add("minVersion", minVersion) - .add("os", os) - .toString(); + .add("arch", arch) + .add("minVersion", minVersion) + .add("os", os) + .toString(); //J+ } @@ -309,14 +313,14 @@ public class PluginCondition implements Cloneable, Serializable osType = osType.toLowerCase(Locale.ENGLISH); //J- - return ((osType.contains("win")) && (PlatformType.WINDOWS == type)) - || ((osType.contains("unix")) && type.isUnix()) - || ((osType.contains("posix")) && type.isPosix()) - || ((osType.contains("mac")) && (PlatformType.MAC == type)) - || ((osType.contains("linux")) && (PlatformType.LINUX == type)) - || ((osType.contains("solaris")) && (PlatformType.SOLARIS == type)) - || ((osType.contains("openbsd")) && (PlatformType.OPENBSD == type)) - || ((osType.contains("freebsd")) && (PlatformType.FREEBSD == type)); + return ((osType.indexOf("win") >= 0) && (PlatformType.WINDOWS == type)) + || ((osType.indexOf("unix") >= 0) && type.isUnix()) + || ((osType.indexOf("posix") >= 0) && type.isPosix()) + || ((osType.indexOf("mac") >= 0) && (PlatformType.MAC == type)) + || ((osType.indexOf("linux") >= 0) && (PlatformType.LINUX == type)) + || ((osType.indexOf("solaris") >= 0) && (PlatformType.SOLARIS == type)) + || ((osType.indexOf("openbsd") >= 0) && (PlatformType.OPENBSD == type)) + || ((osType.indexOf("freebsd") >= 0) && (PlatformType.FREEBSD == type)); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/PluginDescriptor.java new file mode 100644 index 0000000000..6e800faff0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginDescriptor.java @@ -0,0 +1,13 @@ +package sonia.scm.plugin; + +import java.util.Set; + +public interface PluginDescriptor { + + PluginInformation getInformation(); + + PluginCondition getCondition(); + + Set getDependencies(); + +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java b/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java index cab5245f6a..b669cb63fa 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

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

* http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- import com.github.sdorra.ssp.PermissionObject; import com.github.sdorra.ssp.StaticPermissions; -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; +import lombok.Data; import sonia.scm.Validateable; import sonia.scm.util.Util; -import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; //~--- JDK imports ------------------------------------------------------------ /** - * * @author Sebastian Sdorra */ +@Data @StaticPermissions( - value = "plugin", - generatedClass = "PluginPermissions", + value = "plugin", + generatedClass = "PluginPermissions", permissions = {}, - globalPermissions = { "read", "manage" } + globalPermissions = {"read", "manage"}, + custom = true, customGlobal = true ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "plugin-information") -public class PluginInformation - implements PermissionObject, Validateable, Cloneable, Serializable -{ +public class PluginInformation implements PermissionObject, Validateable, Cloneable, Serializable { - /** Field description */ private static final long serialVersionUID = 461382048865977206L; - //~--- methods -------------------------------------------------------------- + private String name; + private String version; + private String displayName; + private String description; + private String author; + private String category; + private String avatarUrl; - /** - * Method description - * - * - * @return - * - * @since 1.11 - */ @Override - public PluginInformation clone() - { + public PluginInformation clone() { PluginInformation clone = new PluginInformation(); - - clone.setArtifactId(artifactId); + clone.setName(name); + clone.setVersion(version); + clone.setDisplayName(displayName); + clone.setDescription(description); clone.setAuthor(author); clone.setCategory(category); - clone.setTags(tags); - - if (condition != null) - { - clone.setCondition(condition.clone()); - } - - clone.setDescription(description); - clone.setGroupId(groupId); - clone.setName(name); - - if (Util.isNotEmpty(screenshots)) - { - clone.setScreenshots(new ArrayList<>(screenshots)); - } - - clone.setState(state); - clone.setUrl(url); - clone.setVersion(version); - clone.setWiki(wiki); - + clone.setAvatarUrl(avatarUrl); return clone; } - /** - * Method description - * - * - * @param obj - * - * @return - */ @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final PluginInformation other = (PluginInformation) obj; - - //J- - return Objects.equal(artifactId, other.artifactId) - && Objects.equal(author, other.author) - && Objects.equal(category, other.category) - && Objects.equal(tags, other.tags) - && Objects.equal(condition, other.condition) - && Objects.equal(description, other.description) - && Objects.equal(groupId, other.groupId) - && Objects.equal(name, other.name) - && Objects.equal(screenshots, other.screenshots) - && Objects.equal(state, other.state) - && Objects.equal(url, other.url) - && Objects.equal(version, other.version) - && Objects.equal(wiki, other.wiki); - //J+ + public String getId() { + return getName(true); } - /** - * Method description - * - * - * @return - */ - @Override - public int hashCode() - { - return Objects.hashCode(artifactId, author, category, tags, condition, - description, groupId, name, screenshots, state, url, version, wiki); - } + public String getName(boolean withVersion) { + StringBuilder id = new StringBuilder(name); - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - //J- - return MoreObjects.toStringHelper(this) - .add("artifactId", artifactId) - .add("author", author) - .add("category", category) - .add("tags", tags) - .add("condition", condition) - .add("description", description) - .add("groupId", groupId) - .add("name", name) - .add("screenshots", screenshots) - .add("state", state) - .add("url", url) - .add("version", version) - .add("wiki", wiki) - .toString(); - //J+ - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getArtifactId() - { - return artifactId; - } - - /** - * Method description - * - * - * @return - */ - public String getAuthor() - { - return author; - } - - /** - * Method description - * - * - * @return - */ - public String getCategory() - { - return category; - } - - /** - * Method description - * - * - * @return - */ - public PluginCondition getCondition() - { - return condition; - } - - /** - * Method description - * - * - * @return - */ - public String getDescription() - { - return description; - } - - /** - * Method description - * - * - * @return - */ - public String getGroupId() - { - return groupId; - } - - /** - * Method description - * - * - * @return - */ - @Override - public String getId() - { - return getId(true); - } - - /** - * Method description - * - * - * @param withVersion - * - * @return - * @since 1.21 - */ - public String getId(boolean withVersion) - { - StringBuilder id = new StringBuilder(groupId); - - id.append(":").append(artifactId); - - if (withVersion) - { + if (withVersion) { id.append(":").append(version); } - return id.toString(); } - /** - * Method description - * - * - * @return - */ - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - public List getScreenshots() - { - return screenshots; - } - - /** - * Method description - * - * - * @return - */ - public PluginState getState() - { - return state; - } - - /** - * Method description - * - * - * @return - */ - public List getTags() - { - return tags; - } - - /** - * Method description - * - * - * @return - */ - public String getUrl() - { - return url; - } - - /** - * Method description - * - * - * @return - */ - public String getVersion() - { - return version; - } - - /** - * Method description - * - * - * @return - */ - public String getWiki() - { - return wiki; - } - - /** - * Method description - * - * - * @return - */ @Override - public boolean isValid() - { - return Util.isNotEmpty(groupId) && Util.isNotEmpty(artifactId) - && Util.isNotEmpty(name) && Util.isNotEmpty(version); + public boolean isValid() { + return Util.isNotEmpty(name) && Util.isNotEmpty(version); } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param artifactId - */ - public void setArtifactId(String artifactId) - { - this.artifactId = artifactId; - } - - /** - * Method description - * - * - * @param author - */ - public void setAuthor(String author) - { - this.author = author; - } - - /** - * Method description - * - * - * @param category - */ - public void setCategory(String category) - { - this.category = category; - } - - /** - * Method description - * - * - * @param condition - */ - public void setCondition(PluginCondition condition) - { - this.condition = condition; - } - - /** - * Method description - * - * - * @param description - */ - public void setDescription(String description) - { - this.description = description; - } - - /** - * Method description - * - * - * @param groupId - */ - public void setGroupId(String groupId) - { - this.groupId = groupId; - } - - /** - * Method description - * - * - * @param name - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Method description - * - * - * @param screenshots - */ - public void setScreenshots(List screenshots) - { - this.screenshots = screenshots; - } - - /** - * Method description - * - * - * @param state - */ - public void setState(PluginState state) - { - this.state = state; - } - - /** - * Method description - * - * - * @param tags - */ - public void setTags(List tags) - { - this.tags = tags; - } - - /** - * Method description - * - * - * @param url - */ - public void setUrl(String url) - { - this.url = url; - } - - /** - * Method description - * - * - * @param version - */ - public void setVersion(String version) - { - this.version = version; - } - - /** - * Method description - * - * - * @param wiki - */ - public void setWiki(String wiki) - { - this.wiki = wiki; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String artifactId; - - /** Field description */ - private String author; - - /** Field description */ - private String category; - - /** Field description */ - private PluginCondition condition; - - /** Field description */ - private String description; - - /** Field description */ - private String groupId; - - /** Field description */ - private String name; - - /** Field description */ - @XmlElement(name = "screenshot") - @XmlElementWrapper(name = "screenshots") - private List screenshots; - - /** Field description */ - private PluginState state; - - /** Field description */ - @XmlElement(name = "tag") - @XmlElementWrapper(name = "tags") - private List tags; - - /** Field description */ - private String url; - - /** Field description */ - private String version; - - /** Field description */ - private String wiki; } diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginInformationComparator.java b/scm-core/src/main/java/sonia/scm/plugin/PluginInformationComparator.java deleted file mode 100644 index f44de35e8a..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginInformationComparator.java +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -import java.util.Comparator; - -/** - * - * @author Sebastian Sdorra - * @since 1.6 - */ -public class PluginInformationComparator - implements Comparator, Serializable -{ - - /** Field description */ - public static final PluginInformationComparator INSTANCE = - new PluginInformationComparator(); - - /** Field description */ - private static final long serialVersionUID = -8339752498853225668L; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param plugin - * @param other - * - * @return - */ - @Override - public int compare(PluginInformation plugin, PluginInformation other) - { - int result = 0; - - result = Util.compare(plugin.getGroupId(), other.getGroupId()); - - if (result == 0) - { - result = Util.compare(plugin.getArtifactId(), other.getArtifactId()); - - if (result == 0) - { - PluginState state = plugin.getState(); - PluginState otherState = other.getState(); - - if ((state != null) && (otherState != null)) - { - result = state.getCompareValue() - otherState.getCompareValue(); - } - else if ((state == null) && (otherState != null)) - { - result = 1; - } - else if ((state != null) && (otherState == null)) - { - result = -1; - } - } - } - - return result; - } -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java b/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java index a843bea773..e82d945024 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java @@ -33,6 +33,8 @@ package sonia.scm.plugin; +//~--- non-JDK imports -------------------------------------------------------- + //~--- JDK imports ------------------------------------------------------------ import java.util.Collection; @@ -66,7 +68,7 @@ public interface PluginLoader * * @return */ - public Collection getInstalledPlugins(); + public Collection getInstalledPlugins(); /** * Returns a {@link ClassLoader} which is able to load classes and resources diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java index b1ec502fc4..4c8fcd7306 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java @@ -33,113 +33,81 @@ package sonia.scm.plugin; -//~--- JDK imports ------------------------------------------------------------ - -import com.google.common.base.Predicate; -import java.io.IOException; -import java.io.InputStream; - -import java.util.Collection; +import java.util.List; +import java.util.Optional; /** + * The plugin manager is responsible for plugin related tasks, such as install, uninstall or updating. * * @author Sebastian Sdorra */ -public interface PluginManager -{ +public interface PluginManager { /** - * Method description - * + * Returns the available plugin with the given name. + * @param name of plugin + * @return optional available plugin. */ - public void clearCache(); + Optional getAvailable(String name); /** - * Method description - * - * - * @param id + * Returns the installed plugin with the given name. + * @param name of plugin + * @return optional installed plugin. */ - public void install(String id); + Optional getInstalled(String name); + /** - * Installs a plugin package from a inputstream. + * Returns all installed plugins. * - * - * @param packageStream package input stream - * - * @throws IOException - * @since 1.21 + * @return a list of installed plugins. */ - public void installPackage(InputStream packageStream) throws IOException; + List getInstalled(); /** - * Method description + * Returns all available plugins. The list contains the plugins which are loaded from the plugin center, but without + * the installed plugins. * - * - * @param id + * @return a list of available plugins. */ - public void uninstall(String id); + List getAvailable(); /** - * Method description + * Returns all updatable plugins. * - * - * @param id + * @return a list of updatable plugins. */ - public void update(String id); - - //~--- get methods ---------------------------------------------------------- + List getUpdatable(); /** - * Method description + * Installs the plugin with the given name from the list of available plugins. * - * - * @param id - * - * @return + * @param name plugin name + * @param restartAfterInstallation restart context after plugin installation */ - public PluginInformation get(String id); + void install(String name, boolean restartAfterInstallation); /** - * Method description + * Marks the plugin with the given name for uninstall. * - * - * @param filter - * - * @return + * @param name plugin name + * @param restartAfterInstallation restart context after plugin has been marked to really uninstall the plugin */ - public Collection get(Predicate filter); + void uninstall(String name, boolean restartAfterInstallation); /** - * Method description - * - * - * @return + * Install all pending plugins and restart the scm context. */ - public Collection getAll(); + void executePendingAndRestart(); /** - * Method description - * - * - * @return + * Cancel all pending plugins. */ - public Collection getAvailable(); + void cancelPending(); /** - * Method description - * - * - * @return + * Update all installed plugins. */ - public Collection getAvailableUpdates(); - - /** - * Method description - * - * - * @return - */ - public Collection getInstalled(); + void updateAll(); } diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginRepository.java b/scm-core/src/main/java/sonia/scm/plugin/PluginRepository.java deleted file mode 100644 index c75e555d9f..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginRepository.java +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; - -import java.io.Serializable; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -public class PluginRepository implements Serializable -{ - - /** Field description */ - private static final long serialVersionUID = -9504354306304731L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - PluginRepository() {} - - /** - * Constructs ... - * - * - * @param id - * @param url - */ - public PluginRepository(String id, String url) - { - this.id = id; - this.url = url; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param obj - * - * @return - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final PluginRepository other = (PluginRepository) obj; - - return Objects.equal(id, other.id) && Objects.equal(url, other.url); - } - - /** - * Method description - * - * - * @return - */ - @Override - public int hashCode() - { - return Objects.hashCode(id, url); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - return MoreObjects.toStringHelper(this) - .add("id", id) - .add("url", url) - .toString(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return id; - } - - /** - * Method description - * - * - * @return - */ - public String getUrl() - { - return url; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String id; - - /** Field description */ - private String url; -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginResources.java b/scm-core/src/main/java/sonia/scm/plugin/PluginResources.java index c6c8b3aead..07e35e0e4f 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginResources.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginResources.java @@ -125,9 +125,9 @@ public class PluginResources { //J- return MoreObjects.toStringHelper(this) - .add("scriptResources", scriptResources) - .add("stylesheetResources", stylesheetResources) - .toString(); + .add("scriptResources", scriptResources) + .add("stylesheetResources", stylesheetResources) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginState.java b/scm-core/src/main/java/sonia/scm/plugin/PluginState.java deleted file mode 100644 index 39803d3455..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginState.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -/** - * - * @author Sebastian Sdorra - */ -public enum PluginState -{ - CORE(100), AVAILABLE(60), INSTALLED(80), NEWER_VERSION_INSTALLED(20), - UPDATE_AVAILABLE(40); - - /** - * Constructs ... - * - * - * @param compareValue - */ - private PluginState(int compareValue) - { - this.compareValue = compareValue; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @since 1.6 - * @return - */ - public int getCompareValue() - { - return compareValue; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final int compareValue; -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginWrapper.java b/scm-core/src/main/java/sonia/scm/plugin/PluginWrapper.java deleted file mode 100644 index 46c3a4a980..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginWrapper.java +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. 2. Redistributions in - * binary form must reproduce the above copyright notice, this list of - * conditions and the following disclaimer in the documentation and/or other - * materials provided with the distribution. 3. Neither the name of SCM-Manager; - * nor the names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -//~--- JDK imports ------------------------------------------------------------ - -import java.nio.file.Path; - -/** - * Wrapper for a {@link Plugin}. The wrapper holds the directory, - * {@link ClassLoader} and {@link WebResourceLoader} of a plugin. - * - * @author Sebastian Sdorra - * @since 2.0.0 - */ -public final class PluginWrapper -{ - - /** - * Constructs a new plugin wrapper. - * - * @param plugin wrapped plugin - * @param classLoader plugin class loader - * @param webResourceLoader web resource loader - * @param directory plugin directory - */ - public PluginWrapper(Plugin plugin, ClassLoader classLoader, - WebResourceLoader webResourceLoader, Path directory) - { - this.plugin = plugin; - this.classLoader = classLoader; - this.webResourceLoader = webResourceLoader; - this.directory = directory; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns plugin class loader. - * - * - * @return plugin class loader - */ - public ClassLoader getClassLoader() - { - return classLoader; - } - - /** - * Returns plugin directory. - * - * - * @return plugin directory - */ - public Path getDirectory() - { - return directory; - } - - /** - * Returns the id of the plugin. - * - * - * @return id of plugin - */ - public String getId() - { - return plugin.getInformation().getId(); - } - - /** - * Returns the plugin. - * - * - * @return plugin - */ - public Plugin getPlugin() - { - return plugin; - } - - /** - * Returns the {@link WebResourceLoader} for this plugin. - * - * - * @return web resource loader - */ - public WebResourceLoader getWebResourceLoader() - { - return webResourceLoader; - } - - //~--- fields --------------------------------------------------------------- - - /** plugin class loader */ - private final ClassLoader classLoader; - - /** plugin directory */ - private final Path directory; - - /** plugin */ - private final Plugin plugin; - - /** plugin web resource loader */ - private final WebResourceLoader webResourceLoader; -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/Plugins.java b/scm-core/src/main/java/sonia/scm/plugin/Plugins.java index 6359850712..f33a254581 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/Plugins.java +++ b/scm-core/src/main/java/sonia/scm/plugin/Plugins.java @@ -65,7 +65,7 @@ public final class Plugins { try { - context = JAXBContext.newInstance(Plugin.class, ScmModule.class); + context = JAXBContext.newInstance(InstalledPluginDescriptor.class, ScmModule.class); } catch (JAXBException ex) { @@ -91,7 +91,7 @@ public final class Plugins * * @return */ - public static Plugin parsePluginDescriptor(Path path) + public static InstalledPluginDescriptor parsePluginDescriptor(Path path) { return parsePluginDescriptor(Files.asByteSource(path.toFile())); } @@ -104,15 +104,15 @@ public final class Plugins * * @return */ - public static Plugin parsePluginDescriptor(ByteSource data) + public static InstalledPluginDescriptor parsePluginDescriptor(ByteSource data) { Preconditions.checkNotNull(data, "data parameter is required"); - Plugin plugin; + InstalledPluginDescriptor plugin; try (InputStream stream = data.openStream()) { - plugin = (Plugin) context.createUnmarshaller().unmarshal(stream); + plugin = (InstalledPluginDescriptor) context.createUnmarshaller().unmarshal(stream); } catch (JAXBException ex) { diff --git a/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java b/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java index 63d5e8fb8f..e1ea622bdf 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java +++ b/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java @@ -206,7 +206,7 @@ public final class SmpArchive * * @throws IOException */ - public Plugin getPlugin() throws IOException + public InstalledPluginDescriptor getPlugin() throws IOException { if (plugin == null) { @@ -219,16 +219,10 @@ public final class SmpArchive throw new PluginException("could not find information section"); } - if (Strings.isNullOrEmpty(info.getGroupId())) + if (Strings.isNullOrEmpty(info.getName())) { throw new PluginException( - "could not find groupId in plugin descriptor"); - } - - if (Strings.isNullOrEmpty(info.getArtifactId())) - { - throw new PluginException( - "could not find artifactId in plugin descriptor"); + "could not find name in plugin descriptor"); } if (Strings.isNullOrEmpty(info.getVersion())) @@ -251,9 +245,9 @@ public final class SmpArchive * * @throws IOException */ - private Plugin createPlugin() throws IOException + private InstalledPluginDescriptor createPlugin() throws IOException { - Plugin p = null; + InstalledPluginDescriptor p = null; NonClosingZipInputStream zis = null; try @@ -418,5 +412,5 @@ public final class SmpArchive private final ByteSource archive; /** Field description */ - private Plugin plugin; + private InstalledPluginDescriptor plugin; } diff --git a/scm-core/src/main/java/sonia/scm/plugin/StatePluginPredicate.java b/scm-core/src/main/java/sonia/scm/plugin/StatePluginPredicate.java deleted file mode 100644 index ef7836f74a..0000000000 --- a/scm-core/src/main/java/sonia/scm/plugin/StatePluginPredicate.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.plugin; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Predicate; - -/** - * - * @author Sebastian Sdorra - */ -public class StatePluginPredicate implements Predicate -{ - - /** - * Constructs ... - * - * - * @param state - */ - public StatePluginPredicate(PluginState state) - { - this.state = state; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param plugin - * - * @return - */ - @Override - public boolean apply(PluginInformation plugin) - { - return state == plugin.getState(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final PluginState state; -} diff --git a/scm-core/src/main/java/sonia/scm/plugin/SubscriberElement.java b/scm-core/src/main/java/sonia/scm/plugin/SubscriberElement.java index 75c83003ce..5cc98d0bf9 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/SubscriberElement.java +++ b/scm-core/src/main/java/sonia/scm/plugin/SubscriberElement.java @@ -36,13 +36,13 @@ package sonia.scm.plugin; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -//~--- JDK imports ------------------------------------------------------------ - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -128,10 +128,10 @@ public final class SubscriberElement { //J- return MoreObjects.toStringHelper(this) - .add("eventClass", eventClass) - .add("subscriberClass", subscriberClass) - .add("description", description) - .toString(); + .add("eventClass", eventClass) + .add("subscriberClass", subscriberClass) + .add("description", description) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java b/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java index 94b31ac844..454003c922 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java +++ b/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java @@ -33,9 +33,8 @@ package sonia.scm.plugin; //~--- JDK imports ------------------------------------------------------------ -import java.net.URL; - import javax.servlet.ServletContext; +import java.net.URL; /** * The WebResourceLoader is able to load web resources. The resources are loaded @@ -53,9 +52,11 @@ public interface WebResourceLoader * Returns a {@link URL} for the given path. The method will return null if no * resources could be found for the given path. * + * Note: The path is a web path and uses "/" as path separator + * * @param path resource path * * @return url object for the given path or null */ - public URL getResource(String path); + URL getResource(String path); } diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/CommandContext.java b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandContext.java new file mode 100644 index 0000000000..44a1cce95a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandContext.java @@ -0,0 +1,20 @@ +package sonia.scm.protocolcommand; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.io.InputStream; +import java.io.OutputStream; + +@Getter +@AllArgsConstructor +public class CommandContext { + + private String command; + private String[] args; + + private InputStream inputStream; + private OutputStream outputStream; + private OutputStream errorStream; + +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreter.java b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreter.java new file mode 100644 index 0000000000..24bf7d30fa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreter.java @@ -0,0 +1,10 @@ +package sonia.scm.protocolcommand; + +public interface CommandInterpreter { + + String[] getParsedArgs(); + + ScmCommandProtocol getProtocolHandler(); + + RepositoryContextResolver getRepositoryContextResolver(); +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreterFactory.java b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreterFactory.java new file mode 100644 index 0000000000..9d6bfa1d7f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreterFactory.java @@ -0,0 +1,10 @@ +package sonia.scm.protocolcommand; + +import sonia.scm.plugin.ExtensionPoint; + +import java.util.Optional; + +@ExtensionPoint +public interface CommandInterpreterFactory { + Optional canHandle(String command); +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContext.java b/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContext.java new file mode 100644 index 0000000000..a64d5a6047 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContext.java @@ -0,0 +1,23 @@ +package sonia.scm.protocolcommand; + +import sonia.scm.repository.Repository; + +import java.nio.file.Path; + +public class RepositoryContext { + private Repository repository; + private Path directory; + + public RepositoryContext(Repository repository, Path directory) { + this.repository = repository; + this.directory = directory; + } + + public Repository getRepository() { + return repository; + } + + public Path getDirectory() { + return directory; + } +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContextResolver.java b/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContextResolver.java new file mode 100644 index 0000000000..2c48ece185 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContextResolver.java @@ -0,0 +1,8 @@ +package sonia.scm.protocolcommand; + +@FunctionalInterface +public interface RepositoryContextResolver { + + RepositoryContext resolve(String[] args); + +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/ScmCommandProtocol.java b/scm-core/src/main/java/sonia/scm/protocolcommand/ScmCommandProtocol.java new file mode 100644 index 0000000000..e7dbf65cad --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/ScmCommandProtocol.java @@ -0,0 +1,9 @@ +package sonia.scm.protocolcommand; + +import java.io.IOException; + +public interface ScmCommandProtocol { + + void handle(CommandContext context, RepositoryContext repositoryContext) throws IOException; + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstactImportHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstactImportHandler.java index 958280a39b..63d4638ab8 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstactImportHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstactImportHandler.java @@ -35,20 +35,16 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Throwables; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.ImportResult.Builder; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; - import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * Abstract base class for directory based {@link ImportHandler} and * {@link AdvancedImportHandler}. @@ -89,9 +85,7 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler * {@inheritDoc} */ @Override - public List importRepositories(RepositoryManager manager) - throws IOException, RepositoryException - { + public List importRepositories(RepositoryManager manager) throws IOException { return doRepositoryImport(manager, true).getImportedDirectories(); } @@ -101,16 +95,7 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler @Override public ImportResult importRepositoriesFromDirectory(RepositoryManager manager) { - try - { - return doRepositoryImport(manager, false); - } - catch (IOException | RepositoryException ex) - { - - // should never happen - throw Throwables.propagate(ex); - } + return doRepositoryImport(manager, false); } /** @@ -123,16 +108,11 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler * @return repository * * @throws IOException - * @throws RepositoryException */ - protected Repository createRepository(File repositoryDirectory, - String repositoryName) - throws IOException, RepositoryException - { + protected Repository createRepository(File repositoryDirectory, String repositoryName) throws IOException { Repository repository = new Repository(); repository.setName(repositoryName); - repository.setPublicReadable(false); repository.setType(getTypeName()); return repository; @@ -148,33 +128,30 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler * @return import result * * @throws IOException - * @throws RepositoryException */ - private ImportResult doRepositoryImport(RepositoryManager manager, - boolean throwExceptions) - throws IOException, RepositoryException - { + private ImportResult doRepositoryImport(RepositoryManager manager, boolean throwExceptions) { Builder builder = ImportResult.builder(); logger.trace("search for repositories to import"); - try - { - - List repositoryNames = - RepositoryUtil.getRepositoryNames(getRepositoryHandler(), - getDirectoryNames()); - - for (String repositoryName : repositoryNames) - { - importRepository(manager, builder, throwExceptions, repositoryName); - } - - } - catch (IOException ex) - { - handleException(ex, throwExceptions); - } + // TODO #8783 +// try +// { +// +// List repositoryNames = +// RepositoryUtil.getRepositoryNames(getRepositoryHandler(), +// getDirectoryNames()); +// +// for (String repositoryName : repositoryNames) +// { +// importRepository(manager, builder, throwExceptions, repositoryName); +// } +// +// } +// catch (IOException ex) +// { +// handleException(ex, throwExceptions); +// } return builder.build(); } @@ -208,93 +185,53 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler * @param manager * @param builder * @param throwExceptions - * @param repositoryName + * @param directoryName * * @throws IOException - * @throws RepositoryException */ private void importRepository(RepositoryManager manager, Builder builder, - boolean throwExceptions, String repositoryName) - throws IOException, RepositoryException + boolean throwExceptions, String directoryName) + throws IOException { - logger.trace("check repository {} for import", repositoryName); + logger.trace("check repository {} for import", directoryName); - Repository repository = manager.get(getTypeName(), repositoryName); - - if (repository == null) - { - try - { - importRepository(manager, repositoryName); - builder.addImportedDirectory(repositoryName); - } - catch (IOException ex) - { - builder.addFailedDirectory(repositoryName); - handleException(ex, throwExceptions); - } - catch (IllegalStateException ex) - { - builder.addFailedDirectory(repositoryName); - handleException(ex, throwExceptions); - } - catch (RepositoryException ex) - { - builder.addFailedDirectory(repositoryName); - handleException(ex, throwExceptions); - } - } - else if (logger.isDebugEnabled()) - { - logger.debug("repository {} is allready managed", repositoryName); - } + // TODO #8783 +// +// Repository repository = manager.get(namespaceAndName); +// +// if (repository == null) +// { +// try +// { +// importRepository(manager, repositoryName); +// builder.addImportedDirectory(repositoryName); +// } +// catch (IOException ex) +// { +// builder.addFailedDirectory(repositoryName); +// handleException(ex, throwExceptions); +// } +// catch (IllegalStateException ex) +// { +// builder.addFailedDirectory(repositoryName); +// handleException(ex, throwExceptions); +// } +// catch (RepositoryException ex) +// { +// builder.addFailedDirectory(repositoryName); +// handleException(ex, throwExceptions); +// } +// } +// else if (logger.isDebugEnabled()) +// { +// logger.debug("repository {} is already managed", repositoryName); +// } } - /** - * Method description - * - * - * @param manager - * @param repositoryName - * - * - * @return - * @throws IOException - * @throws RepositoryException - */ - private void importRepository(RepositoryManager manager, - String repositoryName) - throws IOException, RepositoryException - { - Repository repository = - createRepository(getRepositoryDirectory(repositoryName), repositoryName); - if (logger.isInfoEnabled()) - { - logger.info("import repository {} of type {}", repositoryName, - getTypeName()); - } - - manager.importRepository(repository); - } //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param repositoryName - * - * @return - */ - private File getRepositoryDirectory(String repositoryName) - { - return new File( - getRepositoryHandler().getConfig().getRepositoryDirectory(), - repositoryName); - } - /** * Method description * diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java index 5ad5d0f9ca..1e9cc3d374 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java @@ -38,7 +38,7 @@ package sonia.scm.repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.NotSupportedFeatuerException; +import sonia.scm.FeatureNotSupportedException; import sonia.scm.SCMContextProvider; import sonia.scm.event.ScmEventBus; @@ -56,7 +56,7 @@ import sonia.scm.store.ConfigurationStoreFactory; * * @param */ -public abstract class AbstractRepositoryHandler +public abstract class AbstractRepositoryHandler implements RepositoryHandler { @@ -72,9 +72,11 @@ public abstract class AbstractRepositoryHandler(config)); + new RepositoryHandlerConfigChangedEvent(config)); } //~--- fields --------------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryRoleManager.java b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryRoleManager.java new file mode 100644 index 0000000000..1db0065e8b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryRoleManager.java @@ -0,0 +1,78 @@ +/** + * 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; + +import sonia.scm.HandlerEventType; +import sonia.scm.event.ScmEventBus; + +/** + * Abstract base class for {@link RepositoryRoleManager} implementations. This class + * implements the listener methods of the {@link RepositoryRoleManager} interface. + */ +public abstract class AbstractRepositoryRoleManager implements RepositoryRoleManager { + + /** + * Send a {@link RepositoryRoleEvent} to the {@link ScmEventBus}. + * + * @param event type of change event + * @param repositoryRole repositoryRole that has changed + * @param oldRepositoryRole old repositoryRole + */ + protected void fireEvent(HandlerEventType event, RepositoryRole repositoryRole, RepositoryRole oldRepositoryRole) + { + fireEvent(new RepositoryRoleModificationEvent(event, repositoryRole, oldRepositoryRole)); + } + + /** + * Creates a new {@link RepositoryRoleEvent} and calls {@link #fireEvent(RepositoryRoleEvent)}. + * + * @param repositoryRole repositoryRole that has changed + * @param event type of change event + */ + protected void fireEvent(HandlerEventType event, RepositoryRole repositoryRole) + { + fireEvent(new RepositoryRoleEvent(event, repositoryRole)); + } + + /** + * Send a {@link RepositoryRoleEvent} to the {@link ScmEventBus}. + * + * @param event repositoryRole event + * @since 1.48 + */ + protected void fireEvent(RepositoryRoleEvent event) + { + ScmEventBus.getInstance().post(event); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index 670cab93e1..62dfd476af 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,466 +24,152 @@ * 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.base.Charsets; -import com.google.common.base.Throwables; import com.google.common.io.Resources; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.ConfigurationException; import sonia.scm.io.CommandResult; import sonia.scm.io.ExtendedCommand; -import sonia.scm.io.FileSystem; -import sonia.scm.util.IOUtil; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.plugin.PluginLoader; +import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; import java.io.IOException; - import java.net.URL; -import sonia.scm.store.ConfigurationStoreFactory; +import java.nio.file.Path; + +//~--- JDK imports ------------------------------------------------------------ /** - * - * @author Sebastian Sdorra - * - * * @param + * @author Sebastian Sdorra */ -public abstract class AbstractSimpleRepositoryHandler - extends AbstractRepositoryHandler implements RepositoryDirectoryHandler -{ +public abstract class AbstractSimpleRepositoryHandler + extends AbstractRepositoryHandler implements RepositoryDirectoryHandler { - /** Field description */ public static final String DEFAULT_VERSION_INFORMATION = "unknown"; - /** Field description */ - public static final String DIRECTORY_REPOSITORY = "repositories"; - - /** Field description */ - public static final String DOT = "."; - - /** the logger for AbstractSimpleRepositoryHandler */ + /** + * the logger for AbstractSimpleRepositoryHandler + */ private static final Logger logger = LoggerFactory.getLogger(AbstractSimpleRepositoryHandler.class); - //~--- constructors --------------------------------------------------------- + private final RepositoryLocationResolver repositoryLocationResolver; + private final PluginLoader pluginLoader; - /** - * Constructs ... - * - * - * @param storeFactory - * @param fileSystem - */ public AbstractSimpleRepositoryHandler(ConfigurationStoreFactory storeFactory, - FileSystem fileSystem) - { + RepositoryLocationResolver repositoryLocationResolver, + PluginLoader pluginLoader) { super(storeFactory); - this.fileSystem = fileSystem; + this.repositoryLocationResolver = repositoryLocationResolver; + this.pluginLoader = pluginLoader; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void create(Repository repository) - throws RepositoryException, IOException - { - File directory = getDirectory(repository); - - if (directory.exists()) - { - throw RepositoryAlreadyExistsException.create(repository); - } - - checkPath(directory); - - try - { - fileSystem.create(directory); - create(repository, directory); - postCreate(repository, directory); - } - catch (Exception ex) - { - if (directory.exists()) - { - if (logger.isDebugEnabled()) - { - logger.debug( - "delete repository directory {}, because of failed repository creation", - directory); - } - - fileSystem.destroy(directory); - } - - Throwables.propagateIfPossible(ex, RepositoryException.class, - IOException.class); + public Repository create(Repository repository) { + File nativeDirectory = resolveNativeDirectory(repository.getId()); + try { + create(repository, nativeDirectory); + postCreate(repository, nativeDirectory); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "could not create native repository directory", e); } + return repository; } - /** - * Method description - * - * - * - * @param repository - * @return - */ @Override - public String createResourcePath(Repository repository) - { - StringBuilder path = new StringBuilder("/"); - - path.append(getType().getName()).append("/").append(repository.getName()); - - return path.toString(); + public void delete(Repository repository) { } - /** - * Method description - * - * - * @param repository - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void delete(Repository repository) - throws RepositoryException, IOException - { - File directory = getDirectory(repository); - - if (directory.exists()) - { - fileSystem.destroy(directory); - cleanupEmptyDirectories(config.getRepositoryDirectory(), - directory.getParentFile()); - } - else if (logger.isWarnEnabled()) - { - logger.warn("repository {} not found", repository); - } - } - - /** - * Method description - * - */ - @Override - public void loadConfig() - { + public void loadConfig() { super.loadConfig(); - if (config == null) - { + if (config == null) { config = createInitialConfig(); - - if (config != null) - { - File repositoryDirectory = config.getRepositoryDirectory(); - - if (repositoryDirectory == null) - { - repositoryDirectory = new File( - baseDirectory, - DIRECTORY_REPOSITORY.concat(File.separator).concat( - getType().getName())); - config.setRepositoryDirectory(repositoryDirectory); - } - - IOUtil.mkdirs(repositoryDirectory); - storeConfig(); - } } } - /** - * Method description - * - * - * @param repository - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void modify(Repository repository) - throws RepositoryException, IOException - { + public void modify(Repository repository) { - // nothing todo + // nothing to do } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * - * @return - */ @Override - public File getDirectory(Repository repository) - { - File directory = null; - - if (isConfigured()) - { - File repositoryDirectory = config.getRepositoryDirectory(); - - directory = new File(repositoryDirectory, repository.getName()); - - if (!IOUtil.isChild(repositoryDirectory, directory)) - { - StringBuilder msg = new StringBuilder(directory.getPath()); - - msg.append("is not a child of ").append(repositoryDirectory.getPath()); - - throw new ConfigurationException(msg.toString()); - } - } - else - { + public File getDirectory(String repositoryId) { + File directory; + if (isConfigured()) { + directory = resolveNativeDirectory(repositoryId); + } else { throw new ConfigurationException("RepositoryHandler is not configured"); } - return directory; } - /** - * Method description - * - * - * @return - */ @Override - public String getVersionInformation() - { + public String getVersionInformation() { return DEFAULT_VERSION_INFORMATION; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * @param directory - * - * @return - */ protected ExtendedCommand buildCreateCommand(Repository repository, - File directory) - { + File directory) { throw new UnsupportedOperationException("method is not implemented"); } - /** - * Method description - * - * - * @param repository - * @param directory - * - * @throws IOException - * @throws RepositoryException - */ protected void create(Repository repository, File directory) - throws RepositoryException, IOException - { + throws IOException { ExtendedCommand cmd = buildCreateCommand(repository, directory); CommandResult result = cmd.execute(); - if (!result.isSuccessfull()) - { - StringBuilder msg = new StringBuilder("command exit with error "); - - msg.append(result.getReturnCode()).append(" and message: '"); - msg.append(result.getOutput()).append("'"); - - throw new RepositoryException(msg.toString()); + if (!result.isSuccessfull()) { + throw new IOException(("command exit with error " + result.getReturnCode() + " and message: '" + result.getOutput() + "'")); } } - /** - * Method description - * - * - * @return - */ - protected C createInitialConfig() - { + protected C createInitialConfig() { return null; } - /** - * Method description - * - * - * @param repository - * @param directory - * - * @throws IOException - * @throws RepositoryException - */ protected void postCreate(Repository repository, File directory) - throws IOException, RepositoryException {} - - //~--- get methods ---------------------------------------------------------- + throws IOException { + } /** * Returns the content of a classpath resource or the given default content. * - * - * @param resource path of a classpath resource + * @param resource path of a classpath resource * @param defaultContent default content to return - * * @return content of a classpath resource or defaultContent */ - protected String getStringFromResource(String resource, String defaultContent) - { + protected String getStringFromResource(String resource, String defaultContent) { String content = defaultContent; - try - { - URL url = Resources.getResource(resource); + try { + URL url = pluginLoader.getUberClassLoader().getResource(resource); - if (url != null) - { + if (url != null) { content = Resources.toString(url, Charsets.UTF_8); } - } - catch (IOException ex) - { + } catch (IOException ex) { logger.error("could not read resource", ex); } return content; } - /** - * Returns true if the directory is a repository. - * - * - * @param directory directory to check - * - * @return true if the directory is a repository - * @since 1.9 - */ - protected boolean isRepository(File directory) - { - return new File(directory, DOT.concat(getType().getName())).exists(); + private File resolveNativeDirectory(String repositoryId) { + return repositoryLocationResolver.create(Path.class).getLocation(repositoryId).resolve(REPOSITORIES_NATIVE_DIRECTORY).toFile(); } - - //~--- methods -------------------------------------------------------------- - - /** - * Check path for existing repositories - * - * - * @param directory repository target directory - * - * @throws RepositoryAlreadyExistsException - */ - private void checkPath(File directory) throws RepositoryAlreadyExistsException - { - File repositoryDirectory = config.getRepositoryDirectory(); - File parent = directory.getParentFile(); - - while ((parent != null) &&!repositoryDirectory.equals(parent)) - { - if (logger.isTraceEnabled()) - { - logger.trace("check {} for existing repository", parent); - } - - if (isRepository(parent)) - { - if (logger.isErrorEnabled()) - { - logger.error("parent path {} is a repository", parent); - } - - StringBuilder buffer = new StringBuilder("repository with name "); - buffer.append(directory.getName()).append(" already exists"); - throw new RepositoryAlreadyExistsException(buffer.toString()); - } - - parent = parent.getParentFile(); - } - } - - /** - * Method description - * - * - * @param baseDirectory - * @param directory - */ - private void cleanupEmptyDirectories(File baseDirectory, File directory) - { - if (IOUtil.isChild(baseDirectory, directory)) - { - if (IOUtil.isEmpty(directory)) - { - - // TODO use filesystem - if (directory.delete()) - { - if (logger.isInfoEnabled()) - { - logger.info("successfully deleted directory {}", directory); - } - - cleanupEmptyDirectories(baseDirectory, directory.getParentFile()); - } - else if (logger.isWarnEnabled()) - { - logger.warn("could not delete directory {}", directory); - } - } - else if (logger.isDebugEnabled()) - { - logger.debug("could not remove non empty directory {}", directory); - } - } - else if (logger.isWarnEnabled()) - { - logger.warn("directory {} is not a child of {}", directory, - baseDirectory); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private FileSystem fileSystem; } diff --git a/scm-core/src/main/java/sonia/scm/repository/BasicRepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/BasicRepositoryLocationResolver.java new file mode 100644 index 0000000000..c69b9a4a61 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/BasicRepositoryLocationResolver.java @@ -0,0 +1,15 @@ +package sonia.scm.repository; + +public abstract class BasicRepositoryLocationResolver extends RepositoryLocationResolver { + + private final Class type; + + protected BasicRepositoryLocationResolver(Class type) { + this.type = type; + } + + @Override + public boolean supportsLocationType(Class type) { + return type.isAssignableFrom(this.type); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/BlameLine.java b/scm-core/src/main/java/sonia/scm/repository/BlameLine.java index 771450874c..08bd2dad58 100644 --- a/scm-core/src/main/java/sonia/scm/repository/BlameLine.java +++ b/scm-core/src/main/java/sonia/scm/repository/BlameLine.java @@ -38,10 +38,10 @@ package sonia.scm.repository; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Serializable; +//~--- JDK imports ------------------------------------------------------------ + /** * Single line of a file, in a {@link BlameResult}. * @@ -142,13 +142,13 @@ public class BlameLine implements Serializable { //J- return MoreObjects.toStringHelper(this) - .add("lineNumber", lineNumber) - .add("revision", revision) - .add("author", author) - .add("when", when) - .add("code", code) - .add("description", description) - .toString(); + .add("lineNumber", lineNumber) + .add("revision", revision) + .add("author", author) + .add("when", when) + .add("code", code) + .add("description", description) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/BlameResult.java b/scm-core/src/main/java/sonia/scm/repository/BlameResult.java index c282aa92d1..58fafe9811 100644 --- a/scm-core/src/main/java/sonia/scm/repository/BlameResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/BlameResult.java @@ -39,7 +39,11 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.Lists; -import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; import java.util.Iterator; import java.util.List; @@ -159,9 +163,9 @@ public class BlameResult implements Serializable, Iterable { //J- return MoreObjects.toStringHelper(this) - .add("total", total) - .add("blameLines", blameLines) - .toString(); + .add("total", total) + .add("blameLines", blameLines) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/Branch.java b/scm-core/src/main/java/sonia/scm/repository/Branch.java index ca64085077..c0a7289912 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Branch.java +++ b/scm-core/src/main/java/sonia/scm/repository/Branch.java @@ -66,7 +66,7 @@ public final class Branch implements Serializable * This constructor should only be called from JAXB. * */ - public Branch() {} + Branch() {} /** * Constructs a new branch. @@ -75,10 +75,19 @@ public final class Branch implements Serializable * @param name name of the branch * @param revision latest revision of the branch */ - public Branch(String name, String revision) + Branch(String name, String revision, boolean defaultBranch) { this.name = name; this.revision = revision; + this.defaultBranch = defaultBranch; + } + + public static Branch normalBranch(String name, String revision) { + return new Branch(name, revision, false); + } + + public static Branch defaultBranch(String name, String revision) { + return new Branch(name, revision, true); } //~--- methods -------------------------------------------------------------- @@ -107,7 +116,8 @@ public final class Branch implements Serializable final Branch other = (Branch) obj; return Objects.equal(name, other.name) - && Objects.equal(revision, other.revision); + && Objects.equal(revision, other.revision) + && Objects.equal(defaultBranch, other.defaultBranch); } /** @@ -133,9 +143,9 @@ public final class Branch implements Serializable { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("revision", revision) - .toString(); + .add("name", name) + .add("revision", revision) + .toString(); //J+ } @@ -162,6 +172,10 @@ public final class Branch implements Serializable return revision; } + public boolean isDefaultBranch() { + return defaultBranch; + } + //~--- fields --------------------------------------------------------------- /** name of the branch */ @@ -169,4 +183,6 @@ public final class Branch implements Serializable /** Field description */ private String revision; + + private boolean defaultBranch; } diff --git a/scm-core/src/main/java/sonia/scm/repository/Branches.java b/scm-core/src/main/java/sonia/scm/repository/Branches.java index c0dc88943c..1eb679908e 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Branches.java +++ b/scm-core/src/main/java/sonia/scm/repository/Branches.java @@ -150,8 +150,8 @@ public final class Branches implements Iterable { //J- return MoreObjects.toStringHelper(this) - .add("branches", branches) - .toString(); + .add("branches", branches) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/BrowserResult.java b/scm-core/src/main/java/sonia/scm/repository/BrowserResult.java index 7b7944b0d1..44c6b963ad 100644 --- a/scm-core/src/main/java/sonia/scm/repository/BrowserResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/BrowserResult.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,13 +24,11 @@ * 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 -------------------------------------------------------- @@ -38,10 +36,10 @@ package sonia.scm.repository; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; -import java.util.Iterator; -import java.util.List; //~--- JDK imports ------------------------------------------------------------ @@ -52,224 +50,66 @@ import java.util.List; */ @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "browser-result") -public class BrowserResult implements Iterable, Serializable -{ +public class BrowserResult implements Serializable { - /** Field description */ - private static final long serialVersionUID = 2818662048045182761L; + private String revision; + private String requestedRevision; + private FileObject file; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public BrowserResult() {} - - /** - * Constructs ... - * - * - * @param revision - * @param tag - * @param branch - * @param files - */ - public BrowserResult(String revision, String tag, String branch, - List files) - { - this.revision = revision; - this.tag = tag; - this.branch = branch; - this.files = files; + public BrowserResult() { } - //~--- methods -------------------------------------------------------------- + public BrowserResult(String revision, FileObject file) { + this(revision, revision, file); + } + + public BrowserResult(String revision, String requestedRevision, FileObject file) { + this.revision = revision; + this.requestedRevision = requestedRevision; + this.file = file; + } + + public String getRevision() { + return revision; + } + + public String getRequestedRevision() { + return requestedRevision; + } + + public FileObject getFile() { + return file; + } - /** - * {@inheritDoc} - * - * - * @param obj - * - * @return - */ @Override - public boolean equals(Object obj) - { - if (obj == null) - { + public boolean equals(Object obj) { + if (obj == null) { return false; } - if (getClass() != obj.getClass()) - { + if (getClass() != obj.getClass()) { return false; } final BrowserResult other = (BrowserResult) obj; return Objects.equal(revision, other.revision) - && Objects.equal(tag, other.tag) - && Objects.equal(branch, other.branch) - && Objects.equal(files, other.files); + && Objects.equal(file, other.file); } - /** - * {@inheritDoc} - * - * - * @return - */ @Override - public int hashCode() - { - return Objects.hashCode(revision, tag, branch, files); + public int hashCode() { + return Objects.hashCode(revision, file); } - /** - * Method description - * - * - * @return - */ + @Override - public Iterator iterator() - { - Iterator it = null; - - if (files != null) - { - it = files.iterator(); - } - - return it; - } - - /** - * {@inheritDoc} - * - * - * @return - */ - @Override - public String toString() - { - //J- + public String toString() { return MoreObjects.toStringHelper(this) - .add("revision", revision) - .add("tag", tag) - .add("branch", branch) - .add("files", files) - .toString(); - //J+ + .add("revision", revision) + .add("files", file) + .toString(); } - //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - */ - public String getBranch() - { - return branch; - } - - /** - * Method description - * - * - * @return - */ - public List getFiles() - { - return files; - } - - /** - * Method description - * - * - * @return - */ - public String getRevision() - { - return revision; - } - - /** - * Method description - * - * - * @return - */ - public String getTag() - { - return tag; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param branch - */ - public void setBranch(String branch) - { - this.branch = branch; - } - - /** - * Method description - * - * - * @param files - */ - public void setFiles(List files) - { - this.files = files; - } - - /** - * Method description - * - * - * @param revision - */ - public void setRevision(String revision) - { - this.revision = revision; - } - - /** - * Method description - * - * - * @param tag - */ - public void setTag(String tag) - { - this.tag = tag; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String branch; - - /** Field description */ - @XmlElement(name = "file") - @XmlElementWrapper(name = "files") - private List files; - - /** Field description */ - private String revision; - - /** Field description */ - private String tag; } diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java index ce3fb5424b..7397fecabe 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java +++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java @@ -33,27 +33,19 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Objects; - import sonia.scm.BasicPropertiesAware; -import sonia.scm.Validateable; +import sonia.scm.ModelObject; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; import java.util.Date; import java.util.List; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; /** * Represents a changeset/commit of a repository. @@ -62,43 +54,52 @@ import javax.xml.bind.annotation.XmlRootElement; */ @XmlRootElement(name = "changeset") @XmlAccessorType(XmlAccessType.FIELD) -public class Changeset extends BasicPropertiesAware - implements Validateable, Serializable -{ +public class Changeset extends BasicPropertiesAware implements ModelObject { - /** Field description */ private static final long serialVersionUID = -8373308448928993039L; - //~--- constructors --------------------------------------------------------- + /** + * The author of the changeset + */ + private Person author; /** - * Constructs a new instance of changeset. - * + * The name of the branches on which the changeset was committed. */ + private List branches; + + /** + * The date when the changeset was committed + */ + private Long date; + + /** + * The text of the changeset description + */ + private String description; + + /** + * The changeset identification string + */ + private String id; + + /** + * parent changeset ids + */ + private List parents; + + /** + * The tags associated with the changeset + */ + private List tags; + public Changeset() {} - /** - * Constructs a new instance of changeset. - * - * - * @param id id of the changeset - * @param date date of the changeset - * @param author author of the changeset - */ public Changeset(String id, Long date, Person author) { this(id, date, author, null); } - /** - * Constructs a new instance of changeset. - * - * - * @param id id of the changeset - * @param date date of the changeset - * @param author author of the changeset - * @param description description of the changeset - */ public Changeset(String id, Long date, Person author, String description) { this.id = id; @@ -107,16 +108,6 @@ public class Changeset extends BasicPropertiesAware this.description = description; } - //~--- methods -------------------------------------------------------------- - - /** - * {@inheritDoc} - * - * - * @param obj - * - * @return - */ @Override public boolean equals(Object obj) { @@ -125,22 +116,20 @@ public class Changeset extends BasicPropertiesAware return false; } - if (getClass() != obj.getClass()) - { + if (getClass() != obj.getClass()) { return false; } final Changeset other = (Changeset) obj; //J- - return Objects.equal(id, other.id) + return Objects.equal(id, other.id) && Objects.equal(date, other.date) && Objects.equal(author, other.author) && Objects.equal(description, other.description) && Objects.equal(parents, other.parents) && Objects.equal(tags, other.tags) && Objects.equal(branches, other.branches) - && Objects.equal(modifications, other.modifications) && Objects.equal(properties, other.properties); //J+ } @@ -155,7 +144,7 @@ public class Changeset extends BasicPropertiesAware public int hashCode() { return Objects.hashCode(id, date, author, description, parents, tags, - branches, modifications, properties); + branches, properties); } /** @@ -187,15 +176,24 @@ public class Changeset extends BasicPropertiesAware out.append("branches: ").append(Util.toString(branches)).append("\n"); out.append("tags: ").append(Util.toString(tags)).append("\n"); - if (modifications != null) - { - out.append("modifications: \n").append(modifications); - } - return out.toString(); } - //~--- get methods ---------------------------------------------------------- + + /** + * Returns a timestamp of the creation date of the {@link Changeset}. + * + * @return a timestamp of the creation date of the {@link Changeset} + */ + public Long getCreationDate() { + return getDate(); + } + + @Override + public void setCreationDate(Long timestamp) { + this.setDate(timestamp); + } + /** * Returns the author of the changeset. @@ -203,14 +201,13 @@ public class Changeset extends BasicPropertiesAware * * @return author of the changeset */ - public Person getAuthor() - { + public Person getAuthor() { return author; } /** - * Returns the branches of the changeset. In the most cases a changeset is - * only related to one branch, but in the case of receive hooks it is possible + * Returns the branches of the changeset. In the most cases a changeset is + * only related to one branch, but in the case of receive hooks it is possible * that a changeset is related to more than a branch. * * @@ -220,7 +217,7 @@ public class Changeset extends BasicPropertiesAware { if (branches == null) { - branches = new ArrayList<>(); + branches = new ArrayList(); } return branches; @@ -254,27 +251,28 @@ public class Changeset extends BasicPropertiesAware * * @return id of the changeset */ - public String getId() - { + @Override + public String getId() { return id; } - /** - * Returns the file modifications, which was done with this changeset. - * - * - * @return file modifications - */ - public Modifications getModifications() - { - if (modifications == null) - { - modifications = new Modifications(); - } - - return modifications; + @Override + public void setLastModified(Long timestamp) { + throw new UnsupportedOperationException("changesets are immutable"); } + @Override + public Long getLastModified() { + return null; + } + + @Override + public String getType() { + return "Changeset"; + } + + + /** * Return the ids of the parent changesets. * @@ -286,7 +284,7 @@ public class Changeset extends BasicPropertiesAware { if (parents == null) { - parents = new ArrayList<>(); + parents = new ArrayList(); } return parents; @@ -302,7 +300,7 @@ public class Changeset extends BasicPropertiesAware { if (tags == null) { - tags = new ArrayList<>(); + tags = new ArrayList(); } return tags; @@ -321,8 +319,6 @@ public class Changeset extends BasicPropertiesAware && (date != null); } - //~--- set methods ---------------------------------------------------------- - /** * Sets the author of the changeset. * @@ -378,17 +374,6 @@ public class Changeset extends BasicPropertiesAware 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. * @@ -412,30 +397,4 @@ public class Changeset extends BasicPropertiesAware this.tags = tags; } - //~--- fields --------------------------------------------------------------- - - /** The author of the changeset */ - private Person author; - - /** The name of the branches on which the changeset was committed. */ - private List branches; - - /** The date when the changeset was committed */ - private Long date; - - /** The text of the changeset description */ - private String description; - - /** The changeset identification string */ - private String id; - - /** List of files changed by this changeset */ - @XmlElement(name = "modifications") - private Modifications modifications; - - /** parent changeset ids */ - private List parents; - - /** The tags associated with the changeset */ - private List tags; } diff --git a/scm-core/src/main/java/sonia/scm/repository/ChangesetPagingResult.java b/scm-core/src/main/java/sonia/scm/repository/ChangesetPagingResult.java index df261fbd46..ca1018b7aa 100644 --- a/scm-core/src/main/java/sonia/scm/repository/ChangesetPagingResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/ChangesetPagingResult.java @@ -38,7 +38,11 @@ package sonia.scm.repository; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; import java.util.Iterator; import java.util.List; @@ -78,6 +82,22 @@ public class ChangesetPagingResult implements Iterable, Serializable { this.total = total; this.changesets = changesets; + this.branchName = null; + } + + /** + * Constructs a new changeset paging result for a specific branch. + * + * + * @param total total number of changesets + * @param changesets current list of fetched changesets + * @param branchName branch name this result was created for + */ + public ChangesetPagingResult(int total, List changesets, String branchName) + { + this.total = total; + this.changesets = changesets; + this.branchName = branchName; } //~--- methods -------------------------------------------------------------- @@ -152,9 +172,10 @@ public class ChangesetPagingResult implements Iterable, Serializable { //J- return MoreObjects.toStringHelper(this) - .add("changesets", changesets) - .add("total", total) - .toString(); + .add("changesets", changesets) + .add("total", total) + .add("branch", branchName) + .toString(); //J+ } @@ -182,37 +203,35 @@ public class ChangesetPagingResult implements Iterable, Serializable return total; } - //~--- set methods ---------------------------------------------------------- - - /** - * Sets the current list of changesets. - * - * - * @param changesets current list of changesets - */ - public void setChangesets(List changesets) + void setChangesets(List changesets) { this.changesets = changesets; } - /** - * Sets the total number of changesets - * - * - * @param total total number of changesets - */ - public void setTotal(int total) + void setTotal(int total) { this.total = total; } + void setBranchName(String branchName) { + this.branchName = branchName; + } + + /** + * Returns the branch name this result was created for. This can either be an explicit branch ("give me all + * changesets for branch xyz") or an implicit one ("give me the changesets for the default"). + */ + public String getBranchName() { + return branchName; + } + //~--- fields --------------------------------------------------------------- - /** current list of changesets */ @XmlElement(name = "changeset") @XmlElementWrapper(name = "changesets") private List changesets; - /** total number of changesets */ private int total; + + private String branchName; } diff --git a/scm-core/src/main/java/sonia/scm/repository/DirectoryHealthCheck.java b/scm-core/src/main/java/sonia/scm/repository/DirectoryHealthCheck.java index 0f329d89b2..71f47f2022 100644 --- a/scm-core/src/main/java/sonia/scm/repository/DirectoryHealthCheck.java +++ b/scm-core/src/main/java/sonia/scm/repository/DirectoryHealthCheck.java @@ -178,7 +178,7 @@ public abstract class DirectoryHealthCheck implements HealthCheck else if (handler instanceof RepositoryDirectoryHandler) { File directory = - ((RepositoryDirectoryHandler) handler).getDirectory(repository); + ((RepositoryDirectoryHandler) handler).getDirectory(repository.getId()); if (directory == null) { diff --git a/scm-core/src/main/java/sonia/scm/repository/EscapeUtil.java b/scm-core/src/main/java/sonia/scm/repository/EscapeUtil.java deleted file mode 100644 index a06472256d..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/EscapeUtil.java +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.repository; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.collect.Lists; - -import org.apache.commons.lang.StringEscapeUtils; - -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.List; - -/** - * - * @author Sebastian Sdorra - * @since 1.15 - */ -public final class EscapeUtil -{ - - /** - * Constructs ... - * - */ - private EscapeUtil() {} - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param result - */ - public static void escape(BrowserResult result) - { - result.setBranch(escape(result.getBranch())); - result.setTag(escape(result.getTag())); - - for (FileObject fo : result) - { - escape(fo); - } - } - - /** - * Method description - * - * - * @param result - * @since 1.17 - */ - public static void escape(BlameResult result) - { - for (BlameLine line : result.getBlameLines()) - { - escape(line); - } - } - - /** - * Method description - * - * - * @param line - * @since 1.17 - */ - public static void escape(BlameLine line) - { - line.setDescription(escape(line.getDescription())); - escape(line.getAuthor()); - } - - /** - * Method description - * - * - * @param fo - */ - public static void escape(FileObject fo) - { - fo.setDescription(escape(fo.getDescription())); - fo.setName(fo.getName()); - fo.setPath(fo.getPath()); - } - - /** - * Method description - * - * - * @param changeset - */ - public static void escape(Changeset changeset) - { - changeset.setDescription(escape(changeset.getDescription())); - escape(changeset.getAuthor()); - changeset.setBranches(escapeList(changeset.getBranches())); - changeset.setTags(escapeList(changeset.getTags())); - } - - /** - * Method description - * - * - * @param person - * @since 1.17 - */ - public static void escape(Person person) - { - if (person != null) - { - person.setName(escape(person.getName())); - person.setMail(escape(person.getMail())); - } - } - - /** - * Method description - * - * - * @param result - */ - public static void escape(ChangesetPagingResult result) - { - for (Changeset c : result) - { - escape(c); - } - } - - /** - * Method description - * - * - * @param value - * - * @return - */ - public static String escape(String value) - { - return StringEscapeUtils.escapeHtml(value); - } - - /** - * Method description - * - * - * @param values - * - * @return - */ - public static List escapeList(List values) - { - if (Util.isNotEmpty(values)) - { - List newList = Lists.newArrayList(); - - for (String v : values) - { - newList.add(StringEscapeUtils.escapeHtml(v)); - } - - values = newList; - } - - return values; - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/Feature.java b/scm-core/src/main/java/sonia/scm/repository/Feature.java index 1db351267d..1bcaef4de5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Feature.java +++ b/scm-core/src/main/java/sonia/scm/repository/Feature.java @@ -45,5 +45,10 @@ public enum Feature * The default branch of the repository is a combined branch of all * repository branches. */ - COMBINED_DEFAULT_BRANCH + COMBINED_DEFAULT_BRANCH, + /** + * The repository supports computation of incoming changes (either diff or list of changesets) of one branch + * in respect to another target branch. + */ + INCOMING_REVISION } diff --git a/scm-core/src/main/java/sonia/scm/repository/FileObject.java b/scm-core/src/main/java/sonia/scm/repository/FileObject.java index 9e2e9e5ff1..7dedebb13a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/FileObject.java +++ b/scm-core/src/main/java/sonia/scm/repository/FileObject.java @@ -33,10 +33,9 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import com.google.common.base.Strings; import sonia.scm.LastModifiedAware; import javax.xml.bind.annotation.XmlAccessType; @@ -44,8 +43,11 @@ import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; -//~--- JDK imports ------------------------------------------------------------ +import static java.util.Collections.unmodifiableCollection; /** * The FileObject represents a file or a directory in a repository. @@ -110,14 +112,14 @@ public class FileObject implements LastModifiedAware, Serializable { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("path", path) - .add("directory", directory) - .add("description", description) - .add("length", length) - .add("subRepository", subRepository) - .add("lastModified", lastModified) - .toString(); + .add("name", name) + .add("path", path) + .add("directory", directory) + .add("description", description) + .add("length", length) + .add("subRepository", subRepository) + .add("lastModified", lastModified) + .toString(); //J+ } @@ -181,6 +183,22 @@ public class FileObject implements LastModifiedAware, Serializable return path; } + /** + * Returns the parent path of the file. + * + * @return parent path + */ + public String getParentPath() { + if (Strings.isNullOrEmpty(path)) { + return null; + } + int index = path.lastIndexOf('/'); + if (index > 0) { + return path.substring(0, index); + } + return ""; + } + /** * Return sub repository informations or null if the file is not * sub repository. @@ -284,6 +302,22 @@ public class FileObject implements LastModifiedAware, Serializable this.subRepository = subRepository; } + public Collection getChildren() { + return unmodifiableCollection(children); + } + + public void setChildren(List children) { + this.children = new ArrayList<>(children); + } + + public void addChild(FileObject child) { + this.children.add(child); + } + + public boolean hasChildren() { + return !children.isEmpty(); + } + //~--- fields --------------------------------------------------------------- /** file description */ @@ -307,4 +341,6 @@ public class FileObject implements LastModifiedAware, Serializable /** sub repository informations */ @XmlElement(name = "subrepository") private SubRepository subRepository; + + private Collection children = new ArrayList<>(); } diff --git a/scm-core/src/main/java/sonia/scm/repository/HealthCheckFailure.java b/scm-core/src/main/java/sonia/scm/repository/HealthCheckFailure.java index 8dc7e3ac54..55f1abbaa6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/HealthCheckFailure.java +++ b/scm-core/src/main/java/sonia/scm/repository/HealthCheckFailure.java @@ -36,12 +36,12 @@ package sonia.scm.repository; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -//~--- JDK imports ------------------------------------------------------------ - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +//~--- JDK imports ------------------------------------------------------------ + /** * Single failure of a {@link HealthCheck}. * @@ -134,11 +134,11 @@ public final class HealthCheckFailure { //J- return MoreObjects.toStringHelper(this) - .add("id", id) - .add("summary", summary) - .add("url", url) - .add("description", description) - .toString(); + .add("id", id) + .add("summary", summary) + .add("url", url) + .add("description", description) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/HealthCheckResult.java b/scm-core/src/main/java/sonia/scm/repository/HealthCheckResult.java index 8d22e0fd63..9ff902e3cd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/HealthCheckResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/HealthCheckResult.java @@ -37,10 +37,10 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; -//~--- JDK imports ------------------------------------------------------------ - import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * Result of {@link HealthCheck}. * @@ -52,7 +52,7 @@ public final class HealthCheckResult /** healthy result */ private static final HealthCheckResult HEALTHY = - new HealthCheckResult(ImmutableSet.of()); + new HealthCheckResult(ImmutableSet.of()); //~--- constructors --------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/repository/ImportHandler.java b/scm-core/src/main/java/sonia/scm/repository/ImportHandler.java index a3c567c930..cee142b48e 100644 --- a/scm-core/src/main/java/sonia/scm/repository/ImportHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/ImportHandler.java @@ -35,7 +35,6 @@ package sonia.scm.repository; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; - import java.util.List; /** @@ -56,8 +55,6 @@ public interface ImportHandler * * @return a {@link List} names of imported repositories * @throws IOException - * @throws RepositoryException */ - public List importRepositories(RepositoryManager manager) - throws IOException, RepositoryException; + public List importRepositories(RepositoryManager manager) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/ImportResult.java b/scm-core/src/main/java/sonia/scm/repository/ImportResult.java index 47dc11fcfd..e834a7ec6c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/ImportResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/ImportResult.java @@ -132,9 +132,9 @@ public final class ImportResult { //J- return MoreObjects.toStringHelper(this) - .add("importedDirectories", importedDirectories) - .add("failedDirectories", failedDirectories) - .toString(); + .add("importedDirectories", importedDirectories) + .add("failedDirectories", failedDirectories) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/InitialRepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/InitialRepositoryLocationResolver.java new file mode 100644 index 0000000000..23dbf85f24 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/InitialRepositoryLocationResolver.java @@ -0,0 +1,42 @@ +package sonia.scm.repository; + +import com.google.common.base.CharMatcher; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * A Location Resolver for File based Repository Storage. + *

+ * WARNING: The Locations provided with this class may not be used from the plugins to store any plugin specific files. + *

+ * Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data
+ * Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files
+ * Please use the {@link sonia.scm.store.ConfigurationStoreFactory} and the {@link sonia.scm.store.ConfigurationStore} classes to store configurations + * + * @author Mohamed Karray + * @since 2.0.0 + */ +public class InitialRepositoryLocationResolver { + + private static final String DEFAULT_REPOSITORY_PATH = "repositories"; + + private static final CharMatcher ID_MATCHER = CharMatcher.anyOf("/\\."); + + /** + * Returns the initial path to repository. + * + * @param repositoryId id of the repository + * + * @return initial path of repository + */ + @SuppressWarnings("squid:S2083") // path traversal is prevented with ID_MATCHER + public Path getPath(String repositoryId) { + // avoid path traversal attacks + checkArgument(ID_MATCHER.matchesNoneOf(repositoryId), "repository id contains invalid characters"); + return Paths.get(DEFAULT_REPOSITORY_PATH, repositoryId); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/InternalRepositoryException.java b/scm-core/src/main/java/sonia/scm/repository/InternalRepositoryException.java new file mode 100644 index 0000000000..c53ae32750 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/InternalRepositoryException.java @@ -0,0 +1,34 @@ +package sonia.scm.repository; + +import sonia.scm.ContextEntry; +import sonia.scm.ExceptionWithContext; + +import java.util.List; + +public class InternalRepositoryException extends ExceptionWithContext { + + public InternalRepositoryException(ContextEntry.ContextBuilder context, String message) { + this(context, message, null); + } + + public InternalRepositoryException(ContextEntry.ContextBuilder context, String message, Exception cause) { + this(context.build(), message, cause); + } + + public InternalRepositoryException(Repository repository, String message) { + this(ContextEntry.ContextBuilder.entity(repository), message, null); + } + + public InternalRepositoryException(Repository repository, String message, Exception cause) { + this(ContextEntry.ContextBuilder.entity(repository), message, cause); + } + + public InternalRepositoryException(List context, String message, Exception cause) { + super(context, message, cause); + } + + @Override + public String getCode() { + return null; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/Modifications.java b/scm-core/src/main/java/sonia/scm/repository/Modifications.java index 88bdad679a..a679b0429f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Modifications.java +++ b/scm-core/src/main/java/sonia/scm/repository/Modifications.java @@ -37,20 +37,17 @@ package sonia.scm.repository; import com.google.common.base.Objects; import com.google.common.collect.Lists; - import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -import java.util.List; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.List; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -70,7 +67,8 @@ public class Modifications implements Serializable * Constructs ... * */ - public Modifications() {} + public Modifications() { + } /** * Constructs ... @@ -221,6 +219,10 @@ public class Modifications implements Serializable return removed; } + public String getRevision() { + return revision; + } + //~--- set methods ---------------------------------------------------------- /** @@ -256,20 +258,26 @@ public class Modifications implements Serializable this.removed = removed; } + public void setRevision(String revision) { + this.revision = revision; + } + //~--- fields --------------------------------------------------------------- + private String revision; + /** list of added files */ - @XmlElement(name = "file") + @XmlElement(name = "added") @XmlElementWrapper(name = "added") private List added; /** list of modified files */ - @XmlElement(name = "file") + @XmlElement(name = "modified") @XmlElementWrapper(name = "modified") private List modified; /** list of removed files */ - @XmlElement(name = "file") + @XmlElement(name = "removed") @XmlElementWrapper(name = "removed") private List removed; } diff --git a/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessor.java b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessor.java new file mode 100644 index 0000000000..e5870e91aa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessor.java @@ -0,0 +1,23 @@ +package sonia.scm.repository; + +import sonia.scm.plugin.ExtensionPoint; + + +/** + * A pre processor for {@link Modifications} objects. A pre processor is able to + * modify the object before it is delivered to the user interface. + * + * @author Mohamed Karray + * @since 2.0 + */ +@ExtensionPoint +public interface ModificationsPreProcessor extends PreProcessor { + + /** + * Process the given modifications. + * + * @param modifications modifications to process + */ + @Override + void process(Modifications modifications); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessorFactory.java b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessorFactory.java new file mode 100644 index 0000000000..71a6a38efa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessorFactory.java @@ -0,0 +1,26 @@ +package sonia.scm.repository; + +import sonia.scm.plugin.ExtensionPoint; + + +/** + * This factory create a {@link ModificationsPreProcessor} + * + * @author Mohamed Karray + * @since 2.0 + */ +@ExtensionPoint +public interface ModificationsPreProcessorFactory extends PreProcessorFactory { + + /** + * Create a new {@link ModificationsPreProcessor} for the given repository. + * + * + * @param repository repository + * + * @return {@link ModificationsPreProcessor} for the given repository + */ + @Override + ModificationsPreProcessor createPreProcessor(Repository repository); + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java new file mode 100644 index 0000000000..fd0a72ad7c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java @@ -0,0 +1,63 @@ +package sonia.scm.repository; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +import java.util.Objects; + +public class NamespaceAndName implements Comparable { + + private final String namespace; + private final String name; + + public NamespaceAndName(String namespace, String name) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(namespace), "a non empty namespace is required"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "a non empty name is required"); + this.namespace = namespace; + this.name = name; + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + public String logString() { + return getNamespace() + "/" + getName(); + } + + @Override + public String toString() { + return logString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NamespaceAndName that = (NamespaceAndName) o; + return Objects.equals(namespace, that.namespace) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(namespace, name); + } + + @Override + public int compareTo(NamespaceAndName o) { + int result = namespace.compareTo(o.namespace); + if (result == 0) { + return name.compareTo(o.name); + } + return result; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java new file mode 100644 index 0000000000..d3529294ed --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java @@ -0,0 +1,19 @@ +package sonia.scm.repository; + +import sonia.scm.plugin.ExtensionPoint; + +/** + * Strategy to create a namespace for the new repository. Namespaces are used to order and identify repositories. + */ +@ExtensionPoint +public interface NamespaceStrategy { + + /** + * Create new namespace for the given repository. + * + * @param repository repository + * + * @return namespace + */ + String createNamespace(Repository repository); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/NoCommonHistoryException.java b/scm-core/src/main/java/sonia/scm/repository/NoCommonHistoryException.java new file mode 100644 index 0000000000..ddb7793d20 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/NoCommonHistoryException.java @@ -0,0 +1,22 @@ +package sonia.scm.repository; + +import sonia.scm.BadRequestException; + +import static java.util.Collections.emptyList; + +@SuppressWarnings("squid:MaximumInheritanceDepth") +public class NoCommonHistoryException extends BadRequestException { + + public NoCommonHistoryException() { + this("no common history"); + } + + public NoCommonHistoryException(String message) { + super(emptyList(), message); + } + + @Override + public String getCode() { + return "4iRct4avG1"; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/PathNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/PathNotFoundException.java deleted file mode 100644 index 7048dc2cf7..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/PathNotFoundException.java +++ /dev/null @@ -1,83 +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 sonia.scm.util.Util; - -/** - * Signals that the specified path could be found. - * - * @author Sebastian Sdorra - */ -public class PathNotFoundException extends RepositoryException -{ - - /** Field description */ - private static final long serialVersionUID = 4629690181172951809L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new {@link PathNotFoundException} - * with the specified path. - * - * - * @param path path which could not be found - */ - public PathNotFoundException(String path) - { - super("path \"".concat(Util.nonNull(path)).concat("\" not found")); - this.path = Util.nonNull(path); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Return the path which could not be found. - * - * - * @return path which could not be found - */ - public String getPath() - { - return path; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String path; -} diff --git a/scm-core/src/main/java/sonia/scm/repository/Permission.java b/scm-core/src/main/java/sonia/scm/repository/Permission.java deleted file mode 100644 index 7ea2d701b4..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/Permission.java +++ /dev/null @@ -1,255 +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.base.MoreObjects; -import com.google.common.base.Objects; -import sonia.scm.security.PermissionObject; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; -import java.io.Serializable; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * Permissions controls the access to {@link Repository}. - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "permissions") -@XmlAccessorType(XmlAccessType.FIELD) -public class Permission implements PermissionObject, Serializable -{ - - /** Field description */ - private static final long serialVersionUID = -2915175031430884040L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new {@link Permission}. - * This constructor is used by JAXB. - * - */ - public Permission() {} - - /** - * Constructs a new {@link Permission} with type = {@link PermissionType#READ} - * for the specified user. - * - * - * @param name name of the user - */ - public Permission(String name) - { - this(); - this.name = name; - } - - /** - * Constructs a new {@link Permission} with the specified type for - * the given user. - * - * - * @param name name of the user - * @param type type of the permission - */ - public Permission(String name, PermissionType type) - { - this(name); - this.type = type; - } - - /** - * Constructs a new {@link Permission} with the specified type for - * the given user or group. - * - * - * @param name name of the user or group - * @param type type of the permission - * @param groupPermission true if the permission is a permission for a group - */ - public Permission(String name, PermissionType type, boolean groupPermission) - { - this(name, type); - this.groupPermission = groupPermission; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Returns true if the {@link Permission} is the same as the obj argument. - * - * - * @param obj the reference object with which to compare - * - * @return true if the {@link Permission} is the same as the obj argument - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final Permission other = (Permission) obj; - - return Objects.equal(name, other.name) - && Objects.equal(type, other.type) - && Objects.equal(groupPermission, other.groupPermission); - } - - /** - * Returns the hash code value for the {@link Permission}. - * - * - * @return the hash code value for the {@link Permission} - */ - @Override - public int hashCode() - { - return Objects.hashCode(name, type, groupPermission); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - //J- - return MoreObjects.toStringHelper(this) - .add("name", name) - .add("type", type) - .add("groupPermission", groupPermission) - .toString(); - //J+ - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns the name of the user or group. - * - * - * @return name of the user or group - */ - @Override - public String getName() - { - return name; - } - - /** - * Returns the {@link PermissionType} of the permission. - * - * - * @return {@link PermissionType} of the permission - */ - public PermissionType getType() - { - return type; - } - - /** - * Returns true if the permission is a permission which affects a group. - * - * - * @return true if the permision is a group permission - */ - @Override - public boolean isGroupPermission() - { - return groupPermission; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Sets true if the permission is a group permission. - * - * - * @param groupPermission true if the permission is a group permission - */ - public void setGroupPermission(boolean groupPermission) - { - this.groupPermission = groupPermission; - } - - /** - * The name of the user or group. - * - * - * @param name name of the user or group - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Sets the type of the permission. - * - * - * @param type type of the permission - */ - public void setType(PermissionType type) - { - this.type = type; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private boolean groupPermission = false; - - /** Field description */ - private String name; - - /** Field description */ - private PermissionType type = PermissionType.READ; -} diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java b/scm-core/src/main/java/sonia/scm/repository/PermissionType.java deleted file mode 100644 index bd4d773877..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java +++ /dev/null @@ -1,99 +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; - -/** - * Type of permissionPrefix for a {@link Repository}. - * - * @author Sebastian Sdorra - */ -public enum PermissionType -{ - - /** read permision */ - READ(0, "repository:read:"), - - /** read and write permissionPrefix */ - WRITE(10, "repository:read,write:"), - - /** - * read, write and - * also the ability to manage the properties and permissions - */ - OWNER(100, "repository:*:"); - - /** - * Constructs a new permissionPrefix type - * - * - * @param value - */ - private PermissionType(int value, String permissionPrefix) - { - this.value = value; - this.permissionPrefix = permissionPrefix; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * - * @return - * - * @since 2.0.0 - */ - public String getPermissionPrefix() - { - return permissionPrefix; - } - - /** - * Returns the integer representation of the {@link PermissionType} - * - * - * @return integer representation - */ - public int getValue() - { - return value; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final String permissionPrefix; - - /** Field description */ - private final int value; -} diff --git a/scm-core/src/main/java/sonia/scm/repository/Person.java b/scm-core/src/main/java/sonia/scm/repository/Person.java index b3bf15c49a..8b5e66f61a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Person.java +++ b/scm-core/src/main/java/sonia/scm/repository/Person.java @@ -36,18 +36,16 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Objects; - import sonia.scm.Validateable; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +//~--- JDK imports ------------------------------------------------------------ /** * The {@link Person} (author) of a changeset. @@ -251,9 +249,11 @@ public class Person implements Validateable, Serializable //~--- fields --------------------------------------------------------------- - /** name of the person */ + /** mail address of the person */ private String mail; - /** mail address of the person */ + /** + * name of the person + */ private String name; } diff --git a/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java b/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java index 72e4ed7d63..e64979dde6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java +++ b/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java @@ -36,17 +36,15 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.Inject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.util.Collection; import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -73,14 +71,18 @@ public class PreProcessorUtil * @param fileObjectPreProcessorFactorySet * @param blameLinePreProcessorSet * @param blameLinePreProcessorFactorySet + * @param modificationsPreProcessorFactorySet + * @param modificationsPreProcessorSet */ @Inject public PreProcessorUtil(Set changesetPreProcessorSet, - Set changesetPreProcessorFactorySet, - Set fileObjectPreProcessorSet, - Set fileObjectPreProcessorFactorySet, - Set blameLinePreProcessorSet, - Set blameLinePreProcessorFactorySet) + Set changesetPreProcessorFactorySet, + Set fileObjectPreProcessorSet, + Set fileObjectPreProcessorFactorySet, + Set blameLinePreProcessorSet, + Set blameLinePreProcessorFactorySet, + Set modificationsPreProcessorFactorySet, + Set modificationsPreProcessorSet) { this.changesetPreProcessorSet = changesetPreProcessorSet; this.changesetPreProcessorFactorySet = changesetPreProcessorFactorySet; @@ -88,6 +90,8 @@ public class PreProcessorUtil this.fileObjectPreProcessorFactorySet = fileObjectPreProcessorFactorySet; this.blameLinePreProcessorSet = blameLinePreProcessorSet; this.blameLinePreProcessorFactorySet = blameLinePreProcessorFactorySet; + this.modificationsPreProcessorFactorySet = modificationsPreProcessorFactorySet; + this.modificationsPreProcessorSet = modificationsPreProcessorSet; } //~--- methods -------------------------------------------------------------- @@ -107,14 +111,7 @@ public class PreProcessorUtil blameLine.getLineNumber(), repository.getName()); } - EscapeUtil.escape(blameLine); - - PreProcessorHandler handler = - new PreProcessorHandler<>(blameLinePreProcessorFactorySet, - blameLinePreProcessorSet, repository); - - handler.callPreProcessors(blameLine); - handler.callPreProcessorFactories(blameLine); + handlePreProcess(repository,blameLine,blameLinePreProcessorFactorySet, blameLinePreProcessorSet); } /** @@ -125,22 +122,6 @@ public class PreProcessorUtil * @param blameResult */ public void prepareForReturn(Repository repository, BlameResult blameResult) - { - prepareForReturn(repository, blameResult, true); - } - - /** - * Method description - * - * - * @param repository - * @param blameResult - * @param escape - * - * @since 1.35 - */ - public void prepareForReturn(Repository repository, BlameResult blameResult, - boolean escape) { if (logger.isTraceEnabled()) { @@ -148,17 +129,7 @@ public class PreProcessorUtil repository.getName()); } - if (escape) - { - EscapeUtil.escape(blameResult); - } - - PreProcessorHandler handler = - new PreProcessorHandler<>(blameLinePreProcessorFactorySet, - blameLinePreProcessorSet, repository); - - handler.callPreProcessors(blameResult.getBlameLines()); - handler.callPreProcessorFactories(blameResult.getBlameLines()); + handlePreProcessForIterable(repository, blameResult.getBlameLines(),blameLinePreProcessorFactorySet, blameLinePreProcessorSet); } /** @@ -170,39 +141,13 @@ public class PreProcessorUtil */ public void prepareForReturn(Repository repository, Changeset changeset) { - prepareForReturn(repository, changeset, true); + logger.trace("prepare changeset {} of repository {} for return", changeset.getId(), repository.getName()); + handlePreProcess(repository, changeset, changesetPreProcessorFactorySet, changesetPreProcessorSet); } - /** - * Method description - * - * - * @param repository - * @param changeset - * @param escape - * - * @since 1.35 - */ - public void prepareForReturn(Repository repository, Changeset changeset, - boolean escape) - { - if (logger.isTraceEnabled()) - { - logger.trace("prepare changeset {} of repository {} for return", - changeset.getId(), repository.getName()); - } - - if (escape) - { - EscapeUtil.escape(changeset); - } - - PreProcessorHandler handler = - new PreProcessorHandler<>(changesetPreProcessorFactorySet, - changesetPreProcessorSet, repository); - - handler.callPreProcessors(changeset); - handler.callPreProcessorFactories(changeset); + public void prepareForReturn(Repository repository, Modifications modifications) { + logger.trace("prepare modifications {} of repository {} for return", modifications, repository.getName()); + handlePreProcess(repository, modifications, modificationsPreProcessorFactorySet, modificationsPreProcessorSet); } /** @@ -213,40 +158,24 @@ public class PreProcessorUtil * @param result */ public void prepareForReturn(Repository repository, BrowserResult result) - { - prepareForReturn(repository, result, true); - } - - /** - * Method description - * - * - * @param repository - * @param result - * @param escape - * - * @since 1.35 - */ - public void prepareForReturn(Repository repository, BrowserResult result, - boolean escape) { if (logger.isTraceEnabled()) { - logger.trace("prepare browser result of repository {} for return", - repository.getName()); + logger.trace("prepare browser result of repository {} for return", repository.getName()); } - if (escape) - { - EscapeUtil.escape(result); + PreProcessorHandler handler = new PreProcessorHandler<>(fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet, repository); + handlePreProcessorForFileObject(handler, result.getFile()); + } + + private void handlePreProcessorForFileObject(PreProcessorHandler handler, FileObject fileObject) { + if (fileObject.isDirectory()) { + for (FileObject child : fileObject.getChildren()) { + handlePreProcessorForFileObject(handler, child); + } } - - PreProcessorHandler handler = - new PreProcessorHandler<>(fileObjectPreProcessorFactorySet, - fileObjectPreProcessorSet, repository); - - handler.callPreProcessors(result); - handler.callPreProcessorFactories(result); + handler.callPreProcessorFactories(fileObject); + handler.callPreProcessors(fileObject); } /** @@ -255,12 +184,8 @@ public class PreProcessorUtil * * @param repository * @param result - * @param escape - * - * @since 1.35 */ - public void prepareForReturn(Repository repository, - ChangesetPagingResult result, boolean escape) + public void prepareForReturn(Repository repository, ChangesetPagingResult result) { if (logger.isTraceEnabled()) { @@ -268,30 +193,23 @@ public class PreProcessorUtil repository.getName()); } - if (escape) - { - EscapeUtil.escape(result); - } - - PreProcessorHandler handler = - new PreProcessorHandler<>(changesetPreProcessorFactorySet, - changesetPreProcessorSet, repository); - - handler.callPreProcessors(result); - handler.callPreProcessorFactories(result); + handlePreProcessForIterable(repository,result,changesetPreProcessorFactorySet, changesetPreProcessorSet); } - /** - * Method description - * - * - * @param repository - * @param result - */ - public void prepareForReturn(Repository repository, - ChangesetPagingResult result) - { - prepareForReturn(repository, result, true); + private , P extends PreProcessor> void handlePreProcess(Repository repository, T processedObject, + Collection factories, + Collection

preProcessors) { + PreProcessorHandler handler = new PreProcessorHandler(factories, preProcessors, repository); + handler.callPreProcessors(processedObject); + handler.callPreProcessorFactories(processedObject); + } + + private , F extends PreProcessorFactory, P extends PreProcessor> void handlePreProcessForIterable(Repository repository, I processedObjects, + Collection factories, + Collection

preProcessors) { + PreProcessorHandler handler = new PreProcessorHandler(factories, preProcessors, repository); + handler.callPreProcessors(processedObjects); + handler.callPreProcessorFactories(processedObjects); } //~--- inner classes -------------------------------------------------------- @@ -454,6 +372,10 @@ public class PreProcessorUtil /** Field description */ private final Collection changesetPreProcessorSet; + private final Collection modificationsPreProcessorFactorySet; + + private final Collection modificationsPreProcessorSet; + /** Field description */ private final Collection fileObjectPreProcessorFactorySet; diff --git a/scm-core/src/main/java/sonia/scm/repository/RemoveDeletedRepositoryRole.java b/scm-core/src/main/java/sonia/scm/repository/RemoveDeletedRepositoryRole.java new file mode 100644 index 0000000000..25564dca83 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RemoveDeletedRepositoryRole.java @@ -0,0 +1,48 @@ +package sonia.scm.repository; + +import com.github.legman.Subscribe; +import sonia.scm.EagerSingleton; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; + +import java.util.Optional; + +import static sonia.scm.HandlerEventType.DELETE; + +@EagerSingleton +@Extension +public class RemoveDeletedRepositoryRole { + + private final RepositoryManager repositoryManager; + + @Inject + public RemoveDeletedRepositoryRole(RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + } + + @Subscribe + void handle(RepositoryRoleEvent event) { + if (event.getEventType() == DELETE) { + repositoryManager.getAll() + .forEach(repository -> check(repository, event.getItem())); + } + } + + private void check(Repository repository, RepositoryRole role) { + findPermission(repository, role) + .ifPresent(permission -> removeFromPermissions(repository, permission)); + } + + private Optional findPermission(Repository repository, RepositoryRole item) { + return repository.getPermissions() + .stream() + .filter(repositoryPermission -> item.getName().equals(repositoryPermission.getRole())) + .findFirst(); + } + + private void removeFromPermissions(Repository repository, RepositoryPermission permission) { + repository.removePermission(permission); + repositoryManager.modify(repository); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index b6916d64f3..e35e12bbb0 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -33,25 +33,27 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- - import com.github.sdorra.ssp.PermissionObject; import com.github.sdorra.ssp.StaticPermissions; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -import com.google.common.collect.Lists; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; -import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; -import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; - -//~--- JDK imports ------------------------------------------------------------ +import java.util.Set; /** * Source code repository. @@ -59,90 +61,243 @@ import java.util.List; * @author Sebastian Sdorra */ @StaticPermissions( - value = "repository", - permissions = {"read", "write", "modify", "delete", "healthCheck"} + value = "repository", + permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"}, + custom = true, customGlobal = true ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") -public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject -{ +public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{ - /** Field description */ private static final long serialVersionUID = 3486560714961909711L; - //~--- constructors --------------------------------------------------------- + private String contact; + private Long creationDate; + private String description; + @XmlElement(name = "healthCheckFailure") + @XmlElementWrapper(name = "healthCheckFailures") + private List healthCheckFailures; + private String id; + private Long lastModified; + private String namespace; + private String name; + @XmlElement(name = "permission") + private Set permissions = new HashSet<>(); + private String type; + /** * Constructs a new {@link Repository}. * This constructor is used by JAXB. - * */ - public Repository() {} + public Repository() { + } /** * Constructs a new {@link Repository}. * - * - * - * @param id id of the {@link Repository} + * @param id id of the {@link Repository} * @param type type of the {@link Repository} * @param name name of the {@link Repository} */ - public Repository(String id, String type, String name) - { + public Repository(String id, String type, String namespace, String name) { this.id = id; this.type = type; + this.namespace = namespace; this.name = name; } /** * Constructs a new {@link Repository}. * - * - * - * @param id id of the {@link Repository} - * @param type type of the {@link Repository} - * @param name name of the {@link Repository} - * @param contact email address of a person who is responsible for - * this repository. + * @param id id of the {@link Repository} + * @param type type of the {@link Repository} + * @param name name of the {@link Repository} + * @param namespace namespace of the {@link Repository} + * @param contact email address of a person who is responsible for + * this repository. * @param description a short description of the repository * @param permissions permissions for specific users and groups. */ - public Repository(String id, String type, String name, String contact, - String description, Permission... permissions) - { + public Repository(String id, String type, String namespace, String name, String contact, + String description, RepositoryPermission... permissions) { this.id = id; this.type = type; + this.namespace = namespace; this.name = name; this.contact = contact; this.description = description; - this.permissions = Lists.newArrayList(); - if (Util.isNotEmpty(permissions)) - { + if (Util.isNotEmpty(permissions)) { this.permissions.addAll(Arrays.asList(permissions)); } } - //~--- methods -------------------------------------------------------------- + /** + * Returns a contact email address of a person who is responsible for + * the {@link Repository}. + * + * @return contact email address + */ + public String getContact() { + return contact; + } /** - * Create a clone of this {@link Repository} object. + * Returns a timestamp of the creation date of the {@link Repository}. * + * @return a timestamp of the creation date of the {@link Repository} + */ + public Long getCreationDate() { + return creationDate; + } + + /** + * Returns a short description of the {@link Repository}. * - * @return clone of this {@link Repository} + * @return short description + */ + public String getDescription() { + return description; + } + + /** + * Returns a {@link List} of {@link HealthCheckFailure}s. The {@link List} + * is empty if the repository is healthy. + * + * @return {@link List} of {@link HealthCheckFailure}s + * @since 1.36 + */ + @SuppressWarnings("unchecked") + public List getHealthCheckFailures() { + if (healthCheckFailures == null) { + healthCheckFailures = Collections.EMPTY_LIST; + } + + return healthCheckFailures; + } + + @Override + public String getId() { + return id; + } + + @Override + public Long getLastModified() { + return lastModified; + } + + + public String getName() { + return name; + } + + public String getNamespace() { return namespace; } + + @XmlTransient + public NamespaceAndName getNamespaceAndName() { + return new NamespaceAndName(getNamespace(), getName()); + } + + public Collection getPermissions() { + return Collections.unmodifiableCollection(permissions); + } + + /** + * Returns the type (hg, git, svn ...) of the {@link Repository}. + * + * @return type of the repository */ @Override - public Repository clone() - { + public String getType() { + return type; + } + + /** + * Returns {@code true} if the repository is healthy. + * + * @return {@code true} if the repository is healthy + * @since 1.36 + */ + public boolean isHealthy() { + return Util.isEmpty(healthCheckFailures); + } + + /** + * Returns true if the {@link Repository} is valid. + *

    + *
  • The namespace is valid
  • + *
  • The name is valid
  • + *
  • The type is not empty
  • + *
  • The contact is empty or contains a valid email address
  • + *
+ * + * @return true if the {@link Repository} is valid + */ + @Override + public boolean isValid() { + return ValidationUtil.isRepositoryNameValid(namespace) + && ValidationUtil.isRepositoryNameValid(name) + && Util.isNotEmpty(type) + && ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact)); + } + + public void setContact(String contact) { + this.contact = contact; + } + + public void setCreationDate(Long creationDate) { + this.creationDate = creationDate; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setId(String id) { + this.id = id; + } + + public void setLastModified(Long lastModified) { + this.lastModified = lastModified; + } + + public void setNamespace(String namespace) { this.namespace = namespace; } + + public void setName(String name) { + this.name = name; + } + + public void setPermissions(Collection permissions) { + this.permissions.clear(); + this.permissions.addAll(permissions); + } + + public void addPermission(RepositoryPermission newPermission) { + this.permissions.add(newPermission); + } + + public boolean removePermission(RepositoryPermission permission) { + return this.permissions.remove(permission); + } + + public void setType(String type) { + this.type = type; + } + + public void setHealthCheckFailures(List healthCheckFailures) { + this.healthCheckFailures = healthCheckFailures; + } + + @Override + public Repository clone() { Repository repository = null; - try - { + try { repository = (Repository) super.clone(); - } - catch (CloneNotSupportedException ex) - { + // fix permission reference on clone + repository.permissions = new HashSet<>(permissions); + } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } @@ -152,456 +307,72 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per /** * Copies all properties of the {@link Repository} to the given one. * - * - * @param repository to copies all properties of this one + * @param repository the target {@link Repository} */ - public void copyProperties(Repository repository) - { + public void copyProperties(Repository repository) { + repository.setNamespace(namespace); repository.setName(name); repository.setContact(contact); repository.setCreationDate(creationDate); repository.setLastModified(lastModified); repository.setDescription(description); repository.setPermissions(permissions); - repository.setPublicReadable(publicReadable); - repository.setArchived(archived); // 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) - { - String url = HttpUtil.append(baseUrl, type); - - return HttpUtil.append(url, name); - } - /** * Returns true if the {@link Repository} is the same as the obj argument. * - * * @param obj the reference object with which to compare - * * @return true if the {@link Repository} is the same as the obj argument */ @Override - public boolean equals(Object obj) - { - if (obj == null) - { + public boolean equals(Object obj) { + if (obj == null) { return false; } - if (getClass() != obj.getClass()) - { + if (getClass() != obj.getClass()) { return false; } final Repository other = (Repository) obj; - //J- - return Objects.equal(id, other.id) - && Objects.equal(name, other.name) - && Objects.equal(contact, other.contact) - && Objects.equal(description, other.description) - && Objects.equal(publicReadable, other.publicReadable) - && Objects.equal(archived, other.archived) - && Objects.equal(permissions, other.permissions) - && Objects.equal(type, other.type) - && Objects.equal(creationDate, other.creationDate) - && Objects.equal(lastModified, other.lastModified) - && Objects.equal(properties, other.properties) - && Objects.equal(healthCheckFailures, other.healthCheckFailures); - //J+ + return Objects.equal(id, other.id) + && Objects.equal(namespace, other.namespace) + && Objects.equal(name, other.name) + && Objects.equal(contact, other.contact) + && Objects.equal(description, other.description) + && Objects.equal(permissions, other.permissions) + && Objects.equal(type, other.type) + && Objects.equal(creationDate, other.creationDate) + && Objects.equal(lastModified, other.lastModified) + && Objects.equal(properties, other.properties) + && Objects.equal(healthCheckFailures, other.healthCheckFailures); } - /** - * Returns the hash code value for the {@link Repository}. - * - * - * @return the hash code value for the {@link Repository} - */ @Override - public int hashCode() - { - return Objects.hashCode(id, name, contact, description, publicReadable, - archived, permissions, type, creationDate, lastModified, properties, + public int hashCode() { + return Objects.hashCode(id, namespace, name, contact, description, + permissions, type, creationDate, lastModified, properties, healthCheckFailures); } - /** - * Returns a {@link String} that represents the {@link Repository}. - * - * - * @return {@link String} that represents the {@link Repository} - */ @Override - public String toString() - { - //J- + public String toString() { return MoreObjects.toStringHelper(this) - .add("id", id) - .add("name", name) - .add("contact", contact) - .add("description", description) - .add("publicReadable", publicReadable) - .add("archived", archived) - .add("permissions", permissions) - .add("type", type) - .add("lastModified", lastModified) - .add("creationDate", creationDate) - .add("properties", properties) - .add("healthCheckFailures", healthCheckFailures) - .toString(); - //J+ + .add("id", id) + .add("namespace", namespace) + .add("name", name) + .add("contact", contact) + .add("description", description) + .add("permissions", permissions) + .add("type", type) + .add("lastModified", lastModified) + .add("creationDate", creationDate) + .add("properties", properties) + .add("healthCheckFailures", healthCheckFailures) + .toString(); } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns a contact email address of a person who is responsible for - * the {@link Repository}. - * - * - * @return contact email address - */ - public String getContact() - { - return contact; - } - - /** - * Returns a timestamp of the creation date of the {@link Repository}. - * - * - * @return a timestamp of the creation date of the {@link Repository} - */ - public Long getCreationDate() - { - return creationDate; - } - - /** - * Returns a short description of the {@link Repository}. - * - * - * @return short description - */ - public String getDescription() - { - return description; - } - - /** - * Returns a {@link List} of {@link HealthCheckFailure}s. The {@link List} - * is empty if the repository is healthy. - * - * - * @return {@link List} of {@link HealthCheckFailure}s - * @since 1.36 - */ - @SuppressWarnings("unchecked") - public List getHealthCheckFailures() - { - if (healthCheckFailures == null) - { - healthCheckFailures = Collections.EMPTY_LIST; - } - - return healthCheckFailures; - } - - /** - * Returns the unique id of the {@link Repository}. - * - * - * @return unique id - */ - @Override - public String getId() - { - return id; - } - - /** - * Returns the timestamp of the last modified date of the {@link Repository}. - * - * - * @return timestamp of the last modified date - */ - @Override - public Long getLastModified() - { - return lastModified; - } - - /** - * Returns the name of the {@link Repository}. - * - * - * @return name of the {@link Repository} - */ - public String getName() - { - return name; - } - - /** - * Returns the access permissions of the {@link Repository}. - * - * - * @return access permissions - */ - public List getPermissions() - { - if (permissions == null) - { - permissions = Lists.newArrayList(); - } - - return permissions; - } - - /** - * Returns the type (hg, git, svn ...) of the {@link Repository}. - * - * - * @return type of the repository - */ - @Override - public String getType() - { - return type; - } - - /** - * Returns true if the repository is archived. - * - * - * @return true if the repository is archived - * @since 1.14 - */ - public boolean isArchived() - { - return archived; - } - - /** - * Returns {@code true} if the repository is healthy. - * - * - * @return {@code true} if the repository is healthy - * - * @since 1.36 - */ - public boolean isHealthy() - { - return Util.isEmpty(healthCheckFailures); - } - - /** - * Returns true if the {@link Repository} is public readable. - * - * - * @return true if the {@link Repository} is public readable - */ - public boolean isPublicReadable() - { - return publicReadable; - } - - /** - * Returns true if the {@link Repository} is valid. - *
    - *
  • The name is not empty and contains only A-z, 0-9, _, -, /
  • - *
  • The type is not empty
  • - *
  • The contact is empty or contains a valid email address
  • - *
- * - * - * @return true if the {@link Repository} is valid - */ - @Override - public boolean isValid() - { - return ValidationUtil.isRepositoryNameValid(name) && Util.isNotEmpty(type) - && ((Util.isEmpty(contact)) - || ValidationUtil.isMailAddressValid(contact)); - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Archive or un archive this repository. - * - * - * @param archived true to enable archive - * @since 1.14 - */ - public void setArchived(boolean archived) - { - this.archived = archived; - } - - /** - * Sets the contact of the {@link Repository}. The contact address should be - * a email address of a person who is responsible for the {@link Repository}. - * - * - * @param contact email address of a person who is responsible for - * the {@link Repository} - */ - public void setContact(String contact) - { - this.contact = contact; - } - - /** - * Set the creation date of the {@link Repository}. - * - * - * @param creationDate creation date of the {@link Repository} - */ - public void setCreationDate(Long creationDate) - { - this.creationDate = creationDate; - } - - /** - * Sets a short description of the {@link Repository}. - * - * - * @param description short description - */ - public void setDescription(String description) - { - this.description = description; - } - - /** - * The unique id of the {@link Repository}. - * - * - * @param id unique id - */ - public void setId(String id) - { - this.id = id; - } - - /** - * Set the last modified timestamp of the {@link Repository}. - * - * - * @param lastModified last modified timestamp - */ - public void setLastModified(Long lastModified) - { - this.lastModified = lastModified; - } - - /** - * Set the name of the {@link Repository}. - * - * - * @param name name of the {@link Repository} - */ - public void setName(String name) - { - this.name = name; - } - - /** - * Set the access permissions for the {@link Repository}. - * - * - * @param permissions list of access permissions - */ - public void setPermissions(List permissions) - { - this.permissions = permissions; - } - - /** - * Sets true if the {@link Repository} is public readable. - * - * - * @param publicReadable public readable - */ - public void setPublicReadable(boolean publicReadable) - { - this.publicReadable = publicReadable; - } - - /** - * Sets the type (hg, svn, git ...) of the {@link Repository}. - * - * - * @param type type of the {@link Repository} - */ - public void setType(String type) - { - this.type = type; - } - - /** - * Sets {@link HealthCheckFailure} for a unhealthy repository. - * - * @param healthCheckFailures list of {@link HealthCheckFailure}s - * - * @since 1.36 - */ - public void setHealthCheckFailures(List healthCheckFailures) - { - this.healthCheckFailures = healthCheckFailures; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String contact; - - /** Field description */ - private Long creationDate; - - /** Field description */ - private String description; - - /** - * @since 1.36 - */ - @XmlElement(name = "healthCheckFailure") - @XmlElementWrapper(name = "healthCheckFailures") - private List healthCheckFailures; - - /** Field description */ - private String id; - - /** Field description */ - private Long lastModified; - - /** Field description */ - private String name; - - /** Field description */ - private List permissions; - - /** Field description */ - @XmlElement(name = "public") - private boolean publicReadable = false; - - /** Field description */ - private boolean archived = false; - - /** Field description */ - private String type; } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryAlreadyExistsException.java deleted file mode 100644 index c439b2d0f4..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryAlreadyExistsException.java +++ /dev/null @@ -1,68 +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; - -/** - * This {@link Exception} is thrown when trying to create a repository with - * the same name and type already exists. - * - * @author Sebastian Sdorra - */ -public class RepositoryAlreadyExistsException extends RepositoryException -{ - private static final long serialVersionUID = -774929917214137675L; - - /** - * Creates a new instance. - * - * @param message exception message - */ - public RepositoryAlreadyExistsException(String message) { - super(message); - } - - /** - * Creates a new {@link RepositoryAlreadyExistsException} with an appropriate message. - * - * @param repository repository that already exists - * - * @return new exception with appropriate message - */ - public static RepositoryAlreadyExistsException create(Repository repository){ - StringBuilder buffer = new StringBuilder("repository with name "); - buffer.append(repository.getName()).append(" of type "); - buffer.append(repository.getType()).append("already exists"); - return new RepositoryAlreadyExistsException(buffer.toString()); - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryConfig.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryConfig.java new file mode 100644 index 0000000000..909d95dce2 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryConfig.java @@ -0,0 +1,101 @@ +/** + * 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; + +import sonia.scm.Validateable; +import sonia.scm.config.Configuration; + +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; + +/** + * Basic {@link Repository} configuration class. + * + * @author Sebastian Sdorra + */ +@XmlRootElement +public abstract class RepositoryConfig implements Validateable, Configuration { + + /** true if the plugin is disabled */ + private boolean disabled = false; + /** + * Returns true if the plugin is disabled. + * + * + * @return true if the plugin is disabled + * @since 1.13 + */ + public boolean isDisabled() { + return disabled; + } + + /** + * Returns true if the configuration object is valid. + * + * + * @return true if the configuration object is valid + */ + @Override + public boolean isValid() { + return true; + } + + //~--- set methods ---------------------------------------------------------- + + /** + * Enable or disable the plugin. + * + * + * @param disabled + * @since 1.13 + */ + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + + + /** + * Specifies the identifier of the concrete {@link RepositoryConfig} when checking permissions of an object. + * The permission Strings will have the following format: "configuration:*:ID", where the ID part is defined by this + * method. + * + * For example: "configuration:read:git". + * + * No need to serialize this. + * + * @return identifier of this RepositoryConfig in permission strings + */ + @Override + @XmlTransient // Only for permission checks, don't serialize to XML + public abstract String getId(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryDAO.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryDAO.java index 9a2d4b5662..ce309ecee6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryDAO.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryDAO.java @@ -49,27 +49,22 @@ public interface RepositoryDAO extends GenericDAO /** * Returns true if a repository with specified - * type and name exists in the backend. + * namespace and name exists in the backend. * * - * @param type type of the repository - * @param name name of the repository + * @param namespaceAndName namespace and name of the repository * * @return true if the repository exists */ - public boolean contains(String type, String name); + boolean contains(NamespaceAndName namespaceAndName); //~--- get methods ---------------------------------------------------------- /** - * Returns the repository with the specified type and name or null + * Returns the repository with the specified namespace and name or null * if no such repository exists in the backend. * - * - * @param type - * @param name - * - * @return repository with the specified type and name or null + * @return repository with the specified namespace and name or null */ - public Repository get(String type, String name); + Repository get(NamespaceAndName namespaceAndName); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryDirectoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryDirectoryHandler.java index 3fda4a76be..837aaa496b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryDirectoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryDirectoryHandler.java @@ -40,16 +40,13 @@ import java.io.File; * @author Sebastian Sdorra * @since 1.36 */ -public interface RepositoryDirectoryHandler extends RepositoryHandler -{ +public interface RepositoryDirectoryHandler extends RepositoryHandler { + + String REPOSITORIES_NATIVE_DIRECTORY = "data"; /** - * Method description - * - * - * @param repository - * - * @return + * Get the current directory of the repository for the given id. + * @return the current directory of the given repository */ - public File getDirectory(Repository repository); + File getDirectory(String repositoryId); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryException.java deleted file mode 100644 index 63538d08e3..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryException.java +++ /dev/null @@ -1,95 +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; - -/** - * Base class for all repository exceptions. - * - * @author Sebastian Sdorra - */ -public class RepositoryException extends Exception -{ - - /** Field description */ - private static final long serialVersionUID = -4939196278070910058L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new {@link RepositoryException} with null as its - * error detail message. - * - */ - public RepositoryException() - { - super(); - } - - /** - * Constructs a new {@link RepositoryException} with the specified - * detail message. - * - * - * @param message detail message - */ - public RepositoryException(String message) - { - super(message); - } - - /** - * Constructs a new {@link RepositoryException} with the specified - * detail message and cause. - * - * - * @param cause the cause for the exception - */ - public RepositoryException(Throwable cause) - { - super(cause); - } - - /** - * Constructs a new {@link RepositoryException} with the specified - * detail message and cause. - * - * - * @param message detail message - * @param cause the cause for the exception - */ - public RepositoryException(String message, Throwable cause) - { - super(message, cause); - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java index 8dc5f8418b..aaa090827a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java @@ -36,7 +36,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import sonia.scm.Handler; -import sonia.scm.NotSupportedFeatuerException; +import sonia.scm.FeatureNotSupportedException; import sonia.scm.plugin.ExtensionPoint; /** @@ -47,20 +47,9 @@ import sonia.scm.plugin.ExtensionPoint; */ @ExtensionPoint public interface RepositoryHandler - extends Handler + extends Handler { - /** - * Returns the resource path for the given {@link Repository}. - * The resource path is part of the {@link Repository} url. - * - * - * - * @param repository given {@link Repository} - * @return resource path of the {@link Repository} - */ - public String createResourcePath(Repository repository); - //~--- get methods ---------------------------------------------------------- /** @@ -70,9 +59,9 @@ public interface RepositoryHandler * @return {@link ImportHandler} for the repository type of this handler * @since 1.12 * - * @throws NotSupportedFeatuerException + * @throws FeatureNotSupportedException */ - public ImportHandler getImportHandler() throws NotSupportedFeatuerException; + public ImportHandler getImportHandler() throws FeatureNotSupportedException; /** * Returns informations about the version of the RepositoryHandler. @@ -82,4 +71,7 @@ public interface RepositoryHandler * @since 1.15 */ public String getVersionInformation(); + + @Override + RepositoryType getType(); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandlerConfigChangedEvent.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandlerConfigChangedEvent.java index 844c4a78d6..645274e494 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandlerConfigChangedEvent.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandlerConfigChangedEvent.java @@ -37,7 +37,7 @@ import sonia.scm.event.Event; * @since 2.0.0 */ @Event -public class RepositoryHandlerConfigChangedEvent +public class RepositoryHandlerConfigChangedEvent { private final C configuration; diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandlerNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandlerNotFoundException.java deleted file mode 100644 index 4583902ef8..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandlerNotFoundException.java +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.repository; - -/** - * - * @author Sebastian Sdorra - */ -public class RepositoryHandlerNotFoundException extends RepositoryException -{ - - /** Field description */ - private static final long serialVersionUID = 5270463060802850944L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public RepositoryHandlerNotFoundException() {} - - /** - * Constructs ... - * - * - * @param message - */ - public RepositoryHandlerNotFoundException(String message) - { - super(message); - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java deleted file mode 100644 index 775e4714d8..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm.repository; - -/** - * - * @author Sebastian Sdorra - * - * @since 1.14 - */ -public class RepositoryIsNotArchivedException extends RepositoryException -{ - - /** Field description */ - private static final long serialVersionUID = 7728748133123987511L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public RepositoryIsNotArchivedException() {} - - /** - * Constructs ... - * - * - * @param message - */ - public RepositoryIsNotArchivedException(String message) - { - super(message); - } - - /** - * Constructs ... - * - * - * @param cause - */ - public RepositoryIsNotArchivedException(Throwable cause) - { - super(cause); - } - - /** - * Constructs ... - * - * - * @param message - * @param cause - */ - public RepositoryIsNotArchivedException(String message, Throwable cause) - { - super(message, cause); - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java new file mode 100644 index 0000000000..bdd7a03d62 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -0,0 +1,48 @@ +package sonia.scm.repository; + +import java.util.function.BiConsumer; + +public abstract class RepositoryLocationResolver { + + public abstract boolean supportsLocationType(Class type); + + protected abstract RepositoryLocationResolverInstance create(Class type); + + public final RepositoryLocationResolverInstance forClass(Class type) { + if (!supportsLocationType(type)) { + throw new IllegalStateException("no support for location of class " + type); + } + return create(type); + } + + public interface RepositoryLocationResolverInstance { + + /** + * Get the existing location for the repository. + * @param repositoryId The id of the repository. + * @throws IllegalStateException when there is no known location for the given repository. + */ + T getLocation(String repositoryId); + + /** + * Create a new location for the new repository. + * @param repositoryId The id of the new repository. + * @throws IllegalStateException when there already is a location for the given repository registered. + */ + T createLocation(String repositoryId); + + /** + * Set the location of a new repository. + * @param repositoryId The id of the new repository. + * @throws IllegalStateException when there already is a location for the given repository registered. + */ + void setLocation(String repositoryId, T location); + + /** + * Iterates all repository locations known to this resolver instance and calls the consumer giving the repository id + * and its location for each repository. + * @param consumer This callback will be called for each repository with the repository id and its location. + */ + void forAllLocations(BiConsumer consumer); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index 7cbac2b52e..7f4880c258 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -35,16 +35,12 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.Type; import sonia.scm.TypeManager; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.Collection; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * The central class for managing {@link Repository} objects. @@ -55,7 +51,7 @@ import javax.servlet.http.HttpServletRequest; * @apiviz.uses sonia.scm.repository.RepositoryHandler */ public interface RepositoryManager - extends TypeManager + extends TypeManager { /** @@ -75,26 +71,23 @@ public interface RepositoryManager * @param repository {@link Repository} to import * * @throws IOException - * @throws RepositoryException */ - public void importRepository(Repository repository) - throws IOException, RepositoryException; + public void importRepository(Repository repository) throws IOException; //~--- get methods ---------------------------------------------------------- /** - * Returns a {@link Repository} by its type and name or + * Returns a {@link Repository} by its namespace and name or * null if the {@link Repository} could not be found. * * - * @param type type of the {@link Repository} - * @param name name of the {@link Repository} + * @param namespaceAndName namespace and name of the {@link Repository} * * - * @return {@link Repository} by its type and name or null + * @return {@link Repository} by its namespace and name or null * if the {@link Repository} could not be found */ - public Repository get(String type, String name); + public Repository get(NamespaceAndName namespaceAndName); /** * Returns all configured repository types. @@ -102,42 +95,7 @@ public interface RepositoryManager * * @return all configured repository types */ - public Collection getConfiguredTypes(); - - /** - * Returns the {@link Repository} associated to the request uri. - * - * - * @param request the current http request - * - * @return associated to the request uri - * @since 1.9 - */ - public Repository getFromRequest(HttpServletRequest request); - - /** - * Returns the {@link Repository} associated to the given type and path. - * - * - * @param type type of the repository (hg, git ...) - * @param uri - * - * @return the {@link Repository} associated to the given type and path - * @since 1.9 - */ - public Repository getFromTypeAndUri(String type, String uri); - - /** - * 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); + public Collection getConfiguredTypes(); /** * Returns a {@link RepositoryHandler} by the given type (hg, git, svn ...). diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java index 632b54b741..b7a819e5f9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java @@ -38,13 +38,10 @@ package sonia.scm.repository; import sonia.scm.ManagerDecorator; import sonia.scm.Type; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.Collection; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * Decorator for {@link RepositoryManager}. @@ -53,7 +50,7 @@ import javax.servlet.http.HttpServletRequest; * @since 1.23 */ public class RepositoryManagerDecorator - extends ManagerDecorator + extends ManagerDecorator implements RepositoryManager { @@ -84,27 +81,16 @@ public class RepositoryManagerDecorator * {@inheritDoc} */ @Override - public void importRepository(Repository repository) - throws IOException, RepositoryException - { + public void importRepository(Repository repository) throws IOException { decorated.importRepository(repository); } //~--- get methods ---------------------------------------------------------- - /** - * {@inheritDoc} - * - * - * @param type - * @param name - * - * @return - */ @Override - public Repository get(String type, String name) + public Repository get(NamespaceAndName namespaceAndName) { - return decorated.get(type, name); + return decorated.get(namespaceAndName); } /** @@ -114,7 +100,7 @@ public class RepositoryManagerDecorator * @return */ @Override - public Collection getConfiguredTypes() + public Collection getConfiguredTypes() { return decorated.getConfiguredTypes(); } @@ -132,49 +118,6 @@ public class RepositoryManagerDecorator return decorated; } - /** - * {@inheritDoc} - * - * - * @param request - * - * @return - */ - @Override - public Repository getFromRequest(HttpServletRequest request) - { - return decorated.getFromRequest(request); - } - - /** - * {@inheritDoc} - * - * - * @param type - * @param uri - * - * @return - */ - @Override - public Repository getFromTypeAndUri(String type, String uri) - { - return decorated.getFromTypeAndUri(type, uri); - } - - /** - * {@inheritDoc} - * - * - * @param uri - * - * @return - */ - @Override - public Repository getFromUri(String uri) - { - return decorated.getFromUri(uri); - } - /** * {@inheritDoc} * diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java deleted file mode 100644 index f1649efb43..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java +++ /dev/null @@ -1,68 +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; - -/** - * Signals that the specified {@link Repository} could be found. - * - * @author Sebastian Sdorra - * @since 1.6 - */ -public class RepositoryNotFoundException extends RepositoryException -{ - - /** Field description */ - private static final long serialVersionUID = -6583078808900520166L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new {@link RepositoryNotFoundException} with null as its - * error detail message. - * - */ - public RepositoryNotFoundException() {} - - /** - * Constructs a new {@link RepositoryNotFoundException} with the specified - * error detail message. - * - * - * @param message error detail message - */ - public RepositoryNotFoundException(String message) - { - super(message); - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPathNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPathNotFoundException.java new file mode 100644 index 0000000000..96f76346b3 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPathNotFoundException.java @@ -0,0 +1,12 @@ + +package sonia.scm.repository; + +public class RepositoryPathNotFoundException extends Exception { + + public static final String REPOSITORY_PATH_NOT_FOUND = "Repository path not found"; + + public RepositoryPathNotFoundException() { + super(REPOSITORY_PATH_NOT_FOUND); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java new file mode 100644 index 0000000000..3099c1f74b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java @@ -0,0 +1,278 @@ +/* + 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.base.MoreObjects; +import com.google.common.base.Objects; +import sonia.scm.security.PermissionObject; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Collections.unmodifiableSet; + +//~--- JDK imports ------------------------------------------------------------ + +/** + * Permissions controls the access to {@link Repository}. + * This object should be immutable, but could not be due to mapstruct. Do not modify instances of this because this + * would change the hash code and therefor make it undeletable in a repository. + * + * @author Sebastian Sdorra + */ +@XmlRootElement(name = "permissions") +@XmlAccessorType(XmlAccessType.FIELD) +public class RepositoryPermission implements PermissionObject, Serializable +{ + + private static final long serialVersionUID = -2915175031430884040L; + public static final String REPOSITORY_MODIFIED_EXCEPTION_TEXT = "repository permission must not be modified"; + + private Boolean groupPermission; + private String name; + @XmlElement(name = "verb") + private Set verbs; + private String role; + + /** + * This constructor exists for mapstruct and JAXB, only -- do not use this in "normal" code. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} instead. + */ + @Deprecated + public RepositoryPermission() {} + + public RepositoryPermission(String name, Collection verbs, boolean groupPermission) + { + this.name = name; + this.verbs = new LinkedHashSet<>(verbs); + this.role = null; + this.groupPermission = groupPermission; + } + + public RepositoryPermission(String name, String role, boolean groupPermission) + { + this.name = name; + this.verbs = emptySet(); + this.role = role; + this.groupPermission = groupPermission; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Returns true if the {@link RepositoryPermission} is the same as the obj argument. + * + * + * @param obj the reference object with which to compare + * + * @return true if the {@link RepositoryPermission} is the same as the obj argument + */ + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + final RepositoryPermission other = (RepositoryPermission) obj; + + return Objects.equal(name, other.name) + && verbs.size() == other.verbs.size() + && verbs.containsAll(other.verbs) + && Objects.equal(role, other.role) + && Objects.equal(groupPermission, other.groupPermission); + } + + /** + * Returns the hash code value for the {@link RepositoryPermission}. + * + * + * @return the hash code value for the {@link RepositoryPermission} + */ + @Override + public int hashCode() + { + // Normally we do not have a log of repository permissions having the same size of verbs, but different content. + // Therefore we do not use the verbs themselves for the hash code but only the number of verbs. + return Objects.hashCode(name, verbs == null? -1: verbs.size(), role, groupPermission); + } + + + @Override + public String toString() + { + //J- + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("role", role) + .add("verbs", verbs) + .add("groupPermission", groupPermission) + .toString(); + //J+ + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns the name of the user or group. + * + * + * @return name of the user or group + */ + @Override + public String getName() + { + return name; + } + + /** + * Returns the verb of the permission. + * + * + * @return verb of the permission + */ + public Collection getVerbs() + { + return verbs == null ? emptyList() : Collections.unmodifiableSet(verbs); + } + + /** + * Returns the role of the permission. + * + * + * @return role of the permission + */ + public String getRole() { + return role; + } + + /** + * Returns true if the permission is a permission which affects a group. + * + * + * @return true if the permision is a group permission + */ + @Override + public boolean isGroupPermission() + { + return groupPermission; + } + + //~--- set methods ---------------------------------------------------------- + + /** + * Use this for creation only. This will throw an {@link IllegalStateException} when modified. + * @throws IllegalStateException when modified after the value has been set once. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} + * or {@link RepositoryPermission#RepositoryPermission(String, String, boolean)} instead. + */ + @Deprecated + public void setGroupPermission(boolean groupPermission) + { + if (this.groupPermission != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + this.groupPermission = groupPermission; + } + + /** + * Use this for creation only. This will throw an {@link IllegalStateException} when modified. + * @throws IllegalStateException when modified after the value has been set once. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} + * or {@link RepositoryPermission#RepositoryPermission(String, String, boolean)} instead. + */ + @Deprecated + public void setName(String name) + { + if (this.name != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + this.name = name; + } + + /** + * Use this for creation only. This will throw an {@link IllegalStateException} when modified. + * @throws IllegalStateException when modified after the value has been set once. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryPermission#RepositoryPermission(String, String, boolean)} instead. + */ + @Deprecated + public void setRole(String role) + { + if (this.role != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + this.role = role; + } + + /** + * Use this for creation only. This will throw an {@link IllegalStateException} when modified. + * @throws IllegalStateException when modified after the value has been set once. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} instead. + */ + @Deprecated + public void setVerbs(Collection verbs) + { + if (this.verbs != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + this.verbs = verbs == null? emptySet(): unmodifiableSet(new LinkedHashSet<>(verbs)); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java index 1a5300ad21..cea8574d14 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java @@ -6,13 +6,13 @@ * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE @@ -26,35 +26,21 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.throwingproviders.CheckedProvider; -import sonia.scm.security.ScmSecurityException; - /** * * @author Sebastian Sdorra * @since 1.10 */ -public interface RepositoryProvider extends CheckedProvider -{ - - /** - * Method description - * - * - * @return - * - * @throws ScmSecurityException - */ +public interface RepositoryProvider extends CheckedProvider { @Override - public Repository get() throws ScmSecurityException; + Repository get(); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRequestListener.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRequestListener.java index 219aa9cee2..409052fe19 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryRequestListener.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRequestListener.java @@ -37,12 +37,11 @@ package sonia.scm.repository; import sonia.scm.plugin.ExtensionPoint; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +//~--- JDK imports ------------------------------------------------------------ /** * Listener before a repository request is executed. Repository request are @@ -68,10 +67,6 @@ public interface RepositoryRequestListener * * @return false to abort the request * @throws IOException - * @throws RepositoryException */ - public boolean handleRequest(HttpServletRequest request, - HttpServletResponse response, - Repository repository) - throws IOException, RepositoryException; + boolean handleRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java new file mode 100644 index 0000000000..e5ca046aac --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java @@ -0,0 +1,230 @@ +/* + 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; + +import com.github.sdorra.ssp.PermissionObject; +import com.github.sdorra.ssp.StaticPermissions; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import sonia.scm.ModelObject; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Collections.unmodifiableSet; + +/** + * Custom role with specific permissions related to {@link Repository}. + * This object should be immutable, but could not be due to mapstruct. + */ +@StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"write"}) +@XmlRootElement(name = "roles") +@XmlAccessorType(XmlAccessType.FIELD) +public class RepositoryRole implements ModelObject, PermissionObject { + + private static final long serialVersionUID = -723588336073192740L; + + private static final String REPOSITORY_MODIFIED_EXCEPTION_TEXT = "roles must not be modified"; + + private String name; + @XmlElement(name = "verb") + private Set verbs; + + private Long creationDate; + private Long lastModified; + private String type; + + /** + * This constructor exists for mapstruct and JAXB, only -- do not use this in "normal" code. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryRole#RepositoryRole(String, Collection, String)} instead. + */ + @Deprecated + public RepositoryRole() {} + + public RepositoryRole(String name, Collection verbs, String type) { + this.name = name; + this.verbs = new LinkedHashSet<>(verbs); + this.type = type; + } + + /** + * Returns true if the {@link RepositoryRole} is the same as the obj argument. + * + * + * @param obj the reference object with which to compare + * + * @return true if the {@link RepositoryRole} is the same as the obj argument + */ + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + + final RepositoryRole other = (RepositoryRole) obj; + + return Objects.equal(name, other.name) + && verbs.size() == other.verbs.size() + && verbs.containsAll(other.verbs); + } + + /** + * Returns the hash code value for the {@link RepositoryRole}. + * + * + * @return the hash code value for the {@link RepositoryRole} + */ + @Override + public int hashCode() + { + // Normally we do not have a log of repository permissions having the same size of verbs, but different content. + // Therefore we do not use the verbs themselves for the hash code but only the number of verbs. + return Objects.hashCode(name, verbs == null? -1: verbs.size()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("verbs", verbs) + .toString(); + } + + public String getName() { + return name; + } + + /** + * Returns the verb of the role. + */ + public Collection getVerbs() { + return verbs == null ? emptyList() : Collections.unmodifiableSet(verbs); + } + + @Override + public String getId() { + return name; + } + + @Override + public void setLastModified(Long timestamp) { + this.lastModified = timestamp; + } + + @Override + public Long getCreationDate() { + return creationDate; + } + + @Override + public void setCreationDate(Long timestamp) { + this.creationDate = timestamp; + } + + @Override + public Long getLastModified() { + return lastModified; + } + + @Override + public String getType() { + return type; + } + + public void setType(String type) { + if (this.type != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + this.type = type; + } + + @Override + public boolean isValid() { + return !Strings.isNullOrEmpty(name) && !verbs.isEmpty(); + } + + /** + * Use this for creation only. This will throw an {@link IllegalStateException} when modified. + * @throws IllegalStateException when modified after the value has been set once. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryRole#RepositoryRole(String, Collection, String)} instead. + */ + @Deprecated + public void setName(String name) { + if (this.name != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + this.name = name; + } + + /** + * Use this for creation only. This will throw an {@link IllegalStateException} when modified. + * @throws IllegalStateException when modified after the value has been set once. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryRole#RepositoryRole(String, Collection, String)} instead. + */ + @Deprecated + public void setVerbs(Collection verbs) { + if (this.verbs != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } + this.verbs = verbs == null? emptySet(): unmodifiableSet(new LinkedHashSet<>(verbs)); + } + + @Override + public RepositoryRole clone() { + try { + return (RepositoryRole) super.clone(); + } catch (CloneNotSupportedException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java new file mode 100644 index 0000000000..3d7e53b3a2 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleDAO.java @@ -0,0 +1,10 @@ +package sonia.scm.repository; + +import sonia.scm.GenericDAO; + +import java.util.List; + +public interface RepositoryRoleDAO extends GenericDAO { + @Override + List getAll(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleEvent.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleEvent.java new file mode 100644 index 0000000000..fcd21bfbcd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleEvent.java @@ -0,0 +1,69 @@ +/** + * 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; + +import sonia.scm.HandlerEventType; +import sonia.scm.event.AbstractHandlerEvent; +import sonia.scm.event.Event; + +/** + * The RepositoryRoleEvent is fired if a repository role object changes. + * @since 2.0 + */ +@Event +public class RepositoryRoleEvent extends AbstractHandlerEvent { + + /** + * Constructs a new repositoryRole event. + * + * + * @param eventType event type + * @param repositoryRole changed repositoryRole + */ + public RepositoryRoleEvent(HandlerEventType eventType, RepositoryRole repositoryRole) { + super(eventType, repositoryRole); + } + + /** + * Constructs a new repositoryRole event. + * + * + * @param eventType type of the event + * @param repositoryRole changed repositoryRole + * @param oldRepositoryRole old repositoryRole + */ + public RepositoryRoleEvent(HandlerEventType eventType, RepositoryRole repositoryRole, RepositoryRole oldRepositoryRole) { + super(eventType, repositoryRole, oldRepositoryRole); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleManager.java new file mode 100644 index 0000000000..c7e1971110 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleManager.java @@ -0,0 +1,44 @@ +/** + * 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; + +import sonia.scm.Manager; +import sonia.scm.search.Searchable; + +/** + * The central class for managing {@link RepositoryRole} objects. + * This class is a singleton and is available via injection. + */ +public interface RepositoryRoleManager extends Manager { +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleModificationEvent.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleModificationEvent.java new file mode 100644 index 0000000000..eabeb26b2e --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRoleModificationEvent.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2014, 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; + +import sonia.scm.HandlerEventType; +import sonia.scm.ModificationHandlerEvent; +import sonia.scm.event.Event; + +/** + * Event which is fired whenever a repository role is modified. + * + * @since 2.0 + */ +@Event +public class RepositoryRoleModificationEvent extends RepositoryRoleEvent implements ModificationHandlerEvent +{ + + private final RepositoryRole itemBeforeModification; + + /** + * Constructs a new {@link RepositoryRoleModificationEvent}. + * + * @param eventType type of event + * @param item changed repository role + * @param itemBeforeModification changed repository role before it was modified + */ + public RepositoryRoleModificationEvent(HandlerEventType eventType, RepositoryRole item, RepositoryRole itemBeforeModification) + { + super(eventType, item); + this.itemBeforeModification = itemBeforeModification; + } + + @Override + public RepositoryRole getItemBeforeModification() + { + return itemBeforeModification; + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryUtil.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryUtil.java deleted file mode 100644 index fd955d27b7..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryUtil.java +++ /dev/null @@ -1,315 +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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.io.DirectoryFileFilter; -import sonia.scm.util.IOUtil; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; -import java.io.IOException; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - - -/** - * - * @author Sebastian Sdorra - * @since 1.11 - */ -public final class RepositoryUtil -{ - - /** the logger for RepositoryUtil */ - private static final Logger logger = - LoggerFactory.getLogger(RepositoryUtil.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - private RepositoryUtil() {} - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param directory - * @param names - * - * @return - */ - public static List searchRepositoryDirectories(File directory, - String... names) - { - List repositories = new ArrayList<>(); - - searchRepositoryDirectories(repositories, directory, Arrays.asList(names)); - - return repositories; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * - * @param handler - * @param directoryPath - * @return - * - * @throws IOException - */ - public static String getRepositoryName(AbstractRepositoryHandler handler, - String directoryPath) - throws IOException - { - return getRepositoryName(handler.getConfig().getRepositoryDirectory(), - new File(directoryPath)); - } - - /** - * Method description - * - * - * - * @param config - * @param directoryPath - * @return - * - * @throws IOException - */ - public static String getRepositoryName(SimpleRepositoryConfig config, - String directoryPath) - throws IOException - { - return getRepositoryName(config.getRepositoryDirectory(), - new File(directoryPath)); - } - - /** - * Method description - * - * - * - * @param handler - * @param directory - * @return - * - * @throws IOException - */ - public static String getRepositoryName(AbstractRepositoryHandler handler, - File directory) - throws IOException - { - return getRepositoryName(handler.getConfig().getRepositoryDirectory(), - directory); - } - - /** - * Method description - * - * - * - * @param config - * @param directory - * @return - * - * @throws IOException - */ - public static String getRepositoryName(SimpleRepositoryConfig config, - File directory) - throws IOException - { - return getRepositoryName(config.getRepositoryDirectory(), directory); - } - - /** - * Method description - * - * - * - * @param baseDirectory - * @param directory - * @return - * - * @throws IOException - */ - public static String getRepositoryName(File baseDirectory, File directory) - throws IOException - { - String name = null; - String path = directory.getCanonicalPath(); - int directoryLength = baseDirectory.getCanonicalPath().length(); - - if (directoryLength < path.length()) - { - name = IOUtil.trimSeperatorChars(path.substring(directoryLength)); - - // replace windows path seperator - name = name.replaceAll("\\\\", "/"); - } - else if (logger.isWarnEnabled()) - { - logger.warn("path is shorter as the main repository path"); - } - - return name; - } - - /** - * Method description - * - * - * @param handler - * @param directoryNames - * - * @return - * - * @throws IOException - */ - public static List getRepositoryNames( - AbstractRepositoryHandler handler, String... directoryNames) - throws IOException - { - return getRepositoryNames(handler.getConfig(), directoryNames); - } - - /** - * Method description - * - * - * @param config - * @param directoryNames - * - * @return - * - * @throws IOException - */ - public static List getRepositoryNames(SimpleRepositoryConfig config, - String... directoryNames) - throws IOException - { - return getRepositoryNames(config.getRepositoryDirectory(), directoryNames); - } - - /** - * Method description - * - * - * @param baseDirectory - * @param directoryNames - * - * @return - * - * @throws IOException - */ - public static List getRepositoryNames(File baseDirectory, - String... directoryNames) - throws IOException - { - List repositories = new ArrayList<>(); - List repositoryFiles = searchRepositoryDirectories(baseDirectory, - directoryNames); - - for (File file : repositoryFiles) - { - String name = getRepositoryName(baseDirectory, file); - - if (name != null) - { - repositories.add(name); - } - } - - return repositories; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repositories - * @param directory - * @param names - */ - private static void searchRepositoryDirectories(List repositories, - File directory, List names) - { - boolean found = false; - - for (String name : names) - { - if (new File(directory, name).exists()) - { - found = true; - - break; - } - } - - if (found) - { - repositories.add(directory); - } - else - { - File[] directories = directory.listFiles(DirectoryFileFilter.instance); - - if (directories != null) - { - for (File d : directories) - { - searchRepositoryDirectories(repositories, d, names); - } - } - } - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/RevisionNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RevisionNotFoundException.java deleted file mode 100644 index a0fbd3f6a4..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RevisionNotFoundException.java +++ /dev/null @@ -1,83 +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 sonia.scm.util.Util; - -/** - * Signals that the specified revision could be found. - * - * @author Sebastian Sdorra - */ -public class RevisionNotFoundException extends RepositoryException -{ - - /** Field description */ - private static final long serialVersionUID = -5594008535358811998L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new {@link RevisionNotFoundException} - * with the specified revision. - * - * - * @param revision revision which could not be found - */ - public RevisionNotFoundException(String revision) - { - super("revision \"".concat(Util.nonNull(revision)).concat("\" not found")); - this.revision = Util.nonNull(revision); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Return the revision which could not be found. - * - * - * @return revision which could not be found - */ - public String getRevision() - { - return revision; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String revision; -} diff --git a/scm-core/src/main/java/sonia/scm/repository/SimpleRepositoryConfig.java b/scm-core/src/main/java/sonia/scm/repository/SimpleRepositoryConfig.java deleted file mode 100644 index 3654b9c8b4..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/SimpleRepositoryConfig.java +++ /dev/null @@ -1,122 +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 sonia.scm.Validateable; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; - -import javax.xml.bind.annotation.XmlRootElement; - -/** - * Basic {@link Repository} configuration class. - * - * @author Sebastian Sdorra - */ -@XmlRootElement -public class SimpleRepositoryConfig implements Validateable -{ - - /** - * Returns the directory for the repositories. - * - * - * @return directory for the repositories - */ - public File getRepositoryDirectory() - { - return repositoryDirectory; - } - - /** - * Returns true if the plugin is disabled. - * - * - * @return true if the plugin is disabled - * @since 1.13 - */ - public boolean isDisabled() - { - return disabled; - } - - /** - * Returns true if the configuration object is valid. - * - * - * @return true if the configuration object is valid - */ - @Override - public boolean isValid() - { - return repositoryDirectory != null; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Enable or disable the plugin. - * - * - * @param disabled - * @since 1.13 - */ - public void setDisabled(boolean disabled) - { - this.disabled = disabled; - } - - /** - * Sets the directory for the repositories - * - * - * @param repositoryDirectory directory for repositories - */ - public void setRepositoryDirectory(File repositoryDirectory) - { - this.repositoryDirectory = repositoryDirectory; - } - - //~--- fields --------------------------------------------------------------- - - /** true if the plugin is disabled */ - private boolean disabled = false; - - /** directory for repositories */ - private File repositoryDirectory; -} diff --git a/scm-core/src/main/java/sonia/scm/repository/SubRepository.java b/scm-core/src/main/java/sonia/scm/repository/SubRepository.java index 87e8b1f1e7..55ff0e2269 100644 --- a/scm-core/src/main/java/sonia/scm/repository/SubRepository.java +++ b/scm-core/src/main/java/sonia/scm/repository/SubRepository.java @@ -158,10 +158,10 @@ public class SubRepository implements Serializable { //J- return MoreObjects.toStringHelper(this) - .add("repositoryUrl", repositoryUrl) - .add("browserUrl", browserUrl) - .add("revision", revision) - .toString(); + .add("repositoryUrl", repositoryUrl) + .add("browserUrl", browserUrl) + .add("revision", revision) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/Tag.java b/scm-core/src/main/java/sonia/scm/repository/Tag.java index b07c74ffa1..ae436c1a7a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Tag.java +++ b/scm-core/src/main/java/sonia/scm/repository/Tag.java @@ -37,12 +37,12 @@ package sonia.scm.repository; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -//~--- JDK imports ------------------------------------------------------------ - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +//~--- JDK imports ------------------------------------------------------------ + /** * Represents a tag in a repository. * @@ -126,9 +126,9 @@ public final class Tag { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("revision", revision) - .toString(); + .add("name", name) + .add("revision", revision) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/Tags.java b/scm-core/src/main/java/sonia/scm/repository/Tags.java index 8126d8d106..e89ed314ea 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Tags.java +++ b/scm-core/src/main/java/sonia/scm/repository/Tags.java @@ -139,8 +139,8 @@ public final class Tags implements Iterable { //J- return MoreObjects.toStringHelper(this) - .add("tags", tags) - .toString(); + .add("tags", tags) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/AbstractBundleOrUnbundleCommandResponse.java b/scm-core/src/main/java/sonia/scm/repository/api/AbstractBundleOrUnbundleCommandResponse.java index b9e8376fcd..d1d7ef5045 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/AbstractBundleOrUnbundleCommandResponse.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/AbstractBundleOrUnbundleCommandResponse.java @@ -35,7 +35,6 @@ package sonia.scm.repository.api; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; - /** * Abstract class for bundle or unbundle command. * @@ -97,8 +96,8 @@ public abstract class AbstractBundleOrUnbundleCommandResponse { //J- return MoreObjects.toStringHelper(this) - .add("changesetCount", changesetCount) - .toString(); + .add("changesetCount", changesetCount) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java new file mode 100644 index 0000000000..b5b2f2a08b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java @@ -0,0 +1,68 @@ +package sonia.scm.repository.api; + +import sonia.scm.FeatureNotSupportedException; +import sonia.scm.repository.Feature; +import sonia.scm.repository.spi.DiffCommandRequest; + +import java.util.Set; + +abstract class AbstractDiffCommandBuilder { + + + /** request for the diff command implementation */ + final DiffCommandRequest request = new DiffCommandRequest(); + + private final Set supportedFeatures; + + AbstractDiffCommandBuilder(Set supportedFeatures) { + this.supportedFeatures = supportedFeatures; + } + + /** + * Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given + * here. In other words: What changes would be new to the ancestor changeset given here when the branch would + * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! + * + * @return {@code this} + */ + public T setAncestorChangeset(String revision) + { + if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { + throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name()); + } + request.setAncestorChangeset(revision); + + return self(); + } + + /** + * Show the difference only for the given path. + * + * + * @param path path for difference + * + * @return {@code this} + */ + public T setPath(String path) + { + request.setPath(path); + return self(); + } + + /** + * Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this + * and another revision. + * + * + * @param revision revision for difference + * + * @return {@code this} + */ + public T setRevision(String revision) + { + request.setRevision(revision); + return self(); + } + + abstract T self(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java index 203a01b331..f55abd4598 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java @@ -38,25 +38,22 @@ package sonia.scm.repository.api; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.repository.BlameResult; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryCacheKey; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.BlameCommand; import sonia.scm.repository.spi.BlameCommandRequest; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; import java.io.Serializable; +//~--- JDK imports ------------------------------------------------------------ + /** * Shows changeset information by line for a given file. * Blame is also known as annotate in some SCM systems.
@@ -138,10 +135,9 @@ public final class BlameCommandBuilder * @throws IllegalArgumentException if the path is null or empty * * @throws IOException - * @throws RepositoryException */ public BlameResult getBlameResult(String path) - throws IOException, RepositoryException + throws IOException { Preconditions.checkArgument(!Strings.isNullOrEmpty(path), "path is required"); @@ -189,7 +185,7 @@ public final class BlameCommandBuilder if (!disablePreProcessors && (result != null)) { - preProcessorUtil.prepareForReturn(repository, result, !disableEscaping); + preProcessorUtil.prepareForReturn(repository, result); } return result; @@ -214,24 +210,6 @@ public final class BlameCommandBuilder return this; } - /** - * Disable html escaping for the returned blame lines. By default all - * blame lines are html escaped. - * - * - * @param disableEscaping true to disable the html escaping - * - * @return {@code this} - * - * @since 1.35 - */ - public BlameCommandBuilder setDisableEscaping(boolean disableEscaping) - { - this.disableEscaping = disableEscaping; - - return this; - } - /** * Disable the execution of pre processors. * @@ -366,9 +344,6 @@ public final class BlameCommandBuilder /** the cache */ private final Cache cache; - /** disable escaping */ - private boolean disableEscaping = false; - /** disable change */ private boolean disableCache = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java new file mode 100644 index 0000000000..33f4a482ac --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java @@ -0,0 +1,77 @@ +/** + * 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.api; + +import sonia.scm.repository.Branch; +import sonia.scm.repository.spi.BranchCommand; + +import java.io.IOException; + +/** + * @since 2.0 + */ +public final class BranchCommandBuilder { + + public BranchCommandBuilder(BranchCommand command) { + this.command = command; + } + + /** + * Specifies the source branch, which the new branch should be based on. + * + * @param parentBranch The base branch for the new branch. + * @return This builder. + */ + public BranchCommandBuilder from(String parentBranch) { + request.setParentBranch(parentBranch); + return this; + } + + /** + * Execute the command and create a new branch with the given name. + * @param name The name of the new branch. + * @return The created branch. + * @throws IOException + */ + public Branch branch(String name) { + request.setNewBranch(name); + return command.branch(request); + } + + public void delete(String branchName) { + command.deleteOrClose(branchName); + } + + private BranchCommand command; + private BranchRequest request = new BranchRequest(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchRequest.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchRequest.java new file mode 100644 index 0000000000..8473567156 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchRequest.java @@ -0,0 +1,22 @@ +package sonia.scm.repository.api; + +public class BranchRequest { + private String parentBranch; + private String newBranch; + + public String getParentBranch() { + return parentBranch; + } + + public void setParentBranch(String parentBranch) { + this.parentBranch = parentBranch; + } + + public String getNewBranch() { + return newBranch; + } + + public void setNewBranch(String newBranch) { + this.newBranch = newBranch; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java index 3deaf491d8..968f9ba39d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java @@ -29,30 +29,22 @@ * */ - package sonia.scm.repository.api; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Objects; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.repository.Branch; import sonia.scm.repository.Branches; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryCacheKey; -import sonia.scm.repository.RepositoryException; -import sonia.scm.repository.spi.BlameCommand; import sonia.scm.repository.spi.BranchesCommand; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; + /** * The branches command list all repository branches.
*
@@ -86,10 +78,8 @@ public final class BranchesCommandBuilder * only be called from the {@link RepositoryService}. * * @param cacheManager cache manager - * @param blameCommand implementation of the {@link BlameCommand} - * @param branchesCommand + * @param branchesCommand implementation of the {@link BranchesCommand} * @param repository repository to query - * @param preProcessorUtil */ BranchesCommandBuilder(CacheManager cacheManager, BranchesCommand branchesCommand, Repository repository) @@ -108,9 +98,8 @@ public final class BranchesCommandBuilder * @return branches from the repository * * @throws IOException - * @throws RepositoryException */ - public Branches getBranches() throws RepositoryException, IOException + public Branches getBranches() throws IOException { Branches branches; @@ -139,10 +128,7 @@ public final class BranchesCommandBuilder branches = getBranchesFromCommand(); - if (branches != null) - { - cache.put(key, branches); - } + cache.put(key, branches); } else if (logger.isDebugEnabled()) { @@ -182,10 +168,9 @@ public final class BranchesCommandBuilder * @return * * @throws IOException - * @throws RepositoryException */ private Branches getBranchesFromCommand() - throws RepositoryException, IOException + throws IOException { return new Branches(branchesCommand.getBranches()); } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index f37677f37b..d482c04ea4 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -36,29 +36,22 @@ package sonia.scm.repository.api; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Objects; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; -import sonia.scm.repository.FileObjectNameComparator; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryCacheKey; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.BrowseCommand; import sonia.scm.repository.spi.BrowseCommandRequest; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; import java.io.Serializable; -import java.util.Collections; -import java.util.List; +//~--- JDK imports ------------------------------------------------------------ /** * BrowseCommandBuilder is able to browse the files of a {@link Repository}. @@ -100,7 +93,7 @@ public final class BrowseCommandBuilder * only be called from the {@link RepositoryService}. * * @param cacheManager cache manager - * @param logCommand implementation of the {@link LogCommand} + * @param browseCommand implementation of the {@link BrowseCommand} * @param browseCommand * @param repository repository to query * @param preProcessorUtil @@ -140,11 +133,8 @@ public final class BrowseCommandBuilder * @return files for the given parameters * * @throws IOException - * @throws RepositoryException */ - public BrowserResult getBrowserResult() - throws IOException, RepositoryException - { + public BrowserResult getBrowserResult() throws IOException { BrowserResult result = null; if (disableCache) @@ -185,15 +175,7 @@ public final class BrowseCommandBuilder if (!disablePreProcessors && (result != null)) { - preProcessorUtil.prepareForReturn(repository, result, !disableEscaping); - - List fileObjects = result.getFiles(); - - if (fileObjects != null) - { - fileObjects.sort(FileObjectNameComparator.instance); - result.setFiles(fileObjects); - } + preProcessorUtil.prepareForReturn(repository, result); } return result; @@ -218,24 +200,6 @@ public final class BrowseCommandBuilder return this; } - /** - * Disable html escaping for the returned file objects. By default all - * file objects are html escaped. - * - * - * @param disableEscaping true to disable the html escaping - * - * @return {@code this} - * - * @since 1.35 - */ - public BrowseCommandBuilder setDisableEscaping(boolean disableEscaping) - { - this.disableEscaping = disableEscaping; - - return this; - } - /** * Disabling the last commit means that every call to * {@link FileObject#getDescription()} and @@ -439,9 +403,6 @@ public final class BrowseCommandBuilder /** cache */ private final Cache cache; - /** disable escaping */ - private boolean disableEscaping = false; - /** disables the cache */ private boolean disableCache = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java index 4a25c0df50..1f4defbd8b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java @@ -37,23 +37,21 @@ package sonia.scm.repository.api; import com.google.common.io.ByteSink; import com.google.common.io.Files; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.BundleCommand; import sonia.scm.repository.spi.BundleCommandRequest; -import static com.google.common.base.Preconditions.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; import java.io.OutputStream; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +//~--- JDK imports ------------------------------------------------------------ + /** * The bundle command dumps a repository to a byte source such as a file. The * created bundle can be restored to an empty repository with the @@ -94,11 +92,8 @@ public final class BundleCommandBuilder * @return bundle response * * @throws IOException - * @throws RepositoryException */ - public BundleResponse bundle(File outputFile) - throws IOException, RepositoryException - { + public BundleResponse bundle(File outputFile) throws IOException { checkArgument((outputFile != null) &&!outputFile.exists(), "file is null or exists already"); @@ -120,10 +115,9 @@ public final class BundleCommandBuilder * @return bundle response * * @throws IOException - * @throws RepositoryException */ public BundleResponse bundle(OutputStream outputStream) - throws IOException, RepositoryException + throws IOException { checkNotNull(outputStream, "output stream is required"); @@ -141,13 +135,12 @@ public final class BundleCommandBuilder * @return bundle response * * @throws IOException - * @throws RepositoryException */ public BundleResponse bundle(ByteSink sink) - throws IOException, RepositoryException + throws IOException { checkNotNull(sink, "byte sink is required"); - logger.info("bundle {} to byte sink"); + logger.info("bundle {} to byte sink", sink); return bundleCommand.bundle(new BundleCommandRequest(sink)); } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java index f7b4d75fbd..d1e0cbc5f1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java @@ -33,24 +33,18 @@ package sonia.scm.repository.api; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Preconditions; import com.google.common.base.Strings; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.CatCommand; import sonia.scm.repository.spi.CatCommandRequest; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; /** @@ -106,23 +100,31 @@ public final class CatCommandBuilder } /** - * Passes the content of the given file to the outputstream. + * Passes the content of the given file to the output stream. * - * @param outputStream outputstream for the content + * @param outputStream output stream for the content * @param path file path - * - * @return {@code this} - * - * @throws IOException - * @throws RepositoryException */ - public CatCommandBuilder retriveContent(OutputStream outputStream, - String path) - throws IOException, RepositoryException - { + public void retriveContent(OutputStream outputStream, String path) throws IOException { getCatResult(outputStream, path); + } - return this; + /** + * Returns an output stream with the file content. + * + * @param path file path + */ + public InputStream getStream(String path) throws IOException { + Preconditions.checkArgument(!Strings.isNullOrEmpty(path), + "path is required"); + + CatCommandRequest requestClone = request.clone(); + + requestClone.setPath(path); + + logger.debug("create cat stream for {}", requestClone); + + return catCommand.getCatResultStream(requestClone); } //~--- get methods ---------------------------------------------------------- @@ -134,10 +136,8 @@ public final class CatCommandBuilder * @return content of the file * * @throws IOException - * @throws RepositoryException */ - public String getContent(String path) throws IOException, RepositoryException - { + public String getContent(String path) throws IOException { String content = null; ByteArrayOutputStream baos = null; @@ -182,11 +182,9 @@ public final class CatCommandBuilder * @param path path of the file * * @throws IOException - * @throws RepositoryException */ private void getCatResult(OutputStream outputStream, String path) - throws IOException, RepositoryException - { + throws IOException { Preconditions.checkNotNull(outputStream, "OutputStream is required"); Preconditions.checkArgument(!Strings.isNullOrEmpty(path), "path is required"); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java index ccb0d8c2c0..8e95e314bb 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java @@ -61,5 +61,10 @@ public enum Command /** * @since 1.43 */ - BUNDLE, UNBUNDLE; + BUNDLE, UNBUNDLE, + + /** + * @since 2.0 + */ + MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH, MODIFY; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java index e1d7ba5f1a..03f4361083 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java @@ -36,20 +36,17 @@ package sonia.scm.repository.api; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Preconditions; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.Feature; import sonia.scm.repository.spi.DiffCommand; -import sonia.scm.repository.spi.DiffCommandRequest; -import sonia.scm.util.IOUtil; - -//~--- JDK imports ------------------------------------------------------------ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.Set; + +//~--- JDK imports ------------------------------------------------------------ /** * Shows differences between revisions for a specified file or @@ -72,7 +69,7 @@ import java.io.OutputStream; * @author Sebastian Sdorra * @since 1.17 */ -public final class DiffCommandBuilder +public final class DiffCommandBuilder extends AbstractDiffCommandBuilder { /** @@ -81,18 +78,21 @@ public final class DiffCommandBuilder private static final Logger logger = LoggerFactory.getLogger(DiffCommandBuilder.class); + /** implementation of the diff command */ + private final DiffCommand diffCommand; + //~--- constructors --------------------------------------------------------- /** * Constructs a new {@link DiffCommandBuilder}, this constructor should * only be called from the {@link RepositoryService}. * - * @param implementation of {@link DiffCommand} - * - * @param diffCommand + * @param diffCommand implementation of {@link DiffCommand} + * @param supportedFeatures The supported features of the provider */ - DiffCommandBuilder(DiffCommand diffCommand) + DiffCommandBuilder(DiffCommand diffCommand, Set supportedFeatures) { + super(supportedFeatures); this.diffCommand = diffCommand; } @@ -102,19 +102,12 @@ public final class DiffCommandBuilder * Passes the difference of the given parameter to the outputstream. * * - * @param outputStream outputstream for the difference - * - * @return {@code this} + * @return A consumer that expects the output stream for the difference * * @throws IOException - * @throws RepositoryException */ - public DiffCommandBuilder retriveContent(OutputStream outputStream) - throws IOException, RepositoryException - { - getDiffResult(outputStream); - - return this; + public OutputStreamConsumer retrieveContent() throws IOException { + return getDiffResult(); } //~--- get methods ---------------------------------------------------------- @@ -125,25 +118,12 @@ public final class DiffCommandBuilder * @return content of the difference * * @throws IOException - * @throws RepositoryException */ - public String getContent() throws IOException, RepositoryException - { - String content = null; - ByteArrayOutputStream baos = null; - - try - { - baos = new ByteArrayOutputStream(); - getDiffResult(baos); - content = baos.toString(); + public String getContent() throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + getDiffResult(); + return baos.toString(); } - finally - { - IOUtil.close(baos); - } - - return content; } //~--- set methods ---------------------------------------------------------- @@ -167,69 +147,31 @@ public final class DiffCommandBuilder return this; } - - /** - * Show the difference only for the given path. - * - * - * @param path path for difference - * - * @return {@code this} - */ - public DiffCommandBuilder setPath(String path) - { - request.setPath(path); - - return this; - } - - /** - * Show the difference only for the given revision. - * - * - * @param revision revision for difference - * - * @return {@code this} - */ - public DiffCommandBuilder setRevision(String revision) - { - request.setRevision(revision); - - return this; - } - //~--- get methods ---------------------------------------------------------- /** * Method description * * - * @param outputStream - * @param path - * * @throws IOException - * @throws RepositoryException + * @return */ - private void getDiffResult(OutputStream outputStream) - throws IOException, RepositoryException - { - Preconditions.checkNotNull(outputStream, "OutputStream is required"); + private OutputStreamConsumer getDiffResult() throws IOException { Preconditions.checkArgument(request.isValid(), "path and/or revision is required"); - if (logger.isDebugEnabled()) - { - logger.debug("create diff for {}", request); - } + logger.debug("create diff for {}", request); - diffCommand.getDiffResult(request, outputStream); + return diffCommand.getDiffResult(request); } - //~--- fields --------------------------------------------------------------- + @Override + DiffCommandBuilder self() { + return this; + } - /** implementation of the diff command */ - private final DiffCommand diffCommand; - - /** request for the diff command implementation */ - private final DiffCommandRequest request = new DiffCommandRequest(); + @FunctionalInterface + public interface OutputStreamConsumer { + void accept(OutputStream outputStream) throws IOException; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java new file mode 100644 index 0000000000..a3b1bafe0b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +public interface DiffFile extends Iterable { + + String getOldRevision(); + + String getNewRevision(); + + String getOldPath(); + + String getNewPath(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java new file mode 100644 index 0000000000..193e5e75d5 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +import java.util.OptionalInt; + +public interface DiffLine { + + OptionalInt getOldLineNumber(); + + OptionalInt getNewLineNumber(); + + String getContent(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java new file mode 100644 index 0000000000..b662db4e2d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java @@ -0,0 +1,8 @@ +package sonia.scm.repository.api; + +public interface DiffResult extends Iterable { + + String getOldRevision(); + + String getNewRevision(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java new file mode 100644 index 0000000000..7e152f3d0f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java @@ -0,0 +1,41 @@ +package sonia.scm.repository.api; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Feature; +import sonia.scm.repository.spi.DiffResultCommand; + +import java.io.IOException; +import java.util.Set; + +public class DiffResultCommandBuilder extends AbstractDiffCommandBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(DiffResultCommandBuilder.class); + + private final DiffResultCommand diffResultCommand; + + DiffResultCommandBuilder(DiffResultCommand diffResultCommand, Set supportedFeatures) { + super(supportedFeatures); + this.diffResultCommand = diffResultCommand; + } + + /** + * Returns the content of the difference as parsed objects. + * + * @return content of the difference + */ + public DiffResult getDiffResult() throws IOException { + Preconditions.checkArgument(request.isValid(), + "path and/or revision is required"); + + LOG.debug("create diff result for {}", request); + + return diffResultCommand.getDiffResult(request); + } + + @Override + DiffResultCommandBuilder self() { + return this; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java index fb3900445e..7e016db430 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java @@ -33,24 +33,27 @@ package sonia.scm.repository.api; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Function; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Streams; +import com.google.common.collect.Iterables; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import sonia.scm.io.DeepCopy; import sonia.scm.repository.Changeset; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.spi.HookChangesetProvider; import sonia.scm.repository.spi.HookChangesetRequest; - -import java.io.IOException; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; +import sonia.scm.repository.spi.HookChangesetResponse; //~--- JDK imports ------------------------------------------------------------ +import java.io.IOException; + +import java.util.List; + /** * The {@link HookChangesetBuilder} is able to return all {@link Changeset}s * which are added during the current push/commit. @@ -113,31 +116,39 @@ public final class HookChangesetBuilder */ public Iterable getChangesets() { - Iterable changesets = - provider.handleRequest(request).getChangesets(); + HookChangesetResponse hookChangesetResponse = provider.handleRequest(request); + Iterable changesets = hookChangesetResponse.getChangesets(); if (!disablePreProcessors) { - final Function changesetFunction = c -> { - Changeset copy = null; + changesets = Iterables.transform(changesets, + new Function() + { - try { - copy = DeepCopy.copy(c); - preProcessorUtil.prepareForReturn(repository, copy, - !disableEscaping); - } catch (IOException ex) { - logger.error("could not create a copy of changeset", ex); + @Override + public Changeset apply(Changeset c) + { + Changeset copy = null; + + try + { + copy = DeepCopy.copy(c); + preProcessorUtil.prepareForReturn(repository, copy); + } + catch (IOException ex) + { + logger.error("could not create a copy of changeset", ex); + } + + if (copy == null) + { + copy = c; + } + + return copy; } - if (copy == null) { - copy = c; - } - - return copy; - }; - changesets = Streams.stream(changesets) - .map(changesetFunction::apply) - .collect(Collectors.toList()); + }); } return changesets; @@ -145,24 +156,6 @@ public final class HookChangesetBuilder //~--- set methods ---------------------------------------------------------- - /** - * Disable html escaping for the returned changesets. By default all - * changesets are html escaped. - * - * - * @param disableEscaping true to disable the html escaping - * - * @return {@code this} - * - * @since 1.35 - */ - public HookChangesetBuilder setDisableEscaping(boolean disableEscaping) - { - this.disableEscaping = disableEscaping; - - return this; - } - /** * Disable the execution of pre processors. * @@ -181,9 +174,6 @@ public final class HookChangesetBuilder //~--- fields --------------------------------------------------------------- - /** disable escaping */ - private boolean disableEscaping = false; - /** disable pre processors marker */ private boolean disablePreProcessors = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java new file mode 100644 index 0000000000..c8a3e1ebca --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java @@ -0,0 +1,24 @@ +package sonia.scm.repository.api; + +public interface Hunk extends Iterable { + + default String getRawHeader() { + return String.format("@@ -%s +%s @@", getLineMarker(getOldStart(), getOldLineCount()), getLineMarker(getNewStart(), getNewLineCount())); + } + + default String getLineMarker(int start, int lineCount) { + if (lineCount == 1) { + return Integer.toString(start); + } else { + return String.format("%s,%s", start, lineCount); + } + } + + int getOldStart(); + + int getOldLineCount(); + + int getNewStart(); + + int getNewLineCount(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java index 747cb837a0..6098bdf92b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java @@ -36,22 +36,19 @@ package sonia.scm.repository.api; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; - import sonia.scm.cache.CacheManager; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.IncomingCommand; import sonia.scm.repository.spi.IncomingCommandRequest; -import sonia.scm.security.RepositoryPermission; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * The incoming command shows new {@link Changeset}s found in a different * repository location. @@ -66,8 +63,6 @@ public final class IncomingCommandBuilder * Constructs a new {@link IncomingCommandBuilder}, this constructor should * only be called from the {@link RepositoryService}. * - * @param cacheManager cache manager - * * @param cacheManger * @param command implementation of the {@link IncomingCommand} * @param repository repository to query @@ -91,16 +86,14 @@ public final class IncomingCommandBuilder * @return incoming changesets * * @throws IOException - * @throws RepositoryException */ public ChangesetPagingResult getIncomingChangesets( Repository remoteRepository) - throws IOException, RepositoryException + throws IOException { Subject subject = SecurityUtils.getSubject(); - subject.checkPermission(new RepositoryPermission(remoteRepository, - PermissionType.READ)); + subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString()); request.setRemoteRepository(remoteRepository); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java index de5e2b1b9f..2836fc537d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java @@ -37,25 +37,25 @@ package sonia.scm.repository.api; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import sonia.scm.FeatureNotSupportedException; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.Feature; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryCacheKey; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.LogCommand; import sonia.scm.repository.spi.LogCommandRequest; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; import java.io.Serializable; +import java.util.Set; + +//~--- JDK imports ------------------------------------------------------------ /** * LogCommandBuilder is able to show the history of a file in a @@ -107,19 +107,20 @@ public final class LogCommandBuilder /** * Constructs a new {@link LogCommandBuilder}, this constructor should * only be called from the {@link RepositoryService}. - * - * @param cacheManager cache manager + * @param cacheManager cache manager * @param logCommand implementation of the {@link LogCommand} * @param repository repository to query * @param preProcessorUtil + * @param supportedFeatures The supported features of the provider */ LogCommandBuilder(CacheManager cacheManager, LogCommand logCommand, - Repository repository, PreProcessorUtil preProcessorUtil) + Repository repository, PreProcessorUtil preProcessorUtil, Set supportedFeatures) { this.cache = cacheManager.getCache(CACHE_NAME); this.logCommand = logCommand; this.repository = repository; this.preProcessorUtil = preProcessorUtil; + this.supportedFeatures = supportedFeatures; } //~--- methods -------------------------------------------------------------- @@ -166,11 +167,8 @@ public final class LogCommandBuilder * @return the {@link Changeset} with the given id or null * * @throws IOException - * @throws RepositoryException */ - public Changeset getChangeset(String id) - throws IOException, RepositoryException - { + public Changeset getChangeset(String id) throws IOException { Changeset changeset; if (disableCache) @@ -180,7 +178,7 @@ public final class LogCommandBuilder logger.debug("get changeset for {} with disabled cache", id); } - changeset = logCommand.getChangeset(id); + changeset = logCommand.getChangeset(id, request); } else { @@ -194,7 +192,7 @@ public final class LogCommandBuilder logger.debug("get changeset for {}", id); } - changeset = logCommand.getChangeset(id); + changeset = logCommand.getChangeset(id, request); if (changeset != null) { @@ -228,11 +226,8 @@ public final class LogCommandBuilder * @return all changesets with the given parameters * * @throws IOException - * @throws RepositoryException */ - public ChangesetPagingResult getChangesets() - throws IOException, RepositoryException - { + public ChangesetPagingResult getChangesets() throws IOException { ChangesetPagingResult cpr; if (disableCache) @@ -272,7 +267,7 @@ public final class LogCommandBuilder if (!disablePreProcessors && (cpr != null)) { - preProcessorUtil.prepareForReturn(repository, cpr, !disableEscaping); + preProcessorUtil.prepareForReturn(repository, cpr); } return cpr; @@ -314,24 +309,6 @@ public final class LogCommandBuilder return this; } - /** - * Disable html escaping for the returned changesets. By default all - * changesets are html escaped. - * - * - * @param disableEscaping true to disable the html escaping - * - * @return {@code this} - * - * @since 1.35 - */ - public LogCommandBuilder setDisableEscaping(boolean disableEscaping) - { - this.disableEscaping = disableEscaping; - - return this; - } - /** * Disable the execution of pre processors. * @@ -424,6 +401,21 @@ public final class LogCommandBuilder return this; } + /** + * Compute the incoming changes of the branch set with {@link #setBranch(String)} in respect to the changeset given + * here. In other words: What changesets would be new to the ancestor changeset given here when the branch would + * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! + * + * @return {@code this} + */ + public LogCommandBuilder setAncestorChangeset(String ancestorChangeset) { + if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { + throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name()); + } + request.setAncestorChangeset(ancestorChangeset); + return this; + } + //~--- inner classes -------------------------------------------------------- /** @@ -549,13 +541,11 @@ public final class LogCommandBuilder /** Field description */ private final PreProcessorUtil preProcessorUtil; + private Set supportedFeatures; /** repository to query */ private final Repository repository; - /** disable escaping */ - private boolean disableEscaping = false; - /** disable cache */ private boolean disableCache = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java new file mode 100644 index 0000000000..6c4324c259 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java @@ -0,0 +1,180 @@ +package sonia.scm.repository.api; + +import com.google.common.base.Preconditions; +import sonia.scm.repository.Person; +import sonia.scm.repository.spi.MergeCommand; +import sonia.scm.repository.spi.MergeCommandRequest; +import sonia.scm.repository.util.AuthorUtil; + +import java.util.Set; + +/** + * Use this {@link MergeCommandBuilder} to merge two branches of a repository ({@link #executeMerge()}) or to check if + * the branches could be merged without conflicts ({@link #dryRun()}). To do so, you have to specify the name of + * the target branch ({@link #setTargetBranch(String)}) and the name of the branch that should be merged + * ({@link #setBranchToMerge(String)}). Additionally you can specify an author that should be used for the commit + * ({@link #setAuthor(Person)}) and a message template ({@link #setMessageTemplate(String)}) if you are not doing a dry + * run only. If no author is specified, the logged in user and a default message will be used instead. + * + * To actually merge feature_branch into integration_branch do this: + *


+ *     repositoryService.getMergeCommand()
+ *       .setBranchToMerge("feature_branch")
+ *       .setTargetBranch("integration_branch")
+ *       .executeMerge();
+ * 
+ * + * If the merge is successful, the result will look like this: + *

+ *                            O    <- Merge result (new head of integration_branch)
+ *                            |\
+ *                            | \
+ *  old integration_branch -> O  O <- feature_branch
+ *                            |  |
+ *                            O  O
+ * 
+ * + * To check whether they can be merged without conflicts beforehand do this: + *

+ *     repositoryService.getMergeCommand()
+ *       .setBranchToMerge("feature_branch")
+ *       .setTargetBranch("integration_branch")
+ *       .dryRun()
+ *       .isMergeable();
+ * 
+ * + * Keep in mind that you should always check the result of a merge even though you may have done a dry run + * beforehand, because the branches can change between the dry run and the actual merge. + * + * @since 2.0.0 + */ +public class MergeCommandBuilder { + + private final MergeCommand mergeCommand; + private final MergeCommandRequest request = new MergeCommandRequest(); + + MergeCommandBuilder(MergeCommand mergeCommand) { + this.mergeCommand = mergeCommand; + } + + /** + * Use this to check if merge-strategy is supported by mergeCommand. + * + * @return boolean. + */ + public boolean isSupported(MergeStrategy strategy) { + return mergeCommand.isSupported(strategy); + } + + /** + * Use this to get a Set of all supported merge strategies by merge command. + * + * @return boolean. + */ + public Set getSupportedMergeStrategies() { + return mergeCommand.getSupportedMergeStrategies(); + } + + /** + * Use this to set the branch that should be merged into the target branch. + * + * This is mandatory. + * + * @return This builder instance. + */ + public MergeCommandBuilder setBranchToMerge(String branchToMerge) { + request.setBranchToMerge(branchToMerge); + return this; + } + + /** + * Use this to set the target branch the other branch should be merged into. + * + * This is mandatory. + * + * @return This builder instance. + */ + public MergeCommandBuilder setTargetBranch(String targetBranch) { + request.setTargetBranch(targetBranch); + return this; + } + + /** + * Use this to set the author of the merge commit manually. If this is omitted, the currently logged in user will be + * used instead. + * + * This is optional and for {@link #executeMerge()} only. + * + * @return This builder instance. + */ + public MergeCommandBuilder setAuthor(Person author) { + request.setAuthor(author); + return this; + } + + /** + * Use this to set the strategy of the merge commit manually. + * + * This is optional and for {@link #executeMerge()} only. + * + * @return This builder instance. + */ + public MergeCommandBuilder setMergeStrategy(MergeStrategy strategy) { + if (!mergeCommand.isSupported(strategy)) { + throw new IllegalArgumentException("merge strategy not supported: " + strategy); + } + request.setMergeStrategy(strategy); + return this; + } + + /** + * Use this to set a template for the commit message. If no message is set, a default message will be used. + * + * You can use the placeholder {0} for the branch to be merged and {1} for the target + * branch, eg.: + * + *

+   * ...setMessageTemplate("Merge of {0} into {1}")...
+   * 
+ * + * This is optional and for {@link #executeMerge()} only. + * + * @return This builder instance. + */ + public MergeCommandBuilder setMessageTemplate(String messageTemplate) { + request.setMessageTemplate(messageTemplate); + return this; + } + + /** + * Use this to reset the command. + * @return This builder instance. + */ + public MergeCommandBuilder reset() { + request.reset(); + return this; + } + + /** + * Use this to actually do the merge. If an automatic merge is not possible, {@link MergeCommandResult#isSuccess()} + * will return false. + * + * @return The result of the merge. + */ + public MergeCommandResult executeMerge() { + AuthorUtil.setAuthorIfNotAvailable(request); + Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required"); + return mergeCommand.merge(request); + } + + /** + * Use this to check whether the given branches can be merged autmatically. If this is possible, + * {@link MergeDryRunCommandResult#isMergeable()} will return true. + * + * @return The result whether the given branches can be merged automatically. + */ + public MergeDryRunCommandResult dryRun() { + Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required"); + return mergeCommand.dryRun(request); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java new file mode 100644 index 0000000000..53f712cddc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java @@ -0,0 +1,44 @@ +package sonia.scm.repository.api; + +import java.util.Collection; +import java.util.HashSet; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableCollection; + +/** + * This class keeps the result of a merge of branches. Use {@link #isSuccess()} to check whether the merge was + * sucessfully executed. If the result is false the merge could not be done without conflicts. In this + * case you can use {@link #getFilesWithConflict()} to get a list of files with merge conflicts. + */ +public class MergeCommandResult { + private final Collection filesWithConflict; + + private MergeCommandResult(Collection filesWithConflict) { + this.filesWithConflict = filesWithConflict; + } + + public static MergeCommandResult success() { + return new MergeCommandResult(emptyList()); + } + + public static MergeCommandResult failure(Collection filesWithConflict) { + return new MergeCommandResult(new HashSet<>(filesWithConflict)); + } + + /** + * If this returns true, the merge was successfull. If this returns false there were + * merge conflicts. In this case you can use {@link #getFilesWithConflict()} to check what files could not be merged. + */ + public boolean isSuccess() { + return filesWithConflict.isEmpty(); + } + + /** + * If the merge was not successful ({@link #isSuccess()} returns false) this will give you a list of + * file paths that could not be merged automatically. + */ + public Collection getFilesWithConflict() { + return unmodifiableCollection(filesWithConflict); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeDryRunCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeDryRunCommandResult.java new file mode 100644 index 0000000000..6ebb330aae --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeDryRunCommandResult.java @@ -0,0 +1,22 @@ +package sonia.scm.repository.api; + +/** + * This class keeps the result of a merge dry run. Use {@link #isMergeable()} to check whether an automatic merge is + * possible or not. + */ +public class MergeDryRunCommandResult { + + private final boolean mergeable; + + public MergeDryRunCommandResult(boolean mergeable) { + this.mergeable = mergeable; + } + + /** + * This will return true, when an automatic merge is possible at the moment; false + * otherwise. + */ + public boolean isMergeable() { + return mergeable; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java new file mode 100644 index 0000000000..4ccf1f706b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java @@ -0,0 +1,7 @@ +package sonia.scm.repository.api; + +public enum MergeStrategy { + MERGE_COMMIT, + FAST_FORWARD_IF_POSSIBLE, + SQUASH +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategyNotSupportedException.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategyNotSupportedException.java new file mode 100644 index 0000000000..ae9fbb08b4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategyNotSupportedException.java @@ -0,0 +1,27 @@ +package sonia.scm.repository.api; + +import sonia.scm.BadRequestException; +import sonia.scm.repository.Repository; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class MergeStrategyNotSupportedException extends BadRequestException { + + private static final long serialVersionUID = 256498734456613496L; + + private static final String CODE = "6eRhF9gU41"; + + public MergeStrategyNotSupportedException(Repository repository, MergeStrategy strategy) { + super(entity(repository).build(), createMessage(strategy)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(MergeStrategy strategy) { + return "merge strategy " + strategy + " is not supported by this repository"; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java new file mode 100644 index 0000000000..498746cc60 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java @@ -0,0 +1,109 @@ +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.spi.ModificationsCommand; +import sonia.scm.repository.spi.ModificationsCommandRequest; + +import java.io.IOException; + +/** + * Get the modifications applied to files in a revision. + *

+ * Modifications are for example: Add, Update and Delete + * + * @author Mohamed Karray + * @since 2.0 + */ +@Slf4j +@RequiredArgsConstructor +@Accessors(fluent = true) +public final class ModificationsCommandBuilder { + static final String CACHE_NAME = "sonia.cache.cmd.modifications"; + + private final ModificationsCommand modificationsCommand; + + private final ModificationsCommandRequest request = new ModificationsCommandRequest(); + + private final Repository repository; + + private final Cache cache; + + private final PreProcessorUtil preProcessorUtil; + + @Setter + private boolean disableCache = false; + + @Setter + private boolean disablePreProcessors = false; + + public ModificationsCommandBuilder revision(String revision){ + request.setRevision(revision); + return this; + } + + /** + * Reset each parameter to its default value. + * + * + * @return {@code this} + */ + public ModificationsCommandBuilder reset() { + request.reset(); + this.disableCache = false; + this.disablePreProcessors = false; + return this; + } + + public Modifications getModifications() throws IOException { + Modifications modifications; + if (disableCache) { + log.info("Get modifications for {} with disabled cache", request); + modifications = modificationsCommand.getModifications(request); + } else { + ModificationsCommandBuilder.CacheKey key = new ModificationsCommandBuilder.CacheKey(repository.getId(), request); + if (cache.contains(key)) { + modifications = cache.get(key); + log.debug("Get modifications for {} from the cache", request); + } else { + log.info("Get modifications for {} with enabled cache", request); + modifications = modificationsCommand.getModifications(request); + if (modifications != null) { + cache.put(key, modifications); + log.debug("Modifications for {} added to the cache with key {}", request, key); + } + } + } + if (!disablePreProcessors && (modifications != null)) { + preProcessorUtil.prepareForReturn(repository, modifications); + } + return modifications; + } + + @AllArgsConstructor + @Getter + @Setter + @EqualsAndHashCode + @ToString + class CacheKey implements RepositoryCacheKey { + private final String repositoryId; + private final ModificationsCommandRequest request; + + @Override + public String getRepositoryId() { + return repositoryId; + } + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java new file mode 100644 index 0000000000..05c1babe03 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -0,0 +1,243 @@ +package sonia.scm.repository.api; + +import com.google.common.base.Preconditions; +import com.google.common.io.ByteSource; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Person; +import sonia.scm.repository.spi.ModifyCommand; +import sonia.scm.repository.spi.ModifyCommandRequest; +import sonia.scm.repository.util.AuthorUtil; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.util.IOUtil; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Use this {@link ModifyCommandBuilder} to make file changes to the head of a branch. You can + *

    + *
  • create new files ({@link #createFile(String)} (with the option to overwrite a file, if it already exists; by + * default a {@link sonia.scm.AlreadyExistsException} will be thrown)
  • + *
  • modify existing files ({@link #modifyFile(String)}
  • + *
  • delete existing files ({@link #deleteFile(String)}
  • + *
  • move/rename existing files ({@link #moveFile(String, String)}
  • + *
+ * + * You can collect multiple changes before they are executed with a call to {@link #execute()}. + * + *

Example: + *

+ * commandBuilder
+ *     .setBranch("feature/branch")
+ *     .setCommitMessage("make some changes")
+ *     .setAuthor(new Person())
+ *     .createFile("file/to/create").withData(inputStream)
+ *     .deleteFile("old/file/to/delete")
+ *     .execute();
+ * 
+ *

+ */ +public class ModifyCommandBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandBuilder.class); + + private final ModifyCommand command; + private final File workdir; + + private final ModifyCommandRequest request = new ModifyCommandRequest(); + + ModifyCommandBuilder(ModifyCommand command, WorkdirProvider workdirProvider) { + this.command = command; + this.workdir = workdirProvider.createNewWorkdir(); + } + + /** + * Create a new file. The content of the file will be specified in a subsequent call to + * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}. + * By default, an {@link sonia.scm.AlreadyExistsException} will be thrown, when there already + * exists a file with the given path. You can disable this setting + * {@link WithOverwriteFlagContentLoader#setOverwrite(boolean)} to true. + * @param path The path and the name of the file that should be created. + * @return The loader to specify the content of the new file. + */ + public WithOverwriteFlagContentLoader createFile(String path) { + return new WithOverwriteFlagContentLoader( + (content, overwrite) -> request.addRequest(new ModifyCommandRequest.CreateFileRequest(path, content, overwrite)) + ); + } + + /** + * Modify an existing file. The new content of the file will be specified in a subsequent call to + * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}. + * @param path The path and the name of the file that should be modified. + * @return The loader to specify the new content of the file. + */ + public SimpleContentLoader modifyFile(String path) { + return new SimpleContentLoader( + content -> request.addRequest(new ModifyCommandRequest.ModifyFileRequest(path, content)) + ); + } + + /** + * Delete an existing file. + * @param path The path and the name of the file that should be deleted. + * @return This builder instance. + */ + public ModifyCommandBuilder deleteFile(String path) { + request.addRequest(new ModifyCommandRequest.DeleteFileRequest(path)); + return this; + } + + /** + * Apply the changes and create a new commit with the given message and author. + * @return The revision of the new commit. + */ + public String execute() { + AuthorUtil.setAuthorIfNotAvailable(request); + try { + Preconditions.checkArgument(request.isValid(), "commit message, branch and at least one request are required"); + return command.execute(request); + } finally { + try { + IOUtil.delete(workdir); + } catch (IOException e) { + LOG.warn("could not delete temporary workdir '{}'", workdir, e); + } + } + } + + /** + * Set the commit message for the new commit. + * @return This builder instance. + */ + public ModifyCommandBuilder setCommitMessage(String message) { + request.setCommitMessage(message); + return this; + } + + /** + * Set the author for the new commit. + * @return This builder instance. + */ + public ModifyCommandBuilder setAuthor(Person author) { + request.setAuthor(author); + return this; + } + + /** + * Set the branch the changes should be made upon. + * @return This builder instance. + */ + public ModifyCommandBuilder setBranch(String branch) { + request.setBranch(branch); + return this; + } + + /** + * Set the expected revision of the branch, before the changes are applied. If the branch does not have the + * expected revision, a concurrent modification exception will be thrown when the command is executed and no + * changes will be applied. + * @return This builder instance. + */ + public ModifyCommandBuilder setExpectedRevision(String expectedRevision) { + request.setExpectedRevision(expectedRevision); + return this; + } + + public interface ContentLoader { + /** + * Specify the data of the file using a {@link ByteSource}. + * + * @return The builder instance. + * @throws IOException If the data could not be read. + */ + ModifyCommandBuilder withData(ByteSource data) throws IOException; + + /** + * Specify the data of the file using an {@link InputStream}. + * @return The builder instance. + * @throws IOException If the data could not be read. + */ + ModifyCommandBuilder withData(InputStream data) throws IOException; + } + + public class SimpleContentLoader implements ContentLoader { + + private final Consumer contentConsumer; + + private SimpleContentLoader(Consumer contentConsumer) { + this.contentConsumer = contentConsumer; + } + + @Override + public ModifyCommandBuilder withData(ByteSource data) throws IOException { + File content = loadData(data); + contentConsumer.accept(content); + return ModifyCommandBuilder.this; + } + + @Override + public ModifyCommandBuilder withData(InputStream data) throws IOException { + File content = loadData(data); + contentConsumer.accept(content); + return ModifyCommandBuilder.this; + } + } + + public class WithOverwriteFlagContentLoader implements ContentLoader { + + private final ContentLoader contentLoader; + private boolean overwrite = false; + + private WithOverwriteFlagContentLoader(BiConsumer contentConsumer) { + this.contentLoader = new SimpleContentLoader(file -> contentConsumer.accept(file, overwrite)); + } + + /** + * Set this to true to overwrite the file if it already exists. Otherwise an + * {@link sonia.scm.AlreadyExistsException} will be thrown. + * @return This loader instance. + */ + public WithOverwriteFlagContentLoader setOverwrite(boolean overwrite) { + this.overwrite = overwrite; + return this; + } + + @Override + public ModifyCommandBuilder withData(ByteSource data) throws IOException { + return contentLoader.withData(data); + } + + @Override + public ModifyCommandBuilder withData(InputStream data) throws IOException { + return contentLoader.withData(data); + } + } + + @SuppressWarnings("UnstableApiUsage") // Files only used internal + private File loadData(ByteSource data) throws IOException { + File file = createTemporaryFile(); + data.copyTo(Files.asByteSink(file)); + return file; + } + + @SuppressWarnings("UnstableApiUsage") // Files and ByteStreams only used internal + private File loadData(InputStream data) throws IOException { + File file = createTemporaryFile(); + try (OutputStream out = Files.asByteSink(file).openBufferedStream()) { + ByteStreams.copy(data, out); + } + return file; + } + + private File createTemporaryFile() throws IOException { + return File.createTempFile("upload-", "", workdir); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java index def8c3ae7f..d39c95e0e2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java @@ -30,18 +30,17 @@ */ package sonia.scm.repository.api; -import java.io.IOException; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import sonia.scm.cache.CacheManager; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.OutgoingCommand; import sonia.scm.repository.spi.OutgoingCommandRequest; -import sonia.scm.security.RepositoryPermission; + +import java.io.IOException; /** * Show changesets not found in a remote repository. @@ -62,7 +61,7 @@ public final class OutgoingCommandBuilder * @param repository repository to query * @param preProcessorUtil pre processor util */ - OutgoingCommandBuilder(CacheManager cacheManger, OutgoingCommand command, + OutgoingCommandBuilder(CacheManager cacheManager, OutgoingCommand command, Repository repository, PreProcessorUtil preProcessorUtil) { this.command = command; @@ -80,12 +79,11 @@ public final class OutgoingCommandBuilder * @return outgoing changesets */ public ChangesetPagingResult getOutgoingChangesets( - Repository remoteRepository) throws IOException, RepositoryException + Repository remoteRepository) throws IOException { Subject subject = SecurityUtils.getSubject(); - subject.checkPermission(new RepositoryPermission(remoteRepository, - PermissionType.READ)); + subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString()); request.setRemoteRepository(remoteRepository); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java index 528c93eef9..969ec6ef11 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java @@ -36,22 +36,18 @@ package sonia.scm.repository.api; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.PullCommand; import sonia.scm.repository.spi.PullCommandRequest; -import sonia.scm.security.RepositoryPermission; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; import java.net.URL; +//~--- JDK imports ------------------------------------------------------------ + /** * The pull command pull changes from a other repository. * @@ -93,18 +89,13 @@ public final class PullCommandBuilder * @return informations over the executed pull command * * @throws IOException - * @throws RepositoryException - * + * * @since 1.43 */ - public PullResponse pull(String url) - throws IOException, RepositoryException - { + public PullResponse pull(String url) throws IOException { Subject subject = SecurityUtils.getSubject(); //J- - subject.checkPermission( - new RepositoryPermission(localRepository, PermissionType.WRITE) - ); + subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString()); //J+ URL remoteUrl = new URL(url); @@ -125,20 +116,13 @@ public final class PullCommandBuilder * @return informations over the executed pull command * * @throws IOException - * @throws RepositoryException */ - public PullResponse pull(Repository remoteRepository) - throws IOException, RepositoryException - { + public PullResponse pull(Repository remoteRepository) throws IOException { Subject subject = SecurityUtils.getSubject(); //J- - subject.checkPermission( - new RepositoryPermission(localRepository, PermissionType.WRITE) - ); - subject.checkPermission( - new RepositoryPermission(remoteRepository, PermissionType.READ) - ); + subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString()); + subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString()); //J+ request.reset(); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java index fabe938ff7..a734225281 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java @@ -37,23 +37,18 @@ package sonia.scm.repository.api; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.PushCommand; import sonia.scm.repository.spi.PushCommandRequest; -import sonia.scm.security.RepositoryPermission; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; - import java.net.URL; +//~--- JDK imports ------------------------------------------------------------ + /** * The push command push changes to a other repository. * @@ -91,17 +86,12 @@ public final class PushCommandBuilder * @return informations of the executed push command * * @throws IOException - * @throws RepositoryException */ - public PushResponse push(Repository remoteRepository) - throws IOException, RepositoryException - { + public PushResponse push(Repository remoteRepository) throws IOException { Subject subject = SecurityUtils.getSubject(); //J- - subject.checkPermission( - new RepositoryPermission(remoteRepository, PermissionType.WRITE) - ); + subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString()); //J+ logger.info("push changes to repository {}", remoteRepository.getId()); @@ -120,12 +110,10 @@ public final class PushCommandBuilder * @return informations of the executed push command * * @throws IOException - * @throws RepositoryException * * @since 1.43 */ - public PushResponse push(String url) throws IOException, RepositoryException - { + public PushResponse push(String url) throws IOException { URL remoteUrl = new URL(url); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index 5ae4b33c33..080d66ebf2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,31 +24,28 @@ * 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.api; -//~--- non-JDK imports -------------------------------------------------------- - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.cache.CacheManager; import sonia.scm.repository.Changeset; import sonia.scm.repository.Feature; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.RepositoryServiceProvider; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.util.WorkdirProvider; import java.io.Closeable; import java.io.IOException; +import java.util.Set; +import java.util.stream.Stream; /** * From the {@link RepositoryService} it is possible to access all commands for @@ -68,8 +65,6 @@ import java.io.IOException; * {@link #close()} method. * * @author Sebastian Sdorra - * @since 1.17 - * * @apiviz.uses sonia.scm.repository.Feature * @apiviz.uses sonia.scm.repository.api.Command * @apiviz.uses sonia.scm.repository.api.BlameCommandBuilder @@ -85,39 +80,39 @@ import java.io.IOException; * @apiviz.uses sonia.scm.repository.api.PushCommandBuilder * @apiviz.uses sonia.scm.repository.api.BundleCommandBuilder * @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder + * @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder + * @since 1.17 */ -public final class RepositoryService implements Closeable -{ +public final class RepositoryService implements Closeable { - /** - * the logger for RepositoryService - */ - private static final Logger logger = - LoggerFactory.getLogger(RepositoryService.class); + private static final Logger LOG = LoggerFactory.getLogger(RepositoryService.class); - //~--- constructors --------------------------------------------------------- + private final CacheManager cacheManager; + private final PreProcessorUtil preProcessorUtil; + private final RepositoryServiceProvider provider; + private final Repository repository; + private final Set protocolProviders; + private final WorkdirProvider workdirProvider; /** * Constructs a new {@link RepositoryService}. This constructor should only * be called from the {@link RepositoryServiceFactory}. - * - * @param cacheManager cache manager - * @param provider implementation for {@link RepositoryServiceProvider} - * @param repository the repository - * @param preProcessorUtil + * @param cacheManager cache manager + * @param provider implementation for {@link RepositoryServiceProvider} + * @param repository the repository + * @param workdirProvider */ RepositoryService(CacheManager cacheManager, - RepositoryServiceProvider provider, Repository repository, - PreProcessorUtil preProcessorUtil) - { + RepositoryServiceProvider provider, Repository repository, + PreProcessorUtil preProcessorUtil, Set protocolProviders, WorkdirProvider workdirProvider) { this.cacheManager = cacheManager; this.provider = provider; this.repository = repository; this.preProcessorUtil = preProcessorUtil; + this.protocolProviders = protocolProviders; + this.workdirProvider = workdirProvider; } - //~--- methods -------------------------------------------------------------- - /** * Closes the connection to the repository and releases all locks * and resources. This method should be called in a finally block e.g.: @@ -135,34 +130,24 @@ public final class RepositoryService implements Closeable * */ @Override - public void close() - { - try - { + public void close() { + try { provider.close(); - } - catch (IOException ex) - { - logger.error("cound not close repository service provider", ex); + } catch (IOException ex) { + LOG.error("Could not close repository service provider", ex); } } - //~--- get methods ---------------------------------------------------------- - /** * The blame command shows changeset information by line for a given file. * * @return instance of {@link BlameCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. */ - public BlameCommandBuilder getBlameCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create blame command for repository {}", - repository.getName()); - } + public BlameCommandBuilder getBlameCommand() { + LOG.debug("create blame command for repository {}", + repository.getNamespaceAndName()); return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(), repository, preProcessorUtil); @@ -173,34 +158,41 @@ public final class RepositoryService implements Closeable * * @return instance of {@link BranchesCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. */ - public BranchesCommandBuilder getBranchesCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create branches command for repository {}", - repository.getName()); - } + public BranchesCommandBuilder getBranchesCommand() { + LOG.debug("create branches command for repository {}", + repository.getNamespaceAndName()); return new BranchesCommandBuilder(cacheManager, provider.getBranchesCommand(), repository); } + /** + * The branch command creates new branches. + * + * @return instance of {@link BranchCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + */ + public BranchCommandBuilder getBranchCommand() { + RepositoryPermissions.push(getRepository()).check(); + LOG.debug("create branch command for repository {}", + repository.getNamespaceAndName()); + + return new BranchCommandBuilder(provider.getBranchCommand()); + } + /** * The browse command allows browsing of a repository. * * @return instance of {@link BrowseCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. */ - public BrowseCommandBuilder getBrowseCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create browse command for repository {}", - repository.getName()); - } + public BrowseCommandBuilder getBrowseCommand() { + LOG.debug("create browse command for repository {}", + repository.getNamespaceAndName()); return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(), repository, preProcessorUtil); @@ -211,16 +203,12 @@ public final class RepositoryService implements Closeable * * @return instance of {@link BundleCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. * @since 1.43 */ - public BundleCommandBuilder getBundleCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create bundle command for repository {}", - repository.getName()); - } + public BundleCommandBuilder getBundleCommand() { + LOG.debug("create bundle command for repository {}", + repository.getNamespaceAndName()); return new BundleCommandBuilder(provider.getBundleCommand(), repository); } @@ -230,15 +218,11 @@ public final class RepositoryService implements Closeable * * @return instance of {@link CatCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. */ - public CatCommandBuilder getCatCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create cat command for repository {}", - repository.getName()); - } + public CatCommandBuilder getCatCommand() { + LOG.debug("create cat command for repository {}", + repository.getNamespaceAndName()); return new CatCommandBuilder(provider.getCatCommand()); } @@ -249,36 +233,42 @@ public final class RepositoryService implements Closeable * * @return instance of {@link DiffCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. */ - public DiffCommandBuilder getDiffCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create diff command for repository {}", - repository.getName()); - } + public DiffCommandBuilder getDiffCommand() { + LOG.debug("create diff command for repository {}", + repository.getNamespaceAndName()); - return new DiffCommandBuilder(provider.getDiffCommand()); + return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures()); + } + + /** + * The diff command shows differences between revisions for a specified file + * or the entire revision. + * + * @return instance of {@link DiffResultCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + */ + public DiffResultCommandBuilder getDiffResultCommand() { + LOG.debug("create diff result command for repository {}", + repository.getNamespaceAndName()); + + return new DiffResultCommandBuilder(provider.getDiffResultCommand(), provider.getSupportedFeatures()); } /** * The incoming command shows new {@link Changeset}s found in a different * repository location. * - * * @return instance of {@link IncomingCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. * @since 1.31 */ - public IncomingCommandBuilder getIncomingCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create incoming command for repository {}", - repository.getName()); - } + public IncomingCommandBuilder getIncomingCommand() { + LOG.debug("create incoming command for repository {}", + repository.getNamespaceAndName()); return new IncomingCommandBuilder(cacheManager, provider.getIncomingCommand(), repository, preProcessorUtil); @@ -289,36 +279,39 @@ public final class RepositoryService implements Closeable * * @return instance of {@link LogCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. */ - public LogCommandBuilder getLogCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create log command for repository {}", - repository.getName()); - } + public LogCommandBuilder getLogCommand() { + LOG.debug("create log command for repository {}", + repository.getNamespaceAndName()); return new LogCommandBuilder(cacheManager, provider.getLogCommand(), - repository, preProcessorUtil); + repository, preProcessorUtil, provider.getSupportedFeatures()); } /** - * The outgoing command show changesets not found in a remote repository. + * 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() { + LOG.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. * * @return instance of {@link OutgoingCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. * @since 1.31 */ - public OutgoingCommandBuilder getOutgoingCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create outgoing command for repository {}", - repository.getName()); - } + public OutgoingCommandBuilder getOutgoingCommand() { + LOG.debug("create outgoing command for repository {}", + repository.getNamespaceAndName()); return new OutgoingCommandBuilder(cacheManager, provider.getOutgoingCommand(), repository, preProcessorUtil); @@ -329,35 +322,27 @@ public final class RepositoryService implements Closeable * * @return instance of {@link PullCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. * @since 1.31 */ - public PullCommandBuilder getPullCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create pull command for repository {}", - repository.getName()); - } + public PullCommandBuilder getPullCommand() { + LOG.debug("create pull command for repository {}", + repository.getNamespaceAndName()); return new PullCommandBuilder(provider.getPullCommand(), repository); } /** - * The push command push changes to a other repository. + * The push command pushes changes to a other repository. * * @return instance of {@link PushCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. * @since 1.31 */ - public PushCommandBuilder getPushCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create push command for repository {}", - repository.getName()); - } + public PushCommandBuilder getPushCommand() { + LOG.debug("create push command for repository {}", + repository.getNamespaceAndName()); return new PushCommandBuilder(provider.getPushCommand()); } @@ -367,8 +352,7 @@ public final class RepositoryService implements Closeable * * @return repository of this service */ - public Repository getRepository() - { + public Repository getRepository() { return repository; } @@ -377,15 +361,11 @@ public final class RepositoryService implements Closeable * * @return instance of {@link TagsCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. */ - public TagsCommandBuilder getTagsCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create tags command for repository {}", - repository.getName()); - } + public TagsCommandBuilder getTagsCommand() { + LOG.debug("create tags command for repository {}", + repository.getNamespaceAndName()); return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(), repository); @@ -396,60 +376,89 @@ public final class RepositoryService implements Closeable * * @return instance of {@link UnbundleCommandBuilder} * @throws CommandNotSupportedException if the command is not supported - * by the implementation of the repository service provider. + * by the implementation of the repository service provider. * @since 1.43 */ - public UnbundleCommandBuilder getUnbundleCommand() - { - if (logger.isDebugEnabled()) - { - logger.debug("create bundle command for repository {}", - repository.getName()); - } + public UnbundleCommandBuilder getUnbundleCommand() { + LOG.debug("create unbundle command for repository {}", + repository.getNamespaceAndName()); return new UnbundleCommandBuilder(provider.getUnbundleCommand(), repository); } + /** + * The merge command executes a merge of two branches. It is possible to do a dry run to check, whether the given + * branches can be merged without conflicts. + * + * @return instance of {@link MergeCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + * @since 2.0.0 + */ + public MergeCommandBuilder getMergeCommand() { + LOG.debug("create merge command for repository {}", + repository.getNamespaceAndName()); + + return new MergeCommandBuilder(provider.getMergeCommand()); + } + + /** + * The modify command makes changes to the head of a branch. It is possible to + *

    + *
  • create new files
  • + *
  • delete existing files
  • + *
  • modify/replace files
  • + *
  • move files
  • + *
+ * + * @return instance of {@link ModifyCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + * @since 2.0.0 + */ + public ModifyCommandBuilder getModifyCommand() { + LOG.debug("create modify command for repository {}", + repository.getNamespaceAndName()); + + return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider); + } + /** * Returns true if the command is supported by the repository service. * - * * @param command command - * * @return true if the command is supported */ - public boolean isSupported(Command command) - { + public boolean isSupported(Command command) { return provider.getSupportedCommands().contains(command); } /** * Returns true if the feature is supported by the repository service. * - * * @param feature feature - * * @return true if the feature is supported - * * @since 1.25 */ - public boolean isSupported(Feature feature) - { + public boolean isSupported(Feature feature) { return provider.getSupportedFeatures().contains(feature); } - //~--- fields --------------------------------------------------------------- + public Stream getSupportedProtocols() { + return protocolProviders.stream() + .filter(protocolProvider -> protocolProvider.getType().equals(getRepository().getType())) + .map(this::createProviderInstanceForRepository); + } - /** cache manager */ - private CacheManager cacheManager; + private T createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) { + return protocolProvider.get(repository); + } - /** Field description */ - private PreProcessorUtil preProcessorUtil; - - /** implementation of the repository service provider */ - private RepositoryServiceProvider provider; - - /** the repository */ - private Repository repository; + public T getProtocol(Class clazz) { + return this.getSupportedProtocols() + .filter(scmProtocol -> clazz.isAssignableFrom(scmProtocol.getClass())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(),getRepository().getType()))); + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index e07eee44f0..07c0b8fd47 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -37,37 +37,39 @@ package sonia.scm.repository.api; import com.github.legman.ReferenceType; import com.github.legman.Subscribe; - import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.HandlerEventType; +import sonia.scm.NotFoundException; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.ClearRepositoryCacheEvent; +import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.PostReceiveRepositoryHookEvent; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryCacheKeyPredicate; import sonia.scm.repository.RepositoryEvent; import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceResolver; +import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.security.ScmSecurityException; -//~--- JDK imports ------------------------------------------------------------ - import java.util.Set; -import sonia.scm.event.ScmEventBus; -import sonia.scm.repository.ClearRepositoryCacheEvent; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +//~--- JDK imports ------------------------------------------------------------ /** * The {@link RepositoryServiceFactory} is the entrypoint of the repository api. @@ -134,18 +136,22 @@ public final class RepositoryServiceFactory * @param resolvers a set of {@link RepositoryServiceResolver} * @param preProcessorUtil helper object for pre processor handling * + * @param workdirProvider * @since 1.21 */ @Inject public RepositoryServiceFactory(ScmConfiguration configuration, - CacheManager cacheManager, RepositoryManager repositoryManager, - Set resolvers, PreProcessorUtil preProcessorUtil) + CacheManager cacheManager, RepositoryManager repositoryManager, + Set resolvers, PreProcessorUtil preProcessorUtil, + Set protocolProviders, WorkdirProvider workdirProvider) { this.configuration = configuration; this.cacheManager = cacheManager; this.repositoryManager = repositoryManager; this.resolvers = resolvers; this.preProcessorUtil = preProcessorUtil; + this.protocolProviders = protocolProviders; + this.workdirProvider = workdirProvider; ScmEventBus.getInstance().register(new CacheClearHook(cacheManager)); } @@ -161,7 +167,7 @@ public final class RepositoryServiceFactory * @return a implementation of RepositoryService * for the given type of repository * - * @throws RepositoryNotFoundException if no repository + * @throws NotFoundException if no repository * with the given id is available * @throws RepositoryServiceNotFoundException if no repository service * implementation for this kind of repository is available @@ -169,9 +175,7 @@ public final class RepositoryServiceFactory * @throws ScmSecurityException if current user has not read permissions * for that repository */ - public RepositoryService create(String repositoryId) - throws RepositoryNotFoundException - { + public RepositoryService create(String repositoryId) { Preconditions.checkArgument(!Strings.isNullOrEmpty(repositoryId), "a non empty repositoryId is required"); @@ -179,8 +183,7 @@ public final class RepositoryServiceFactory if (repository == null) { - throw new RepositoryNotFoundException( - "could not find a repository with id ".concat(repositoryId)); + throw new NotFoundException(Repository.class, repositoryId); } return create(repository); @@ -190,13 +193,12 @@ public final class RepositoryServiceFactory * Creates a new RepositoryService for the given repository. * * - * @param type type of the repository - * @param name name of the repository + * @param namespaceAndName namespace and name of the repository * * @return a implementation of RepositoryService * for the given type of repository * - * @throws RepositoryNotFoundException if no repository + * @throws NotFoundException if no repository * with the given id is available * @throws RepositoryServiceNotFoundException if no repository service * implementation for this kind of repository is available @@ -204,24 +206,16 @@ public final class RepositoryServiceFactory * @throws ScmSecurityException if current user has not read permissions * for that repository */ - public RepositoryService create(String type, String name) - throws RepositoryNotFoundException + public RepositoryService create(NamespaceAndName namespaceAndName) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(type), - "a non empty type is required"); - Preconditions.checkArgument(!Strings.isNullOrEmpty(name), - "a non empty name is required"); + Preconditions.checkArgument(namespaceAndName != null, + "a non empty namespace and name is required"); - Repository repository = repositoryManager.get(type, name); + Repository repository = repositoryManager.get(namespaceAndName); if (repository == null) { - StringBuilder msg = - new StringBuilder("could not find a repository with type "); - - msg.append(type).append(" and name ").append(name); - - throw new RepositoryNotFoundException(msg.toString()); + throw notFound(entity(namespaceAndName)); } return create(repository); @@ -253,7 +247,7 @@ public final class RepositoryServiceFactory for (RepositoryServiceResolver resolver : resolvers) { - RepositoryServiceProvider provider = resolver.reslove(repository); + RepositoryServiceProvider provider = resolver.resolve(repository); if (provider != null) { @@ -265,7 +259,7 @@ public final class RepositoryServiceFactory } service = new RepositoryService(cacheManager, provider, repository, - preProcessorUtil); + preProcessorUtil, protocolProviders, workdirProvider); break; } @@ -337,7 +331,6 @@ public final class RepositoryServiceFactory /** * Clear caches on repository delete event. * - * @param repository changed repository * @param event repository event */ @Subscribe(referenceType = ReferenceType.STRONG) @@ -381,4 +374,8 @@ public final class RepositoryServiceFactory /** service resolvers */ private final Set resolvers; + + private Set protocolProviders; + + private final WorkdirProvider workdirProvider; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java new file mode 100644 index 0000000000..c987510491 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java @@ -0,0 +1,19 @@ +package sonia.scm.repository.api; + +/** + * An ScmProtocol represents a concrete protocol provided by the SCM-Manager instance + * to interact with a repository depending on its type. There may be multiple protocols + * available for a repository type (eg. http and ssh). + */ +public interface ScmProtocol { + + /** + * The type of the concrete protocol, eg. "http" or "ssh". + */ + String getType(); + + /** + * The URL to access the repository providing this protocol. + */ + String getUrl(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java new file mode 100644 index 0000000000..591b8167e5 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java @@ -0,0 +1,31 @@ +package sonia.scm.repository.api; + +import sonia.scm.plugin.ExtensionPoint; +import sonia.scm.repository.Repository; + +/** + * Provider for scm native protocols. + * + * @param type of protocol + * + * @since 2.0.0 + */ +@ExtensionPoint(multi = true) +public interface ScmProtocolProvider { + + /** + * Returns type of repository (e.g.: git, svn, hg, etc.) + * + * @return name of type + */ + String getType(); + + /** + * Returns protocol for the given repository. + * + * @param repository repository + * + * @return protocol for repository + */ + T get(Repository repository); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/TagsCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/TagsCommandBuilder.java index 65ceef0828..3a6ea16b10 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/TagsCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/TagsCommandBuilder.java @@ -36,25 +36,21 @@ package sonia.scm.repository.api; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Objects; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryCacheKey; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.Tag; import sonia.scm.repository.Tags; import sonia.scm.repository.spi.TagsCommand; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * The tags command list all repository tags.
*
@@ -88,8 +84,7 @@ public final class TagsCommandBuilder * only be called from the {@link RepositoryService}. * * @param cacheManager cache manager - * @param logCommand implementation of the {@link TagsCommand} - * @param command + * @param command implementation of the {@link TagsCommand} * @param repository repository */ TagsCommandBuilder(CacheManager cacheManager, TagsCommand command, @@ -109,10 +104,8 @@ public final class TagsCommandBuilder * @return tags from the repository * * @throws IOException - * @throws RepositoryException */ - public Tags getTags() throws RepositoryException, IOException - { + public Tags getTags() throws IOException { Tags tags; if (disableCache) @@ -183,9 +176,8 @@ public final class TagsCommandBuilder * @return * * @throws IOException - * @throws RepositoryException */ - private Tags getTagsFromCommand() throws RepositoryException, IOException + private Tags getTagsFromCommand() throws IOException { List tagList = command.getTags(); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/UnbundleCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/UnbundleCommandBuilder.java index ff28e5c668..bfb998c6fe 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/UnbundleCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/UnbundleCommandBuilder.java @@ -37,25 +37,22 @@ package sonia.scm.repository.api; import com.google.common.io.ByteSource; import com.google.common.io.Files; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.UnbundleCommand; import sonia.scm.repository.spi.UnbundleCommandRequest; -import static com.google.common.base.Preconditions.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; import java.io.InputStream; - import java.util.zip.GZIPInputStream; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +//~--- JDK imports ------------------------------------------------------------ + /** * The unbundle command can restore an empty repository from a bundle. The * bundle can be created with the {@link BundleCommandBuilder}. @@ -97,10 +94,9 @@ public final class UnbundleCommandBuilder * @return unbundle response * * @throws IOException - * @throws RepositoryException */ public UnbundleResponse unbundle(File inputFile) - throws IOException, RepositoryException + throws IOException { checkArgument((inputFile != null) && inputFile.exists(), "existing file is required"); @@ -122,10 +118,9 @@ public final class UnbundleCommandBuilder * @return unbundle response * * @throws IOException - * @throws RepositoryException */ public UnbundleResponse unbundle(InputStream inputStream) - throws IOException, RepositoryException + throws IOException { checkNotNull(inputStream, "input stream is required"); logger.info("unbundle archive from stream"); @@ -142,10 +137,9 @@ public final class UnbundleCommandBuilder * @return unbundle response * * @throws IOException - * @throws RepositoryException */ public UnbundleResponse unbundle(ByteSource byteSource) - throws IOException, RepositoryException + throws IOException { checkNotNull(byteSource, "byte source is required"); logger.info("unbundle from byte source"); @@ -186,7 +180,7 @@ public final class UnbundleCommandBuilder { @Override - public InputStream openStream() throws IOException + public InputStream openStream() { return inputStream; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BlameCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BlameCommand.java index 80c155a534..9953746fd1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BlameCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BlameCommand.java @@ -36,12 +36,11 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import sonia.scm.repository.BlameResult; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -59,8 +58,6 @@ public interface BlameCommand * @return * * @throws IOException - * @throws RepositoryException */ - public BlameResult getBlameResult(BlameCommandRequest request) - throws IOException, RepositoryException; + BlameResult getBlameResult(BlameCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java new file mode 100644 index 0000000000..d5ba7f8dca --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java @@ -0,0 +1,46 @@ +/** + * 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.spi; + + +import sonia.scm.repository.Branch; +import sonia.scm.repository.api.BranchRequest; + +/** + * @since 2.0 + */ +public interface BranchCommand { + Branch branch(BranchRequest name); + + void deleteOrClose(String branchName); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java index a5659a8ffc..ba512ed4d2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java @@ -35,14 +35,12 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import sonia.scm.repository.Branch; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; - import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -58,7 +56,6 @@ public interface BranchesCommand * @return * * @throws IOException - * @throws RepositoryException */ - public List getBranches() throws RepositoryException, IOException; + List getBranches() throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java index 4afc02b125..ee37d6243e 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java @@ -36,12 +36,11 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import sonia.scm.repository.BrowserResult; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -59,8 +58,6 @@ public interface BrowseCommand * @return * * @throws IOException - * @throws RepositoryException */ - public BrowserResult getBrowserResult(BrowseCommandRequest request) - throws IOException, RepositoryException; + BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 445533455f..39da9a9ace 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -37,7 +37,6 @@ package sonia.scm.repository.spi; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; - /** * * @author Sebastian Sdorra @@ -129,12 +128,12 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest { //J- return MoreObjects.toStringHelper(this) - .add("path", getPath()) - .add("revision", getRevision()) - .add("recursive", recursive) - .add("disableLastCommit", disableLastCommit) - .add("disableSubRepositoryDetection", disableSubRepositoryDetection) - .toString(); + .add("path", getPath()) + .add("revision", getRevision()) + .add("recursive", recursive) + .add("disableLastCommit", disableLastCommit) + .add("disableSubRepositoryDetection", disableSubRepositoryDetection) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BundleCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BundleCommand.java index 8a0f21a714..7b6404c556 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BundleCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BundleCommand.java @@ -35,13 +35,12 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.BundleResponse; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * Service provider implementation for the bundle command. * @@ -60,8 +59,7 @@ public interface BundleCommand * @return bundle response * * @throws IOException - * @throws RepositoryException */ public BundleResponse bundle(BundleCommandRequest request) - throws IOException, RepositoryException; + throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/CannotDeleteDefaultBranchException.java b/scm-core/src/main/java/sonia/scm/repository/spi/CannotDeleteDefaultBranchException.java new file mode 100644 index 0000000000..0053d5384a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/CannotDeleteDefaultBranchException.java @@ -0,0 +1,19 @@ +package sonia.scm.repository.spi; + +import sonia.scm.ContextEntry; +import sonia.scm.ExceptionWithContext; +import sonia.scm.repository.Repository; + +public class CannotDeleteDefaultBranchException extends ExceptionWithContext { + + public static final String CODE = "78RhWxTIw1"; + + public CannotDeleteDefaultBranchException(Repository repository, String branchName) { + super(ContextEntry.ContextBuilder.entity("Branch", branchName).in(repository).build(), "default branch cannot be deleted"); + } + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java index b9a6f52ab0..81600230db 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java @@ -33,13 +33,8 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; /** @@ -47,19 +42,9 @@ import java.io.OutputStream; * @author Sebastian Sdorra * @since 1.17 */ -public interface CatCommand -{ +public interface CatCommand { - /** - * Method description - * - * - * @param request - * @param output - * - * @throws IOException - * @throws RepositoryException - */ - public void getCatResult(CatCommandRequest request, OutputStream output) - throws IOException, RepositoryException; + void getCatResult(CatCommandRequest request, OutputStream output) throws IOException; + + InputStream getCatResultStream(CatCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java index a4d0c24444..162c57cbc2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java @@ -33,14 +33,9 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.api.DiffCommandBuilder; import java.io.IOException; -import java.io.OutputStream; /** * @@ -55,12 +50,9 @@ public interface DiffCommand * * * @param request - * @param output - * * @throws IOException * @throws RuntimeException - * @throws RepositoryException + * @return */ - public void getDiffResult(DiffCommandRequest request, OutputStream output) - throws IOException, RepositoryException; + DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java index 5cfd57f71b..e26b2eb5aa 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java @@ -109,7 +109,10 @@ public final class DiffCommandRequest extends FileBaseCommandRequest this.format = format; } - //~--- get methods ---------------------------------------------------------- + public void setAncestorChangeset(String ancestorChangeset) { + this.ancestorChangeset = ancestorChangeset; + } +//~--- get methods ---------------------------------------------------------- /** * Return the output format of the diff command. @@ -124,8 +127,13 @@ public final class DiffCommandRequest extends FileBaseCommandRequest return format; } - //~--- fields --------------------------------------------------------------- + public String getAncestorChangeset() { + return ancestorChangeset; + } +//~--- fields --------------------------------------------------------------- /** diff format */ private DiffFormat format = DiffFormat.NATIVE; + + private String ancestorChangeset; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java new file mode 100644 index 0000000000..ee50178d76 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java @@ -0,0 +1,9 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffResult; + +import java.io.IOException; + +public interface DiffResultCommand { + DiffResult getDiffResult(DiffCommandRequest request) throws IOException; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java index ad70315e5f..9f563345fd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java @@ -38,10 +38,10 @@ package sonia.scm.repository.spi; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Serializable; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -117,9 +117,9 @@ public abstract class FileBaseCommandRequest { //J- return MoreObjects.toStringHelper(this) - .add("path", path) - .add("revision", revision) - .toString(); + .add("path", path) + .add("revision", revision) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java b/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java index 899892a5b7..fd785e2138 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java @@ -35,16 +35,17 @@ package sonia.scm.repository.spi; import com.google.inject.Inject; import com.google.inject.Provider; - +import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.RepositoryHookEvent; import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.api.HookContext; import sonia.scm.repository.api.HookContextFactory; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + /** * * @author Sebastian Sdorra @@ -72,56 +73,25 @@ public final class HookEventFacade //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param id - * - * @return - * - * @throws RepositoryException - */ - public HookEventHandler handle(String id) throws RepositoryException - { - return handle(repositoryManagerProvider.get().get(id)); - } - - /** - * Method description - * - * - * @param type - * @param repositoryName - * - * @return - * - * @throws RepositoryException - */ - public HookEventHandler handle(String type, String repositoryName) - throws RepositoryException - { - return handle(repositoryManagerProvider.get().get(type, repositoryName)); - } - - /** - * Method description - * - * - * @param repository - * - * @return - * - * @throws RepositoryException - */ - public HookEventHandler handle(Repository repository) - throws RepositoryException - { + public HookEventHandler handle(String id) { + Repository repository = repositoryManagerProvider.get().get(id); if (repository == null) { - throw new RepositoryNotFoundException("could not find repository"); + throw notFound(entity("Repository", id)); } + return handle(repository); + } + public HookEventHandler handle(NamespaceAndName namespaceAndName) { + Repository repository = repositoryManagerProvider.get().get(namespaceAndName); + if (repository == null) + { + throw notFound(entity(namespaceAndName)); + } + return handle(repository); + } + + public HookEventHandler handle(Repository repository) { return new HookEventHandler(repositoryManagerProvider.get(), hookContextFactory, repository); } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java new file mode 100644 index 0000000000..b933abf559 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java @@ -0,0 +1,38 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.ScmProtocol; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; + +public abstract class HttpScmProtocol implements ScmProtocol { + + private final Repository repository; + private final String basePath; + + public HttpScmProtocol(Repository repository, String basePath) { + this.repository = repository; + this.basePath = basePath; + } + + @Override + public String getType() { + return "http"; + } + + @Override + public String getUrl() { + return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", repository.getNamespace(), repository.getName())).toASCIIString(); + } + + public final void serve(HttpServletRequest request, HttpServletResponse response, ServletConfig config) throws ServletException, IOException { + serve(request, response, repository, config); + } + + protected abstract void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) throws ServletException, IOException; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/IncomingCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/IncomingCommand.java index 8b3341f77b..669f307acd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/IncomingCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/IncomingCommand.java @@ -35,12 +35,11 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -49,18 +48,5 @@ import java.io.IOException; public interface IncomingCommand { - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - public ChangesetPagingResult getIncomingChangesets( - IncomingCommandRequest request) - throws IOException, RepositoryException; + ChangesetPagingResult getIncomingChangesets(IncomingCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java b/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java new file mode 100644 index 0000000000..c1b7229036 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java @@ -0,0 +1,91 @@ +package sonia.scm.repository.spi; + +import lombok.extern.slf4j.Slf4j; +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.ScmProtocolProvider; + +import javax.inject.Provider; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +@Slf4j +public abstract class InitializingHttpScmProtocolWrapper implements ScmProtocolProvider { + + private final Provider delegateProvider; + private final Provider pathInfoStore; + private final ScmConfiguration scmConfiguration; + + private volatile boolean isInitialized = false; + + + protected InitializingHttpScmProtocolWrapper(Provider delegateProvider, Provider pathInfoStore, ScmConfiguration scmConfiguration) { + this.delegateProvider = delegateProvider; + this.pathInfoStore = pathInfoStore; + this.scmConfiguration = scmConfiguration; + } + + protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException { + httpServlet.init(config); + } + + @Override + public HttpScmProtocol get(Repository repository) { + if (!repository.getType().equals(getType())) { + throw new IllegalArgumentException(String.format("cannot handle repository with type %s with protocol for type %s", repository.getType(), getType())); + } + return new ProtocolWrapper(repository, computeBasePath()); + } + + private String computeBasePath() { + return getPathFromScmPathInfoIfAvailable().orElse(getPathFromConfiguration()); + } + + private Optional getPathFromScmPathInfoIfAvailable() { + try { + ScmPathInfoStore scmPathInfoStore = pathInfoStore.get(); + if (scmPathInfoStore != null && scmPathInfoStore.get() != null) { + return of(scmPathInfoStore.get().getRootUri().toASCIIString()); + } + } catch (Exception e) { + log.debug("could not get ScmPathInfoStore from context", e); + } + return empty(); + } + + private String getPathFromConfiguration() { + log.debug("using base path from configuration: {}", scmConfiguration.getBaseUrl()); + return scmConfiguration.getBaseUrl(); + } + + private class ProtocolWrapper extends HttpScmProtocol { + + public ProtocolWrapper(Repository repository, String basePath) { + super(repository, basePath); + } + + @Override + protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) throws ServletException, IOException { + if (!isInitialized) { + synchronized (InitializingHttpScmProtocolWrapper.this) { + if (!isInitialized) { + ScmProviderHttpServlet httpServlet = delegateProvider.get(); + initializeServlet(config, httpServlet); + isInitialized = true; + } + } + } + + delegateProvider.get().service(request, response, repository); + } + + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java b/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java new file mode 100644 index 0000000000..c7c378500b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java @@ -0,0 +1,23 @@ +package sonia.scm.repository.spi; + +import sonia.scm.ContextEntry; +import sonia.scm.ExceptionWithContext; +import sonia.scm.repository.Repository; + +public class IntegrateChangesFromWorkdirException extends ExceptionWithContext { + + private static final String CODE = "CHRM7IQzo1"; + + public IntegrateChangesFromWorkdirException(Repository repository, String message) { + super(ContextEntry.ContextBuilder.entity(repository).build(), message); + } + + public IntegrateChangesFromWorkdirException(Repository repository, String message, Exception cause) { + super(ContextEntry.ContextBuilder.entity(repository).build(), message, cause); + } + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java index 168c279c19..21d9ece4de 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java @@ -37,45 +37,19 @@ package sonia.scm.repository.spi; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra * @since 1.17 */ -public interface LogCommand -{ +public interface LogCommand { - /** - * Method description - * - * - * @param id - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - public Changeset getChangeset(String id) - throws IOException, RepositoryException; + Changeset getChangeset(String id, LogCommandRequest request) throws IOException; - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - public ChangesetPagingResult getChangesets(LogCommandRequest request) - throws IOException, RepositoryException; + ChangesetPagingResult getChangesets(LogCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java index 4c7734667a..92cd41662b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java @@ -38,10 +38,10 @@ package sonia.scm.repository.spi; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Serializable; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -84,7 +84,8 @@ public final class LogCommandRequest implements Serializable, Resetable && Objects.equal(pagingStart, other.pagingStart) && Objects.equal(pagingLimit, other.pagingLimit) && Objects.equal(path, other.path) - && Objects.equal(branch, other.branch); + && Objects.equal(branch, other.branch) + && Objects.equal(ancestorChangeset, other.ancestorChangeset); //J+ } @@ -98,7 +99,7 @@ public final class LogCommandRequest implements Serializable, Resetable public int hashCode() { return Objects.hashCode(startChangeset, endChangeset, pagingStart, - pagingLimit, path, branch); + pagingLimit, path, branch, ancestorChangeset); } /** @@ -114,6 +115,7 @@ public final class LogCommandRequest implements Serializable, Resetable pagingLimit = 20; path = null; branch = null; + ancestorChangeset = null; } /** @@ -127,13 +129,14 @@ public final class LogCommandRequest implements Serializable, Resetable { //J- return MoreObjects.toStringHelper(this) - .add("startChangeset", startChangeset) - .add("endChangeset", endChangeset) - .add("pagingStart", pagingStart) - .add("pagingLimit", pagingLimit) - .add("path", path) - .add("branch", branch) - .toString(); + .add("startChangeset", startChangeset) + .add("endChangeset", endChangeset) + .add("pagingStart", pagingStart) + .add("pagingLimit", pagingLimit) + .add("path", path) + .add("branch", branch) + .add("ancestorChangeset", ancestorChangeset) + .toString(); //J+ } @@ -205,6 +208,10 @@ public final class LogCommandRequest implements Serializable, Resetable this.startChangeset = startChangeset; } + public void setAncestorChangeset(String ancestorChangeset) { + this.ancestorChangeset = ancestorChangeset; + } + //~--- get methods ---------------------------------------------------------- /** @@ -284,6 +291,10 @@ public final class LogCommandRequest implements Serializable, Resetable return pagingLimit < 0; } + public String getAncestorChangeset() { + return ancestorChangeset; + } + //~--- fields --------------------------------------------------------------- /** Field description */ @@ -303,4 +314,6 @@ public final class LogCommandRequest implements Serializable, Resetable /** Field description */ private String startChangeset; + + private String ancestorChangeset; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java new file mode 100644 index 0000000000..a62e373dca --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java @@ -0,0 +1,17 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.MergeCommandResult; +import sonia.scm.repository.api.MergeDryRunCommandResult; +import sonia.scm.repository.api.MergeStrategy; + +import java.util.Set; + +public interface MergeCommand { + MergeCommandResult merge(MergeCommandRequest request); + + MergeDryRunCommandResult dryRun(MergeCommandRequest request); + + boolean isSupported(MergeStrategy strategy); + + Set getSupportedMergeStrategies(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java new file mode 100644 index 0000000000..0751920d01 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java @@ -0,0 +1,105 @@ +package sonia.scm.repository.spi; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import sonia.scm.Validateable; +import sonia.scm.repository.Person; +import sonia.scm.repository.api.MergeStrategy; +import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor; + +import java.io.Serializable; + +public class MergeCommandRequest implements Validateable, Resetable, Serializable, Cloneable, CommandWithAuthor { + + private static final long serialVersionUID = -2650236557922431528L; + + private String branchToMerge; + private String targetBranch; + private Person author; + private String messageTemplate; + private MergeStrategy mergeStrategy; + + public String getBranchToMerge() { + return branchToMerge; + } + + public void setBranchToMerge(String branchToMerge) { + this.branchToMerge = branchToMerge; + } + + public String getTargetBranch() { + return targetBranch; + } + + public void setTargetBranch(String targetBranch) { + this.targetBranch = targetBranch; + } + + public Person getAuthor() { + return author; + } + + public void setAuthor(Person author) { + this.author = author; + } + + public String getMessageTemplate() { + return messageTemplate; + } + + public void setMessageTemplate(String messageTemplate) { + this.messageTemplate = messageTemplate; + } + + public MergeStrategy getMergeStrategy() { + return mergeStrategy; + } + + public void setMergeStrategy(MergeStrategy mergeStrategy) { + this.mergeStrategy = mergeStrategy; + } + + public boolean isValid() { + return !Strings.isNullOrEmpty(getBranchToMerge()) + && !Strings.isNullOrEmpty(getTargetBranch()); + } + + public void reset() { + this.setBranchToMerge(null); + this.setTargetBranch(null); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + + final MergeCommandRequest other = (MergeCommandRequest) obj; + + return Objects.equal(branchToMerge, other.branchToMerge) + && Objects.equal(targetBranch, other.targetBranch) + && Objects.equal(author, other.author) + && Objects.equal(mergeStrategy, other.mergeStrategy); + } + + @Override + public int hashCode() { + return Objects.hashCode(branchToMerge, targetBranch, author, mergeStrategy); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("branchToMerge", branchToMerge) + .add("targetBranch", targetBranch) + .add("author", author) + .add("mergeStrategy", mergeStrategy) + .toString(); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java new file mode 100644 index 0000000000..322468f827 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java @@ -0,0 +1,52 @@ +/* + * 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.spi; + +import sonia.scm.repository.Modifications; + +import java.io.IOException; + +/** + * Command to get the modifications applied to files in a revision. + * + * Modifications are for example: Add, Update, Delete + * + * @author Mohamed Karray + * @since 2.0 + */ +public interface ModificationsCommand { + + Modifications getModifications(String revision) throws IOException; + + Modifications getModifications(ModificationsCommandRequest request) throws IOException; + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java new file mode 100644 index 0000000000..8a910a3396 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java @@ -0,0 +1,24 @@ +package sonia.scm.repository.spi; + + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@ToString +@EqualsAndHashCode +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ModificationsCommandRequest implements Resetable { + private String revision; + + @Override + public void reset() { + revision = null; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java new file mode 100644 index 0000000000..1bc64e0e08 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java @@ -0,0 +1,17 @@ +package sonia.scm.repository.spi; + +import java.io.File; +import java.io.IOException; + +public interface ModifyCommand { + + String execute(ModifyCommandRequest request); + + interface Worker { + void delete(String toBeDeleted) throws IOException; + + void create(String toBeCreated, File file, boolean overwrite) throws IOException; + + void modify(String path, File file) throws IOException; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java new file mode 100644 index 0000000000..43814f8729 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -0,0 +1,154 @@ +package sonia.scm.repository.spi; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.Validateable; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor; +import sonia.scm.util.IOUtil; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ModifyCommandRequest implements Resetable, Validateable, CommandWithAuthor { + + private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandRequest.class); + + private final List requests = new ArrayList<>(); + + private Person author; + private String commitMessage; + private String branch; + private String expectedRevision; + + @Override + public void reset() { + requests.clear(); + author = null; + commitMessage = null; + branch = null; + } + + public void addRequest(PartialRequest request) { + this.requests.add(request); + } + + public void setAuthor(Person author) { + this.author = author; + } + + public void setCommitMessage(String commitMessage) { + this.commitMessage = commitMessage; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public List getRequests() { + return Collections.unmodifiableList(requests); + } + + public Person getAuthor() { + return author; + } + + public String getCommitMessage() { + return commitMessage; + } + + public String getBranch() { + return branch; + } + + public String getExpectedRevision() { + return expectedRevision; + } + + @Override + public boolean isValid() { + return StringUtils.isNotEmpty(commitMessage) && !requests.isEmpty(); + } + + public void setExpectedRevision(String expectedRevision) { + this.expectedRevision = expectedRevision; + } + + public interface PartialRequest { + void execute(ModifyCommand.Worker worker) throws IOException; + } + + public static class DeleteFileRequest implements PartialRequest { + private final String path; + + public DeleteFileRequest(String path) { + this.path = path; + } + + @Override + public void execute(ModifyCommand.Worker worker) throws IOException { + worker.delete(path); + } + } + + private abstract static class ContentModificationRequest implements PartialRequest { + + private final File content; + + ContentModificationRequest(File content) { + this.content = content; + } + + File getContent() { + return content; + } + + void cleanup() { + if (content.exists()) { + try { + IOUtil.delete(content); + } catch (IOException e) { + LOG.warn("could not delete temporary file {}", content, e); + } + } + } + } + + public static class CreateFileRequest extends ContentModificationRequest { + + private final String path; + private final boolean overwrite; + + public CreateFileRequest(String path, File content, boolean overwrite) { + super(content); + this.path = path; + this.overwrite = overwrite; + } + + @Override + public void execute(ModifyCommand.Worker worker) throws IOException { + worker.create(path, getContent(), overwrite); + cleanup(); + } + } + + public static class ModifyFileRequest extends ContentModificationRequest { + + private final String path; + + public ModifyFileRequest(String path, File content) { + super(content); + this.path = path; + } + + @Override + public void execute(ModifyCommand.Worker worker) throws IOException { + worker.modify(path, getContent()); + cleanup(); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java new file mode 100644 index 0000000000..a1ff6b0eb3 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java @@ -0,0 +1,88 @@ +package sonia.scm.repository.spi; + +import org.apache.commons.lang.StringUtils; +import sonia.scm.ContextEntry; +import sonia.scm.repository.Repository; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static sonia.scm.AlreadyExistsException.alreadyExists; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +/** + * This "interface" is not really intended to be used as an interface but rather as + * a base class to reduce code redundancy in Worker instances. + */ +public interface ModifyWorkerHelper extends ModifyCommand.Worker { + + @Override + default void delete(String toBeDeleted) throws IOException { + Path fileToBeDeleted = new File(getWorkDir(), toBeDeleted).toPath(); + try { + Files.delete(fileToBeDeleted); + } catch (NoSuchFileException e) { + throw notFound(createFileContext(toBeDeleted)); + } + doScmDelete(toBeDeleted); + } + + void doScmDelete(String toBeDeleted); + + @Override + default void create(String toBeCreated, File file, boolean overwrite) throws IOException { + Path targetFile = new File(getWorkDir(), toBeCreated).toPath(); + createDirectories(targetFile); + if (overwrite) { + Files.move(file.toPath(), targetFile, REPLACE_EXISTING); + } else { + try { + Files.move(file.toPath(), targetFile); + } catch (FileAlreadyExistsException e) { + throw alreadyExists(createFileContext(toBeCreated)); + } + } + addFileToScm(toBeCreated, targetFile); + } + + default void modify(String path, File file) throws IOException { + Path targetFile = new File(getWorkDir(), path).toPath(); + createDirectories(targetFile); + if (!targetFile.toFile().exists()) { + throw notFound(createFileContext(path)); + } + Files.move(file.toPath(), targetFile, REPLACE_EXISTING); + addFileToScm(path, targetFile); + } + + void addFileToScm(String name, Path file); + + default ContextEntry.ContextBuilder createFileContext(String path) { + ContextEntry.ContextBuilder contextBuilder = entity("file", path); + if (!StringUtils.isEmpty(getBranch())) { + contextBuilder.in("branch", getBranch()); + } + contextBuilder.in(getRepository()); + return contextBuilder; + } + + default void createDirectories(Path targetFile) throws IOException { + try { + Files.createDirectories(targetFile.getParent()); + } catch (FileAlreadyExistsException e) { + throw alreadyExists(createFileContext(targetFile.toString())); + } + } + + File getWorkDir(); + + Repository getRepository(); + + String getBranch(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/OutgoingCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/OutgoingCommand.java index 9e5509e945..c37950abe2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/OutgoingCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/OutgoingCommand.java @@ -35,12 +35,11 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -58,9 +57,6 @@ public interface OutgoingCommand * @return * * @throws IOException - * @throws RepositoryException */ - public ChangesetPagingResult getOutgoingChangesets( - OutgoingCommandRequest request) - throws IOException, RepositoryException; + ChangesetPagingResult getOutgoingChangesets(OutgoingCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/PagedRemoteCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/PagedRemoteCommandRequest.java index b1e15a0389..b6545bdc4d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/PagedRemoteCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/PagedRemoteCommandRequest.java @@ -36,7 +36,6 @@ package sonia.scm.repository.spi; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; - /** * * @author Sebastian Sdorra @@ -86,10 +85,10 @@ public abstract class PagedRemoteCommandRequest extends RemoteCommandRequest //J- return MoreObjects.toStringHelper(this) - .add("remoteURL", remoteRepository) - .add("pagingStart", pagingStart) - .add("pagingLimit", pagingLimit) - .toString(); + .add("remoteURL", remoteRepository) + .add("pagingStart", pagingStart) + .add("pagingLimit", pagingLimit) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/PullCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/PullCommand.java index 533f8e1351..e7aa044353 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/PullCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/PullCommand.java @@ -34,13 +34,12 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.PullResponse; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -58,8 +57,6 @@ public interface PullCommand * @return * * @throws IOException - * @throws RepositoryException */ - public PullResponse pull(PullCommandRequest request) - throws IOException, RepositoryException; + PullResponse pull(PullCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/PushCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/PushCommand.java index c77947937a..9530add4f8 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/PushCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/PushCommand.java @@ -34,13 +34,12 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.PushResponse; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -58,8 +57,6 @@ public interface PushCommand * @return * * @throws IOException - * @throws RepositoryException */ - public PushResponse push(PushCommandRequest request) - throws IOException, RepositoryException; + PushResponse push(PushCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RemoteCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/RemoteCommandRequest.java index 1994afd29c..bbc902cd2a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RemoteCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RemoteCommandRequest.java @@ -37,13 +37,12 @@ package sonia.scm.repository.spi; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; - import sonia.scm.repository.Repository; -//~--- JDK imports ------------------------------------------------------------ - import java.net.URL; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -103,9 +102,9 @@ public abstract class RemoteCommandRequest implements Resetable { //J- return MoreObjects.toStringHelper(this) - .add("remoteRepository", remoteRepository) - .add("remoteUrl", remoteUrl) - .toString(); + .add("remoteRepository", remoteRepository) + .add("remoteUrl", remoteUrl) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java index 976f38fffb..cdd0417cf7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java @@ -33,20 +33,17 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import sonia.scm.repository.Feature; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.CommandNotSupportedException; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Closeable; import java.io.IOException; - import java.util.Collections; import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -104,6 +101,17 @@ public abstract class RepositoryServiceProvider implements Closeable throw new CommandNotSupportedException(Command.BRANCHES); } + /** + * Method description + * + * + * @return + */ + public BranchCommand getBranchCommand() + { + throw new CommandNotSupportedException(Command.BRANCH); + } + /** * Method description * @@ -150,6 +158,11 @@ public abstract class RepositoryServiceProvider implements Closeable throw new CommandNotSupportedException(Command.DIFF); } + public DiffResultCommand getDiffResultCommand() + { + throw new CommandNotSupportedException(Command.DIFF_RESULT); + } + /** * Method description * @@ -173,6 +186,16 @@ public abstract class RepositoryServiceProvider implements Closeable throw new CommandNotSupportedException(Command.LOG); } + /** + * Get the corresponding {@link ModificationsCommand} implemented from the Plugins + * + * @return the corresponding {@link ModificationsCommand} implemented from the Plugins + * @throws CommandNotSupportedException if there is no Implementation + */ + public ModificationsCommand getModificationsCommand() { + throw new CommandNotSupportedException(Command.MODIFICATIONS); + } + /** * Method description * @@ -244,4 +267,20 @@ public abstract class RepositoryServiceProvider implements Closeable { throw new CommandNotSupportedException(Command.UNBUNDLE); } + + /** + * @since 2.0 + */ + public MergeCommand getMergeCommand() + { + throw new CommandNotSupportedException(Command.MERGE); + } + + /** + * @since 2.0 + */ + public ModifyCommand getModifyCommand() + { + throw new CommandNotSupportedException(Command.MODIFY); + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceResolver.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceResolver.java index f0ed4c0814..d747bbdce0 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceResolver.java @@ -33,8 +33,6 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import sonia.scm.plugin.ExtensionPoint; import sonia.scm.repository.Repository; @@ -55,5 +53,5 @@ public interface RepositoryServiceResolver * * @return */ - public RepositoryServiceProvider reslove(Repository repository); + public RepositoryServiceProvider resolve(Repository repository); } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java new file mode 100644 index 0000000000..3a9dad52d6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java @@ -0,0 +1,16 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.Repository; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public interface ScmProviderHttpServlet { + + void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException; + + void init(ServletConfig config) throws ServletException; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java new file mode 100644 index 0000000000..c5dd55d277 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java @@ -0,0 +1,28 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.Repository; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ScmProviderHttpServletDecorator implements ScmProviderHttpServlet { + + private final ScmProviderHttpServlet object; + + public ScmProviderHttpServletDecorator(ScmProviderHttpServlet object) { + this.object = object; + } + + @Override + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { + object.service(request, response, repository); + } + + @Override + public void init(ServletConfig config) throws ServletException { + object.init(config); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java new file mode 100644 index 0000000000..531a25e91d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java @@ -0,0 +1,15 @@ +package sonia.scm.repository.spi; + +import sonia.scm.DecoratorFactory; +import sonia.scm.plugin.ExtensionPoint; + +@ExtensionPoint +public interface ScmProviderHttpServletDecoratorFactory extends DecoratorFactory { + /** + * Has to return true if this factory provides a decorator for the given scm type (eg. "git", "hg" or + * "svn"). + * @param type The current scm type this factory can provide a decorator for. + * @return true when the provided decorator should be used for the given scm type. + */ + boolean handlesScmType(String type); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java new file mode 100644 index 0000000000..3793d4b935 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java @@ -0,0 +1,33 @@ +package sonia.scm.repository.spi; + +import com.google.inject.Inject; +import sonia.scm.util.Decorators; + +import javax.inject.Provider; +import java.util.List; +import java.util.Set; + +import static java.util.stream.Collectors.toList; + +public abstract class ScmProviderHttpServletProvider implements Provider { + + @Inject(optional = true) + private Set decoratorFactories; + + private final String type; + + protected ScmProviderHttpServletProvider(String type) { + this.type = type; + } + + @Override + public ScmProviderHttpServlet get() { + return Decorators.decorate(getRootServlet(), getDecoratorsForType()); + } + + private List getDecoratorsForType() { + return decoratorFactories.stream().filter(d -> d.handlesScmType(type)).collect(toList()); + } + + protected abstract ScmProviderHttpServlet getRootServlet(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/TagsCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/TagsCommand.java index 5de358d94f..c2e6c9092d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/TagsCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/TagsCommand.java @@ -34,15 +34,13 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.Tag; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -57,7 +55,6 @@ public interface TagsCommand * @return * * @throws IOException - * @throws RepositoryException */ - public List getTags() throws RepositoryException, IOException; + public List getTags() throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/UnbundleCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/UnbundleCommand.java index 4777e0c11b..07e35e2715 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/UnbundleCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/UnbundleCommand.java @@ -35,13 +35,12 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.UnbundleResponse; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * Service provider implementation for the unbundle command. * @@ -60,8 +59,7 @@ public interface UnbundleCommand * @return unbundle response * * @throws IOException - * @throws RepositoryException */ public UnbundleResponse unbundle(UnbundleCommandRequest request) - throws IOException, RepositoryException; + throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java b/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java new file mode 100644 index 0000000000..5735889c3c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java @@ -0,0 +1,29 @@ +package sonia.scm.repository.util; + +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; +import sonia.scm.repository.Person; +import sonia.scm.user.User; + +public class AuthorUtil { + + public static void setAuthorIfNotAvailable(CommandWithAuthor request) { + if (request.getAuthor() == null) { + request.setAuthor(createAuthorFromSubject()); + } + } + + private static Person createAuthorFromSubject() { + Subject subject = SecurityUtils.getSubject(); + User user = subject.getPrincipals().oneByType(User.class); + String name = user.getDisplayName(); + String email = user.getMail(); + return new Person(name, email); + } + + public interface CommandWithAuthor { + Person getAuthor(); + + void setAuthor(Person person); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/CloseableWrapper.java b/scm-core/src/main/java/sonia/scm/repository/util/CloseableWrapper.java new file mode 100644 index 0000000000..a33af3ecb1 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/CloseableWrapper.java @@ -0,0 +1,21 @@ +package sonia.scm.repository.util; + +import java.util.function.Consumer; + +public class CloseableWrapper implements AutoCloseable { + + private final T wrapped; + private final Consumer cleanup; + + public CloseableWrapper(T wrapped, Consumer cleanup) { + this.wrapped = wrapped; + this.cleanup = cleanup; + } + + public T get() { return wrapped; } + + @Override + public void close() { + cleanup.accept(wrapped); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java b/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java new file mode 100644 index 0000000000..eaa9487f3c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java @@ -0,0 +1,76 @@ +package sonia.scm.repository.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; + +import java.io.File; +import java.io.IOException; + +public abstract class SimpleWorkdirFactory implements WorkdirFactory { + + private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class); + + private final WorkdirProvider workdirProvider; + + public SimpleWorkdirFactory(WorkdirProvider workdirProvider) { + this.workdirProvider = workdirProvider; + } + + @Override + public WorkingCopy createWorkingCopy(C context, String initialBranch) { + try { + File directory = workdirProvider.createNewWorkdir(); + ParentAndClone parentAndClone = cloneRepository(context, directory, initialBranch); + return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::closeWorkdir, this::closeCentral, directory); + } catch (IOException e) { + throw new InternalRepositoryException(getScmRepository(context), "could not clone repository in temporary directory", e); + } + } + + protected abstract Repository getScmRepository(C context); + + @SuppressWarnings("squid:S00112") + // We do allow implementations to throw arbitrary exceptions here, so that we can handle them in closeCentral + protected abstract void closeRepository(R repository) throws Exception; + @SuppressWarnings("squid:S00112") + // We do allow implementations to throw arbitrary exceptions here, so that we can handle them in closeWorkdir + protected abstract void closeWorkdirInternal(W workdir) throws Exception; + + protected abstract ParentAndClone cloneRepository(C context, File target, String initialBranch) throws IOException; + + private void closeCentral(R repository) { + try { + closeRepository(repository); + } catch (Exception e) { + logger.warn("could not close temporary repository clone", e); + } + } + + private void closeWorkdir(W repository) { + try { + closeWorkdirInternal(repository); + } catch (Exception e) { + logger.warn("could not close temporary repository clone", e); + } + } + + protected static class ParentAndClone { + private final R parent; + private final W clone; + + public ParentAndClone(R parent, W clone) { + this.parent = parent; + this.clone = clone; + } + + public R getParent() { + return parent; + } + + public W getClone() { + return clone; + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java new file mode 100644 index 0000000000..e1df5e99f3 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java @@ -0,0 +1,5 @@ +package sonia.scm.repository.util; + +public interface WorkdirFactory { + WorkingCopy createWorkingCopy(C context, String initialBranch); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java new file mode 100644 index 0000000000..35dae56faa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java @@ -0,0 +1,29 @@ +package sonia.scm.repository.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class WorkdirProvider { + + private final File poolDirectory; + + public WorkdirProvider() { + this(new File(System.getProperty("scm.workdir" , System.getProperty("java.io.tmpdir")), "scm-work")); + } + + public WorkdirProvider(File poolDirectory) { + this.poolDirectory = poolDirectory; + if (!poolDirectory.exists() && !poolDirectory.mkdirs()) { + throw new IllegalStateException("could not create pool directory " + poolDirectory); + } + } + + public File createNewWorkdir() { + try { + return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile(); + } catch (IOException e) { + throw new RuntimeException("could not create temporary workdir", e); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java b/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java new file mode 100644 index 0000000000..12c859fb8c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java @@ -0,0 +1,51 @@ +package sonia.scm.repository.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.util.IOUtil; + +import java.io.File; +import java.io.IOException; +import java.util.function.Consumer; + +public class WorkingCopy implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(WorkingCopy.class); + + private final File directory; + private final W workingRepository; + private final R centralRepository; + private final Consumer cleanupWorkdir; + private final Consumer cleanupCentral; + + public WorkingCopy(W workingRepository, R centralRepository, Consumer cleanupWorkdir, Consumer cleanupCentral, File directory) { + this.directory = directory; + this.workingRepository = workingRepository; + this.centralRepository = centralRepository; + this.cleanupCentral = cleanupCentral; + this.cleanupWorkdir = cleanupWorkdir; + } + + public W getWorkingRepository() { + return workingRepository; + } + + public R getCentralRepository() { + return centralRepository; + } + + public File getDirectory() { + return directory; + } + + @Override + public void close() { + try { + cleanupWorkdir.accept(workingRepository); + cleanupCentral.accept(centralRepository); + IOUtil.delete(directory); + } catch (IOException e) { + LOG.warn("could not delete temporary workdir '{}'", directory, e); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/resources/Resource.java b/scm-core/src/main/java/sonia/scm/resources/Resource.java deleted file mode 100644 index 83b737453b..0000000000 --- a/scm-core/src/main/java/sonia/scm/resources/Resource.java +++ /dev/null @@ -1,76 +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.resources; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; -import java.io.OutputStream; - -/** - * This class represents a web resource (Stylesheet or JavaScript file). - * - * @author Sebastian Sdorra - * @since 1.12 - */ -public interface Resource -{ - - /** - * Copies the content of the resource to the given {@link OutputStream}. - * - * - * @param output stream to copy the content of the resource - * - * @throws IOException - */ - public void copyTo(OutputStream output) throws IOException; - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns the name of the resource. - * - * - * @return name of the resource - */ - public String getName(); - - /** - * Returns the type of the resource. - * - * - * @return type of resource - */ - public ResourceType getType(); -} diff --git a/scm-core/src/main/java/sonia/scm/resources/ResourceHandler.java b/scm-core/src/main/java/sonia/scm/resources/ResourceHandler.java deleted file mode 100644 index f48f0d71fd..0000000000 --- a/scm-core/src/main/java/sonia/scm/resources/ResourceHandler.java +++ /dev/null @@ -1,75 +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.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.plugin.ExtensionPoint; - -//~--- JDK imports ------------------------------------------------------------ - -import java.net.URL; - -/** - * - * @author Sebastian Sdorra - */ -@ExtensionPoint -public interface ResourceHandler -{ - - /** - * Method description - * - * - * @return - */ - public String getName(); - - /** - * Method description - * - * - * @return - */ - public URL getResource(); - - /** - * Method description - * - * - * @return - */ - public ResourceType getType(); -} diff --git a/scm-core/src/main/java/sonia/scm/resources/ResourceHandlerComparator.java b/scm-core/src/main/java/sonia/scm/resources/ResourceHandlerComparator.java deleted file mode 100644 index c818eb6c73..0000000000 --- a/scm-core/src/main/java/sonia/scm/resources/ResourceHandlerComparator.java +++ /dev/null @@ -1,73 +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.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -import java.util.Comparator; - -/** - * - * @author Sebastian Sdorra - */ -public class ResourceHandlerComparator - implements Comparator, Serializable -{ - - /** Field description */ - private static final long serialVersionUID = -1760229246326556762L; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param handler - * @param otherHandler - * - * @return - */ - @Override - public int compare(ResourceHandler handler, ResourceHandler otherHandler) - { - return Util.compare(handler.getName(), otherHandler.getName()); - } -} diff --git a/scm-core/src/main/java/sonia/scm/resources/ResourceManager.java b/scm-core/src/main/java/sonia/scm/resources/ResourceManager.java deleted file mode 100644 index c287b8a7eb..0000000000 --- a/scm-core/src/main/java/sonia/scm/resources/ResourceManager.java +++ /dev/null @@ -1,70 +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.resources; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.List; - -/** - * This class collects and manages {@link Resource} - * which are used by the web interface. - * - * @author Sebastian Sdorra - * @since 1.12 - */ -public interface ResourceManager -{ - - /** - * Returns the resource with given name and type or - * null if no such resource exists. - * - * - * @param type type of the resource - * @param name name of the resource - * - * @return the resource with given name and type - */ - public Resource getResource(ResourceType type, String name); - - /** - * Returns the resources of the given type. - * - * - * @param type type of the resources to return - * - * @return resources of the given type - */ - public List getResources(ResourceType type); -} diff --git a/scm-core/src/main/java/sonia/scm/resources/ResourceNameComparator.java b/scm-core/src/main/java/sonia/scm/resources/ResourceNameComparator.java deleted file mode 100644 index 910e545db7..0000000000 --- a/scm-core/src/main/java/sonia/scm/resources/ResourceNameComparator.java +++ /dev/null @@ -1,79 +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.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -import java.util.Comparator; - -/** - * Compare {@link Resource} objects by its name. - * - * @author Sebastian Sdorra - * @since 1.16 - */ -public class ResourceNameComparator - implements Comparator, Serializable -{ - - /** Field description */ - public static final ResourceNameComparator INSTANCE = - new ResourceNameComparator(); - - /** Field description */ - private static final long serialVersionUID = 3474356901608301437L; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param resource - * @param otherResource - * - * @return - */ - @Override - public int compare(Resource resource, Resource otherResource) - { - return Util.compare(resource.getName(), otherResource.getName()); - } -} diff --git a/scm-core/src/main/java/sonia/scm/resources/ResourceType.java b/scm-core/src/main/java/sonia/scm/resources/ResourceType.java deleted file mode 100644 index faf3ac2a54..0000000000 --- a/scm-core/src/main/java/sonia/scm/resources/ResourceType.java +++ /dev/null @@ -1,102 +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.resources; - -/** - * This class represents the type of {@link Resource}. - * - * @author Sebastian Sdorra - */ -public enum ResourceType -{ - - /** - * Resource type for javascript resources - */ - SCRIPT("text/javascript", "js"), - - /** - * Resource type for stylesheet (css) resources - */ - STYLESHEET("text/css", "css"); - - /** - * Constructs a new resource type - * - * - * @param contentType content type of the resource type - * @param extension file extension of the resource type - */ - private ResourceType(String contentType, String extension) - { - this.contentType = contentType; - this.extension = extension; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns the content type of the resource type. - * - * - * @return content type of the resource type - * - * @since 1.12 - */ - public String getContentType() - { - return contentType; - } - - /** - * Returns the file extension of the resource type. - * - * - * @return file extension of the resource type - * - * @since 1.12 - */ - public String getExtension() - { - return extension; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final String contentType; - - /** Field description */ - private final String extension; -} diff --git a/scm-core/src/main/java/sonia/scm/search/SearchRequest.java b/scm-core/src/main/java/sonia/scm/search/SearchRequest.java index e3998e2c6a..63f34346c2 100644 --- a/scm-core/src/main/java/sonia/scm/search/SearchRequest.java +++ b/scm-core/src/main/java/sonia/scm/search/SearchRequest.java @@ -70,6 +70,12 @@ public class SearchRequest this.ignoreCase = ignoreCase; } + public SearchRequest(String query, boolean ignoreCase, int maxResults) { + this.query = query; + this.ignoreCase = ignoreCase; + this.maxResults = maxResults; + } + //~--- get methods ---------------------------------------------------------- /** diff --git a/scm-core/src/main/java/sonia/scm/search/SearchUtil.java b/scm-core/src/main/java/sonia/scm/search/SearchUtil.java index 10ce9ed1d1..3d86b113ff 100644 --- a/scm-core/src/main/java/sonia/scm/search/SearchUtil.java +++ b/scm-core/src/main/java/sonia/scm/search/SearchUtil.java @@ -37,16 +37,18 @@ package sonia.scm.search; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import sonia.scm.TransformFilter; import sonia.scm.util.Util; +//~--- JDK imports ------------------------------------------------------------ + import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Locale; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra @@ -90,16 +92,13 @@ public final class SearchUtil { result = true; - if (Util.isNotEmpty(other)) + for (String o : other) { - for (String o : other) + if ((o == null) ||!o.matches(query)) { - if ((o == null) ||!o.matches(query)) - { - result = false; + result = false; - break; - } + break; } } } @@ -125,16 +124,13 @@ public final class SearchUtil if (!value.matches(query)) { - if (Util.isNotEmpty(other)) + for (String o : other) { - for (String o : other) + if ((o != null) && o.matches(query)) { - if ((o != null) && o.matches(query)) - { - result = true; + result = true; - break; - } + break; } } } @@ -157,24 +153,29 @@ public final class SearchUtil * * @return */ - public static Collection search(SearchRequest searchRequest, - Collection collection, TransformFilter filter) + public static Collection search(SearchRequest searchRequest, + Collection collection, TransformFilter filter) { - List items = new ArrayList<>(); + List items = new ArrayList<>(); int index = 0; int counter = 0; + Iterator it = collection.iterator(); - for (final T aCollection : collection) { - T item = filter.accept(aCollection); + while (it.hasNext()) + { + R item = filter.accept(it.next()); - if (item != null) { + if (item != null) + { index++; - if (searchRequest.getStartWith() <= index) { + if (searchRequest.getStartWith() <= index) + { items.add(item); counter++; - if (searchRequest.getMaxResults() <= counter) { + if (searchRequest.getMaxResults() <= counter) + { break; } } diff --git a/scm-core/src/main/java/sonia/scm/security/AccessToken.java b/scm-core/src/main/java/sonia/scm/security/AccessToken.java index 714b09eff8..3341500199 100644 --- a/scm-core/src/main/java/sonia/scm/security/AccessToken.java +++ b/scm-core/src/main/java/sonia/scm/security/AccessToken.java @@ -31,77 +31,105 @@ package sonia.scm.security; import java.util.Date; +import java.util.Map; import java.util.Optional; +import java.util.Set; /** * An access token can be used to access scm-manager without providing username and password. An {@link AccessToken} can * be issued from a restful webservice endpoint by providing credentials. After the token was issued, the token must be * send along with every request. The token should be send in its compact representation as bearer authorization header * or as cookie. - * + * * @author Sebastian Sdorra * @since 2.0.0 */ public interface AccessToken { - + /** * Returns unique id of the access token. - * + * * @return unique id */ String getId(); - + /** * Returns name of subject which identifies the principal. - * + * * @return name of subject */ String getSubject(); - + /** * Returns optional issuer. The issuer identifies the principal that issued the token. - * + * * @return optional issuer */ Optional getIssuer(); - + /** * Returns time at which the token was issued. - * + * * @return time at which the token was issued */ Date getIssuedAt(); - + /** * Returns the expiration time of token. - * + * * @return expiration time */ Date getExpiration(); - + /** - * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this + * Returns refresh expiration of token. + * + * @return refresh expiration + */ + Optional getRefreshExpiration(); + + /** + * Returns id of the parent key. + * + * @return parent key id + */ + Optional getParentKey(); + + /** + * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this * token. For example we could issue a token which can only be used to read a single repository. for more informations * please have a look at {@link Scope}. - * + * * @return scope of token. */ Scope getScope(); - + + /** + * Returns name of groups, in which the user should be a member. + * + * @return name of groups + */ + Set getGroups(); + /** * Returns an optional value of a custom token field. - * + * * @param type of field * @param key key of token field - * + * * @return optional value of custom field */ Optional getCustom(String key); - + /** * Returns compact representation of token. - * + * * @return compact representation */ String compact(); + + /** + * Returns read only map of all claim keys with their values. + */ + Map getClaims(); } diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java index dd7986c22a..afe81ac27f 100644 --- a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java @@ -74,21 +74,31 @@ public interface AccessTokenBuilder { * Sets the expiration for the token. * * @param count expiration count - * @param unit expirtation unit + * @param unit expiration unit * * @return {@code this} */ AccessTokenBuilder expiresIn(long count, TimeUnit unit); - + + /** + * Sets the time how long this token may be refreshed. Set this to 0 (zero) to disable automatic refresh. + * + * @param count Time unit count. If set to 0, automatic refresh is disabled. + * @param unit time unit + * + * @return {@code this} + */ + AccessTokenBuilder refreshableFor(long count, TimeUnit unit); + /** * Reduces the permissions of the token by providing a scope. - * + * * @param scope scope of token - * + * * @return {@code this} */ AccessTokenBuilder scope(Scope scope); - + /** * Creates a new {@link AccessToken} with the provided settings. * diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java new file mode 100644 index 0000000000..999c693b8f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java @@ -0,0 +1,30 @@ +package sonia.scm.security; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Generates cookies and invalidates access token cookies. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public interface AccessTokenCookieIssuer { + + /** + * Creates a cookie for token authentication and attaches it to the response. + * + * @param request http servlet request + * @param response http servlet response + * @param accessToken access token + */ + void authenticate(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken); + /** + * Invalidates the authentication cookie. + * + * @param request http servlet request + * @param response http servlet response + */ + void invalidate(HttpServletRequest request, HttpServletResponse response); + +} diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenValidator.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenValidator.java new file mode 100644 index 0000000000..24a92929f9 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenValidator.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2014, 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.security; + +import sonia.scm.plugin.ExtensionPoint; + +/** + * Validates an {@link AccessToken}. The validator is called during authentication + * with an {@link AccessToken}. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +@ExtensionPoint +public interface AccessTokenValidator { + + /** + * Returns {@code true} if the {@link AccessToken} is valid. If the token is not valid and the + * method returns {@code false}, the authentication is treated as failed. + * + * @param token the access token to verify + * + * @return {@code true} if the token is valid + */ + boolean validate(AccessToken token); +} diff --git a/scm-core/src/main/java/sonia/scm/security/AnonymousToken.java b/scm-core/src/main/java/sonia/scm/security/AnonymousToken.java new file mode 100644 index 0000000000..1712a04a75 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AnonymousToken.java @@ -0,0 +1,16 @@ +package sonia.scm.security; + +import org.apache.shiro.authc.AuthenticationToken; + +public class AnonymousToken implements AuthenticationToken { + //Anonymous Token does not need an implementation + @Override + public Object getPrincipal() { + return null; + } + + @Override + public Object getCredentials() { + return null; + } +} diff --git a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java index c9e1283a96..cc7ff87534 100644 --- a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java @@ -89,8 +89,12 @@ public class AssignedPermission implements PermissionObject, Serializable */ public AssignedPermission(String name, String permission) { - this.name = name; - this.permission = permission; + this(name, new PermissionDescriptor(permission)); + } + + public AssignedPermission(String name, PermissionDescriptor permission) + { + this(name, false, permission); } /** @@ -103,6 +107,12 @@ public class AssignedPermission implements PermissionObject, Serializable */ public AssignedPermission(String name, boolean groupPermission, String permission) + { + this(name, groupPermission, new PermissionDescriptor(permission)); + } + + public AssignedPermission(String name, boolean groupPermission, + PermissionDescriptor permission) { this.name = name; this.groupPermission = groupPermission; @@ -151,10 +161,10 @@ public class AssignedPermission implements PermissionObject, Serializable { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("groupPermisison", groupPermission) - .add("permission", permission) - .toString(); + .add("name", name) + .add("groupPermission", groupPermission) + .add("permission", permission) + .toString(); //J+ } @@ -173,12 +183,9 @@ public class AssignedPermission implements PermissionObject, Serializable } /** - * Returns the string representation of the permission. - * - * - * @return string representation of the permission + * Returns the description of the permission. */ - public String getPermission() + public PermissionDescriptor getPermission() { return permission; } @@ -205,5 +212,5 @@ public class AssignedPermission implements PermissionObject, Serializable private String name; /** string representation of the permission */ - private String permission; + private PermissionDescriptor permission; } diff --git a/scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java new file mode 100644 index 0000000000..9ebea488a5 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java @@ -0,0 +1,155 @@ +/** + * 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.security; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import sonia.scm.HandlerEventType; +import sonia.scm.event.Event; + +import java.io.Serializable; + +//~--- JDK imports ------------------------------------------------------------ + +/** + * Event which is fired after a {@link StoredAssignedPermission} was added, + * removed or changed. + * + * @author Sebastian Sdorra + * @since 1.31 + */ +@Event +public final class AssignedPermissionEvent implements Serializable +{ + + /** serial version uid */ + private static final long serialVersionUID = 706824497813169009L; + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs a new AssignedPermissionEvent. + * + * + * @param type type of the event + * @param permission permission object which has changed + */ + public AssignedPermissionEvent(HandlerEventType type, + AssignedPermission permission) + { + this.type = type; + this.permission = permission; + } + + //~--- methods -------------------------------------------------------------- + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + final AssignedPermissionEvent other = + (AssignedPermissionEvent) obj; + + return Objects.equal(type, other.type) + && Objects.equal(permission, other.permission); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() + { + return Objects.hashCode(type, permission); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + //J- + return MoreObjects.toStringHelper(this) + .add("type", type) + .add("permission", permission) + .toString(); + //J+ + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Return the type of the event. + * + * + * @return type of event + */ + public HandlerEventType getEventType() + { + return type; + } + + /** + * Returns the changed permission object. + * + * + * @return changed permission + */ + public AssignedPermission getPermission() + { + return permission; + } + + //~--- fields --------------------------------------------------------------- + + /** changed permission */ + private AssignedPermission permission; + + /** type of the event */ + private HandlerEventType type; +} diff --git a/scm-core/src/main/java/sonia/scm/security/Authentications.java b/scm-core/src/main/java/sonia/scm/security/Authentications.java new file mode 100644 index 0000000000..65332e20f4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/Authentications.java @@ -0,0 +1,15 @@ +package sonia.scm.security; + +import org.apache.shiro.SecurityUtils; +import sonia.scm.SCMContext; + +public class Authentications { + + public static boolean isAuthenticatedSubjectAnonymous() { + return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal()); + } + + public static boolean isSubjectAnonymous(String principal) { + return SCMContext.USER_ANONYMOUS.equals(principal); + } +} diff --git a/scm-core/src/main/java/sonia/scm/security/AuthorizationCollector.java b/scm-core/src/main/java/sonia/scm/security/AuthorizationCollector.java index d1151c3b35..b12d8b6978 100644 --- a/scm-core/src/main/java/sonia/scm/security/AuthorizationCollector.java +++ b/scm-core/src/main/java/sonia/scm/security/AuthorizationCollector.java @@ -34,6 +34,7 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.subject.PrincipalCollection; import sonia.scm.plugin.ExtensionPoint; /** @@ -42,15 +43,16 @@ import sonia.scm.plugin.ExtensionPoint; * @author Sebastian Sdorra * @since 2.0.0 */ -@ExtensionPoint(multi = false) +@ExtensionPoint public interface AuthorizationCollector { /** * Returns {@link AuthorizationInfo} for the authenticated user. * + * @param principalCollection collected principals * * @return {@link AuthorizationInfo} for authenticated user */ - public AuthorizationInfo collect(); + AuthorizationInfo collect(PrincipalCollection principalCollection); } diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java index 82555fd0b7..6ec64a67de 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java @@ -35,19 +35,21 @@ package sonia.scm.security; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSet.Builder; -import org.apache.shiro.authc.*; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.DisabledAccountException; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UnknownAccountException; +import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.subject.SimplePrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; import sonia.scm.user.User; import sonia.scm.user.UserDAO; +import java.util.Collections; + import static com.google.common.base.Preconditions.checkArgument; /** @@ -57,8 +59,7 @@ import static com.google.common.base.Preconditions.checkArgument; * @author Sebastian Sdorra * @since 2.0.0 */ -public final class DAORealmHelper -{ +public final class DAORealmHelper { /** * the logger for DAORealmHelper @@ -69,8 +70,6 @@ public final class DAORealmHelper private final UserDAO userDAO; - private final GroupDAO groupDAO; - private final String realm; //~--- constructors --------------------------------------------------------- @@ -81,14 +80,12 @@ public final class DAORealmHelper * * @param loginAttemptHandler login attempt handler for wrapping credentials matcher * @param userDAO user dao - * @param groupDAO group dao * @param realm name of realm */ - public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO, String realm) { + public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, String realm) { this.loginAttemptHandler = loginAttemptHandler; this.realm = realm; this.userDAO = userDAO; - this.groupDAO = groupDAO; } //~--- get methods ---------------------------------------------------------- @@ -103,18 +100,16 @@ public final class DAORealmHelper public CredentialsMatcher wrapCredentialsMatcher(CredentialsMatcher credentialsMatcher) { return new RetryLimitPasswordMatcher(loginAttemptHandler, credentialsMatcher); } - + /** - * Method description + * Creates {@link AuthenticationInfo} from a {@link UsernamePasswordToken}. The method accepts + * {@link AuthenticationInfo} as argument, so that the caller does not need to cast. * + * @param token authentication token, it must be {@link UsernamePasswordToken} * - * @param token - * - * @return - * - * @throws AuthenticationException + * @return authentication info */ - public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { + public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) { checkArgument(token instanceof UsernamePasswordToken, "%s is required", UsernamePasswordToken.class); UsernamePasswordToken upt = (UsernamePasswordToken) token; @@ -124,16 +119,18 @@ public final class DAORealmHelper } /** - * Method description + * Returns a builder for {@link AuthenticationInfo}. * + * @param principal name of principal (username) * - * @param principal - * @param credentials - * @param scope - * - * @return + * @return authentication info builder */ - public AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) { + public AuthenticationInfoBuilder authenticationInfoBuilder(String principal) { + return new AuthenticationInfoBuilder(principal); + } + + + private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) { checkArgument(!Strings.isNullOrEmpty(principal), "username is required"); LOG.debug("try to authenticate {}", principal); @@ -151,7 +148,6 @@ public final class DAORealmHelper collection.add(principal, realm); collection.add(user, realm); - collection.add(collectGroups(principal), realm); collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm); String creds = credentials; @@ -165,20 +161,67 @@ public final class DAORealmHelper //~--- methods -------------------------------------------------------------- - private GroupNames collectGroups(String principal) { - Builder builder = ImmutableSet.builder(); + /** + * Builder class for {@link AuthenticationInfo}. + */ + public class AuthenticationInfoBuilder { - builder.add(GroupNames.AUTHENTICATED); + private final String principal; - for (Group group : groupDAO.getAll()) { - if (group.isMember(principal)) { - builder.add(group.getName()); - } + private String credentials; + private Scope scope; + private Iterable groups = Collections.emptySet(); + + private AuthenticationInfoBuilder(String principal) { + this.principal = principal; + } + + /** + * With credentials uses the given credentials for the {@link AuthenticationInfo}, this is particularly important + * for caching purposes. + * + * @param credentials credentials such as password + * + * @return {@code this} + */ + public AuthenticationInfoBuilder withCredentials(String credentials) { + this.credentials = credentials; + return this; + } + + /** + * With the scope object it is possible to limit the access permissions to scm-manager. + * + * @param scope scope object + * + * @return {@code this} + */ + public AuthenticationInfoBuilder withScope(Scope scope) { + this.scope = scope; + return this; + } + +// /** +// * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info. +// * +// * @param groups extra groups +// * +// * @return {@code this} +// */ +// public AuthenticationInfoBuilder withGroups(Iterable groups) { +// this.groups = groups; +// return this; +// } + + /** + * Build creates the authentication info from the given information. + * + * @return authentication info + */ + public AuthenticationInfo build() { + return getAuthenticationInfo(principal, credentials, scope); } - GroupNames groups = new GroupNames(builder.build()); - LOG.debug("collected following groups for principal {}: {}", principal, groups); - return groups; } private static class RetryLimitPasswordMatcher implements CredentialsMatcher { diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java index 803ae5d714..dd59de2ac8 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java @@ -30,10 +30,11 @@ */ package sonia.scm.security; -import javax.inject.Inject; -import sonia.scm.group.GroupDAO; +import sonia.scm.cache.CacheManager; import sonia.scm.user.UserDAO; +import javax.inject.Inject; + /** * Factory to create {@link DAORealmHelper} instances. * @@ -44,20 +45,19 @@ public final class DAORealmHelperFactory { private final LoginAttemptHandler loginAttemptHandler; private final UserDAO userDAO; - private final GroupDAO groupDAO; + private final CacheManager cacheManager; /** * Constructs a new instance. - * * @param loginAttemptHandler login attempt handler * @param userDAO user dao - * @param groupDAO group dao + * @param cacheManager */ @Inject - public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO) { + public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, CacheManager cacheManager) { this.loginAttemptHandler = loginAttemptHandler; this.userDAO = userDAO; - this.groupDAO = groupDAO; + this.cacheManager = cacheManager; } /** @@ -68,7 +68,7 @@ public final class DAORealmHelperFactory { * @return new {@link DAORealmHelper} instance. */ public DAORealmHelper create(String realm) { - return new DAORealmHelper(loginAttemptHandler, userDAO, groupDAO, realm); + return new DAORealmHelper(loginAttemptHandler, userDAO, realm); } } diff --git a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java index ddb8a699a7..8c3afbd90f 100644 --- a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java +++ b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java @@ -45,8 +45,6 @@ import sonia.scm.util.IOUtil; //~--- JDK imports ------------------------------------------------------------ -import com.sun.jersey.core.util.Base64; - import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; @@ -60,6 +58,7 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; +import java.util.Base64; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; @@ -162,10 +161,17 @@ public class DefaultCipherHandler implements CipherHandler { * @return decrypted value */ public String decode(char[] plainKey, String value) { - String result = null; - + Base64.Decoder decoder = Base64.getUrlDecoder(); try { - byte[] encodedInput = Base64.decode(value); + return decode(plainKey, value, decoder); + } catch (IllegalArgumentException e) { + return decode(plainKey, value, Base64.getDecoder()); + } + } + + private String decode(char[] plainKey, String value, Base64.Decoder decoder) { + try { + byte[] encodedInput = decoder.decode(value); byte[] salt = new byte[SALT_LENGTH]; byte[] encoded = new byte[encodedInput.length - SALT_LENGTH]; @@ -181,12 +187,10 @@ public class DefaultCipherHandler implements CipherHandler { byte[] decoded = cipher.doFinal(encoded); - result = new String(decoded, ENCODING); + return new String(decoded, ENCODING); } catch (IOException | GeneralSecurityException ex) { throw new CipherException("could not decode string", ex); } - - return result; } @Override @@ -222,7 +226,7 @@ public class DefaultCipherHandler implements CipherHandler { System.arraycopy(salt, 0, result, 0, SALT_LENGTH); System.arraycopy(encodedInput, 0, result, SALT_LENGTH, result.length - SALT_LENGTH); - res = new String(Base64.encode(result), ENCODING); + res = new String(Base64.getUrlEncoder().encode(result), ENCODING); } catch (IOException | GeneralSecurityException ex) { throw new CipherException("could not encode string", ex); } diff --git a/scm-core/src/main/java/sonia/scm/security/Permission.java b/scm-core/src/main/java/sonia/scm/security/Permission.java new file mode 100644 index 0000000000..a7aa2798e7 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/Permission.java @@ -0,0 +1,13 @@ +package sonia.scm.security; + +import com.github.sdorra.ssp.PermissionObject; +import com.github.sdorra.ssp.StaticPermissions; + +@StaticPermissions( + value = "permission", + permissions = {}, + globalPermissions = {"list", "read", "assign"}, + custom = true, customGlobal = true +) +public interface Permission extends PermissionObject { +} diff --git a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java index acde1b447a..3ea7737b4e 100644 --- a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java @@ -39,7 +39,6 @@ import com.google.common.base.Objects; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; @@ -67,19 +66,8 @@ public class PermissionDescriptor implements Serializable */ public PermissionDescriptor() {} - /** - * Constructs ... - * - * - * @param displayName - * @param description - * @param value - */ - public PermissionDescriptor(String displayName, String description, - String value) + public PermissionDescriptor(String value) { - this.displayName = displayName; - this.description = description; this.value = value; } @@ -103,9 +91,7 @@ public class PermissionDescriptor implements Serializable final PermissionDescriptor other = (PermissionDescriptor) obj; - return Objects.equal(displayName, other.displayName) - && Objects.equal(description, other.description) - && Objects.equal(value, other.value); + return Objects.equal(value, other.value); } /** @@ -114,7 +100,7 @@ public class PermissionDescriptor implements Serializable @Override public int hashCode() { - return Objects.hashCode(displayName, description, value); + return value == null? -1: value.hashCode(); } /** @@ -126,38 +112,14 @@ public class PermissionDescriptor implements Serializable //J- return MoreObjects.toStringHelper(this) - .add("displayName", displayName) - .add("description", description) - .add("value", value) - .toString(); + .add("value", value) + .toString(); //J+ } //~--- get methods ---------------------------------------------------------- - /** - * Returns the description of the permission. - * - * - * @return description - */ - public String getDescription() - { - return description; - } - - /** - * Returns the display name of the permission. - * - * - * @return display name - */ - public String getDisplayName() - { - return displayName; - } - /** * Returns the string representation of the permission. * @@ -171,13 +133,6 @@ public class PermissionDescriptor implements Serializable //~--- fields --------------------------------------------------------------- - /** description */ - private String description; - - /** display name */ - @XmlElement(name = "display-name") - private String displayName; - /** value */ private String value; } diff --git a/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java b/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java deleted file mode 100644 index ae98d70e47..0000000000 --- a/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java +++ /dev/null @@ -1,232 +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.security; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; - -import org.apache.shiro.authz.Permission; - -import sonia.scm.repository.PermissionType; -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - -/** - * This class represents the permission to a repository of a user. - * - * @author Sebastian Sdorra - * @since 1.21 - */ -public final class RepositoryPermission - implements StringablePermission, Serializable -{ - - /** - * Type string of the permission - * @since 1.31 - */ - public static final String TYPE = "repository"; - - /** Field description */ - public static final String WILDCARD = "*"; - - /** Field description */ - private static final long serialVersionUID = 3832804235417228043L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param repository - * @param permissionType - */ - public RepositoryPermission(Repository repository, - PermissionType permissionType) - { - this(repository.getId(), permissionType); - } - - /** - * Constructs ... - * - * - * @param repositoryId - * @param permissionType - */ - public RepositoryPermission(String repositoryId, - PermissionType permissionType) - { - this.repositoryId = repositoryId; - this.permissionType = permissionType; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param obj - * - * @return - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final RepositoryPermission other = (RepositoryPermission) obj; - - return Objects.equal(repositoryId, other.repositoryId) - && Objects.equal(permissionType, other.permissionType); - } - - /** - * Method description - * - * - * @return - */ - @Override - public int hashCode() - { - return Objects.hashCode(repositoryId, permissionType); - } - - /** - * Method description - * - * - * @param p - * - * @return - */ - @Override - public boolean implies(Permission p) - { - boolean result = false; - - if (p instanceof RepositoryPermission) - { - RepositoryPermission rp = (RepositoryPermission) p; - - //J- - result = (repositoryId.equals(WILDCARD) || repositoryId.equals(rp.repositoryId)) - && (permissionType.getValue() >= rp.permissionType.getValue()); - //J+ - } - - return result; - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - //J- - return MoreObjects.toStringHelper(this) - .add("repositoryId", repositoryId) - .add("permissionType", permissionType) - .toString(); - //J+ - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getAsString() - { - StringBuilder buffer = new StringBuilder(TYPE); - - buffer.append(":").append(repositoryId).append(":").append(permissionType); - - return buffer.toString(); - } - - /** - * Method description - * - * - * @return - */ - public PermissionType getPermissionType() - { - return permissionType; - } - - /** - * Method description - * - * - * @return - */ - public String getRepositoryId() - { - return repositoryId; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private PermissionType permissionType; - - /** Field description */ - private String repositoryId; -} diff --git a/scm-core/src/main/java/sonia/scm/security/Role.java b/scm-core/src/main/java/sonia/scm/security/Role.java index 470c3983f0..c41021b6e6 100644 --- a/scm-core/src/main/java/sonia/scm/security/Role.java +++ b/scm-core/src/main/java/sonia/scm/security/Role.java @@ -41,9 +41,6 @@ package sonia.scm.security; public final class Role { - /** Field description */ - public static final String ADMIN = "admin"; - /** Field description */ public static final String USER = "user"; diff --git a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java index f43afcb7f2..174b64f5e6 100644 --- a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java +++ b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java @@ -32,13 +32,8 @@ package sonia.scm.security; -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Predicate; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.List; +import java.util.Collection; +import java.util.function.Predicate; /** * The SecuritySystem manages global permissions. @@ -57,7 +52,7 @@ public interface SecuritySystem * * @return stored permission */ - public StoredAssignedPermission addPermission(AssignedPermission permission); + void addPermission(AssignedPermission permission); /** * Delete stored permission. @@ -65,51 +60,17 @@ public interface SecuritySystem * * @param permission permission to be deleted */ - public void deletePermission(StoredAssignedPermission permission); - - /** - * Delete stored permission. - * - * - * @param id id of the permission - */ - public void deletePermission(String id); - - /** - * Modify stored permission. - * - * - * @param permission stored permisison - */ - public void modifyPermission(StoredAssignedPermission permission); + void deletePermission(AssignedPermission permission); //~--- get methods ---------------------------------------------------------- - /** - * Return all stored permissions. - * - * - * @return stored permission - */ - public List getAllPermissions(); - /** * Return all available permissions. * * * @return available permissions */ - public List getAvailablePermissions(); - - /** - * Return the stored permission which is stored with the given id. - * - * - * @param id id of the stored permission - * - * @return stored permission - */ - public StoredAssignedPermission getPermission(String id); + Collection getAvailablePermissions(); /** * Returns all stored permissions which are matched by the given @@ -120,6 +81,5 @@ public interface SecuritySystem * * @return filtered permissions */ - public List getPermissions( - Predicate predicate); + Collection getPermissions(Predicate predicate); } diff --git a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java b/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java deleted file mode 100644 index 1c2d0af156..0000000000 --- a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java +++ /dev/null @@ -1,155 +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.security; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; -import sonia.scm.HandlerEventType; -import sonia.scm.event.Event; - -import java.io.Serializable; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * Event which is fired after a {@link StoredAssignedPermission} was added, - * removed or changed. - * - * @author Sebastian Sdorra - * @since 1.31 - */ -@Event -public final class StoredAssignedPermissionEvent implements Serializable -{ - - /** serial version uid */ - private static final long serialVersionUID = 706824497813169009L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new StoredAssignedPermissionEvent. - * - * - * @param type type of the event - * @param permission permission object which has changed - */ - public StoredAssignedPermissionEvent(HandlerEventType type, - StoredAssignedPermission permission) - { - this.type = type; - this.permission = permission; - } - - //~--- methods -------------------------------------------------------------- - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final StoredAssignedPermissionEvent other = - (StoredAssignedPermissionEvent) obj; - - return Objects.equal(type, other.type) - && Objects.equal(permission, other.permission); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() - { - return Objects.hashCode(type, permission); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() - { - //J- - return MoreObjects.toStringHelper(this) - .add("type", type) - .add("permission", permission) - .toString(); - //J+ - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Return the type of the event. - * - * - * @return type of event - */ - public HandlerEventType getEventType() - { - return type; - } - - /** - * Returns the changed permission object. - * - * - * @return changed permission - */ - public StoredAssignedPermission getPermission() - { - return permission; - } - - //~--- fields --------------------------------------------------------------- - - /** changed permission */ - private StoredAssignedPermission permission; - - /** type of the event */ - private HandlerEventType type; -} diff --git a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java index 4c5b5c4e53..b2175f304a 100644 --- a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java @@ -28,29 +28,19 @@ */ package sonia.scm.security; -import com.google.common.collect.ImmutableList; import com.google.inject.Inject; - -import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.subject.SimplePrincipalCollection; - +import sonia.scm.AlreadyExistsException; +import sonia.scm.NotFoundException; import sonia.scm.group.Group; -import sonia.scm.group.GroupException; import sonia.scm.group.GroupManager; -import sonia.scm.group.GroupNames; import sonia.scm.plugin.Extension; import sonia.scm.user.User; -import sonia.scm.user.UserException; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; - -import java.io.IOException; - -import java.util.Collection; - /** * Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated * users with the local database. @@ -62,15 +52,12 @@ import java.util.Collection; public final class SyncingRealmHelper { private final AdministrationContext ctx; - + private final UserManager userManager; private final GroupManager groupManager; - private final UserManager userManager; - /** * Constructs a new SyncingRealmHelper. * - * * @param ctx administration context * @param userManager user manager * @param groupManager group manager @@ -82,39 +69,20 @@ public final class SyncingRealmHelper { this.groupManager = groupManager; } - //~--- methods -------------------------------------------------------------- /** * Create {@link AuthenticationInfo} from user and groups. * * * @param realm name of the realm * @param user authenticated user - * @param groups groups of the authenticated user * * @return authentication info */ - public AuthenticationInfo createAuthenticationInfo(String realm, User user, - String... groups) { - return createAuthenticationInfo(realm, user, ImmutableList.copyOf(groups)); - } - - /** - * Create {@link AuthenticationInfo} from user and groups. - * - * - * @param realm name of the realm - * @param user authenticated user - * @param groups groups of the authenticated user - * - * @return authentication info - */ - public AuthenticationInfo createAuthenticationInfo(String realm, User user, - Collection groups) { + public AuthenticationInfo createAuthenticationInfo(String realm, User user) { SimplePrincipalCollection collection = new SimplePrincipalCollection(); collection.add(user.getId(), realm); collection.add(user, realm); - collection.add(new GroupNames(groups), realm); return new SimpleAuthenticationInfo(collection, user.getPassword()); } @@ -126,17 +94,19 @@ public final class SyncingRealmHelper { */ public void store(final Group group) { ctx.runAsAdmin(() -> { - try { - if (groupManager.get(group.getId()) != null) { + if (groupManager.get(group.getId()) != null) { + try { groupManager.modify(group); + } catch (NotFoundException e) { + throw new IllegalStateException("got NotFoundException though group " + group.getName() + " could be loaded", e); } - else { + } else { + try { groupManager.create(group); + } catch (AlreadyExistsException e) { + throw new IllegalStateException("got AlreadyExistsException though group " + group.getName() + " could not be loaded", e); } } - catch (GroupException | IOException ex) { - throw new AuthenticationException("could not store group", ex); - } }); } @@ -147,17 +117,20 @@ public final class SyncingRealmHelper { */ public void store(final User user) { ctx.runAsAdmin(() -> { - try { - if (userManager.contains(user.getName())) { + if (userManager.contains(user.getName())) { + try { userManager.modify(user); + } catch (NotFoundException e) { + throw new IllegalStateException("got NotFoundException though user " + user.getName() + " could be loaded", e); } - else { + } else { + try { userManager.create(user); + } catch (AlreadyExistsException e) { + throw new IllegalStateException("got AlreadyExistsException though user " + user.getName() + " could not be loaded", e); + } } - catch (UserException | IOException ex) { - throw new AuthenticationException("could not store user", ex); - } }); } } diff --git a/scm-core/src/main/java/sonia/scm/security/TokenClaimsValidator.java b/scm-core/src/main/java/sonia/scm/security/TokenClaimsValidator.java deleted file mode 100644 index 4389e7bfb7..0000000000 --- a/scm-core/src/main/java/sonia/scm/security/TokenClaimsValidator.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2014, 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.security; - -import java.util.Map; -import sonia.scm.plugin.ExtensionPoint; - -/** - * Validates the claims of a jwt token. The validator is called durring authentication - * with a jwt token. - * - * @author Sebastian Sdorra - * @since 2.0.0 - */ -@ExtensionPoint -public interface TokenClaimsValidator { - - /** - * Returns {@code true} if the claims is valid. If the token is not valid and the - * method returns {@code false}, the authentication is treated as failed. - * - * @param claims token claims - * - * @return {@code true} if the claims is valid - */ - boolean validate(Map claims); -} diff --git a/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java index 941b3923d1..4cc124c5d7 100644 --- a/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java @@ -32,9 +32,25 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** - * The BlobStoreFactory can be used to create new or get existing - * {@link BlobStore}s. + * The BlobStoreFactory can be used to create a new or get an existing {@link BlobStore}s. + *
+ * You can either create a global {@link BlobStore} or a {@link BlobStore} for a specific repository. To create a global + * {@link BlobStore} call: + *
+ *     blobStoreFactory
+ *       .withName("name")
+ *       .build();
+ * 
+ * To create a {@link BlobStore} for a specific repository call: + *
+ *     blobStoreFactory
+ *       .withName("name")
+ *       .forRepository(repository)
+ *       .build();
+ * 
* * @author Sebastian Sdorra * @since 1.23 @@ -45,13 +61,79 @@ package sonia.scm.store; public interface BlobStoreFactory { /** - * Returns a {@link BlobStore} with the given name, if the {@link BlobStore} - * with the given name does not exists the factory will create a new one. + * Creates a new or gets an existing {@link BlobStore}. Instead of calling this method you should use the floating API + * from {@link #withName(String)}. * - * - * @param name name of the {@link BlobStore} - * - * @return {@link BlobStore} with the given name + * @param storeParameters The parameters for the blob store. + * @return A new or an existing {@link BlobStore} for the given parameters. */ - public BlobStore getBlobStore(String name); + BlobStore getStore(final StoreParameters storeParameters); + + /** + * Use this to create a new or get an existing {@link BlobStore} with a floating API. + * @param name The name for the {@link BlobStore}. + * @return Floating API to either specify a repository or directly build a global {@link BlobStore}. + */ + default FloatingStoreParameters.Builder withName(String name) { + return new FloatingStoreParameters(this).new Builder(name); + } +} + +final class FloatingStoreParameters implements StoreParameters { + + private String name; + private String repositoryId; + + private final BlobStoreFactory factory; + + FloatingStoreParameters(BlobStoreFactory factory) { + this.factory = factory; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getRepositoryId() { + return repositoryId; + } + + public class Builder { + + Builder(String name) { + FloatingStoreParameters.this.name = name; + } + + /** + * Use this to create or get a {@link BlobStore} for a specific repository. This step is optional. If you want to + * have a global {@link BlobStore}, omit this. + * @param repository The optional repository for the {@link BlobStore}. + * @return Floating API to finish the call. + */ + public FloatingStoreParameters.Builder forRepository(Repository repository) { + FloatingStoreParameters.this.repositoryId = repository.getId(); + return this; + } + + /** + * Use this to create or get a {@link BlobStore} for a specific repository. This step is optional. If you want to + * have a global {@link BlobStore}, omit this. + * @param repositoryId The id of the optional repository for the {@link BlobStore}. + * @return Floating API to finish the call. + */ + public FloatingStoreParameters.Builder forRepository(String repositoryId) { + FloatingStoreParameters.this.repositoryId = repositoryId; + return this; + } + + /** + * Creates or gets the {@link BlobStore} with the given name and (if specified) the given repository. If no + * repository is given, the {@link BlobStore} will be global. + */ + public BlobStore build(){ + return factory.getStore(FloatingStoreParameters.this); + } + } } diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java index 7cfebd69c1..7ff1633dc3 100644 --- a/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java @@ -32,31 +32,115 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** - * The ConfigurationEntryStoreFactory can be used to create new or get existing - * {@link ConfigurationEntryStore}s. Note: the default implementation - * uses the same location as the {@link StoreFactory}, so be sure that the - * store names are unique for all {@link ConfigurationEntryStore}s and - * {@link Store}s. - * + * The ConfigurationEntryStoreFactory can be used to create new or get existing {@link ConfigurationEntryStore}s. + *
+ * Note: the default implementation uses the same location as the {@link ConfigurationStoreFactory}, so be sure + * that the store names are unique for all {@link ConfigurationEntryStore}s and {@link ConfigurationEntryStore}s. + *
+ * You can either create a global {@link ConfigurationEntryStore} or a {@link ConfigurationEntryStore} for a specific + * repository. To create a global {@link ConfigurationEntryStore} call: + *
+ *     configurationEntryStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .build();
+ * 
+ * To create a {@link ConfigurationEntryStore} for a specific repository call: + *
+ *     configurationEntryStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .forRepository(repository)
+ *       .build();
+ * 
+ * * @author Sebastian Sdorra * @since 1.31 * * @apiviz.landmark * @apiviz.uses sonia.scm.store.ConfigurationEntryStore */ -public interface ConfigurationEntryStoreFactory -{ +public interface ConfigurationEntryStoreFactory { /** - * Get an existing {@link ConfigurationEntryStore} or create a new one. + * Creates a new or gets an existing {@link ConfigurationEntryStore}. Instead of calling this method you should use + * the floating API from {@link #withType(Class)}. * - * - * @param type type of the store objects - * @param name name of the store - * @param type of the store objects - * - * @return {@link ConfigurationEntryStore} with given name and type + * @param storeParameters The parameters for the {@link ConfigurationEntryStore}. + * @return A new or an existing {@link ConfigurationEntryStore} for the given parameters. */ - public ConfigurationEntryStore getStore(Class type, String name); + ConfigurationEntryStore getStore(final TypedStoreParameters storeParameters); + + /** + * Use this to create a new or get an existing {@link ConfigurationEntryStore} with a floating API. + * @param type The type for the {@link ConfigurationEntryStore}. + * @return Floating API to set the name and either specify a repository or directly build a global + * {@link ConfigurationEntryStore}. + */ + default TypedFloatingConfigurationEntryStoreParameters.Builder withType(Class type) { + return new TypedFloatingConfigurationEntryStoreParameters(this).new Builder(type); + } +} + +final class TypedFloatingConfigurationEntryStoreParameters { + + private final TypedStoreParametersImpl parameters = new TypedStoreParametersImpl<>(); + private final ConfigurationEntryStoreFactory factory; + + TypedFloatingConfigurationEntryStoreParameters(ConfigurationEntryStoreFactory factory) { + this.factory = factory; + } + + public class Builder { + + Builder(Class type) { + parameters.setType(type); + } + + /** + * Use this to set the name for the {@link ConfigurationEntryStore}. + * @param name The name for the {@link ConfigurationEntryStore}. + * @return Floating API to either specify a repository or directly build a global {@link ConfigurationEntryStore}. + */ + public OptionalRepositoryBuilder withName(String name) { + parameters.setName(name); + return new OptionalRepositoryBuilder(); + } + } + + public class OptionalRepositoryBuilder { + + /** + * Use this to create or get a {@link ConfigurationEntryStore} for a specific repository. This step is optional. If + * you want to have a global {@link ConfigurationEntryStore}, omit this. + * @param repository The optional repository for the {@link ConfigurationEntryStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(Repository repository) { + parameters.setRepositoryId(repository.getId()); + return this; + } + + /** + * Use this to create or get a {@link ConfigurationEntryStore} for a specific repository. This step is optional. If + * you want to have a global {@link ConfigurationEntryStore}, omit this. + * @param repositoryId The id of the optional repository for the {@link ConfigurationEntryStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(String repositoryId) { + parameters.setRepositoryId(repositoryId); + return this; + } + + /** + * Creates or gets the {@link ConfigurationEntryStore} with the given name and (if specified) the given repository. + * If no repository is given, the {@link ConfigurationEntryStore} will be global. + */ + public ConfigurationEntryStore build(){ + return factory.getStore(parameters); + } + } } diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java index a7f21dd304..b1c38f1a00 100644 --- a/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java +++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java @@ -33,6 +33,10 @@ package sonia.scm.store; +import java.util.Optional; + +import static java.util.Optional.ofNullable; + /** * ConfigurationStore for configuration objects. Note: the default * implementation use JAXB to marshall the configuration objects. @@ -50,7 +54,17 @@ public interface ConfigurationStore * * @return configuration object from store */ - public T get(); + T get(); + + /** + * Returns the configuration object from store. + * + * + * @return configuration object from store + */ + default Optional getOptional() { + return ofNullable(get()); + } //~--- set methods ---------------------------------------------------------- @@ -58,7 +72,7 @@ public interface ConfigurationStore * Stores the given configuration object to the store. * * - * @param obejct configuration object to store + * @param object configuration object to store */ - public void set(T obejct); + void set(T object); } diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java index d9a97de98d..cef39a303b 100644 --- a/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java @@ -33,27 +33,114 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** - * The ConfigurationStoreFactory can be used to create new or get existing - * {@link ConfigurationStore} objects. + * The ConfigurationStoreFactory can be used to create new or get existing {@link ConfigurationStore} objects. + *
+ * Note: the default implementation uses the same location as the {@link ConfigurationEntryStoreFactory}, so be + * sure that the store names are unique for all {@link ConfigurationEntryStore}s and {@link ConfigurationStore}s. + *
+ * You can either create a global {@link ConfigurationStore} or a {@link ConfigurationStore} for a specific repository. + * To create a global {@link ConfigurationStore} call: + *
+ *     configurationStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .build();
+ * 
+ * To create a {@link ConfigurationStore} for a specific repository call: + *
+ *     configurationStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .forRepository(repository)
+ *       .build();
+ * 
* * @author Sebastian Sdorra * * @apiviz.landmark * @apiviz.uses sonia.scm.store.ConfigurationStore */ -public interface ConfigurationStoreFactory -{ +public interface ConfigurationStoreFactory { /** - * Get an existing {@link ConfigurationStore} or create a new one. + * Creates a new or gets an existing {@link ConfigurationStore}. Instead of calling this method you should use the + * floating API from {@link #withType(Class)}. * - * - * @param type type of the store objects - * @param name name of the store - * @param type of the store objects - * - * @return {@link ConfigurationStore} of the given type and name + * @param storeParameters The parameters for the {@link ConfigurationStore}. + * @return A new or an existing {@link ConfigurationStore} for the given parameters. */ - public ConfigurationStore getStore(Class type, String name); + ConfigurationStore getStore(final TypedStoreParameters storeParameters); + + /** + * Use this to create a new or get an existing {@link ConfigurationStore} with a floating API. + * @param type The type for the {@link ConfigurationStore}. + * @return Floating API to set the name and either specify a repository or directly build a global + * {@link ConfigurationStore}. + */ + default TypedFloatingConfigurationStoreParameters.Builder withType(Class type) { + return new TypedFloatingConfigurationStoreParameters(this).new Builder(type); + } +} + +final class TypedFloatingConfigurationStoreParameters { + + private final TypedStoreParametersImpl parameters = new TypedStoreParametersImpl<>(); + private final ConfigurationStoreFactory factory; + + TypedFloatingConfigurationStoreParameters(ConfigurationStoreFactory factory) { + this.factory = factory; + } + + public class Builder { + + Builder(Class type) { + parameters.setType(type); + } + + /** + * Use this to set the name for the {@link ConfigurationStore}. + * @param name The name for the {@link ConfigurationStore}. + * @return Floating API to either specify a repository or directly build a global {@link ConfigurationStore}. + */ + public OptionalRepositoryBuilder withName(String name) { + parameters.setName(name); + return new OptionalRepositoryBuilder(); + } + } + + public class OptionalRepositoryBuilder { + + /** + * Use this to create or get a {@link ConfigurationStore} for a specific repository. This step is optional. If you + * want to have a global {@link ConfigurationStore}, omit this. + * @param repository The optional repository for the {@link ConfigurationStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(Repository repository) { + parameters.setRepositoryId(repository.getId()); + return this; + } + + /** + * Use this to create or get a {@link ConfigurationStore} for a specific repository. This step is optional. If you + * want to have a global {@link ConfigurationStore}, omit this. + * @param repositoryId The id of the optional repository for the {@link ConfigurationStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(String repositoryId) { + parameters.setRepositoryId(repositoryId); + return this; + } + + /** + * Creates or gets the {@link ConfigurationStore} with the given name and (if specified) the given repository. If no + * repository is given, the {@link ConfigurationStore} will be global. + */ + public ConfigurationStore build(){ + return factory.getStore(parameters); + } + } } diff --git a/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java index caed974ee4..cc149d46ce 100644 --- a/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java @@ -32,9 +32,27 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** - * The DataStoreFactory can be used to create new or get existing - * {@link DataStore}s. + * The DataStoreFactory can be used to create new or get existing {@link DataStore}s. + *
+ * You can either create a global {@link DataStore} or a {@link DataStore} for a specific repository. + * To create a global {@link DataStore} call: + *
+ *     dataStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .build();
+ * 
+ * To create a {@link DataStore} for a specific repository call: + *
+ *     dataStoreFactory
+ *       .withType(PersistedType.class)
+ *       .withName("name")
+ *       .forRepository(repository)
+ *       .build();
+ * 
* * @author Sebastian Sdorra * @since 1.23 @@ -45,14 +63,81 @@ package sonia.scm.store; public interface DataStoreFactory { /** - * Get an existing {@link DataStore} or create a new one. + * Creates a new or gets an existing {@link DataStore}. Instead of calling this method you should use the + * floating API from {@link #withType(Class)}. * - * - * @param type type of the store objects - * @param name name of the store - * @param type of the store objects - * - * @return {@link DataStore} with given name and type + * @param storeParameters The parameters for the {@link DataStore}. + * @return A new or an existing {@link DataStore} for the given parameters. */ - public DataStore getStore(Class type, String name); + DataStore getStore(final TypedStoreParameters storeParameters); + + /** + * Use this to create a new or get an existing {@link DataStore} with a floating API. + * @param type The type for the {@link DataStore}. + * @return Floating API to set the name and either specify a repository or directly build a global + * {@link DataStore}. + */ + default TypedFloatingDataStoreParameters.Builder withType(Class type) { + return new TypedFloatingDataStoreParameters(this).new Builder(type); + } +} + +final class TypedFloatingDataStoreParameters { + + private final TypedStoreParametersImpl parameters = new TypedStoreParametersImpl<>(); + private final DataStoreFactory factory; + + TypedFloatingDataStoreParameters(DataStoreFactory factory) { + this.factory = factory; + } + + public class Builder { + + Builder(Class type) { + parameters.setType(type); + } + + /** + * Use this to set the name for the {@link DataStore}. + * @param name The name for the {@link DataStore}. + * @return Floating API to either specify a repository or directly build a global {@link DataStore}. + */ + public OptionalRepositoryBuilder withName(String name) { + parameters.setName(name); + return new OptionalRepositoryBuilder(); + } + } + + public class OptionalRepositoryBuilder { + + /** + * Use this to create or get a {@link DataStore} for a specific repository. This step is optional. If you + * want to have a global {@link DataStore}, omit this. + * @param repository The optional repository for the {@link DataStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(Repository repository) { + parameters.setRepositoryId(repository.getId()); + return this; + } + + /** + * Use this to create or get a {@link DataStore} for a specific repository. This step is optional. If you + * want to have a global {@link DataStore}, omit this. + * @param repositoryId The id of the optional repository for the {@link DataStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(String repositoryId) { + parameters.setRepositoryId(repositoryId); + return this; + } + + /** + * Creates or gets the {@link DataStore} with the given name and (if specified) the given repository. If no + * repository is given, the {@link DataStore} will be global. + */ + public DataStore build(){ + return factory.getStore(parameters); + } + } } diff --git a/scm-core/src/main/java/sonia/scm/store/MultiEntryStore.java b/scm-core/src/main/java/sonia/scm/store/MultiEntryStore.java index 9a35cee0e0..c1a8863758 100644 --- a/scm-core/src/main/java/sonia/scm/store/MultiEntryStore.java +++ b/scm-core/src/main/java/sonia/scm/store/MultiEntryStore.java @@ -32,6 +32,10 @@ package sonia.scm.store; +import java.util.Optional; + +import static java.util.Optional.ofNullable; + /** * Base class for {@link BlobStore} and {@link DataStore}. * @@ -67,4 +71,16 @@ public interface MultiEntryStore { * @return item with the given id */ public T get(String id); + + /** + * Returns the item with the given id from the store. + * + * + * @param id id of the item to return + * + * @return item with the given id + */ + default Optional getOptional(String id) { + return ofNullable(get(id)); + } } diff --git a/scm-core/src/main/java/sonia/scm/store/StoreParameters.java b/scm-core/src/main/java/sonia/scm/store/StoreParameters.java new file mode 100644 index 0000000000..38404398cc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/store/StoreParameters.java @@ -0,0 +1,16 @@ +package sonia.scm.store; + +import sonia.scm.repository.Repository; + +/** + * The fields of the {@link StoreParameters} are used from the {@link BlobStoreFactory} to create a store. + * + * @author Mohamed Karray + * @since 2.0.0 + */ +public interface StoreParameters { + + String getName(); + + String getRepositoryId(); +} diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java new file mode 100644 index 0000000000..060a9706f8 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java @@ -0,0 +1,19 @@ +package sonia.scm.store; + +import sonia.scm.repository.Repository; + +/** + * The fields of the {@link TypedStoreParameters} are used from the {@link ConfigurationStoreFactory}, + * {@link ConfigurationEntryStoreFactory} and {@link DataStoreFactory} to create a type safe store. + * + * @author Mohamed Karray + * @since 2.0.0 + */ +public interface TypedStoreParameters { + + Class getType(); + + String getName(); + + String getRepositoryId(); +} diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java new file mode 100644 index 0000000000..3abf1d2192 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java @@ -0,0 +1,34 @@ +package sonia.scm.store; + +class TypedStoreParametersImpl implements TypedStoreParameters { + private Class type; + private String name; + private String repositoryId; + + @Override + public Class getType() { + return type; + } + + void setType(Class type) { + this.type = type; + } + + @Override + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @Override + public String getRepositoryId() { + return repositoryId; + } + + void setRepositoryId(String repositoryId) { + this.repositoryId = repositoryId; + } +} diff --git a/scm-core/src/main/java/sonia/scm/template/TemplateEngine.java b/scm-core/src/main/java/sonia/scm/template/TemplateEngine.java index bfeca8f42c..0f26fd49c4 100644 --- a/scm-core/src/main/java/sonia/scm/template/TemplateEngine.java +++ b/scm-core/src/main/java/sonia/scm/template/TemplateEngine.java @@ -37,6 +37,7 @@ package sonia.scm.template; import java.io.IOException; import java.io.Reader; +import java.util.Locale; /** * The {@link TemplateEngine} searches for {@link Template}s and prepares the @@ -59,11 +60,42 @@ public interface TemplateEngine * * @param templatePath path of the template * - * @return template associated withe the given path or null + * @return template associated with the given path or null * * @throws IOException */ - public Template getTemplate(String templatePath) throws IOException; + Template getTemplate(String templatePath) throws IOException; + + /** + * Returns the template associated with the given path and the given language + * or the default version. To do this, the template path will be extended + * with the language. For example `my-template.type` will be extended to + * `my-template_en.type`, if the language is {@link Locale#ENGLISH}. If no + * dedicated template can be found, the template without the extension will + * be used. + *

The template engine + * will search the template in the folder of the web application and in + * the classpath. This method will return null, + * if no template could be found for the given path. + * + * + * @param templatePath path of the template + * @param locale the preferred language + * + * @return template associated with the given path, extended by the language + * if found, or null + * + * @throws IOException + */ + default Template getTemplate(String templatePath, Locale locale) throws IOException { + String templatePathWithLanguage = extendWithLanguage(templatePath, locale); + Template templateForLanguage = getTemplate(templatePathWithLanguage); + if (templateForLanguage == null) { + return getTemplate(templatePath); + } else { + return templateForLanguage; + } + } /** * Creates a template of the given reader. Note some template implementations @@ -79,7 +111,7 @@ public interface TemplateEngine * * @since 1.22 */ - public Template getTemplate(String templateIdentifier, Reader reader) + Template getTemplate(String templateIdentifier, Reader reader) throws IOException; /** @@ -87,5 +119,12 @@ public interface TemplateEngine * * @return type of template engine */ - public TemplateType getType(); + TemplateType getType(); + + static String extendWithLanguage(String templatePath, Locale locale) { + int lastIndexOfDot = templatePath.lastIndexOf('.'); + String filename = templatePath.substring(0, lastIndexOfDot); + String extension = templatePath.substring(lastIndexOfDot); + return filename + "_" + locale.getLanguage() + extension; + } } diff --git a/scm-core/src/main/java/sonia/scm/template/Viewable.java b/scm-core/src/main/java/sonia/scm/template/Viewable.java new file mode 100644 index 0000000000..47fd8af57b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/template/Viewable.java @@ -0,0 +1,90 @@ +/** + * 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.template; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +/** + * A viewable holds the path to a template and the context object which is used to render the template. Viewables can + * be used as return type of jax-rs resources. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class Viewable { + + private final String path; + private final Object context; + + public Viewable(String path, Object context) { + this.path = path; + this.context = context; + } + + public String getPath() { + return path; + } + + public Object getContext() { + return context; + } + + @Override + public int hashCode() { + return Objects.hashCode(path, context); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Viewable other = (Viewable) obj; + return !Objects.equal(this.path, other.path) + && Objects.equal(this.context, other.context); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("path", path) + .add("context", context) + .toString(); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java b/scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java new file mode 100644 index 0000000000..35eaa134fd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java @@ -0,0 +1,15 @@ +package sonia.scm.update; + +import java.io.IOException; +import java.nio.file.Path; + +public interface BlobDirectoryAccess { + + void forBlobDirectories(BlobDirectoryConsumer blobDirectoryConsumer) throws IOException; + + void moveToRepositoryBlobStore(Path blobDirectory, String newDirectoryName, String repositoryId) throws IOException; + + interface BlobDirectoryConsumer { + void accept(Path directory) throws IOException; + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/GroupV1PropertyReader.java b/scm-core/src/main/java/sonia/scm/update/GroupV1PropertyReader.java new file mode 100644 index 0000000000..a7703c1b76 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/GroupV1PropertyReader.java @@ -0,0 +1,15 @@ +package sonia.scm.update; + +import java.util.Map; + +public class GroupV1PropertyReader implements V1PropertyReader { + @Override + public String getStoreName() { + return "group-properties-v1"; + } + + @Override + public Instance createInstance(Map all) { + return new MapBasedPropertyReaderInstance(all); + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/MapBasedPropertyReaderInstance.java b/scm-core/src/main/java/sonia/scm/update/MapBasedPropertyReaderInstance.java new file mode 100644 index 0000000000..65e6a71958 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/MapBasedPropertyReaderInstance.java @@ -0,0 +1,36 @@ +package sonia.scm.update; + +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +public class MapBasedPropertyReaderInstance implements V1PropertyReader.Instance { + private final Stream> all; + + public MapBasedPropertyReaderInstance(Map all) { + this(all.entrySet().stream()); + } + + private MapBasedPropertyReaderInstance(Stream> all) { + this.all = all; + } + + @Override + public void forEachEntry(BiConsumer propertiesForNameConsumer) { + all.forEach(e -> call(e.getKey(), e.getValue(), propertiesForNameConsumer)); + } + + @Override + public V1PropertyReader.Instance havingAnyOf(String... keys) { + return new MapBasedPropertyReaderInstance(all.filter(e -> e.getValue().hasAny(keys))); + } + + @Override + public V1PropertyReader.Instance havingAllOf(String... keys) { + return new MapBasedPropertyReaderInstance(all.filter(e -> e.getValue().hasAll(keys))); + } + + private void call(String repositoryId, V1Properties properties, BiConsumer propertiesForNameConsumer) { + propertiesForNameConsumer.accept(repositoryId, properties); + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/PropertyFileAccess.java b/scm-core/src/main/java/sonia/scm/update/PropertyFileAccess.java new file mode 100644 index 0000000000..726d589d02 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/PropertyFileAccess.java @@ -0,0 +1,53 @@ +package sonia.scm.update; + +import java.io.IOException; +import java.nio.file.Path; + +public interface PropertyFileAccess { + + /** + * Use this to rename a configuration file. + * @param oldName The old file name. + * @return Object to specify the new file name. + */ + Target renameGlobalConfigurationFrom(String oldName); + + interface Target { + /** + * Renames a file to the new name given here. + * @param newName The new file name. + * @throws IOException If the file could not be renamed. + */ + void to(String newName) throws IOException; + } + + /** + * Creates a tool object for store migration from v1 to v2. + * @param name The name of the store to be handled. + * @return The tool object for the named store. + */ + StoreFileTools forStoreName(String name); + + interface StoreFileTools { + /** + * Iterates over all found store files (that is, files ending with ".xml") and calls the + * given consumer for each file, giving the file and the name of the data file. + * @param storeFileConsumer This consumer will be called for each file found. + * @throws IOException May be thrown when an exception occurs. + */ + void forStoreFiles(FileConsumer storeFileConsumer) throws IOException; + + /** + * Moves a data file to the new location for a repository with the given id. If there + * is no directory for the given repository id, nothing will be done. + * @param storeFile The name of the store file. + * @param repositoryId The id of the repository as the new target for the store file. + * @throws IOException When the file could not be moved. + */ + void moveAsRepositoryStore(Path storeFile, String repositoryId) throws IOException; + } + + interface FileConsumer { + void accept(Path file, String storeName) throws IOException; + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/RepositoryV1PropertyReader.java b/scm-core/src/main/java/sonia/scm/update/RepositoryV1PropertyReader.java new file mode 100644 index 0000000000..5faf4500cd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/RepositoryV1PropertyReader.java @@ -0,0 +1,16 @@ +package sonia.scm.update; + +import java.util.Map; + +public class RepositoryV1PropertyReader implements V1PropertyReader { + @Override + public String getStoreName() { + return "repository-properties-v1"; + } + + @Override + public Instance createInstance(Map all) { + return new MapBasedPropertyReaderInstance(all); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java b/scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java new file mode 100644 index 0000000000..33afe9f76b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java @@ -0,0 +1,11 @@ +package sonia.scm.update; + +import sonia.scm.repository.Repository; + +/** + * Use this in {@link sonia.scm.migration.UpdateStep}s only to read repository objects directly from locations given by + * {@link sonia.scm.repository.RepositoryLocationResolver}. + */ +public interface UpdateStepRepositoryMetadataAccess { + Repository read(T location); +} diff --git a/scm-core/src/main/java/sonia/scm/update/UserV1PropertyReader.java b/scm-core/src/main/java/sonia/scm/update/UserV1PropertyReader.java new file mode 100644 index 0000000000..b29c311d1c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/UserV1PropertyReader.java @@ -0,0 +1,15 @@ +package sonia.scm.update; + +import java.util.Map; + +public class UserV1PropertyReader implements V1PropertyReader { + @Override + public String getStoreName() { + return "user-properties-v1"; + } + + @Override + public Instance createInstance(Map all) { + return new MapBasedPropertyReaderInstance(all); + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/V1Properties.java b/scm-core/src/main/java/sonia/scm/update/V1Properties.java new file mode 100644 index 0000000000..7df65efdb6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/V1Properties.java @@ -0,0 +1,59 @@ +package sonia.scm.update; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.stream.Stream.empty; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "properties") +public class V1Properties { + @XmlElement(name = "item") + private List properties; + + public V1Properties() { + } + + public V1Properties(V1Property... properties) { + this(asList(properties)); + } + + public V1Properties(List properties) { + this.properties = properties; + } + + public String get(String key) { + return getOptional(key).orElse(null); + } + + public Optional getOptional(String key) { + return streamProps().filter(p -> key.equals(p.getKey())).map(V1Property::getValue).findFirst(); + } + + public Optional getBoolean(String key) { + return getOptional(key).map(Boolean::valueOf); + } + + public > Optional getEnum(String key, Class enumType) { + return getOptional(key).map(name -> Enum.valueOf(enumType, name)); + } + + public boolean hasAny(String[] keys) { + return streamProps().anyMatch(p -> stream(keys).anyMatch(k -> k.equals(p.getKey()))); + } + + public boolean hasAll(String[] keys) { + return stream(keys).allMatch(k -> streamProps().anyMatch(p -> k.equals(p.getKey()))); + } + + private Stream streamProps() { + return properties == null? empty(): properties.stream(); + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/V1Property.java b/scm-core/src/main/java/sonia/scm/update/V1Property.java new file mode 100644 index 0000000000..c3dcf6a590 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/V1Property.java @@ -0,0 +1,49 @@ +package sonia.scm.update; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Objects; + +@XmlAccessorType(XmlAccessType.FIELD) +public class V1Property { + private String key; + private String value; + + public V1Property() { + } + + public V1Property(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + V1Property that = (V1Property) o; + return Objects.equals(key, that.key) && + Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + return "V1Property{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/V1PropertyDAO.java b/scm-core/src/main/java/sonia/scm/update/V1PropertyDAO.java new file mode 100644 index 0000000000..46b1773c57 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/V1PropertyDAO.java @@ -0,0 +1,14 @@ +package sonia.scm.update; + +/** + * Use this to access old properties from an instance of SCM-Manager v1. + */ +public interface V1PropertyDAO { + /** + * Creates an instance of a property reader to process old properties. + * @param reader The reader for the origin of the properties (for example + * {@link V1PropertyReader#REPOSITORY_PROPERTY_READER} for properties of repositories). + * @return The reader instance. + */ + V1PropertyReader.Instance getProperties(V1PropertyReader reader); +} diff --git a/scm-core/src/main/java/sonia/scm/update/V1PropertyReader.java b/scm-core/src/main/java/sonia/scm/update/V1PropertyReader.java new file mode 100644 index 0000000000..4e5cc55115 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/V1PropertyReader.java @@ -0,0 +1,35 @@ +package sonia.scm.update; + +import java.util.Map; +import java.util.function.BiConsumer; + +public interface V1PropertyReader { + + V1PropertyReader REPOSITORY_PROPERTY_READER = new RepositoryV1PropertyReader(); + V1PropertyReader USER_PROPERTY_READER = new UserV1PropertyReader(); + V1PropertyReader GROUP_PROPERTY_READER = new GroupV1PropertyReader(); + + String getStoreName(); + + Instance createInstance(Map all); + + interface Instance { + /** + * Will call the given consumer for each id of the corresponding entity with its list of + * properties converted from v1. + * For example for repositories this will call the consumer with the id of each repository + * that had properties attached in v1. + */ + void forEachEntry(BiConsumer propertiesForNameConsumer); + + /** + * Filters for entities only having at least one property with a given key name. + */ + Instance havingAnyOf(String... keys); + + /** + * Filters for entities only having properties for all given key name. + */ + Instance havingAllOf(String... keys); + } +} diff --git a/scm-core/src/main/java/sonia/scm/url/ModelUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/ModelUrlProvider.java deleted file mode 100644 index ea60645d00..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/ModelUrlProvider.java +++ /dev/null @@ -1,58 +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.url; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public interface ModelUrlProvider -{ - - /** - * Method description - * - * - * @param name - * - * @return - */ - public String getDetailUrl(String name); - - /** - * Method description - * - * - * @return - */ - public String getAllUrl(); - -} diff --git a/scm-core/src/main/java/sonia/scm/url/RepositoryUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/RepositoryUrlProvider.java deleted file mode 100644 index ebc3f5b907..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/RepositoryUrlProvider.java +++ /dev/null @@ -1,153 +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.url; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public interface RepositoryUrlProvider extends ModelUrlProvider -{ - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - public String getBlameUrl(String repositoryId, String path, String revision); - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - public String getBrowseUrl(String repositoryId, String path, String revision); - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * @param start - * @param limit - * - * @return - */ - public String getChangesetUrl(String repositoryId, String path, - String revision, int start, int limit); - - /** - * Method description - * - * - * @param repositoryId - * @param revision - * - * @return - * - * @since 1.12 - */ - public String getChangesetUrl(String repositoryId, String revision); - - /** - * Method description - * - * - * @param repositoryId - * @param start - * @param limit - * - * @return - */ - public String getChangesetUrl(String repositoryId, int start, int limit); - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - public String getContentUrl(String repositoryId, String path, - String revision); - - /** - * Method description - * - * - * @param type - * @param name - * - * @return - * @since 1.11 - */ - public String getDetailUrl(String type, String name); - - /** - * Method description - * - * - * @param repositoryId - * @param revision - * - * @return - */ - public String getDiffUrl(String repositoryId, String revision); - - /** - * Method description - * - * - * @param repositoryId - * - * @return - * @since 1.18 - */ - public String getTagsUrl(String repositoryId); -} diff --git a/scm-core/src/main/java/sonia/scm/url/RestModelUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/RestModelUrlProvider.java deleted file mode 100644 index 3974135a53..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/RestModelUrlProvider.java +++ /dev/null @@ -1,97 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public class RestModelUrlProvider implements ModelUrlProvider -{ - - /** - * Constructs ... - * - * - * @param baseUrl - * @param modelSuffix - * @param extension - */ - public RestModelUrlProvider(String baseUrl, String modelSuffix, - String extension) - { - this.base = HttpUtil.append(baseUrl, modelSuffix); - this.extension = extension; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getAllUrl() - { - return base.concat(extension); - } - - /** - * Method description - * - * - * @param name - * - * @return - */ - @Override - public String getDetailUrl(String name) - { - return HttpUtil.append(base, name).concat(extension); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - protected String base; - - /** Field description */ - protected String extension; -} diff --git a/scm-core/src/main/java/sonia/scm/url/RestRepositoryUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/RestRepositoryUrlProvider.java deleted file mode 100644 index 6c4f0f3235..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/RestRepositoryUrlProvider.java +++ /dev/null @@ -1,278 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.UrlBuilder; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public class RestRepositoryUrlProvider extends RestModelUrlProvider - implements RepositoryUrlProvider -{ - - /** Field description */ - public static final String PARAMETER_LIMIT = "limit"; - - /** Field description */ - public static final String PARAMETER_PATH = "path"; - - /** Field description */ - public static final String PARAMETER_REVISION = "revision"; - - /** Field description */ - public static final String PARAMETER_START = "start"; - - /** Field description */ - public static final String PART_BLAME = "blame"; - - /** Field description */ - public static final String PART_BROWSE = "browse"; - - /** - * @since 1.12 - */ - public static final String PART_CHANGESET = "changeset"; - - /** Field description */ - public static final String PART_CHANGESETS = "changesets"; - - /** Field description */ - public static final String PART_CONTENT = "content"; - - /** Field description */ - public static final String PART_DIFF = "diff"; - - /** Field description */ - public static final String PART_TAGS = "tags"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param baseUrl - * @param modelSuffix - * @param extension - */ - public RestRepositoryUrlProvider(String baseUrl, String modelSuffix, - String extension) - { - super(baseUrl, modelSuffix, extension); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - public String getBlameUrl(String repositoryId, String path, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new UrlBuilder(base).appendUrlPart(repositoryId).appendUrlPart( - PART_BLAME).append(extension).appendParameter( - PARAMETER_PATH, path).appendParameter( - PARAMETER_REVISION, revision).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - public String getBrowseUrl(String repositoryId, String path, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new UrlBuilder(base).appendUrlPart(repositoryId).appendUrlPart( - PART_BROWSE).append(extension).appendParameter( - PARAMETER_PATH, path).appendParameter( - PARAMETER_REVISION, revision).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * @param start - * @param limit - * - * @return - */ - @Override - public String getChangesetUrl(String repositoryId, String path, - String revision, int start, int limit) - { - revision = UrlUtil.fixRevision(revision); - - return new UrlBuilder(base).appendUrlPart(repositoryId).appendUrlPart( - PART_CHANGESETS).append(extension).appendParameter( - PARAMETER_PATH, path).appendParameter( - PARAMETER_REVISION, revision).appendParameter( - PARAMETER_START, start).appendParameter( - PARAMETER_LIMIT, limit).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param start - * @param limit - * - * @return - */ - @Override - public String getChangesetUrl(String repositoryId, int start, int limit) - { - return new UrlBuilder(base).appendUrlPart(repositoryId).appendUrlPart( - PART_CHANGESETS).append(extension).appendParameter( - PARAMETER_START, start).appendParameter( - PARAMETER_LIMIT, limit).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param revision - * - * @return - * - * @since 1.12 - */ - @Override - public String getChangesetUrl(String repositoryId, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new UrlBuilder(base).appendUrlPart(repositoryId).appendUrlPart( - PART_CHANGESET).appendUrlPart(revision).append(extension).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - public String getContentUrl(String repositoryId, String path, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new UrlBuilder(base).appendUrlPart(repositoryId).appendUrlPart( - PART_CONTENT).appendParameter(PARAMETER_PATH, path).appendParameter( - PARAMETER_REVISION, revision).toString(); - } - - /** - * Method description - * - * - * @param type - * @param name - * - * @return - * @since 1.11 - */ - @Override - public String getDetailUrl(String type, String name) - { - return new UrlBuilder(base).appendUrlPart(type).appendUrlPart(name).append( - extension).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param revision - * - * @return - */ - @Override - public String getDiffUrl(String repositoryId, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new UrlBuilder(base).appendUrlPart(repositoryId).appendUrlPart( - PART_DIFF).appendParameter(PARAMETER_REVISION, revision).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * - * @return - * @since 1.18 - */ - @Override - public String getTagsUrl(String repositoryId) - { - return new UrlBuilder(base).appendUrlPart(repositoryId).appendUrlPart( - PART_TAGS).append(extension).toString(); - } -} diff --git a/scm-core/src/main/java/sonia/scm/url/RestSecurityUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/RestSecurityUrlProvider.java deleted file mode 100644 index 513f728708..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/RestSecurityUrlProvider.java +++ /dev/null @@ -1,95 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * - * @author Sebastian Sdorra - * @since 1.41 - */ -public class RestSecurityUrlProvider implements SecurityUrlProvider -{ - - /** Field description */ - private static final String PATH_ENCRYPT = "security/cipher/encrypt"; - - /** Field description */ - private static final String PATH_KEY = "security/key"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param baseUrl - */ - public RestSecurityUrlProvider(String baseUrl) - { - this.baseUrl = baseUrl; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getEncryptUrl() - { - return HttpUtil.append(baseUrl, PATH_ENCRYPT); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String getGenerateKeyUrl() - { - return HttpUtil.append(baseUrl, PATH_KEY); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final String baseUrl; -} diff --git a/scm-core/src/main/java/sonia/scm/url/RestUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/RestUrlProvider.java deleted file mode 100644 index 4abba6db0b..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/RestUrlProvider.java +++ /dev/null @@ -1,190 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public class RestUrlProvider implements UrlProvider -{ - - /** Field description */ - public static final String PART_API = "api/rest/"; - - /** Field description */ - public static final String PART_AUTHENTICATION = "auth/access_token"; - - /** Field description */ - public static final String PART_CONFIG = "config"; - - /** Field description */ - public static final String PART_GROUP = "groups"; - - /** Field description */ - public static final String PART_REPOSITORIES = "repositories"; - - /** Field description */ - public static final String PART_STATE = "auth"; - - /** Field description */ - public static final String PART_USER = "users"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param baseUrl - * @param extension - */ - public RestUrlProvider(String baseUrl, String extension) - { - this.baseUrl = HttpUtil.append(baseUrl, PART_API); - this.extension = extension; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getAuthenticationUrl() - { - return HttpUtil.append(baseUrl, PART_AUTHENTICATION).concat(extension); - } - - /** - * Method description - * - * - * @return - * - * @since 1.43 - */ - @Override - public String getBaseUrl() - { - return baseUrl; - } - - /** - * Method description - * - * - * @return - */ - @Override - public String getConfigUrl() - { - return HttpUtil.append(baseUrl, PART_CONFIG).concat(extension); - } - - /** - * Method description - * - * - * @return - */ - @Override - public ModelUrlProvider getGroupUrlProvider() - { - return new RestModelUrlProvider(baseUrl, PART_GROUP, extension); - } - - /** - * Method description - * - * - * @return - */ - @Override - public RepositoryUrlProvider getRepositoryUrlProvider() - { - return new RestRepositoryUrlProvider(baseUrl, PART_REPOSITORIES, extension); - } - - /** - * Method description - * - * - * @return - */ - @Override - public SecurityUrlProvider getSecurityUrlProvider() - { - return new RestSecurityUrlProvider(baseUrl); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String getStateUrl() - { - return HttpUtil.append(baseUrl, PART_STATE).concat(extension); - } - - /** - * Method description - * - * - * @return - */ - @Override - public ModelUrlProvider getUserUrlProvider() - { - return new RestModelUrlProvider(baseUrl, PART_USER, extension); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - protected String baseUrl; - - /** Field description */ - protected String extension; -} diff --git a/scm-core/src/main/java/sonia/scm/url/SecurityUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/SecurityUrlProvider.java deleted file mode 100644 index e000c9b754..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/SecurityUrlProvider.java +++ /dev/null @@ -1,59 +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.url; - -/** - * - * @author Sebastian Sdorra - * @since 1.41 - */ -public interface SecurityUrlProvider -{ - - /** - * Method description - * - * - * @return - */ - public String getGenerateKeyUrl(); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getEncryptUrl(); -} diff --git a/scm-core/src/main/java/sonia/scm/url/UrlProvider.java b/scm-core/src/main/java/sonia/scm/url/UrlProvider.java deleted file mode 100644 index 1ab798d60b..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/UrlProvider.java +++ /dev/null @@ -1,110 +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.url; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public interface UrlProvider -{ - - /** - * Method description - * - * - * @return - */ - public String getAuthenticationUrl(); - - /** - * Method description - * - * - * @return - * - * @since 1.43 - */ - public String getBaseUrl(); - - /** - * Method description - * - * - * @return - */ - public String getConfigUrl(); - - /** - * Method description - * - * - * @return - */ - public ModelUrlProvider getGroupUrlProvider(); - - /** - * Method description - * - * - * @return - */ - public RepositoryUrlProvider getRepositoryUrlProvider(); - - /** - * Method description - * - * - * @return - * - * @since 1.41 - */ - public SecurityUrlProvider getSecurityUrlProvider(); - - /** - * Method description - * - * - * @return - */ - public String getStateUrl(); - - /** - * Method description - * - * - * @return - */ - public ModelUrlProvider getUserUrlProvider(); -} diff --git a/scm-core/src/main/java/sonia/scm/url/UrlProviderFactory.java b/scm-core/src/main/java/sonia/scm/url/UrlProviderFactory.java deleted file mode 100644 index bb4b998817..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/UrlProviderFactory.java +++ /dev/null @@ -1,97 +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.url; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public final class UrlProviderFactory -{ - - /** Field description */ - public static final String TYPE_RESTAPI_JSON = "json-rest-api"; - - /** Field description */ - public static final String TYPE_RESTAPI_XML = "xml-rest-api"; - - /** Field description */ - public static final String TYPE_WUI = "wui"; - - /** Field description */ - private static final String EXTENSION_JSON = ".json"; - - /** Field description */ - private static final String EXTENSION_XML = ".xml"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - private UrlProviderFactory() {} - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param baseUrl - * @param type - * - * @return - */ - public static UrlProvider createUrlProvider(String baseUrl, String type) - { - UrlProvider provider = null; - - if (TYPE_RESTAPI_JSON.equals(type)) - { - provider = new RestUrlProvider(baseUrl, EXTENSION_JSON); - } - else if (TYPE_RESTAPI_XML.equals(type)) - { - provider = new RestUrlProvider(baseUrl, EXTENSION_XML); - } - else if (TYPE_WUI.equals(type)) - { - provider = new WUIUrlProvider(baseUrl); - } - - return provider; - } -} diff --git a/scm-core/src/main/java/sonia/scm/url/UrlUtil.java b/scm-core/src/main/java/sonia/scm/url/UrlUtil.java deleted file mode 100644 index 0d90da10b6..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/UrlUtil.java +++ /dev/null @@ -1,80 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.Util; - -/** - * - * @author Sebastian Sdorra - * @since 1.11 - */ -public final class UrlUtil -{ - - /** - * Constructs ... - * - */ - private UrlUtil() {} - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param revision - * - * @return - */ - public static String fixRevision(String revision) - { - String fixedRevision = revision; - - if (Util.isNotEmpty(revision)) - { - int index = revision.indexOf(':'); - - if (index > 0) - { - fixedRevision = revision.substring(index + 1); - } - } - - return fixedRevision; - } -} diff --git a/scm-core/src/main/java/sonia/scm/url/WUIModelUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/WUIModelUrlProvider.java deleted file mode 100644 index bef0ef3b68..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/WUIModelUrlProvider.java +++ /dev/null @@ -1,91 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public class WUIModelUrlProvider implements ModelUrlProvider -{ - - /** - * Constructs ... - * - * - * @param baseUrl - * @param component - */ - public WUIModelUrlProvider(String baseUrl, String component) - { - this.url = HttpUtil.appendHash(baseUrl, component); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getAllUrl() - { - return url; - } - - /** - * Method description - * - * - * @param name - * - * @return - */ - @Override - public String getDetailUrl(String name) - { - return url.concat(WUIUrlBuilder.SEPARATOR).concat(name); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String url; -} diff --git a/scm-core/src/main/java/sonia/scm/url/WUIRepositoryUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/WUIRepositoryUrlProvider.java deleted file mode 100644 index 7809adbcc3..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/WUIRepositoryUrlProvider.java +++ /dev/null @@ -1,274 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * - * @author Sebastian Sdorra - */ -public class WUIRepositoryUrlProvider extends WUIModelUrlProvider - implements RepositoryUrlProvider -{ - - /** Field description */ - public static final String COMPONENT_BROWSER = "repositoryBrowser"; - - /** - * @since 1.15 - */ - public static final String COMPONENT_CHANGESET = "changesetPanel"; - - /** Field description */ - public static final String COMPONENT_CHANGESETS = - "repositoryChangesetViewerPanel"; - - /** Field description */ - public static final String COMPONENT_CONTENT = "contentPanel"; - - /** Field description */ - public static final String COMPONENT_DETAIL = "repositoryPanel"; - - /** Field description */ - public static final String COMPONENT_DIFF = "diffPanel"; - - /** Field description */ - public static final String VIEW_BLAME = "blame"; - - /** - * @since 1.12 - */ - public static final String VIEW_CHANGESET = "changeset"; - - /** Field description */ - public static final String VIEW_CONTENT = "content"; - - /** Field description */ - public static final String VIEW_HISTORY = "history"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param baseUrl - * @param component - */ - public WUIRepositoryUrlProvider(String baseUrl, String component) - { - super(baseUrl, component); - this.baseUrl = baseUrl; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - public String getBlameUrl(String repositoryId, String path, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new WUIUrlBuilder(baseUrl, COMPONENT_CONTENT).append( - repositoryId).append(revision).append(path).append(VIEW_BLAME).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - public String getBrowseUrl(String repositoryId, String path, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new WUIUrlBuilder(baseUrl, COMPONENT_BROWSER).append( - repositoryId).append(revision).append(path).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * @param start - * @param limit - * - * @return - */ - @Override - public String getChangesetUrl(String repositoryId, String path, - String revision, int start, int limit) - { - revision = UrlUtil.fixRevision(revision); - - // TODO handle start and limit - return new WUIUrlBuilder(baseUrl, COMPONENT_CONTENT).append( - repositoryId).append(revision).append(path).append( - VIEW_HISTORY).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param start - * @param limit - * - * @return - */ - @Override - public String getChangesetUrl(String repositoryId, int start, int limit) - { - return new WUIUrlBuilder(baseUrl, COMPONENT_CHANGESETS).append( - repositoryId).append(start).append(limit).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param revision - * - * @return - * - * @since 1.12 - */ - @Override - public String getChangesetUrl(String repositoryId, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new WUIUrlBuilder(baseUrl, - COMPONENT_CHANGESET).append(repositoryId).append(revision).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - public String getContentUrl(String repositoryId, String path, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new WUIUrlBuilder(baseUrl, COMPONENT_CONTENT).append( - repositoryId).append(revision).append(path).append( - VIEW_HISTORY).toString(); - } - - /** - * Method description - * - * - * @param type - * @param name - * - * @return - * @since 1.11 - */ - @Override - public String getDetailUrl(String type, String name) - { - name = type.concat(HttpUtil.SEPARATOR_PATH).concat(name); - - return new WUIUrlBuilder(baseUrl, COMPONENT_DETAIL).append(name).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * @param revision - * - * @return - */ - @Override - public String getDiffUrl(String repositoryId, String revision) - { - revision = UrlUtil.fixRevision(revision); - - return new WUIUrlBuilder(baseUrl, - COMPONENT_DIFF).append(repositoryId).append(revision).toString(); - } - - /** - * Method description - * - * - * @param repositoryId - * - * @return - * @since 1.18 - */ - @Override - public String getTagsUrl(String repositoryId) - { - return getBrowseUrl(repositoryId, null, null); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String baseUrl; -} diff --git a/scm-core/src/main/java/sonia/scm/url/WUIUrlBuilder.java b/scm-core/src/main/java/sonia/scm/url/WUIUrlBuilder.java deleted file mode 100644 index c9744effe3..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/WUIUrlBuilder.java +++ /dev/null @@ -1,123 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public class WUIUrlBuilder -{ - - /** Field description */ - public static final String NULL = "null"; - - /** Field description */ - public static final String SEPARATOR = ";"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param baseUrl - * @param component - */ - public WUIUrlBuilder(String baseUrl, String component) - { - this.url = HttpUtil.appendHash(baseUrl, component); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param value - * - * @return - */ - public WUIUrlBuilder append(String value) - { - if (value == null) - { - value = NULL; - } - - if (!this.url.endsWith(SEPARATOR)) - { - this.url = this.url.concat(SEPARATOR); - } - - this.url = this.url.concat(value); - - return this; - } - - /** - * Method description - * - * - * @param value - * - * @return - */ - public WUIUrlBuilder append(int value) - { - return append(String.valueOf(value)); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - return url; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String url; -} diff --git a/scm-core/src/main/java/sonia/scm/url/WUIUrlProvider.java b/scm-core/src/main/java/sonia/scm/url/WUIUrlProvider.java deleted file mode 100644 index bd94bf562e..0000000000 --- a/scm-core/src/main/java/sonia/scm/url/WUIUrlProvider.java +++ /dev/null @@ -1,182 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * @since 1.9 - * @author Sebastian Sdorra - */ -public class WUIUrlProvider implements UrlProvider -{ - - /** Field description */ - public static final String COMPONENT_CONFIG = "scmConfig"; - - /** Field description */ - public static final String COMPONENT_GROUP = "groupPanel"; - - /** Field description */ - public static final String COMPONENT_REPOSITORY = "repositoryPanel"; - - /** Field description */ - public static final String COMPONENT_USER = "userPanel"; - - /** Field description */ - public static final String PART_INDEX = "index.html"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param baseUrl - */ - public WUIUrlProvider(String baseUrl) - { - this.baseUrl = HttpUtil.append(baseUrl, PART_INDEX); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns the baseUrl, because there is no authentication url. - * - * - * @returns the baseUrl, because there is no authentication url - * - * @return - */ - @Override - public String getAuthenticationUrl() - { - return baseUrl; - } - - /** - * Method description - * - * - * @return - * - * @since 1.43 - */ - @Override - public String getBaseUrl() - { - return baseUrl; - } - - /** - * Method description - * - * - * @return - */ - @Override - public String getConfigUrl() - { - return HttpUtil.appendHash(baseUrl, COMPONENT_CONFIG); - } - - /** - * Method description - * - * - * @return - */ - @Override - public ModelUrlProvider getGroupUrlProvider() - { - return new WUIModelUrlProvider(baseUrl, COMPONENT_GROUP); - } - - /** - * Method description - * - * - * @return - */ - @Override - public RepositoryUrlProvider getRepositoryUrlProvider() - { - return new WUIRepositoryUrlProvider(baseUrl, COMPONENT_REPOSITORY); - } - - /** - * Method description - * - * - * @return - */ - @Override - public SecurityUrlProvider getSecurityUrlProvider() - { - throw new UnsupportedOperationException( - "this provider does not support security url provider."); - } - - /** - * Returns the baseUrl, because there is no state url. - * - * - * @return the baseUrl, because there is no state url - */ - @Override - public String getStateUrl() - { - return baseUrl; - } - - /** - * Method description - * - * - * @return - */ - @Override - public ModelUrlProvider getUserUrlProvider() - { - return new WUIModelUrlProvider(baseUrl, COMPONENT_USER); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String baseUrl; -} diff --git a/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java new file mode 100644 index 0000000000..b0f8117e82 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java @@ -0,0 +1,20 @@ +package sonia.scm.user; + +import sonia.scm.BadRequestException; +import sonia.scm.ContextEntry; + +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class ChangePasswordNotAllowedException extends BadRequestException { + + private static final String CODE = "9BR7qpDAe1"; + public static final String WRONG_USER_TYPE = "Users of type %s are not allowed to change password"; + + public ChangePasswordNotAllowedException(ContextEntry.ContextBuilder context, String type) { + super(context.build(), String.format(WRONG_USER_TYPE, type)); + } + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/user/DisplayUser.java b/scm-core/src/main/java/sonia/scm/user/DisplayUser.java new file mode 100644 index 0000000000..7a11dfbab4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/DisplayUser.java @@ -0,0 +1,32 @@ +package sonia.scm.user; + +import sonia.scm.ReducedModelObject; + +public class DisplayUser implements ReducedModelObject { + + private final String id; + private final String displayName; + private final String mail; + + public static DisplayUser from(User user) { + return new DisplayUser(user.getId(), user.getDisplayName(), user.getMail()); + } + + private DisplayUser(String id, String displayName, String mail) { + this.id = id; + this.displayName = displayName; + this.mail = mail; + } + + public String getId() { + return id; + } + + public String getDisplayName() { + return displayName; + } + + public String getMail() { + return mail; + } +} diff --git a/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java new file mode 100644 index 0000000000..6f1bfd9954 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java @@ -0,0 +1,19 @@ +package sonia.scm.user; + +import sonia.scm.BadRequestException; +import sonia.scm.ContextEntry; + +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class InvalidPasswordException extends BadRequestException { + + private static final String CODE = "8YR7aawFW1"; + + public InvalidPasswordException(ContextEntry.ContextBuilder context) { + super(context.build(), "The given old password does not match with the stored one."); + } + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java index dc7105825d..d974c13a0d 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -41,6 +41,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; +import sonia.scm.ReducedModelObject; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; @@ -55,10 +56,15 @@ import java.security.Principal; * * @author Sebastian Sdorra */ -@StaticPermissions("user") +@StaticPermissions( + value = "user", + globalPermissions = {"create", "list", "autocomplete"}, + permissions = {"read", "modify", "delete", "changePassword"}, + custom = true, customGlobal = true +) @XmlRootElement(name = "users") @XmlAccessorType(XmlAccessType.FIELD) -public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject +public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject { /** Field description */ @@ -99,6 +105,24 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject this.mail = mail; } + /** + * Constructs ... + * + * + * @param name + * @param displayName + * @param mail + */ + public User(String name, String displayName, String mail, String password, String type, boolean active) + { + this.name = name; + this.displayName = displayName; + this.mail = mail; + this.password = password; + this.type = type; + this.active = active; + } + //~--- methods -------------------------------------------------------------- /** @@ -151,12 +175,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject { boolean result = false; - if (user.isAdmin() != admin) - { - result = true; - user.setAdmin(admin); - } - if (user.isActive() != active) { result = true; @@ -223,7 +241,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject && Objects.equal(displayName, other.displayName) && Objects.equal(mail, other.mail) && Objects.equal(type, other.type) - && Objects.equal(admin, other.admin) && Objects.equal(active, other.active) && Objects.equal(password, other.password) && Objects.equal(creationDate, other.creationDate) @@ -240,7 +257,7 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject @Override public int hashCode() { - return Objects.hashCode(name, displayName, mail, type, admin, password, + return Objects.hashCode(name, displayName, mail, type, password, active, creationDate, lastModified, properties); } @@ -259,17 +276,16 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("displayName",displayName) - .add("mail", mail) - .add("password", pwd) - .add("admin", admin) - .add("type", type) - .add("active", active) - .add("creationDate", creationDate) - .add("lastModified", lastModified) - .add("properties", properties) - .toString(); + .add("name", name) + .add("displayName",displayName) + .add("mail", mail) + .add("password", pwd) + .add("type", type) + .add("active", active) + .add("creationDate", creationDate) + .add("lastModified", lastModified) + .add("properties", properties) + .toString(); //J+ } @@ -379,17 +395,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject return active; } - /** - * Method description - * - * - * @return - */ - public boolean isAdmin() - { - return admin; - } - /** * Method description * @@ -418,17 +423,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject this.active = active; } - /** - * Method description - * - * - * @param admin - */ - public void setAdmin(boolean admin) - { - this.admin = admin; - } - /** * Method description * @@ -512,9 +506,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject /** Field description */ private boolean active = true; - /** Field description */ - private boolean admin = false; - /** Field description */ private Long creationDate; diff --git a/scm-core/src/main/java/sonia/scm/user/UserAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/user/UserAlreadyExistsException.java deleted file mode 100644 index 8c8f2542f0..0000000000 --- a/scm-core/src/main/java/sonia/scm/user/UserAlreadyExistsException.java +++ /dev/null @@ -1,59 +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.user; - -/** - * This {@link Exception} is thrown when trying to create a user that already exists. - * - * @author Sebastian Sdorra - */ -public class UserAlreadyExistsException extends UserException -{ - - /** Field description */ - private static final long serialVersionUID = 9182294539718090814L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new instance. - * - * @param message message of exception - * @since 1.5 - */ - public UserAlreadyExistsException(String message) - { - super(message); - } -} diff --git a/scm-core/src/main/java/sonia/scm/user/UserDisplayManager.java b/scm-core/src/main/java/sonia/scm/user/UserDisplayManager.java new file mode 100644 index 0000000000..159025f5ec --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/UserDisplayManager.java @@ -0,0 +1,6 @@ +package sonia.scm.user; + +import sonia.scm.DisplayManager; + +public interface UserDisplayManager extends DisplayManager { +} diff --git a/scm-core/src/main/java/sonia/scm/user/UserException.java b/scm-core/src/main/java/sonia/scm/user/UserException.java deleted file mode 100644 index b8587d5299..0000000000 --- a/scm-core/src/main/java/sonia/scm/user/UserException.java +++ /dev/null @@ -1,87 +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.user; - -/** - * - * @author Sebastian Sdorra - */ -public class UserException extends Exception -{ - - /** Field description */ - private static final long serialVersionUID = 4147943028529739021L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public UserException() {} - - /** - * Constructs ... - * - * - * @param message - */ - public UserException(String message) - { - super(message); - } - - /** - * Constructs ... - * - * - * @param throwable - */ - public UserException(Throwable throwable) - { - super(throwable); - } - - /** - * Constructs ... - * - * - * @param message - * @param throwable - */ - public UserException(String message, Throwable throwable) - { - super(message, throwable); - } -} diff --git a/scm-core/src/main/java/sonia/scm/user/UserManager.java b/scm-core/src/main/java/sonia/scm/user/UserManager.java index e21eb761dc..a735918c59 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManager.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManager.java @@ -38,6 +38,8 @@ package sonia.scm.user; import sonia.scm.Manager; import sonia.scm.search.Searchable; +import java.util.Collection; + /** * The central class for managing {@link User} objects. * This class is a singleton and is available via injection. @@ -45,7 +47,7 @@ import sonia.scm.search.Searchable; * @author Sebastian Sdorra */ public interface UserManager - extends Manager, Searchable + extends Manager, Searchable { /** @@ -68,4 +70,22 @@ public interface UserManager * @since 1.14 */ public String getDefaultType(); + + default boolean isTypeDefault(User user) { + return getDefaultType().equals(user.getType()); + } + + /** + * Changes the password of the logged in user. + * @param oldPassword The current encrypted password of the user. + * @param newPassword The new encrypted password of the user. + */ + void changePasswordForLoggedInUser(String oldPassword, String newPassword); + + /** + * Overwrites the password for the given user id. This needs user write privileges. + * @param userId The id of the user to change the password for. + * @param newPassword The new encrypted password. + */ + void overwritePassword(String userId, String newPassword); } diff --git a/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java b/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java index 5ea14ca14e..0b88d856ff 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java @@ -38,17 +38,17 @@ package sonia.scm.user; import sonia.scm.ManagerDecorator; import sonia.scm.search.SearchRequest; -//~--- JDK imports ------------------------------------------------------------ - import java.util.Collection; +//~--- JDK imports ------------------------------------------------------------ + /** * Decorator for {@link UserManager}. * * @author Sebastian Sdorra * @since 1.23 */ -public class UserManagerDecorator extends ManagerDecorator +public class UserManagerDecorator extends ManagerDecorator implements UserManager { @@ -121,7 +121,16 @@ public class UserManagerDecorator extends ManagerDecorator return decorated.getDefaultType(); } - //~--- fields --------------------------------------------------------------- + @Override + public void changePasswordForLoggedInUser(String oldPassword, String newPassword) { + decorated.changePasswordForLoggedInUser(oldPassword, newPassword); + } + + @Override + public void overwritePassword(String userId, String newPassword) { + decorated.overwritePassword(userId, newPassword); + } +//~--- fields --------------------------------------------------------------- /** Field description */ private final UserManager decorated; diff --git a/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java b/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java deleted file mode 100644 index 16ee045a75..0000000000 --- a/scm-core/src/main/java/sonia/scm/user/UserNotFoundException.java +++ /dev/null @@ -1,89 +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.user; - -/** - * The UserNotFoundException is thrown e.g. from the - * modify method of the {@link UserManager}, if the user does not exists. - * - * @author Sebastian Sdorra - * @since 1.28 - */ -public class UserNotFoundException extends UserException -{ - - /** Field description */ - private static final long serialVersionUID = 2560311805598995047L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new UserNotFoundException. - * - */ - public UserNotFoundException() {} - - /** - * Constructs a new UserNotFoundException. - * - * - * @param message message for the exception - */ - public UserNotFoundException(String message) - { - super(message); - } - - /** - * Constructs a new UserNotFoundException. - * - * - * @param throwable root cause - */ - public UserNotFoundException(Throwable throwable) - { - super(throwable); - } - - /** - * Constructs a new UserNotFoundException. - * - * - * @param message message for the exception - * @param throwable root cause - */ - public UserNotFoundException(String message, Throwable throwable) - { - super(message, throwable); - } -} diff --git a/scm-core/src/main/java/sonia/scm/util/CRLFInjectionException.java b/scm-core/src/main/java/sonia/scm/util/CRLFInjectionException.java new file mode 100644 index 0000000000..16dca8e910 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/util/CRLFInjectionException.java @@ -0,0 +1,8 @@ +package sonia.scm.util; + +public class CRLFInjectionException extends IllegalArgumentException{ + + public CRLFInjectionException(String message) { + super(message); + } +} diff --git a/scm-core/src/main/java/sonia/scm/util/Comparables.java b/scm-core/src/main/java/sonia/scm/util/Comparables.java new file mode 100644 index 0000000000..1fb0c5e358 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/util/Comparables.java @@ -0,0 +1,88 @@ +package sonia.scm.util; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; + +public final class Comparables { + + private static final CacheLoader beanInfoCacheLoader = new CacheLoader() { + @Override + public BeanInfo load(Class type) throws IntrospectionException { + return Introspector.getBeanInfo(type); + } + }; + + private static final LoadingCache beanInfoCache = CacheBuilder.newBuilder() + .maximumSize(50) // limit the cache to avoid consuming to much memory on miss usage + .build(beanInfoCacheLoader); + + private Comparables() { + } + + public static Comparator comparator(Class type, String sortBy) { + BeanInfo info = createBeanInfo(type); + PropertyDescriptor propertyDescriptor = findPropertyDescriptor(sortBy, info); + + Method readMethod = propertyDescriptor.getReadMethod(); + checkIfPropertyIsComparable(readMethod, sortBy); + + return new MethodComparator<>(readMethod); + } + + private static void checkIfPropertyIsComparable(Method readMethod, String sortBy) { + checkArgument(isReturnTypeComparable(readMethod), "property %s is not comparable", sortBy); + } + + private static boolean isReturnTypeComparable(Method readMethod) { + return Comparable.class.isAssignableFrom(readMethod.getReturnType()); + } + + private static PropertyDescriptor findPropertyDescriptor(String sortBy, BeanInfo info) { + PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors(); + + Optional sortByPropertyDescriptor = Arrays.stream(propertyDescriptors) + .filter(p -> p.getName().equals(sortBy)) + .findFirst(); + + return sortByPropertyDescriptor.orElseThrow(() -> new IllegalArgumentException("could not find property " + sortBy)); + } + + private static BeanInfo createBeanInfo(Class type) { + return beanInfoCache.getUnchecked(type); + } + + private static class MethodComparator implements Comparator { + + private final Method readMethod; + + private MethodComparator(Method readMethod) { + this.readMethod = readMethod; + } + + @Override + @SuppressWarnings("unchecked") + public int compare(T left, T right) { + try { + Comparable leftResult = (Comparable) readMethod.invoke(left); + Comparable rightResult = (Comparable) readMethod.invoke(right); + return leftResult.compareTo(rightResult); + } catch (IllegalAccessException | InvocationTargetException ex) { + throw new IllegalArgumentException("failed to invoke read method", ex); + } + } + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/util/Decorators.java b/scm-core/src/main/java/sonia/scm/util/Decorators.java similarity index 99% rename from scm-webapp/src/main/java/sonia/scm/util/Decorators.java rename to scm-core/src/main/java/sonia/scm/util/Decorators.java index 41c75660a9..6465631d03 100644 --- a/scm-webapp/src/main/java/sonia/scm/util/Decorators.java +++ b/scm-core/src/main/java/sonia/scm/util/Decorators.java @@ -37,7 +37,6 @@ package sonia.scm.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.DecoratorFactory; /** diff --git a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java index 1a1394dd0c..2744addc62 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -50,6 +50,7 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; +import java.util.Arrays; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -81,9 +82,9 @@ public final class HttpUtil /** * Name of bearer authentication cookie. - * + * * TODO find a better place - * + * * @since 2.0.0 */ public static final String COOKIE_BEARER_AUTHENTICATION = "X-Bearer-Token"; @@ -96,7 +97,7 @@ public final class HttpUtil * @since 2.0.0 */ public static final String HEADER_AUTHORIZATION = "Authorization"; - + /** * content-length header * @since 1.46 @@ -247,13 +248,23 @@ public final class HttpUtil //~--- methods -------------------------------------------------------------- + /** + * Joins all path elements together separated by {@code {@link #SEPARATOR_PATH}}. + * + * @param pathElements path elements + * + * @return concatenated path + * @since 2.0.0 + */ + public static String concatenate(String... pathElements) { + return Arrays.stream(pathElements).reduce(HttpUtil::append).orElse(""); + } + /** * Appends the suffix to given uri. * - * - * @param uri uri + * @param uri uri * @param suffix suffix - * * @return * @since 1.9 */ @@ -333,8 +344,7 @@ public final class HttpUtil "parameter \"{}\" contains a character which could be an indicator for a crlf injection", parameter); - throw new IllegalArgumentException( - "parameter contains an illegal character"); + throw new CRLFInjectionException("parameter contains an illegal character"); } } diff --git a/scm-core/src/main/java/sonia/scm/util/IOUtil.java b/scm-core/src/main/java/sonia/scm/util/IOUtil.java index 49807104c7..4058c089f4 100644 --- a/scm-core/src/main/java/sonia/scm/util/IOUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/IOUtil.java @@ -37,14 +37,11 @@ package sonia.scm.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.io.Command; import sonia.scm.io.CommandResult; import sonia.scm.io.SimpleCommand; import sonia.scm.io.ZipUnArchiver; -//~--- JDK imports ------------------------------------------------------------ - import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -55,12 +52,13 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -229,7 +227,7 @@ public final class IOUtil while (byteCount > 0) { int max = (byteCount < bufferSize) - ? byteCount + ? (int) byteCount : bufferSize; len = in.read(buffer, 0, max); @@ -471,8 +469,14 @@ public final class IOUtil { if (!directory.exists() &&!directory.mkdirs()) { - throw new IllegalStateException( - "could not create directory ".concat(directory.getPath())); + // Sometimes, the previous check simply has the wrong result (either the 'exists()' returnes false though the + // directory exists or 'mkdirs()' returns false though the directory was created successfully. + // We therefore have to double check here. Funny though, in these cases a second check with 'directory.exists()' + // still returns false. As it seems, 'directory.getAbsoluteFile().exists()' creates a new object that fixes this + // problem. + if (!directory.getAbsoluteFile().exists()) { + throw new IllegalStateException("could not create directory ".concat(directory.getPath())); + } } } @@ -592,7 +596,7 @@ public final class IOUtil public static List searchAll(String[] path, String cmd, String checkParameter) { - List cmds = new ArrayList<>(); + List cmds = new ArrayList(); if (isCommandAvailable(cmd, checkParameter)) { diff --git a/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java b/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java index 2f1fb4bb04..243f5e72d7 100644 --- a/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java @@ -33,14 +33,14 @@ package sonia.scm.util; -import com.google.common.base.Splitter; -import com.google.common.collect.Streams; +//~--- non-JDK imports -------------------------------------------------------- + import sonia.scm.Validateable; -import java.util.Arrays; -import java.util.regex.Matcher; import java.util.regex.Pattern; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -50,17 +50,17 @@ public final class ValidationUtil /** Field description */ private static final String REGEX_MAIL = - "^[A-z0-9][\\w.-]*@[A-z0-9][\\w\\-\\.]*\\.[A-z0-9][A-z0-9-]+$"; + "^[A-Za-z0-9][\\w.-]*@[A-Za-z0-9][\\w\\-\\.]*\\.[A-Za-z0-9][A-Za-z0-9-]+$"; /** Field description */ - private static final String REGEX_NAME = - "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$"; + public static final String REGEX_NAME = + "^[A-Za-z0-9\\.\\-_][A-Za-z0-9\\.\\-_@]*$"; + + public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$"; /** Field description */ - private static final String REGEX_REPOSITORYNAME = - "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_/]*$"; + private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME); - private static final Pattern REPO_NAME_REGEX = Pattern.compile(REGEX_REPOSITORYNAME); //~--- constructors --------------------------------------------------------- /** @@ -119,24 +119,37 @@ public final class ValidationUtil * * @return */ - public static boolean isNotContaining(String value, String... notAllowedStrings) { - return !Util.isNotEmpty(value) || (notAllowedStrings != null) && Arrays.stream(notAllowedStrings) - .noneMatch(value::contains); + public static boolean isNotContaining(String value, + String... notAllowedStrings) + { + boolean result = Util.isNotEmpty(value); + + if (result && (notAllowedStrings != null)) + { + for (String nas : notAllowedStrings) + { + if (value.indexOf(nas) >= 0) + { + result = false; + + break; + } + } + } + + return result; } /** - * Method description + * Returns {@code true} if the repository name is valid. * - * - * @param name + * @param name repository name * @since 1.9 * - * @return + * @return {@code true} if repository name is valid */ public static boolean isRepositoryNameValid(String name) { - return Util.isNotEmpty(name) && Streams.stream(Splitter.on('/').split(name)) - .map(REPO_NAME_REGEX::matcher) - .allMatch(Matcher::matches); + return PATTERN_REPOSITORYNAME.matcher(name).matches(); } /** diff --git a/scm-core/src/main/java/sonia/scm/util/WebUtil.java b/scm-core/src/main/java/sonia/scm/util/WebUtil.java index 2fc0876668..a9337b0598 100644 --- a/scm-core/src/main/java/sonia/scm/util/WebUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/WebUtil.java @@ -49,6 +49,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; +import java.util.function.Function; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -266,7 +267,12 @@ public final class WebUtil */ public static boolean isGzipSupported(HttpServletRequest request) { - String enc = request.getHeader(HEADER_ACCEPTENCODING); + return isGzipSupported(request::getHeader); + } + + public static boolean isGzipSupported(Function headerResolver) + { + String enc = headerResolver.apply(HEADER_ACCEPTENCODING); return (enc != null) && enc.contains("gzip"); } diff --git a/scm-core/src/main/java/sonia/scm/version/Version.java b/scm-core/src/main/java/sonia/scm/version/Version.java index 96f7a9ff07..cdc1a222c0 100644 --- a/scm-core/src/main/java/sonia/scm/version/Version.java +++ b/scm-core/src/main/java/sonia/scm/version/Version.java @@ -39,10 +39,10 @@ import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.ComparisonChain; -//~--- JDK imports ------------------------------------------------------------ - import java.util.Locale; +//~--- JDK imports ------------------------------------------------------------ + /** * Version object for comparing and parsing versions. * diff --git a/scm-core/src/main/java/sonia/scm/web/AbstractRepositoryJsonEnricher.java b/scm-core/src/main/java/sonia/scm/web/AbstractRepositoryJsonEnricher.java new file mode 100644 index 0000000000..2cb4674d24 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/AbstractRepositoryJsonEnricher.java @@ -0,0 +1,40 @@ +package sonia.scm.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import static java.util.Collections.singletonMap; +import static sonia.scm.web.VndMediaType.REPOSITORY; +import static sonia.scm.web.VndMediaType.REPOSITORY_COLLECTION; + +public abstract class AbstractRepositoryJsonEnricher extends JsonEnricherBase { + + public AbstractRepositoryJsonEnricher(ObjectMapper objectMapper) { + super(objectMapper); + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(REPOSITORY, context)) { + JsonNode repositoryNode = context.getResponseEntity(); + enrichRepositoryNode(repositoryNode); + } else if (resultHasMediaType(REPOSITORY_COLLECTION, context)) { + JsonNode repositoryCollectionNode = context.getResponseEntity().get("_embedded").withArray("repositories"); + repositoryCollectionNode.elements().forEachRemaining(this::enrichRepositoryNode); + } + } + + private void enrichRepositoryNode(JsonNode repositoryNode) { + String namespace = repositoryNode.get("namespace").asText(); + String name = repositoryNode.get("name").asText(); + + enrichRepositoryNode(repositoryNode, namespace, name); + } + + protected abstract void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name); + + protected void addLink(JsonNode repositoryNode, String linkName, String link) { + JsonNode hrefNode = createObject(singletonMap("href", value(link))); + addPropertyNode(repositoryNode.get("_links"), linkName, hrefNode); + } +} diff --git a/scm-core/src/main/java/sonia/scm/web/JsonEnricher.java b/scm-core/src/main/java/sonia/scm/web/JsonEnricher.java new file mode 100644 index 0000000000..067136d71f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/JsonEnricher.java @@ -0,0 +1,15 @@ +package sonia.scm.web; + +import sonia.scm.plugin.ExtensionPoint; + +/** + * Implementing this extension point you can post process json response objects. + * To do so, you get a {@link JsonEnricherContext} with the complete json tree, + * that can be modified. + */ +@ExtensionPoint +public interface JsonEnricher { + + void enrich(JsonEnricherContext context); + +} diff --git a/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java new file mode 100644 index 0000000000..6bdd321c86 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java @@ -0,0 +1,40 @@ +package sonia.scm.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Map; + +public abstract class JsonEnricherBase implements JsonEnricher { + + private final ObjectMapper objectMapper; + + protected JsonEnricherBase(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) { + return mediaType.equalsIgnoreCase(context.getResponseMediaType().toString()); + } + + protected JsonNode value(Object object) { + return objectMapper.convertValue(object, JsonNode.class); + } + + protected ObjectNode createObject() { + return objectMapper.createObjectNode(); + } + + protected ObjectNode createObject(Map values) { + ObjectNode object = createObject(); + + values.forEach((key, value) -> object.set(key, value(value))); + + return object; + } + + protected void addPropertyNode(JsonNode parent, String newKey, JsonNode child) { + ((ObjectNode) parent).set(newKey, child); + } +} diff --git a/scm-core/src/main/java/sonia/scm/web/JsonEnricherContext.java b/scm-core/src/main/java/sonia/scm/web/JsonEnricherContext.java new file mode 100644 index 0000000000..3b18a34f3f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/JsonEnricherContext.java @@ -0,0 +1,45 @@ +package sonia.scm.web; + +import com.fasterxml.jackson.databind.JsonNode; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +/** + * Process data for the {@link JsonEnricher} extension point giving context for + * post processing json results. + */ +public class JsonEnricherContext { + + private URI requestUri; + private MediaType responseMediaType; + private JsonNode responseEntity; + + public JsonEnricherContext(URI requestUri, MediaType responseMediaType, JsonNode responseEntity) { + this.requestUri = requestUri; + this.responseMediaType = responseMediaType; + this.responseEntity = responseEntity; + } + + /** + * The URI of the originating request. + */ + public URI getRequestUri() { + return requestUri; + } + + /** + * The media type of the response. Using this you can determine the content of the result. + * @see VndMediaType + */ + public MediaType getResponseMediaType() { + return responseMediaType; + } + + /** + * The json result represented by nodes, that can be modified. + */ + public JsonNode getResponseEntity() { + return responseEntity; + } +} diff --git a/scm-core/src/main/java/sonia/scm/web/UserAgent.java b/scm-core/src/main/java/sonia/scm/web/UserAgent.java index b3aa1eff01..e501ac8bdf 100644 --- a/scm-core/src/main/java/sonia/scm/web/UserAgent.java +++ b/scm-core/src/main/java/sonia/scm/web/UserAgent.java @@ -126,10 +126,10 @@ public final class UserAgent { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("browser", browser) - .add("basicAuthenticationCharset", basicAuthenticationCharset) - .toString(); + .add("name", name) + .add("browser", browser) + .add("basicAuthenticationCharset", basicAuthenticationCharset) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java new file mode 100644 index 0000000000..24250b26ba --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -0,0 +1,70 @@ +package sonia.scm.web; + +import javax.ws.rs.core.MediaType; + +/** + * Vendor media types used by SCMM. + */ +public class VndMediaType { + + private static final String VERSION = "2"; + private static final String TYPE = "application"; + private static final String SUBTYPE_PREFIX = "vnd.scmm-"; + public static final String PREFIX = TYPE + "/" + SUBTYPE_PREFIX; + public static final String SUFFIX = "+json;v=" + VERSION; + public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX; + public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION; + + public static final String INDEX = PREFIX + "index" + SUFFIX; + public static final String USER = PREFIX + "user" + SUFFIX; + public static final String GROUP = PREFIX + "group" + SUFFIX; + public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX; + public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; + public static final String REPOSITORY_PERMISSION = PREFIX + "repositoryPermission" + SUFFIX; + public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; + public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX; + public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX; + public static final String TAG = PREFIX + "tag" + SUFFIX; + public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX; + public static final String BRANCH = PREFIX + "branch" + SUFFIX; + public static final String BRANCH_REQUEST = PREFIX + "branchRequest" + SUFFIX; + public static final String DIFF = PLAIN_TEXT_PREFIX + "diff" + PLAIN_TEXT_SUFFIX; + public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; + public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; + public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; + public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX; + public static final String CONFIG = PREFIX + "config" + SUFFIX; + public static final String REPOSITORY_VERB_COLLECTION = PREFIX + "repositoryVerbCollection" + SUFFIX; + public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX; + public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; + public static final String PLUGIN = PREFIX + "plugin" + SUFFIX; + public static final String PLUGIN_COLLECTION = PREFIX + "pluginCollection" + SUFFIX; + public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX; + public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX; + @SuppressWarnings("squid:S2068") + public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX; + @SuppressWarnings("squid:S2068") + public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX; + public static final String PERMISSION_COLLECTION = PREFIX + "permissionCollection" + SUFFIX; + public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX; + public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX; + + public static final String NAMESPACE_STRATEGIES = PREFIX + "namespaceStrategies" + SUFFIX; + + public static final String ME = PREFIX + "me" + SUFFIX; + public static final String SOURCE = PREFIX + "source" + SUFFIX; + public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX; + + public static final String REPOSITORY_ROLE = PREFIX + "repositoryRole" + SUFFIX; + public static final String REPOSITORY_ROLE_COLLECTION = PREFIX + "repositoryRoleCollection" + SUFFIX; + + private VndMediaType() { + } + + /** + * Checks whether the given media type is a media type used by SCMM. + */ + public static boolean isVndType(MediaType type) { + return type.getType().equals(TYPE) && type.getSubtype().startsWith(SUBTYPE_PREFIX); + } +} diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java index b88dda8a97..3b64e6b5ac 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java @@ -37,31 +37,27 @@ package sonia.scm.web.filter; import com.google.inject.Inject; import com.google.inject.Singleton; - import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.subject.Subject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousToken; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import sonia.scm.web.WebTokenGenerator; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +//~--- JDK imports ------------------------------------------------------------ /** * Handles authentication, if a one of the {@link WebTokenGenerator} returns @@ -128,12 +124,13 @@ public class AuthenticationFilter extends HttpFilter } else if (subject.isAuthenticated()) { - logger.trace("user is allready authenticated"); + logger.trace("user is already authenticated"); processChain(request, response, chain, subject); } - else if (isAnonymousAccessEnabled()) + else if (isAnonymousAccessEnabled() && !HttpUtil.isWUIRequest(request)) { logger.trace("anonymous access granted"); + subject.login(new AnonymousToken()); processChain(request, response, chain, subject); } else @@ -301,7 +298,7 @@ public class AuthenticationFilter extends HttpFilter } } - chain.doFilter(new SecurityHttpServletRequestWrapper(request, username), + chain.doFilter(new PropagatePrincipleServletRequestWrapper(request, username), response); } diff --git a/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java new file mode 100644 index 0000000000..8b1868309b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java @@ -0,0 +1,38 @@ +package sonia.scm.web.filter; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.util.HttpUtil; +import sonia.scm.web.UserAgent; +import sonia.scm.web.UserAgentParser; +import sonia.scm.web.WebTokenGenerator; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +public class HttpProtocolServletAuthenticationFilterBase extends AuthenticationFilter { + + private final UserAgentParser userAgentParser; + + protected HttpProtocolServletAuthenticationFilterBase( + ScmConfiguration configuration, + Set tokenGenerators, + UserAgentParser userAgentParser) { + super(configuration, tokenGenerators); + this.userAgentParser = userAgentParser; + } + + @Override + protected void handleUnauthorized(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + UserAgent userAgent = userAgentParser.parse(request); + if (userAgent.isBrowser()) { + // we can proceed the filter chain because the HttpProtocolServlet will render the ui if the client is a browser + chain.doFilter(request, response); + } else { + HttpUtil.sendUnauthorized(request, response); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java index 7517f4848c..a062fdb360 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java @@ -33,44 +33,32 @@ package sonia.scm.web.filter; -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Splitter; - import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.subject.Subject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import sonia.scm.ArgumentIsInvalidException; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecorator; import sonia.scm.security.Role; import sonia.scm.security.ScmSecurityException; import sonia.scm.util.HttpUtil; -import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Iterator; - -import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.shiro.authz.AuthorizationException; +import java.io.IOException; /** * Abstract http filter to check repository permissions. * * @author Sebastian Sdorra */ -public abstract class PermissionFilter extends HttpFilter +public abstract class PermissionFilter extends ScmProviderHttpServletDecorator { /** the logger for PermissionFilter */ @@ -86,23 +74,14 @@ public abstract class PermissionFilter extends HttpFilter * * @since 1.21 */ - public PermissionFilter(ScmConfiguration configuration) + protected PermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) { + super(delegate); this.configuration = configuration; } //~--- get methods ---------------------------------------------------------- - /** - * Returns the requested repository. - * - * - * @param request current http request - * - * @return requested repository - */ - protected abstract Repository getRepository(HttpServletRequest request); - /** * Returns true if the current request is a write request. * @@ -122,66 +101,38 @@ public abstract class PermissionFilter extends HttpFilter * * @param request http request * @param response http response - * @param chain filter chain * * @throws IOException * @throws ServletException */ @Override - protected void doFilter(HttpServletRequest request, - HttpServletResponse response, FilterChain chain) + public void service(HttpServletRequest request, + HttpServletResponse response, Repository repository) throws IOException, ServletException { Subject subject = SecurityUtils.getSubject(); try { - Repository repository = getRepository(request); + boolean writeRequest = isWriteRequest(request); - if (repository != null) + if (hasPermission(repository, writeRequest)) { - boolean writeRequest = isWriteRequest(request); + logger.trace("{} access to repository {} for user {} granted", + getActionAsString(writeRequest), repository.getName(), + getUserName(subject)); - if (hasPermission(repository, writeRequest)) - { - logger.trace("{} access to repository {} for user {} granted", - getActionAsString(writeRequest), repository.getName(), - getUserName(subject)); - - chain.doFilter(request, response); - } - else - { - logger.info("{} access to repository {} for user {} denied", - getActionAsString(writeRequest), repository.getName(), - getUserName(subject)); - - sendAccessDenied(request, response, subject); - } + super.service(request, response, repository); } else { - logger.debug("repository not found"); + logger.info("{} access to repository {} for user {} denied", + getActionAsString(writeRequest), repository.getName(), + getUserName(subject)); - response.sendError(HttpServletResponse.SC_NOT_FOUND); + sendAccessDenied(request, response, subject); } } - catch (ArgumentIsInvalidException ex) - { - if (logger.isTraceEnabled()) - { - logger.trace( - "wrong request at ".concat(request.getRequestURI()).concat( - " send redirect"), ex); - } - else if (logger.isWarnEnabled()) - { - logger.warn("wrong request at {} send redirect", - request.getRequestURI()); - } - - response.sendRedirect(getRepositoryRootHelpUrl(request)); - } catch (ScmSecurityException | AuthorizationException ex) { logger.warn("user " + subject.getPrincipal() + " has not enough permissions", ex); @@ -222,29 +173,6 @@ public abstract class PermissionFilter extends HttpFilter HttpUtil.sendUnauthorized(response, configuration.getRealmDescription()); } - /** - * Extracts the type of the repositroy from url. - * - * - * @param request http request - * - * @return type of repository - */ - private String extractType(HttpServletRequest request) - { - Iterator it = Splitter.on( - HttpUtil.SEPARATOR_PATH).omitEmptyStrings().split( - request.getRequestURI()).iterator(); - String type = it.next(); - - if (Util.isNotEmpty(request.getContextPath())) - { - type = it.next(); - } - - return type; - } - /** * Send access denied to the servlet response. * @@ -285,25 +213,6 @@ public abstract class PermissionFilter extends HttpFilter : "read"; } - /** - * Returns the repository root help url. - * - * - * @param request current http request - * - * @return repository root help url - */ - private String getRepositoryRootHelpUrl(HttpServletRequest request) - { - String type = extractType(request); - String helpUrl = HttpUtil.getCompleteUrl(request, - "/api/rest/help/repository-root/"); - - helpUrl = helpUrl.concat(type).concat(".html"); - - return helpUrl; - } - /** * Returns the username from the given subject or anonymous. * @@ -339,11 +248,11 @@ public abstract class PermissionFilter extends HttpFilter if (writeRequest) { - permitted = RepositoryPermissions.write(repository).isPermitted(); + permitted = RepositoryPermissions.push(repository).isPermitted(); } else { - permitted = RepositoryPermissions.read(repository).isPermitted(); + permitted = RepositoryPermissions.pull(repository).isPermitted(); } return permitted; diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PropagatePrincipleServletRequestWrapper.java b/scm-core/src/main/java/sonia/scm/web/filter/PropagatePrincipleServletRequestWrapper.java new file mode 100644 index 0000000000..2b40b0e73f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/filter/PropagatePrincipleServletRequestWrapper.java @@ -0,0 +1,54 @@ +/** + * 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; + +//~--- JDK imports ------------------------------------------------------------ + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +public class PropagatePrincipleServletRequestWrapper extends HttpServletRequestWrapper { + + private final String principal; + + public PropagatePrincipleServletRequestWrapper(HttpServletRequest request, String principal) { + super(request); + this.principal = principal; + } + + @Override + public String getRemoteUser() { + return principal; + } +} diff --git a/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java deleted file mode 100644 index ea0d90c915..0000000000 --- a/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.web.filter; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Throwables; -import com.google.inject.ProvisionException; - -import org.apache.shiro.authz.AuthorizationException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.config.ScmConfiguration; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryProvider; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.servlet.http.HttpServletRequest; - -/** - * - * @author Sebastian Sdorra - * @since 1.9 - */ -public abstract class ProviderPermissionFilter extends PermissionFilter -{ - - /** - * the logger for ProviderPermissionFilter - */ - private static final Logger logger = - LoggerFactory.getLogger(ProviderPermissionFilter.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param configuration - * @param repositoryProvider - * @since 1.21 - */ - public ProviderPermissionFilter(ScmConfiguration configuration, - RepositoryProvider repositoryProvider) - { - super(configuration); - this.repositoryProvider = repositoryProvider; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * - * @return - */ - @Override - protected Repository getRepository(HttpServletRequest request) - { - Repository repository = null; - - try - { - repository = repositoryProvider.get(); - } - catch (ProvisionException ex) - { - Throwables.propagateIfPossible(ex.getCause(), - IllegalStateException.class, AuthorizationException.class); - logger.error("could not get repository from request", ex); - } - - return repository; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final RepositoryProvider repositoryProvider; -} diff --git a/scm-core/src/main/java/sonia/scm/web/filter/RegexPermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/RegexPermissionFilter.java deleted file mode 100644 index 40c41a5954..0000000000 --- a/scm-core/src/main/java/sonia/scm/web/filter/RegexPermissionFilter.java +++ /dev/null @@ -1,133 +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 sonia.scm.config.ScmConfiguration; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryManager; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.servlet.http.HttpServletRequest; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class RegexPermissionFilter extends PermissionFilter -{ - - /** Field description */ - public static final Pattern PATTERN_REPOSITORYNAME = - Pattern.compile("/[^/]+/([^/]+)(?:/.*)?"); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param configuration - * @param repositoryManager - */ - public RegexPermissionFilter(ScmConfiguration configuration, - RepositoryManager repositoryManager) - { - super(configuration); - this.repositoryManager = repositoryManager; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - protected abstract String getType(); - - /** - * Method description - * - * - * @param request - * - * @return - */ - @Override - protected Repository getRepository(HttpServletRequest request) - { - Repository repository = null; - String uri = request.getRequestURI(); - - uri = uri.substring(request.getContextPath().length()); - - Matcher m = PATTERN_REPOSITORYNAME.matcher(uri); - - if (m.matches()) - { - String repositoryname = m.group(1); - - repository = getRepository(repositoryname); - } - - return repository; - } - - /** - * Method description - * - * - * @param name - * - * @return - */ - protected Repository getRepository(String name) - { - return repositoryManager.get(getType(), name); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private RepositoryManager repositoryManager; -} diff --git a/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java b/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java deleted file mode 100644 index 2cd78ce807..0000000000 --- a/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.web.filter; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; - -/** - * - * @author Sebastian Sdorra - */ -public class SecurityHttpServletRequestWrapper extends HttpServletRequestWrapper -{ - - /** - * Constructs ... - * - * - * @param request - * @param principal - */ - public SecurityHttpServletRequestWrapper(HttpServletRequest request, - String principal) - { - super(request); - this.principal = principal; - } - - //~--- get methods ---------------------------------------------------------- - - @Override - public String getRemoteUser() - { - return principal; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final String principal; -} diff --git a/scm-core/src/main/java/sonia/scm/web/proxy/ProxyConfiguration.java b/scm-core/src/main/java/sonia/scm/web/proxy/ProxyConfiguration.java index 7a9d986465..bc30142052 100644 --- a/scm-core/src/main/java/sonia/scm/web/proxy/ProxyConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/web/proxy/ProxyConfiguration.java @@ -37,7 +37,11 @@ package sonia.scm.web.proxy; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; import java.net.URL; import java.util.Collections; import java.util.Set; @@ -140,13 +144,13 @@ public class ProxyConfiguration { //J- return MoreObjects.toStringHelper(this) - .add("url", url) - .add("copyRequestHeaders", copyRequestHeaders) - .add("requestHeaderExcludes", requestHeaderExcludes) - .add("copyResponseHeaders", copyResponseHeaders) - .add("responseHeaderExcludes", responseHeaderExcludes) - .add("cacheEnabled", cacheEnabled) - .toString(); + .add("url", url) + .add("copyRequestHeaders", copyRequestHeaders) + .add("requestHeaderExcludes", requestHeaderExcludes) + .add("copyResponseHeaders", copyResponseHeaders) + .add("responseHeaderExcludes", responseHeaderExcludes) + .add("cacheEnabled", cacheEnabled) + .toString(); //J+ } diff --git a/scm-core/src/main/java/sonia/scm/xml/IndentXMLStreamWriter.java b/scm-core/src/main/java/sonia/scm/xml/IndentXMLStreamWriter.java index 81e973f379..f900ceb234 100644 --- a/scm-core/src/main/java/sonia/scm/xml/IndentXMLStreamWriter.java +++ b/scm-core/src/main/java/sonia/scm/xml/IndentXMLStreamWriter.java @@ -45,7 +45,7 @@ import javax.xml.stream.XMLStreamWriter; * @author Sebastian Sdorra * @since 1.31 */ -public final class IndentXMLStreamWriter implements XMLStreamWriter +public final class IndentXMLStreamWriter implements XMLStreamWriter, AutoCloseable { /** line separator */ @@ -475,7 +475,7 @@ public final class IndentXMLStreamWriter implements XMLStreamWriter //~--- fields --------------------------------------------------------------- /** indent string */ - private String indent = " "; + private String indent = " "; /** current level */ private int level = 0; diff --git a/scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java b/scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java new file mode 100644 index 0000000000..9b8d718851 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java @@ -0,0 +1,25 @@ +package sonia.scm.xml; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +/** + * JAXB adapter for {@link Instant} objects. + * + * @since 2.0.0 + */ +public class XmlInstantAdapter extends XmlAdapter { + + @Override + public String marshal(Instant instant) { + return DateTimeFormatter.ISO_INSTANT.format(instant); + } + + @Override + public Instant unmarshal(String text) { + TemporalAccessor parsed = DateTimeFormatter.ISO_INSTANT.parse(text); + return Instant.from(parsed); + } +} diff --git a/scm-core/src/main/resources/sonia/scm/config/admin-account.xml b/scm-core/src/main/resources/sonia/scm/config/admin-account.xml deleted file mode 100644 index 980545d8bd..0000000000 --- a/scm-core/src/main/resources/sonia/scm/config/admin-account.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - scmadmin - SCM Administrator - scm-admin@scm-manager.org - $shiro1$SHA-512$8192$$yrNahBVDa4Gz+y5gat4msdjyvjtHlVE+N5nTl4WIDhtBFwhSIib13mKJt1sWmVqgHDWi3VwX7fkdkJ2+WToTbw== - true - xml - diff --git a/scm-core/src/main/resources/sonia/scm/config/anonymous-account.xml b/scm-core/src/main/resources/sonia/scm/config/anonymous-account.xml deleted file mode 100644 index 7a6a058e5b..0000000000 --- a/scm-core/src/main/resources/sonia/scm/config/anonymous-account.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - anonymous - SCM Anonymous - scm-anonymous@scm-manager.org - false - xml - diff --git a/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java b/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java new file mode 100644 index 0000000000..4fb9dfd4fa --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java @@ -0,0 +1,44 @@ +package sonia.scm; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(TempDirectory.class) +class BasicContextProviderTest { + + private Path baseDirectory; + + private BasicContextProvider context; + + @BeforeEach + void setUpContext(@TempDirectory.TempDir Path baseDirectory) { + this.baseDirectory = baseDirectory; + context = new BasicContextProvider(baseDirectory.toFile(), "x.y.z", Stage.PRODUCTION); + } + + @Test + void shouldReturnAbsolutePathAsIs(@TempDirectory.TempDir Path path) { + Path absolutePath = path.toAbsolutePath(); + Path resolved = context.resolve(absolutePath); + + assertThat(resolved).isSameAs(absolutePath); + } + + @Test + void shouldResolveRelatePath() { + Path path = Paths.get("repos", "42"); + Path resolved = context.resolve(path); + + assertThat(resolved).isAbsolute(); + assertThat(resolved).startsWithRaw(baseDirectory); + assertThat(resolved).endsWithRaw(path); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/ManagerTest.java b/scm-core/src/test/java/sonia/scm/ManagerTest.java new file mode 100644 index 0000000000..b26fdb3217 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/ManagerTest.java @@ -0,0 +1,110 @@ +package sonia.scm; + +import org.junit.Test; +import org.mockito.Mock; + +import java.util.Collection; +import java.util.Comparator; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.assertEquals; + +public class ManagerTest { + + private int givenItemCount = 0; + + private Manager manager = new ManagerForTesting(); + + @Mock + private Comparator comparator; + private Predicate predicate = x -> true; + + @Test(expected = IllegalArgumentException.class) + public void validatesPageNumber() { + manager.getPage(predicate, comparator, -1, 5); + } + + @Test(expected = IllegalArgumentException.class) + public void validatesPageSize() { + manager.getPage(predicate, comparator, 2, 0); + } + + @Test + public void getsNoPage() { + givenItemCount = 0; + PageResult singlePage = manager.getPage(predicate, comparator, 0, 5); + assertEquals(0, singlePage.getEntities().size()); + assertEquals(givenItemCount, singlePage.getOverallCount()); + } + + @Test + public void getsSinglePageWithoutEnoughItems() { + givenItemCount = 3; + PageResult singlePage = manager.getPage(predicate, comparator, 0, 4); + assertEquals(3, singlePage.getEntities().size() ); + assertEquals(givenItemCount, singlePage.getOverallCount()); + } + + @Test + public void getsSinglePageWithExactCountOfItems() { + givenItemCount = 3; + PageResult singlePage = manager.getPage(predicate, comparator, 0, 3); + assertEquals(3, singlePage.getEntities().size() ); + assertEquals(givenItemCount, singlePage.getOverallCount()); + } + + @Test + public void getsTwoPages() { + givenItemCount = 3; + PageResult page1 = manager.getPage(predicate, comparator, 0, 2); + assertEquals(2, page1.getEntities().size()); + assertEquals(givenItemCount, page1.getOverallCount()); + + PageResult page2 = manager.getPage(predicate, comparator, 1, 2); + assertEquals(1, page2.getEntities().size()); + assertEquals(givenItemCount, page2.getOverallCount()); + } + + private class ManagerForTesting implements Manager { + + @Override + public void refresh(ModelObject object) {} + + @Override + public ModelObject get(String id) { return null; } + + @Override + public Collection getAll() { + return IntStream.range(0, givenItemCount).boxed().collect(toList()); + } + + @Override + public Collection getAll(Predicate filter, Comparator comparator) { return getAll(); } + + @Override + public Collection getAll(int start, int limit) { return null; } + + @Override + public Collection getAll(Comparator comparator, int start, int limit) { return null; } + + @Override + public TypedObject create(TypedObject object) { return null; } + + @Override + public void delete(TypedObject object) {} + + @Override + public void modify(TypedObject object) {} + + @Override + public void close() {} + + @Override + public void init(SCMContextProvider context) {} + + @Override + public Long getLastModified() { return null; } + } +} diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java new file mode 100644 index 0000000000..ff658cc26a --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java @@ -0,0 +1,74 @@ +package sonia.scm.api.v2.resources; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class HalAppenderMapperTest { + + @Mock + private HalAppender appender; + + private HalEnricherRegistry registry; + private HalAppenderMapper mapper; + + @BeforeEach + void beforeEach() { + registry = new HalEnricherRegistry(); + mapper = new HalAppenderMapper(); + mapper.setRegistry(registry); + } + + @Test + void shouldAppendSimpleLink() { + registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com")); + + mapper.applyEnrichers(appender, "hello"); + + verify(appender).appendLink("42", "https://hitchhiker.com"); + } + + @Test + void shouldCallMultipleEnrichers() { + registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com")); + registry.register(String.class, (ctx, appender) -> appender.appendLink("21", "https://scm.hitchhiker.com")); + + mapper.applyEnrichers(appender, "hello"); + + verify(appender).appendLink("42", "https://hitchhiker.com"); + verify(appender).appendLink("21", "https://scm.hitchhiker.com"); + } + + @Test + void shouldAppendLinkByUsingSourceFromContext() { + registry.register(String.class, (ctx, appender) -> { + Optional rel = ctx.oneByType(String.class); + appender.appendLink(rel.get(), "https://hitchhiker.com"); + }); + + mapper.applyEnrichers(appender, "42"); + + verify(appender).appendLink("42", "https://hitchhiker.com"); + } + + @Test + void shouldAppendLinkByUsingMultipleContextEntries() { + registry.register(Integer.class, (ctx, appender) -> { + Optional rel = ctx.oneByType(Integer.class); + Optional href = ctx.oneByType(String.class); + appender.appendLink(String.valueOf(rel.get()), href.get()); + }); + + mapper.applyEnrichers(appender, Integer.valueOf(42), "https://hitchhiker.com"); + + verify(appender).appendLink("42", "https://hitchhiker.com"); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java new file mode 100644 index 0000000000..1aecb5ad46 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java @@ -0,0 +1,44 @@ +package sonia.scm.api.v2.resources; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; + +class HalEnricherContextTest { + + @Test + void shouldCreateContextFromSingleObject() { + HalEnricherContext context = HalEnricherContext.of("hello"); + assertThat(context.oneByType(String.class)).contains("hello"); + } + + @Test + void shouldCreateContextFromMultipleObjects() { + HalEnricherContext context = HalEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L)); + assertThat(context.oneByType(String.class)).contains("hello"); + assertThat(context.oneByType(Integer.class)).contains(42); + assertThat(context.oneByType(Long.class)).contains(21L); + } + + @Test + void shouldReturnEmptyOptionalForUnknownTypes() { + HalEnricherContext context = HalEnricherContext.of(); + assertThat(context.oneByType(String.class)).isNotPresent(); + } + + @Test + void shouldReturnRequiredObject() { + HalEnricherContext context = HalEnricherContext.of("hello"); + assertThat(context.oneRequireByType(String.class)).isEqualTo("hello"); + } + + @Test + void shouldThrowAnNoSuchElementExceptionForUnknownTypes() { + HalEnricherContext context = HalEnricherContext.of(); + assertThrows(NoSuchElementException.class, () -> context.oneRequireByType(String.class)); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java new file mode 100644 index 0000000000..6a863d2f04 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java @@ -0,0 +1,60 @@ +package sonia.scm.api.v2.resources; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HalEnricherRegistryTest { + + private HalEnricherRegistry registry; + + @BeforeEach + void setUpObjectUnderTest() { + registry = new HalEnricherRegistry(); + } + + @Test + void shouldRegisterTheEnricher() { + SampleHalEnricher enricher = new SampleHalEnricher(); + registry.register(String.class, enricher); + + Iterable enrichers = registry.allByType(String.class); + assertThat(enrichers).containsOnly(enricher); + } + + @Test + void shouldRegisterMultipleEnrichers() { + SampleHalEnricher one = new SampleHalEnricher(); + registry.register(String.class, one); + + SampleHalEnricher two = new SampleHalEnricher(); + registry.register(String.class, two); + + Iterable enrichers = registry.allByType(String.class); + assertThat(enrichers).containsOnly(one, two); + } + + @Test + void shouldRegisterEnrichersForDifferentTypes() { + SampleHalEnricher one = new SampleHalEnricher(); + registry.register(String.class, one); + + SampleHalEnricher two = new SampleHalEnricher(); + registry.register(Integer.class, two); + + Iterable enrichers = registry.allByType(String.class); + assertThat(enrichers).containsOnly(one); + + enrichers = registry.allByType(Integer.class); + assertThat(enrichers).containsOnly(two); + } + + private static class SampleHalEnricher implements HalEnricher { + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + + } + } + +} diff --git a/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java new file mode 100644 index 0000000000..67a275c0ad --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java @@ -0,0 +1,56 @@ +package sonia.scm.config; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.user.UserManager; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ScmConfigurationChangedListenerTest { + + @Mock + UserManager userManager; + + ScmConfiguration scmConfiguration = new ScmConfiguration(); + + @InjectMocks + ScmConfigurationChangedListener listener = new ScmConfigurationChangedListener(userManager); + + @Test + void shouldCreateAnonymousUserIfAnoymousAccessEnabled() { + when(userManager.contains(any())).thenReturn(false); + + ScmConfiguration changes = new ScmConfiguration(); + changes.setAnonymousAccessEnabled(true); + scmConfiguration.load(changes); + + listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); + verify(userManager).create(any()); + } + + @Test + void shouldNotCreateAnonymousUserIfAlreadyExists() { + when(userManager.contains(any())).thenReturn(true); + + ScmConfiguration changes = new ScmConfiguration(); + changes.setAnonymousAccessEnabled(true); + scmConfiguration.load(changes); + + listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); + verify(userManager, never()).create(any()); + } + + @Test + void shouldNotCreateAnonymousUserIfAnonymousAccessDisabled() { + //anonymous access disabled by default + listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); + verify(userManager, never()).create(any()); + } +} diff --git a/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java b/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java new file mode 100644 index 0000000000..c3648fd4b8 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java @@ -0,0 +1,87 @@ +package sonia.scm.filter; + +import com.google.inject.util.Providers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.WriterInterceptorContext; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GZipResponseFilterTest { + + @Mock + private HttpServletRequest request; + + @Mock + private WriterInterceptorContext context; + + @Mock + private MultivaluedMap headers; + + private GZipResponseFilter filter; + + @BeforeEach + void setupObjectUnderTest() { + filter = new GZipResponseFilter(Providers.of(request)); + } + + @Test + void shouldSkipGZipCompression() throws IOException { + when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("deflate, br"); + + filter.aroundWriteTo(context); + + verifySkipped(); + } + + @Test + void shouldSkipGZipCompressionWithoutAcceptEncodingHeader() throws IOException { + filter.aroundWriteTo(context); + + verifySkipped(); + } + + private void verifySkipped() throws IOException { + verify(context, never()).getOutputStream(); + verify(context).proceed(); + } + + + @Nested + class AcceptGZipEncoding { + + @BeforeEach + void setUpContext() { + when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("gzip, deflate, br"); + when(context.getHeaders()).thenReturn(headers); + when(context.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + } + + @Test + void shouldEncode() throws IOException { + filter.aroundWriteTo(context); + + verify(headers).remove(HttpHeaders.CONTENT_LENGTH); + verify(headers).add(HttpHeaders.CONTENT_ENCODING, "gzip"); + + verify(context).setOutputStream(any(GZIPOutputStream.class)); + verify(context, times(2)).setOutputStream(any(OutputStream.class)); + } + + } + + +} diff --git a/scm-core/src/test/java/sonia/scm/io/DeepCopyTest.java b/scm-core/src/test/java/sonia/scm/io/DeepCopyTest.java index 31a6185182..0f42e87b0c 100644 --- a/scm-core/src/test/java/sonia/scm/io/DeepCopyTest.java +++ b/scm-core/src/test/java/sonia/scm/io/DeepCopyTest.java @@ -34,16 +34,16 @@ package sonia.scm.io; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Objects; - import org.junit.Test; -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; import java.io.Serializable; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +//~--- JDK imports ------------------------------------------------------------ + /** * Unit tests for {@link DeepCopy}. * diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpClientTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpClientTest.java index 59549c24dd..f8f0a00df2 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpClientTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpClientTest.java @@ -36,7 +36,7 @@ import static org.junit.Assert.*; import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * @@ -105,4 +105,4 @@ public class AdvancedHttpClientTest { assertEquals(URL, request.getUrl()); assertEquals("PROPFIND", request.getMethod()); } -} \ No newline at end of file +} diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestTest.java index 5b2d675e97..08023f3dea 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestTest.java @@ -35,7 +35,7 @@ import org.junit.Test; import static org.junit.Assert.*; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * @@ -54,4 +54,4 @@ public class AdvancedHttpRequestTest { assertEquals(AdvancedHttpRequest.class, ahr.self().getClass()); } -} \ No newline at end of file +} diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java index b1e069fcad..92ca488ddf 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java @@ -33,21 +33,20 @@ package sonia.scm.net.ahc; import com.google.common.base.Charsets; import com.google.common.io.ByteSource; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import static org.hamcrest.Matchers.instanceOf; +import org.junit.Test; import static org.junit.Assert.*; -import static org.mockito.Mockito.when; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; /** * diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpResponseTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpResponseTest.java index 6fc149f36c..0d67d0e931 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpResponseTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpResponseTest.java @@ -44,7 +44,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; import static org.mockito.Mockito.*; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * @@ -211,4 +211,4 @@ public class AdvancedHttpResponseTest { assertFalse(response.isSuccessful()); } -} \ No newline at end of file +} diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/BaseHttpRequestTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/BaseHttpRequestTest.java index 9b3ce995f5..aa735aa2a5 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/BaseHttpRequestTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/BaseHttpRequestTest.java @@ -42,7 +42,7 @@ import org.junit.Before; import org.junit.runner.RunWith; import org.mockito.Mock; import static org.mockito.Mockito.*; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * @@ -139,4 +139,4 @@ public class BaseHttpRequestTest { assertThat(request.queryStrings("a", i1), instanceOf(AdvancedHttpRequest.class)); } -} \ No newline at end of file +} diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/ByteSourceContentTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/ByteSourceContentTest.java index bd2eaf364b..ad0b51b7e6 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/ByteSourceContentTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/ByteSourceContentTest.java @@ -40,7 +40,7 @@ import static org.junit.Assert.*; import org.junit.runner.RunWith; import org.mockito.Mock; import static org.mockito.Mockito.*; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * @@ -58,7 +58,7 @@ public class ByteSourceContentTest { ByteSource source = ByteSource.wrap("abc".getBytes(Charsets.UTF_8)); ByteSourceContent content = new ByteSourceContent(source); content.prepare(request); - verify(request).contentLength(3L); + verify(request).contentLength(3l); } @Test diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/FileContentTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/FileContentTest.java index 963ef86531..5bfba029c0 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/FileContentTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/FileContentTest.java @@ -44,7 +44,7 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import static org.junit.Assert.*; @@ -76,7 +76,7 @@ public class FileContentTest FileContent content = create("abc"); content.prepare(request); - verify(request).contentLength(3L); + verify(request).contentLength(3l); } /** diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/FormContentBuilderTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/FormContentBuilderTest.java index 56cc6d7379..8995680a1d 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/FormContentBuilderTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/FormContentBuilderTest.java @@ -39,7 +39,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import static org.mockito.Mockito.*; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * @@ -92,4 +92,4 @@ public class FormContentBuilderTest { assertEquals(content, captor.getValue()); } -} \ No newline at end of file +} diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/RawContentTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/RawContentTest.java index f4ee8b7031..0d679a3440 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/RawContentTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/RawContentTest.java @@ -39,7 +39,7 @@ import static org.junit.Assert.*; import org.junit.runner.RunWith; import org.mockito.Mock; import static org.mockito.Mockito.*; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * @@ -69,4 +69,4 @@ public class RawContentTest { assertEquals("abc", baos.toString("UTF-8")); } -} \ No newline at end of file +} diff --git a/scm-core/src/test/java/sonia/scm/plugin/AvailablePluginTest.java b/scm-core/src/test/java/sonia/scm/plugin/AvailablePluginTest.java new file mode 100644 index 0000000000..bfdf74fdb1 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/plugin/AvailablePluginTest.java @@ -0,0 +1,32 @@ +package sonia.scm.plugin; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +class AvailablePluginTest { + + @Mock + private AvailablePluginDescriptor descriptor; + + @Test + void shouldReturnNewPendingPluginOnInstall() { + AvailablePlugin plugin = new AvailablePlugin(descriptor); + assertThat(plugin.isPending()).isFalse(); + + AvailablePlugin installed = plugin.install(); + assertThat(installed.isPending()).isTrue(); + } + + @Test + void shouldThrowIllegalStateExceptionIfAlreadyPending() { + AvailablePlugin plugin = new AvailablePlugin(descriptor).install(); + assertThrows(IllegalStateException.class, () -> plugin.install()); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java b/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java index 95addf388f..07e182216f 100644 --- a/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java +++ b/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java @@ -85,7 +85,7 @@ public class SmpArchiveTest public void testExtract() throws IOException, ParserConfigurationException, SAXException { - File archive = createArchive("sonia.sample", "sample", "1.0"); + File archive = createArchive("sonia.sample", "1.0"); File target = tempFolder.newFolder(); IOUtil.mkdirs(target); @@ -112,8 +112,8 @@ public class SmpArchiveTest @Test public void testGetPlugin() throws IOException { - File archive = createArchive("sonia.sample", "sample", "1.0"); - Plugin plugin = SmpArchive.create(archive).getPlugin(); + File archive = createArchive("sonia.sample", "1.0"); + InstalledPluginDescriptor plugin = SmpArchive.create(archive).getPlugin(); assertNotNull(plugin); @@ -121,8 +121,7 @@ public class SmpArchiveTest assertNotNull(info); - assertEquals("sonia.sample", info.getGroupId()); - assertEquals("sample", info.getArtifactId()); + assertEquals("sonia.sample", info.getName()); assertEquals("1.0", info.getVersion()); } @@ -132,22 +131,9 @@ public class SmpArchiveTest * @throws IOException */ @Test(expected = PluginException.class) - public void testWithMissingArtifactId() throws IOException + public void testWithMissingName() throws IOException { - File archive = createArchive("sonia.sample", null, "1.0"); - - SmpArchive.create(archive).getPlugin(); - } - - /** - * Method description - * - * @throws IOException - */ - @Test(expected = PluginException.class) - public void testWithMissingGroupId() throws IOException - { - File archive = createArchive(null, "sample", "1.0"); + File archive = createArchive( null, "1.0"); SmpArchive.create(archive).getPlugin(); } @@ -160,7 +146,7 @@ public class SmpArchiveTest @Test(expected = PluginException.class) public void testWithMissingVersion() throws IOException { - File archive = createArchive("sonia.sample", "sample", null); + File archive = createArchive("sonia.sample", null); SmpArchive.create(archive).getPlugin(); } @@ -169,13 +155,12 @@ public class SmpArchiveTest * Method description * * - * @param groupId - * @param artifactId + * @param name * @param version * * @return */ - private File createArchive(String groupId, String artifactId, String version) + private File createArchive(String name, String version) { File archiveFile; @@ -183,7 +168,7 @@ public class SmpArchiveTest { File descriptor = tempFolder.newFile(); - writeDescriptor(descriptor, groupId, artifactId, version); + writeDescriptor(descriptor, name, version); archiveFile = tempFolder.newFile(); try (ZipOutputStream zos = @@ -229,14 +214,13 @@ public class SmpArchiveTest * * * @param descriptor - * @param groupId - * @param artifactId + * @param name * @param version * * @throws IOException */ - private void writeDescriptor(File descriptor, String groupId, - String artifactId, String version) + private void writeDescriptor(File descriptor, String name, + String version) throws IOException { try @@ -252,8 +236,7 @@ public class SmpArchiveTest writer.writeStartDocument(); writer.writeStartElement("plugin"); writer.writeStartElement("information"); - writeElement(writer, "groupId", groupId); - writeElement(writer, "artifactId", artifactId); + writeElement(writer, "name", name); writeElement(writer, "version", version); writer.writeEndElement(); diff --git a/scm-core/src/test/java/sonia/scm/repository/FileObjectTest.java b/scm-core/src/test/java/sonia/scm/repository/FileObjectTest.java new file mode 100644 index 0000000000..bbd9d0d483 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/FileObjectTest.java @@ -0,0 +1,32 @@ +package sonia.scm.repository; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class FileObjectTest { + + @Test + public void getParentPath() { + FileObject file = create("a/b/c"); + assertEquals("a/b", file.getParentPath()); + } + + @Test + public void getParentPathWithoutParent() { + FileObject file = create("a"); + assertEquals("", file.getParentPath()); + } + + @Test + public void getParentPathOfRoot() { + FileObject file = create(""); + assertNull(file.getParentPath()); + } + + private FileObject create(String path) { + FileObject file = new FileObject(); + file.setPath(path); + return file; + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java b/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java new file mode 100644 index 0000000000..e4cd22b060 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java @@ -0,0 +1,45 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.File; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith({MockitoExtension.class}) +class InitialRepositoryLocationResolverTest { + + private InitialRepositoryLocationResolver resolver = new InitialRepositoryLocationResolver(); + + @Test + void shouldComputeInitialPath() { + Path path = resolver.getPath("42"); + + assertThat(path).isRelative(); + assertThat(path.toString()).isEqualTo("repositories" + File.separator + "42"); + } + + @Test + void shouldThrowIllegalArgumentExceptionIfIdHasASlash() { + Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("../../../passwd")); + } + + @Test + void shouldThrowIllegalArgumentExceptionIfIdHasABackSlash() { + Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("..\\..\\..\\users.ntlm")); + } + + @Test + void shouldThrowIllegalArgumentExceptionIfIdIsDotDot() { + Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("..")); + } + + @Test + void shouldThrowIllegalArgumentExceptionIfIdIsDot() { + Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath(".")); + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/RemoveDeletedRepositoryRoleTest.java b/scm-core/src/test/java/sonia/scm/repository/RemoveDeletedRepositoryRoleTest.java new file mode 100644 index 0000000000..f2f88f9ea7 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RemoveDeletedRepositoryRoleTest.java @@ -0,0 +1,91 @@ +package sonia.scm.repository; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import sonia.scm.HandlerEventType; + +import java.util.Arrays; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.quality.Strictness.LENIENT; +import static sonia.scm.HandlerEventType.DELETE; + +@ExtendWith(MockitoExtension.class) +class RemoveDeletedRepositoryRoleTest { + + static final Repository REPOSITORY = createRepositoryWithRoles("with", "deleted", "kept"); + + @Mock + RepositoryManager manager; + @Captor + ArgumentCaptor modifyCaptor; + + private RemoveDeletedRepositoryRole removeDeletedRepositoryRole; + + @BeforeEach + void init() { + removeDeletedRepositoryRole = new RemoveDeletedRepositoryRole(manager); + doNothing().when(manager).modify(modifyCaptor.capture()); + } + + @Test + void shouldRemoveDeletedPermission() { + when(manager.getAll()).thenReturn(Collections.singletonList(REPOSITORY)); + + removeDeletedRepositoryRole.handle(new RepositoryRoleEvent(DELETE, createRole("deleted"))); + + verify(manager).modify(any()); + Assertions.assertThat(modifyCaptor.getValue().getPermissions()) + .containsExactly(createPermission("kept")); + } + + @Test + @MockitoSettings(strictness = LENIENT) + void shouldDoNothingForEventsWithUnusedRole() { + when(manager.getAll()).thenReturn(Collections.singletonList(REPOSITORY)); + + removeDeletedRepositoryRole.handle(new RepositoryRoleEvent(DELETE, createRole("unused"))); + + verify(manager, never()).modify(any()); + } + + @Test + @MockitoSettings(strictness = LENIENT) + void shouldDoNothingForEventsOtherThanDelete() { + when(manager.getAll()).thenReturn(Collections.singletonList(REPOSITORY)); + + Arrays.stream(HandlerEventType.values()) + .filter(type -> type != DELETE) + .forEach( + type -> removeDeletedRepositoryRole.handle(new RepositoryRoleEvent(type, createRole("deleted"))) + ); + + verify(manager, never()).modify(any()); + } + + private RepositoryRole createRole(String name) { + return new RepositoryRole(name, Collections.singleton("x"), "x"); + } + + static Repository createRepositoryWithRoles(String name, String... roles) { + Repository repository = new Repository("x", "git", "space", name); + Arrays.stream(roles).forEach(role -> repository.addPermission(createPermission(role))); + return repository; + } + + private static RepositoryPermission createPermission(String role) { + return new RepositoryPermission("user", role, false); + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java new file mode 100644 index 0000000000..c7f05c5f3c --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java @@ -0,0 +1,57 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +class RepositoryPermissionTest { + + @Test + void shouldBeEqualWithSameVerbs() { + RepositoryPermission permission1 = new RepositoryPermission("name", asList("one", "two"), false); + RepositoryPermission permission2 = new RepositoryPermission("name", asList("two", "one"), false); + + assertThat(permission1).isEqualTo(permission2); + } + + @Test + void shouldHaveSameHashCodeWithSameVerbs() { + long hash1 = new RepositoryPermission("name", asList("one", "two"), false).hashCode(); + long hash2 = new RepositoryPermission("name", asList("two", "one"), false).hashCode(); + + assertThat(hash1).isEqualTo(hash2); + } + + @Test + void shouldNotBeEqualWithSameVerbs() { + RepositoryPermission permission1 = new RepositoryPermission("name", asList("one", "two"), false); + RepositoryPermission permission2 = new RepositoryPermission("name", asList("three", "one"), false); + + assertThat(permission1).isNotEqualTo(permission2); + } + + @Test + void shouldNotBeEqualWithDifferentType() { + RepositoryPermission permission1 = new RepositoryPermission("name", asList("one"), false); + RepositoryPermission permission2 = new RepositoryPermission("name", asList("one"), true); + + assertThat(permission1).isNotEqualTo(permission2); + } + + @Test + void shouldNotBeEqualWithDifferentName() { + RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one"), false); + RepositoryPermission permission2 = new RepositoryPermission("name2", asList("one"), false); + + assertThat(permission1).isNotEqualTo(permission2); + } + + @Test + void shouldBeEqualWithRedundantVerbs() { + RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one", "two"), false); + RepositoryPermission permission2 = new RepositoryPermission("name1", asList("one", "two", "two"), false); + + assertThat(permission1).isEqualTo(permission2); + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java index 98e714b67f..6053e10ad5 100644 --- a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java @@ -1,62 +1,22 @@ -/** - * 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 org.junit.jupiter.api.Test; -import org.junit.Test; +import java.util.Arrays; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.assertThat; -/** - * - * @author Sebastian Sdorra - */ -public class RepositoryTest -{ +class RepositoryTest { - /** - * Method description - * - */ @Test - public void testCreateUrl() - { - Repository repository = new Repository("123", "hg", "test/repo"); + void shouldCreateNewPermissionOnClone() { + Repository repository = new Repository(); + repository.setPermissions(Arrays.asList(new RepositoryPermission("one", "role", false))); - 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/")); + Repository cloned = repository.clone(); + cloned.setPermissions(Arrays.asList(new RepositoryPermission("two", "role", false))); + + assertThat(repository.getPermissions()).extracting(r -> r.getName()).containsOnly("one"); } + } diff --git a/scm-core/src/test/java/sonia/scm/repository/api/HookContextTest.java b/scm-core/src/test/java/sonia/scm/repository/api/HookContextTest.java index 2f746ec5ad..939858a52e 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/HookContextTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/HookContextTest.java @@ -33,15 +33,12 @@ package sonia.scm.repository.api; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import java.util.List; -import org.junit.Test; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; import org.junit.Before; +import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Changeset; import sonia.scm.repository.Person; import sonia.scm.repository.PreProcessorUtil; @@ -51,6 +48,16 @@ import sonia.scm.repository.spi.HookChangesetRequest; import sonia.scm.repository.spi.HookChangesetResponse; import sonia.scm.repository.spi.HookContextProvider; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + /** * Unit tests for {@link HookContext}. * @@ -64,7 +71,7 @@ public class HookContextTest { @Mock private Repository repository; - + @Mock private PreProcessorUtil preProcessorUtil; @@ -79,7 +86,6 @@ public class HookContextTest { */ @Before public void setUpMocks(){ - when(repository.getName()).thenReturn("test"); when(provider.getChangesetProvider()).thenReturn(changesetProvider); when(provider.getSupportedFeatures()).thenReturn(Sets.newHashSet(HookFeature.CHANGESET_PROVIDER)); @@ -138,4 +144,4 @@ public class HookContextTest { assertFalse(context.isFeatureSupported(HookFeature.BRANCH_PROVIDER)); } -} \ No newline at end of file +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java b/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java new file mode 100644 index 0000000000..086df81741 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java @@ -0,0 +1,53 @@ +package sonia.scm.repository.api; + +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +class HunkTest { + + @Test + void shouldGetComplexHeader() { + String rawHeader = createHunk(2, 3, 4, 5).getRawHeader(); + + assertThat(rawHeader).isEqualTo("@@ -2,3 +4,5 @@"); + } + + @Test + void shouldReturnSingleNumberForOne() { + String rawHeader = createHunk(42, 1, 5, 1).getRawHeader(); + + assertThat(rawHeader).isEqualTo("@@ -42 +5 @@"); + } + + private Hunk createHunk(int oldStart, int oldLineCount, int newStart, int newLineCount) { + return new Hunk() { + @Override + public int getOldStart() { + return oldStart; + } + + @Override + public int getOldLineCount() { + return oldLineCount; + } + + @Override + public int getNewStart() { + return newStart; + } + + @Override + public int getNewLineCount() { + return newLineCount; + } + + @Override + public Iterator iterator() { + return null; + } + }; + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java new file mode 100644 index 0000000000..6e46841cd2 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java @@ -0,0 +1,210 @@ +package sonia.scm.repository.api; + +import com.google.common.io.ByteSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; +import sonia.scm.repository.Person; +import sonia.scm.repository.spi.ModifyCommand; +import sonia.scm.repository.spi.ModifyCommandRequest; +import sonia.scm.repository.util.WorkdirProvider; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +class ModifyCommandBuilderTest { + + @Mock + ModifyCommand command; + @Mock + WorkdirProvider workdirProvider; + @Mock + ModifyCommand.Worker worker; + + ModifyCommandBuilder commandBuilder; + Path workdir; + + @BeforeEach + void initWorkdir(@TempDirectory.TempDir Path temp) throws IOException { + workdir = Files.createDirectory(temp.resolve("workdir")); + lenient().when(workdirProvider.createNewWorkdir()).thenReturn(workdir.toFile()); + commandBuilder = new ModifyCommandBuilder(command, workdirProvider); + } + + @BeforeEach + void initRequestCaptor() { + when(command.execute(any())).thenAnswer( + invocation -> { + ModifyCommandRequest request = invocation.getArgument(0); + for (ModifyCommandRequest.PartialRequest r : request.getRequests()) { + r.execute(worker); + } + return "target"; + } + ); + } + + @Test + void shouldReturnTargetRevisionFromCommit() { + String targetRevision = initCommand() + .deleteFile("toBeDeleted") + .execute(); + + assertThat(targetRevision).isEqualTo("target"); + } + + @Test + void shouldExecuteDelete() throws IOException { + initCommand() + .deleteFile("toBeDeleted") + .execute(); + + verify(worker).delete("toBeDeleted"); + } + + @Test + void shouldExecuteCreateWithByteSourceContent() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); + + initCommand() + .createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateWithInputStreamContent() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); + + initCommand() + .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateWithOverwriteFalseAsDefault() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture()); + + initCommand() + .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(overwriteCaptor.getValue()).isFalse(); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateWithOverwriteIfSet() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture()); + + initCommand() + .createFile("toBeCreated").setOverwrite(true).withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(overwriteCaptor.getValue()).isTrue(); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateMultipleTimes() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); + + initCommand() + .createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes())) + .createFile("toBeCreated_2").withData(new ByteArrayInputStream("content_2".getBytes())) + .execute(); + + List createdNames = nameCaptor.getAllValues(); + assertThat(createdNames.get(0)).isEqualTo("toBeCreated_1"); + assertThat(createdNames.get(1)).isEqualTo("toBeCreated_2"); + assertThat(contentCaptor).contains("content_1", "content_2"); + } + + @Test + void shouldExecuteModify() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).modify(nameCaptor.capture(), any()); + + initCommand() + .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeModified"); + assertThat(contentCaptor).contains("content"); + } + + private ModifyCommandBuilder initCommand() { + return commandBuilder + .setBranch("branch") + .setCommitMessage("message") + .setAuthor(new Person()); + } + + @Test + void shouldDeleteTemporaryFiles(@TempDirectory.TempDir Path temp) throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + doNothing().when(worker).modify(nameCaptor.capture(), fileCaptor.capture()); + + initCommand() + .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(Files.list(temp)).isEmpty(); + } + + private static class ExtractContent implements Answer { + private final List contentCaptor; + + public ExtractContent(List contentCaptor) { + this.contentCaptor = contentCaptor; + } + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return contentCaptor.add(Files.readAllLines(((File) invocation.getArgument(1)).toPath()).get(0)); + } + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java new file mode 100644 index 0000000000..330a4c956a --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java @@ -0,0 +1,74 @@ +package sonia.scm.repository.api; + +import org.junit.Test; +import sonia.scm.repository.Repository; +import sonia.scm.repository.spi.HttpScmProtocol; +import sonia.scm.repository.spi.RepositoryServiceProvider; + +import javax.servlet.ServletConfig; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.util.IterableUtil.sizeOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +public class RepositoryServiceTest { + + private final RepositoryServiceProvider provider = mock(RepositoryServiceProvider.class); + private final Repository repository = new Repository("", "git", "space", "repo"); + + @Test + public void shouldReturnMatchingProtocolsFromProvider() { + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null); + Stream supportedProtocols = repositoryService.getSupportedProtocols(); + + assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1); + } + + @Test + public void shouldFindKnownProtocol() { + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null); + + 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()), null); + + assertThrows(IllegalArgumentException.class, () -> { + repositoryService.getProtocol(UnknownScmProtocol.class); + }); + } + + private static class DummyHttpProtocol extends HttpScmProtocol { + public DummyHttpProtocol(Repository repository) { + super(repository, ""); + } + + @Override + public void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) { + } + } + + private static class DummyScmProtocolProvider implements ScmProtocolProvider { + @Override + public String getType() { + return "git"; + } + + @Override + public ScmProtocol get(Repository repository) { + return new DummyHttpProtocol(repository); + } + } + + private interface UnknownScmProtocol extends ScmProtocol {} +} diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java new file mode 100644 index 0000000000..1fd772fee3 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java @@ -0,0 +1,40 @@ +package sonia.scm.repository.spi; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import sonia.scm.repository.Repository; + +import javax.servlet.ServletConfig; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class HttpScmProtocolTest { + + @TestFactory + Stream shouldCreateCorrectUrlsWithContextPath() { + return Stream.of("http://localhost/scm", "http://localhost/scm/") + .map(url -> assertResultingUrl(url, "http://localhost/scm/repo/space/name")); + } + + @TestFactory + Stream shouldCreateCorrectUrlsWithPort() { + return Stream.of("http://localhost:8080", "http://localhost:8080/") + .map(url -> assertResultingUrl(url, "http://localhost:8080/repo/space/name")); + } + + DynamicTest assertResultingUrl(String baseUrl, String expectedUrl) { + String actualUrl = createInstanceOfHttpScmProtocol(baseUrl).getUrl(); + return DynamicTest.dynamicTest(baseUrl + " -> " + expectedUrl, () -> assertThat(actualUrl).isEqualTo(expectedUrl)); + } + + private HttpScmProtocol createInstanceOfHttpScmProtocol(String baseUrl) { + return new HttpScmProtocol(new Repository("", "", "space", "name"), baseUrl) { + @Override + protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) { + } + }; + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java new file mode 100644 index 0000000000..676549a874 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java @@ -0,0 +1,120 @@ +package sonia.scm.repository.spi; + +import com.google.inject.ProvisionException; +import com.google.inject.util.Providers; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.stubbing.OngoingStubbing; +import sonia.scm.api.v2.resources.ScmPathInfo; +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.Repository; + +import javax.inject.Provider; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class InitializingHttpScmProtocolWrapperTest { + + private static final Repository REPOSITORY = new Repository("", "git", "space", "name"); + + @Mock + private ScmProviderHttpServlet delegateServlet; + @Mock + private ScmPathInfoStore pathInfoStore; + @Mock + private ScmConfiguration scmConfiguration; + private Provider pathInfoStoreProvider; + + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private ServletConfig servletConfig; + + private InitializingHttpScmProtocolWrapper wrapper; + + @Before + public void init() { + initMocks(this); + pathInfoStoreProvider = mock(Provider.class); + when(pathInfoStoreProvider.get()).thenReturn(pathInfoStore); + + wrapper = new InitializingHttpScmProtocolWrapper(Providers.of(this.delegateServlet), pathInfoStoreProvider, scmConfiguration) { + @Override + public String getType() { + return "git"; + } + }; + when(scmConfiguration.getBaseUrl()).thenReturn("http://example.com/scm"); + } + + @Test + public void shouldUsePathFromPathInfo() { + mockSetPathInfo(); + + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } + + @Test + public void shouldUseConfigurationWhenPathInfoNotSet() { + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } + + @Test + public void shouldUseConfigurationWhenNotInRequestScope() { + when(pathInfoStoreProvider.get()).thenThrow(new ProvisionException("test")); + + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } + + @Test + public void shouldInitializeAndDelegateRequestThroughFilter() throws ServletException, IOException { + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + httpScmProtocol.serve(request, response, servletConfig); + + verify(delegateServlet).init(servletConfig); + verify(delegateServlet).service(request, response, REPOSITORY); + } + + @Test + public void shouldInitializeOnlyOnce() throws ServletException, IOException { + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + httpScmProtocol.serve(request, response, servletConfig); + httpScmProtocol.serve(request, response, servletConfig); + + verify(delegateServlet, times(1)).init(servletConfig); + verify(delegateServlet, times(2)).service(request, response, REPOSITORY); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldFailForIllegalScmType() { + HttpScmProtocol httpScmProtocol = wrapper.get(new Repository("", "other", "space", "name")); + } + + private OngoingStubbing mockSetPathInfo() { + return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/")); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java b/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java new file mode 100644 index 0000000000..04e7b72202 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java @@ -0,0 +1,92 @@ +package sonia.scm.repository.util; + + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.Repository; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SimpleWorkdirFactoryTest { + + private static final Repository REPOSITORY = new Repository("1", "git", "space", "X"); + + private final Closeable parent = mock(Closeable.class); + private final Closeable clone = mock(Closeable.class); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + private SimpleWorkdirFactory simpleWorkdirFactory; + + private String initialBranchForLastCloneCall; + + @Before + public void initFactory() throws IOException { + WorkdirProvider workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); + simpleWorkdirFactory = new SimpleWorkdirFactory(workdirProvider) { + @Override + protected Repository getScmRepository(Context context) { + return REPOSITORY; + } + + @Override + protected void closeRepository(Closeable repository) throws IOException { + repository.close(); + } + + @Override + protected void closeWorkdirInternal(Closeable workdir) throws Exception { + workdir.close(); + } + + @Override + protected ParentAndClone cloneRepository(Context context, File target, String initialBranch) { + initialBranchForLastCloneCall = initialBranch; + return new ParentAndClone<>(parent, clone); + } + }; + } + + @Test + public void shouldCreateParentAndClone() { + Context context = new Context(); + try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) { + assertThat(workingCopy.getCentralRepository()).isSameAs(parent); + assertThat(workingCopy.getWorkingRepository()).isSameAs(clone); + } + } + + @Test + public void shouldCloseParent() throws IOException { + Context context = new Context(); + try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {} + + verify(parent).close(); + } + + @Test + public void shouldCloseClone() throws IOException { + Context context = new Context(); + try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {} + + verify(clone).close(); + } + + @Test + public void shouldPropagateInitialBranch() { + Context context = new Context(); + try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, "some")) { + assertThat(initialBranchForLastCloneCall).isEqualTo("some"); + } + } + + private static class Context {} +} diff --git a/scm-core/src/test/java/sonia/scm/resources/ResourceHandlerComparatorTest.java b/scm-core/src/test/java/sonia/scm/resources/ResourceHandlerComparatorTest.java deleted file mode 100644 index 2573a6ec08..0000000000 --- a/scm-core/src/test/java/sonia/scm/resources/ResourceHandlerComparatorTest.java +++ /dev/null @@ -1,141 +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.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.net.URL; - -import java.util.Arrays; - -/** - * - * @author Sebastian Sdorra - */ -public class ResourceHandlerComparatorTest -{ - - /** - * Method description - * - */ - @Test - public void testCompare() - { - ResourceHandler[] handlers = new ResourceHandler[4]; - - handlers[0] = new DummyResourceHandler("xyz"); - handlers[1] = new DummyResourceHandler("abc"); - handlers[2] = new DummyResourceHandler(null); - handlers[3] = new DummyResourceHandler("mno"); - Arrays.sort(handlers, new ResourceHandlerComparator()); - assertEquals("abc", handlers[0].getName()); - assertEquals("mno", handlers[1].getName()); - assertEquals("xyz", handlers[2].getName()); - assertEquals(null, handlers[3].getName()); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 2011-01-18 - * @author Sebastian Sdorra - */ - private static class DummyResourceHandler implements ResourceHandler - { - - /** - * Constructs ... - * - * - * @param name - */ - public DummyResourceHandler(String name) - { - this.name = name; - } - - //~--- get methods -------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public String getName() - { - return name; - } - - /** - * Method description - * - * - * @return - */ - @Override - public URL getResource() - { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * Method description - * - * - * @return - */ - @Override - public ResourceType getType() - { - throw new UnsupportedOperationException("Not supported yet."); - } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private String name; - } -} diff --git a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java new file mode 100644 index 0000000000..0fbcc20ac0 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java @@ -0,0 +1,131 @@ +package sonia.scm.security; + +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.DisabledAccountException; +import org.apache.shiro.authc.UnknownAccountException; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.subject.PrincipalCollection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.group.GroupDAO; +import sonia.scm.user.User; +import sonia.scm.user.UserDAO; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DAORealmHelperTest { + + @Mock + private LoginAttemptHandler loginAttemptHandler; + + @Mock + private UserDAO userDAO; + + @Mock + private GroupDAO groupDAO; + + private DAORealmHelper helper; + + @BeforeEach + void setUpObjectUnderTest() { + helper = new DAORealmHelper(loginAttemptHandler, userDAO, "hitchhiker"); + } + + @Test + void shouldThrowExceptionWithoutUsername() { + assertThrows(IllegalArgumentException.class, () -> helper.authenticationInfoBuilder(null).build()); + } + + @Test + void shouldThrowExceptionWithEmptyUsername() { + assertThrows(IllegalArgumentException.class, () -> helper.authenticationInfoBuilder("").build()); + } + + @Test + void shouldThrowExceptionWithUnknownUser() { + assertThrows(UnknownAccountException.class, () -> helper.authenticationInfoBuilder("trillian").build()); + } + + @Test + void shouldThrowExceptionOnDisabledAccount() { + User user = new User("trillian"); + user.setActive(false); + when(userDAO.get("trillian")).thenReturn(user); + + assertThrows(DisabledAccountException.class, () -> helper.authenticationInfoBuilder("trillian").build()); + } + + @Test + void shouldReturnAuthenticationInfo() { + User user = new User("trillian"); + when(userDAO.get("trillian")).thenReturn(user); + + AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build(); + PrincipalCollection principals = authenticationInfo.getPrincipals(); + assertThat(principals.oneByType(User.class)).isSameAs(user); + assertThat(principals.oneByType(Scope.class)).isEmpty(); + } + + @Test + void shouldReturnAuthenticationInfoWithScope() { + User user = new User("trillian"); + when(userDAO.get("trillian")).thenReturn(user); + + Scope scope = Scope.valueOf("user:*", "group:*"); + + AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian") + .withScope(scope) + .build(); + + PrincipalCollection principals = authenticationInfo.getPrincipals(); + assertThat(principals.oneByType(Scope.class)).isSameAs(scope); + } + + @Test + void shouldReturnAuthenticationInfoWithCredentials() { + User user = new User("trillian"); + when(userDAO.get("trillian")).thenReturn(user); + + AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian") + .withCredentials("secret") + .build(); + + assertThat(authenticationInfo.getCredentials()).isEqualTo("secret"); + } + + @Test + void shouldReturnAuthenticationInfoWithCredentialsFromUser() { + User user = new User("trillian"); + user.setPassword("secret"); + when(userDAO.get("trillian")).thenReturn(user); + + AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build(); + + assertThat(authenticationInfo.getCredentials()).isEqualTo("secret"); + } + + @Test + void shouldThrowExceptionWithWrongTypeOfToken() { + assertThrows(IllegalArgumentException.class, () -> helper.getAuthenticationInfo(BearerToken.valueOf("__bearer__"))); + } + + @Test + void shouldGetAuthenticationInfo() { + User user = new User("trillian"); + when(userDAO.get("trillian")).thenReturn(user); + + AuthenticationInfo authenticationInfo = helper.getAuthenticationInfo(new UsernamePasswordToken("trillian", "secret")); + + PrincipalCollection principals = authenticationInfo.getPrincipals(); + assertThat(principals.oneByType(User.class)).isSameAs(user); + assertThat(principals.oneByType(Scope.class)).isEmpty(); + + assertThat(authenticationInfo.getCredentials()).isNull(); + } +} diff --git a/scm-core/src/test/java/sonia/scm/security/DefaultCipherHandlerTest.java b/scm-core/src/test/java/sonia/scm/security/DefaultCipherHandlerTest.java index 1fe78191de..e193c86f20 100644 --- a/scm-core/src/test/java/sonia/scm/security/DefaultCipherHandlerTest.java +++ b/scm-core/src/test/java/sonia/scm/security/DefaultCipherHandlerTest.java @@ -40,7 +40,7 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; import static org.mockito.Mockito.*; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.SCMContextProvider; /** @@ -107,4 +107,4 @@ public class DefaultCipherHandlerTest { assertEquals("hallo123", cipher.decode(cipher.encode("hallo123"))); } -} \ No newline at end of file +} diff --git a/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java deleted file mode 100644 index e8180ca24a..0000000000 --- a/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java +++ /dev/null @@ -1,87 +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.security; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import sonia.scm.repository.PermissionType; - -import static org.junit.Assert.*; - -/** - * - * @author Sebastian Sdorra - */ -public class RepositoryPermissionTest -{ - - /** - * Method description - * - */ - @Test - public void testImplies() - { - RepositoryPermission p = new RepositoryPermission("asd", - PermissionType.READ); - - assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ))); - assertFalse(p.implies(new RepositoryPermission("asd", - PermissionType.OWNER))); - assertFalse(p.implies(new RepositoryPermission("asd", - PermissionType.WRITE))); - p = new RepositoryPermission("asd", PermissionType.OWNER); - assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ))); - assertFalse(p.implies(new RepositoryPermission("bdb", - PermissionType.READ))); - } - - /** - * Method description - * - */ - @Test - public void testImpliesWithWildcard() - { - RepositoryPermission p = new RepositoryPermission("*", - PermissionType.OWNER); - - assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ))); - assertTrue(p.implies(new RepositoryPermission("bdb", - PermissionType.OWNER))); - assertTrue(p.implies(new RepositoryPermission("cgd", - PermissionType.WRITE))); - } -} diff --git a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java index 2f322ecbe3..ca7c2efdc6 100644 --- a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java @@ -36,37 +36,33 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Throwables; - -import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; - +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.AlreadyExistsException; import sonia.scm.group.Group; -import sonia.scm.group.GroupException; import sonia.scm.group.GroupManager; -import sonia.scm.group.GroupNames; import sonia.scm.user.User; -import sonia.scm.user.UserException; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; import sonia.scm.web.security.PrivilegedAction; -import static org.hamcrest.Matchers.*; +import java.io.IOException; -import static org.junit.Assert.*; - -import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ -import java.io.IOException; - /** * Unit tests for {@link SyncingRealmHelper}. * @@ -109,33 +105,13 @@ public class SyncingRealmHelperTest { helper = new SyncingRealmHelper(ctx, userManager, groupManager); } - /** - * Tests {@link SyncingRealmHelper#createAuthenticationInfo(String, User, String...)}. - */ - @Test - public void testCreateAuthenticationInfo() { - User user = new User("tricia"); - AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test", - user, "heartOfGold"); - - assertNotNull(authInfo); - assertEquals("tricia", authInfo.getPrincipals().getPrimaryPrincipal()); - assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test")); - assertEquals(user, authInfo.getPrincipals().oneByType(User.class)); - - GroupNames groups = authInfo.getPrincipals().oneByType(GroupNames.class); - - assertThat(groups, hasItem("heartOfGold")); - } - /** * Tests {@link SyncingRealmHelper#store(Group)}. * - * @throws GroupException * @throws IOException */ @Test - public void testStoreGroupCreate() throws GroupException, IOException { + public void testStoreGroupCreate() { Group group = new Group("unit-test", "heartOfGold"); helper.store(group); @@ -143,27 +119,21 @@ public class SyncingRealmHelperTest { } /** - * Tests {@link SyncingRealmHelper#store(Group)} with thrown {@link GroupException}. - * - * @throws GroupException - * @throws IOException + * Tests {@link SyncingRealmHelper#store(Group)}. */ - @Test(expected = AuthenticationException.class) - public void testStoreGroupFailure() throws GroupException, IOException { + @Test(expected = IllegalStateException.class) + public void testStoreGroupFailure() { Group group = new Group("unit-test", "heartOfGold"); - doThrow(GroupException.class).when(groupManager).create(group); + doThrow(AlreadyExistsException.class).when(groupManager).create(group); helper.store(group); } /** * Tests {@link SyncingRealmHelper#store(Group)} with an existing group. - * - * @throws GroupException - * @throws IOException */ @Test - public void testStoreGroupModify() throws GroupException, IOException { + public void testStoreGroupModify(){ Group group = new Group("unit-test", "heartOfGold"); when(groupManager.get("heartOfGold")).thenReturn(group); @@ -175,11 +145,10 @@ public class SyncingRealmHelperTest { /** * Tests {@link SyncingRealmHelper#store(User)}. * - * @throws UserException * @throws IOException */ @Test - public void testStoreUserCreate() throws UserException, IOException { + public void testStoreUserCreate() { User user = new User("tricia"); helper.store(user); @@ -187,27 +156,21 @@ public class SyncingRealmHelperTest { } /** - * Tests {@link SyncingRealmHelper#store(User)} with a thrown {@link UserException}. - * - * @throws UserException - * @throws IOException + * Tests {@link SyncingRealmHelper#store(User)} with a thrown {@link AlreadyExistsException}. */ - @Test(expected = AuthenticationException.class) - public void testStoreUserFailure() throws UserException, IOException { + @Test(expected = IllegalStateException.class) + public void testStoreUserFailure() { User user = new User("tricia"); - doThrow(UserException.class).when(userManager).create(user); + doThrow(AlreadyExistsException.class).when(userManager).create(user); helper.store(user); } /** * Tests {@link SyncingRealmHelper#store(User)} with an existing user. - * - * @throws UserException - * @throws IOException */ @Test - public void testStoreUserModify() throws UserException, IOException { + public void testStoreUserModify(){ when(userManager.contains("tricia")).thenReturn(Boolean.TRUE); User user = new User("tricia"); @@ -215,4 +178,16 @@ public class SyncingRealmHelperTest { helper.store(user); verify(userManager, times(1)).modify(user); } + + + @Test + public void builderShouldSetValues() { + User user = new User("ziltoid"); + AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test", user); + + assertNotNull(authInfo); + assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal()); + assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test")); + assertEquals(user, authInfo.getPrincipals().oneByType(User.class)); + } } diff --git a/scm-core/src/test/java/sonia/scm/update/MapBasedPropertyReaderInstanceTest.java b/scm-core/src/test/java/sonia/scm/update/MapBasedPropertyReaderInstanceTest.java new file mode 100644 index 0000000000..f214203866 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/update/MapBasedPropertyReaderInstanceTest.java @@ -0,0 +1,61 @@ +package sonia.scm.update; + +import com.google.common.collect.ImmutableMap; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +class MapBasedPropertyReaderInstanceTest { + + Map executedCalls = new HashMap<>(); + + BiConsumer consumer = (key, properties) -> executedCalls.put(key, properties); + + MapBasedPropertyReaderInstance instance = new MapBasedPropertyReaderInstance( + ImmutableMap.of( + "o1", new V1Properties( + new V1Property("k1", "v1-1"), + new V1Property("k2", "v1-2"), + new V1Property("k3", "v1-3") + ), + "o2", new V1Properties( + new V1Property("k1", "v2-1"), + new V1Property("k2", "v2-2") + ), + "o3", new V1Properties( + new V1Property("k1", "v3-1") + ) + ) + ); + + @Test + void shouldCallBackForEachObjectIfNotFiltered() { + instance.forEachEntry(consumer); + + Assertions.assertThat(executedCalls).hasSize(3); + } + + @Test + void shouldCallBackOnlyObjectsHavingAtLeastOneOfGivenKey() { + instance.havingAnyOf("k2", "k3").forEachEntry(consumer); + + Assertions.assertThat(executedCalls).hasSize(2).containsKeys("o1", "o2"); + } + + @Test + void shouldCallBackOnlyObjectsHavingAllOfGivenKey() { + instance.havingAllOf("k2", "k3").forEachEntry(consumer); + + Assertions.assertThat(executedCalls).hasSize(1).containsKeys("o1"); + } + + @Test + void shouldCombineFilters() { + instance.havingAnyOf("k2", "k3").havingAllOf("k3").forEachEntry(consumer); + + Assertions.assertThat(executedCalls).hasSize(1).containsKeys("o1"); + } +} diff --git a/scm-core/src/test/java/sonia/scm/url/JSONRestModelUrlProviderTest.java b/scm-core/src/test/java/sonia/scm/url/JSONRestModelUrlProviderTest.java deleted file mode 100644 index 778ca9883b..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/JSONRestModelUrlProviderTest.java +++ /dev/null @@ -1,70 +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.url; - -/** - * - * @author Sebastian Sdorra - */ -public class JSONRestModelUrlProviderTest extends RestModelUrlProviderTestBase -{ - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected UrlProvider createUrlProvider(String baseUrl) - { - return UrlProviderFactory.createUrlProvider(baseUrl, - UrlProviderFactory.TYPE_RESTAPI_JSON); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - protected String getExtension() - { - return EXTENSION_JSON; - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/JSONRestRepositoryUrlProviderTest.java b/scm-core/src/test/java/sonia/scm/url/JSONRestRepositoryUrlProviderTest.java deleted file mode 100644 index 68c38d0446..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/JSONRestRepositoryUrlProviderTest.java +++ /dev/null @@ -1,71 +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.url; - -/** - * - * @author Sebastian Sdorra - */ -public class JSONRestRepositoryUrlProviderTest - extends RestRepositoryUrlProviderTestBase -{ - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected RepositoryUrlProvider createRepositoryUrlProvider(String baseUrl) - { - return UrlProviderFactory.createUrlProvider(baseUrl, - UrlProviderFactory.TYPE_RESTAPI_JSON).getRepositoryUrlProvider(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - protected String getExtension() - { - return EXTENSION_JSON; - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/JSONRestUrlProviderTest.java b/scm-core/src/test/java/sonia/scm/url/JSONRestUrlProviderTest.java deleted file mode 100644 index a887efa96c..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/JSONRestUrlProviderTest.java +++ /dev/null @@ -1,70 +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.url; - -/** - * - * @author Sebastian Sdorra - */ -public class JSONRestUrlProviderTest extends RestUrlProviderTestBase -{ - - /** - * Method description - * - * - * - * @param baseUrl - * @return - */ - @Override - protected UrlProvider createUrlProvider(String baseUrl) - { - return UrlProviderFactory.createUrlProvider(baseUrl, - UrlProviderFactory.TYPE_RESTAPI_JSON); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - protected String getExtension() - { - return EXTENSION_JSON; - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/ModelUrlProviderTestBase.java b/scm-core/src/test/java/sonia/scm/url/ModelUrlProviderTestBase.java deleted file mode 100644 index 14d38c1928..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/ModelUrlProviderTestBase.java +++ /dev/null @@ -1,184 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class ModelUrlProviderTestBase extends UrlTestBase -{ - - /** Field description */ - public static final String ITEM = "hitchhiker"; - - /** Field description */ - public static final String MODEL_GROUPS = "groups"; - - /** Field description */ - public static final String MODEL_REPOSITORY = "repositories"; - - /** Field description */ - public static final String MODEL_USERS = "users"; - - /** Field description */ - private static final String[] MODELS = new String[] { MODEL_REPOSITORY, - MODEL_USERS, MODEL_GROUPS }; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param baseUrl - * @return - */ - protected abstract ModelUrlProvider createGroupModelUrlProvider( - String baseUrl); - - /** - * Method description - * - * - * - * @param baseUrl - * @return - */ - protected abstract ModelUrlProvider createRepositoryModelUrlProvider( - String baseUrl); - - /** - * Method description - * - * - * - * @param baseUrl - * @return - */ - protected abstract ModelUrlProvider createUserModelUrlProvider( - String baseUrl); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * @param model - * - * @return - */ - protected abstract String getExpectedAllUrl(String baseUrl, String model); - - /** - * Method description - * - * - * @param baseUrl - * @param model - * @param item - * - * @return - */ - protected abstract String getExpectedDetailUrl(String baseUrl, String model, - String item); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Test - public void testGetAllUrl() - { - for (String model : MODELS) - { - assertEquals(getExpectedAllUrl(BASEURL, model), - createModelUrlProvider(BASEURL, model).getAllUrl()); - } - } - - /** - * Method description - * - */ - @Test - public void testGetDetailUrl() - { - for (String model : MODELS) - { - assertEquals(getExpectedDetailUrl(BASEURL, model, ITEM), - createModelUrlProvider(BASEURL, model).getDetailUrl(ITEM)); - } - } - - /** - * Method description - * - * - * - * @param baseUrl - * @param model - * - * @return - */ - private ModelUrlProvider createModelUrlProvider(String baseUrl, String model) - { - ModelUrlProvider urlProvider = null; - - if (MODEL_REPOSITORY.equals(model)) - { - urlProvider = createRepositoryModelUrlProvider(baseUrl); - } - else if (MODEL_USERS.equals(model)) - { - urlProvider = createUserModelUrlProvider(baseUrl); - } - else if (MODEL_GROUPS.equals(model)) - { - urlProvider = createGroupModelUrlProvider(baseUrl); - } - - return urlProvider; - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/RepositoryUrlProviderTestBase.java b/scm-core/src/test/java/sonia/scm/url/RepositoryUrlProviderTestBase.java deleted file mode 100644 index 0c2e36dfd3..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/RepositoryUrlProviderTestBase.java +++ /dev/null @@ -1,265 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class RepositoryUrlProviderTestBase extends UrlTestBase -{ - - /** Field description */ - private static final String NAME = "scm/main"; - - /** Field description */ - private static final String PATH = "scm-webapp/pom.xml"; - - /** Field description */ - private static final String REPOSITORY_ID = - "E3882BE7-7D0D-421B-B178-B2AA9E897135"; - - /** Field description */ - private static final String REVISION = "b282fb2dd12a"; - - /** Field description */ - private static final String TYPE = "hg"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - protected abstract RepositoryUrlProvider createRepositoryUrlProvider( - String baseUrl); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - protected abstract String getExpectedBlameUrl(String baseUrl, - String repositoryId, String path, String revision); - - /** - * Method description - * - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - protected abstract String getExpectedBrowseUrl(String baseUrl, - String repositoryId, String path, String revision); - - /** - * Method description - * - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * @param start - * @param limit - * - * @return - */ - protected abstract String getExpectedChangesetUrl(String baseUrl, - String repositoryId, String path, String revision, int start, - int limit); - - /** - * Method description - * - * - * - * @param baseUrl - * @param repositoryId - * @param start - * @param limit - * - * @return - */ - protected abstract String getExpectedChangesetUrl(String baseUrl, - String repositoryId, int start, int limit); - - /** - * Method description - * - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - protected abstract String getExpectedContentUrl(String baseUrl, - String repositoryId, String path, String revision); - - /** - * Method description - * - * - * - * @param baseUrl - * @param type - * @param name - * - * @return - * @since 1.11 - */ - protected abstract String getExpectedDetailUrl(String baseUrl, String type, - String name); - - /** - * Method description - * - * - * - * @param baseUrl - * @param repositoryId - * @param revision - * - * @return - */ - protected abstract String getExpectedDiffUrl(String baseUrl, - String repositoryId, String revision); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Test - public void testGetBlameUrl() - { - assertEquals( - getExpectedBlameUrl(BASEURL, REPOSITORY_ID, PATH, REVISION), - createRepositoryUrlProvider(BASEURL).getBlameUrl( - REPOSITORY_ID, PATH, REVISION)); - } - - /** - * Method description - * - */ - @Test - public void testGetBrowserUrl() - { - assertEquals( - getExpectedBrowseUrl(BASEURL, REPOSITORY_ID, PATH, REVISION), - createRepositoryUrlProvider(BASEURL).getBrowseUrl( - REPOSITORY_ID, PATH, REVISION)); - } - - /** - * Method description - * - */ - @Test - public void testGetChangesetUrl() - { - assertEquals( - getExpectedChangesetUrl(BASEURL, REPOSITORY_ID, PATH, REVISION, 0, 20), - createRepositoryUrlProvider(BASEURL).getChangesetUrl( - REPOSITORY_ID, PATH, REVISION, 0, 20)); - assertEquals( - getExpectedChangesetUrl(BASEURL, REPOSITORY_ID, 0, 20), - createRepositoryUrlProvider(BASEURL).getChangesetUrl( - REPOSITORY_ID, 0, 20)); - } - - /** - * Method description - * - */ - @Test - public void testGetContentUrl() - { - assertEquals( - getExpectedContentUrl(BASEURL, REPOSITORY_ID, PATH, REVISION), - createRepositoryUrlProvider(BASEURL).getContentUrl( - REPOSITORY_ID, PATH, REVISION)); - } - - /** - * Method description - * - */ - @Test - public void testGetDetailUrl() - { - assertEquals(getExpectedDetailUrl(BASEURL, TYPE, NAME), - createRepositoryUrlProvider(BASEURL).getDetailUrl(TYPE, NAME)); - } - - /** - * Method description - * - */ - @Test - public void testGetDiffUrl() - { - assertEquals(getExpectedDiffUrl(BASEURL, REPOSITORY_ID, REVISION), - createRepositoryUrlProvider(BASEURL).getDiffUrl(REPOSITORY_ID, - REVISION)); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/RestModelUrlProviderTestBase.java b/scm-core/src/test/java/sonia/scm/url/RestModelUrlProviderTestBase.java deleted file mode 100644 index 2e7f25818f..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/RestModelUrlProviderTestBase.java +++ /dev/null @@ -1,159 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class RestModelUrlProviderTestBase - extends ModelUrlProviderTestBase -{ - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - protected abstract UrlProvider createUrlProvider(String baseUrl); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - protected abstract String getExtension(); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected ModelUrlProvider createGroupModelUrlProvider(String baseUrl) - { - return createUrlProvider(baseUrl).getGroupUrlProvider(); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected ModelUrlProvider createRepositoryModelUrlProvider(String baseUrl) - { - return createUrlProvider(baseUrl).getRepositoryUrlProvider(); - } - - /** - * Method description - * - * - * @param baseUrl - * @param urlPart - * - * @return - */ - protected String createRestUrl(String baseUrl, String urlPart) - { - return createRestUrl(baseUrl, urlPart, getExtension()); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected ModelUrlProvider createUserModelUrlProvider(String baseUrl) - { - return createUrlProvider(baseUrl).getUserUrlProvider(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * @param model - * - * @return - */ - @Override - protected String getExpectedAllUrl(String baseUrl, String model) - { - return createRestUrl(baseUrl, model); - } - - /** - * Method description - * - * - * @param baseUrl - * @param model - * @param item - * - * @return - */ - @Override - protected String getExpectedDetailUrl(String baseUrl, String model, - String item) - { - return createRestUrl(baseUrl, - model.concat(HttpUtil.SEPARATOR_PATH).concat(item)); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/RestRepositoryUrlProviderTestBase.java b/scm-core/src/test/java/sonia/scm/url/RestRepositoryUrlProviderTestBase.java deleted file mode 100644 index 31783f7eea..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/RestRepositoryUrlProviderTestBase.java +++ /dev/null @@ -1,230 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class RestRepositoryUrlProviderTestBase - extends RepositoryUrlProviderTestBase -{ - - /** Field description */ - public static final String URLPART_PREFIX = "repositories"; - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - protected abstract String getExtension(); - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - protected String getExpectedBlameUrl(String baseUrl, String repositoryId, - String path, String revision) - { - return createRestUrl( - baseUrl, - repositoryId.concat(HttpUtil.SEPARATOR_PATH).concat("blame")).concat( - "?path=").concat(path).concat("&revision=").concat(revision); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - protected String getExpectedBrowseUrl(String baseUrl, String repositoryId, - String path, String revision) - { - return createRestUrl( - baseUrl, - repositoryId.concat(HttpUtil.SEPARATOR_PATH).concat("browse")).concat( - "?path=").concat(path).concat("&revision=").concat(revision); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * @param start - * @param limit - * - * @return - */ - @Override - protected String getExpectedChangesetUrl(String baseUrl, String repositoryId, - String path, String revision, int start, int limit) - { - return createRestUrl( - baseUrl, - repositoryId.concat(HttpUtil.SEPARATOR_PATH).concat( - "changesets")).concat("?path=").concat(path).concat( - "&revision=").concat(revision).concat("&start=").concat( - String.valueOf(start)).concat("&limit=").concat( - String.valueOf(limit)); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param start - * @param limit - * - * @return - */ - @Override - protected String getExpectedChangesetUrl(String baseUrl, String repositoryId, - int start, int limit) - { - return createRestUrl( - baseUrl, - repositoryId.concat(HttpUtil.SEPARATOR_PATH).concat( - "changesets")).concat("?start=").concat(String.valueOf(start)).concat( - "&limit=").concat(String.valueOf(limit)); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - protected String getExpectedContentUrl(String baseUrl, String repositoryId, - String path, String revision) - { - return createRestUrl( - baseUrl, - "repositories".concat(HttpUtil.SEPARATOR_PATH).concat( - repositoryId).concat(HttpUtil.SEPARATOR_PATH).concat( - "content"), "").concat("?path=").concat(path).concat( - "&revision=").concat(revision); - } - - /** - * Method description - * - * - * @param baseUrl - * @param type - * @param name - * - * @return - */ - @Override - protected String getExpectedDetailUrl(String baseUrl, String type, - String name) - { - return createRestUrl(baseUrl, - type.concat(HttpUtil.SEPARATOR_PATH).concat(name)); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param revision - * - * @return - */ - @Override - protected String getExpectedDiffUrl(String baseUrl, String repositoryId, - String revision) - { - return createRestUrl( - baseUrl, - "repositories".concat(HttpUtil.SEPARATOR_PATH).concat( - repositoryId).concat(HttpUtil.SEPARATOR_PATH).concat( - "diff"), "").concat("?revision=").concat(revision); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * @param urlPart - * - * @return - */ - private String createRestUrl(String baseUrl, String urlPart) - { - return createRestUrl( - baseUrl, - URLPART_PREFIX.concat(HttpUtil.SEPARATOR_PATH).concat(urlPart), - getExtension()); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/RestUrlProviderTestBase.java b/scm-core/src/test/java/sonia/scm/url/RestUrlProviderTestBase.java deleted file mode 100644 index 3b39e22816..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/RestUrlProviderTestBase.java +++ /dev/null @@ -1,109 +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.url; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class RestUrlProviderTestBase extends UrlProviderTestBase -{ - - /** - * Method description - * - * - * @return - */ - protected abstract String getExtension(); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * @param urlPart - * - * @return - */ - protected String createRestUrl(String baseUrl, String urlPart) - { - return createRestUrl(baseUrl, urlPart, getExtension()); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected String getExpectedAuthenticationUrl(String baseUrl) - { - return createRestUrl(baseUrl, "auth/access_token"); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected String getExpectedConfigUrl(String baseUrl) - { - return createRestUrl(baseUrl, "config"); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected String getExpectedStateUrl(String baseUrl) - { - return createRestUrl(baseUrl, "auth"); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/UrlProviderTestBase.java b/scm-core/src/test/java/sonia/scm/url/UrlProviderTestBase.java deleted file mode 100644 index 61c35b7b64..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/UrlProviderTestBase.java +++ /dev/null @@ -1,156 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class UrlProviderTestBase extends UrlTestBase -{ - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param baseUrl - * @return - */ - protected abstract UrlProvider createUrlProvider(String baseUrl); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - protected abstract String getExpectedAuthenticationUrl(String baseUrl); - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - protected abstract String getExpectedConfigUrl(String baseUrl); - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - protected abstract String getExpectedStateUrl(String baseUrl); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Test - public void testGetAuthenticationUrl() - { - assertEquals(getExpectedAuthenticationUrl(BASEURL), - createUrlProvider(BASEURL).getAuthenticationUrl()); - } - - /** - * Method description - * - */ - @Test - public void testGetConfigUrl() - { - assertEquals(getExpectedConfigUrl(BASEURL), - createUrlProvider(BASEURL).getConfigUrl()); - } - - /** - * Method description - * - */ - @Test - public void testGetGroupUrlProvider() - { - assertNotNull(createUrlProvider(BASEURL).getGroupUrlProvider()); - } - - /** - * Method description - * - */ - @Test - public void testGetStateUrl() - { - assertEquals(getExpectedStateUrl(BASEURL), - createUrlProvider(BASEURL).getStateUrl()); - } - - /** - * Method description - * - */ - @Test - public void testGetUserRepositoryUrlProvider() - { - assertNotNull(createUrlProvider(BASEURL).getRepositoryUrlProvider()); - } - - /** - * Method description - * - */ - @Test - public void testGetUserUrlProvider() - { - assertNotNull(createUrlProvider(BASEURL).getUserUrlProvider()); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/UrlTestBase.java b/scm-core/src/test/java/sonia/scm/url/UrlTestBase.java deleted file mode 100644 index 28202cb420..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/UrlTestBase.java +++ /dev/null @@ -1,133 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * - * @author Sebastian Sdorra - */ -public abstract class UrlTestBase -{ - - /** Field description */ - public static final String EXTENSION_JSON = ".json"; - - /** Field description */ - public static final String EXTENSION_XML = ".xml"; - - /** Field description */ - public static final String URLSUFFIX_INDEX = "/index.html"; - - /** Field description */ - public static final String URLSUFFIX_RESTAPI = "/api/rest/"; - - /** Field description */ - protected static final String BASEURL = "http://scm.scm-manager.org/scm"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - protected String createBaseRestUrl(String baseUrl) - { - return baseUrl.concat(URLSUFFIX_RESTAPI); - } - - /** - * Method description - * - * - * @param baseUrl - * @param urlPart - * @param extension - * - * @return - */ - protected String createRestUrl(String baseUrl, String urlPart, - String extension) - { - return createBaseRestUrl(baseUrl).concat(urlPart).concat(extension); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - protected String createWuiUrl(String baseUrl) - { - return baseUrl.concat(URLSUFFIX_INDEX); - } - - /** - * Method description - * - * - * @param baseUrl - * @param param - * - * @return - */ - protected String createWuiUrl(String baseUrl, String param) - { - return baseUrl.concat(URLSUFFIX_INDEX).concat( - HttpUtil.SEPARATOR_HASH).concat(param); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - protected UrlProvider createWuiUrlProvider(String baseUrl) - { - return UrlProviderFactory.createUrlProvider(baseUrl, - UrlProviderFactory.TYPE_WUI); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/UrlUtilTest.java b/scm-core/src/test/java/sonia/scm/url/UrlUtilTest.java deleted file mode 100644 index 255d7912df..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/UrlUtilTest.java +++ /dev/null @@ -1,60 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * - * @author Sebastian Sdorra - */ -public class UrlUtilTest -{ - - /** - * Method description - * - */ - @Test - public void testFixRevision() - { - assertEquals("42694c4a4a7a", UrlUtil.fixRevision("42694c4a4a7a")); - assertEquals("42694c4a4a7a", UrlUtil.fixRevision("298:42694c4a4a7a")); - assertNull(UrlUtil.fixRevision(null)); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/WUIModelUrlProviderTest.java b/scm-core/src/test/java/sonia/scm/url/WUIModelUrlProviderTest.java deleted file mode 100644 index 24b2d6d11f..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/WUIModelUrlProviderTest.java +++ /dev/null @@ -1,158 +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.url; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -public class WUIModelUrlProviderTest extends ModelUrlProviderTestBase -{ - - /** - * Constructs ... - * - */ - public WUIModelUrlProviderTest() - { - modelMap = new HashMap<>(); - modelMap.put(MODEL_REPOSITORY, "repositoryPanel"); - modelMap.put(MODEL_USERS, "userPanel"); - modelMap.put(MODEL_GROUPS, "groupPanel"); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected ModelUrlProvider createGroupModelUrlProvider(String baseUrl) - { - return createWuiUrlProvider(baseUrl).getGroupUrlProvider(); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected ModelUrlProvider createRepositoryModelUrlProvider(String baseUrl) - { - return createWuiUrlProvider(baseUrl).getRepositoryUrlProvider(); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected ModelUrlProvider createUserModelUrlProvider(String baseUrl) - { - return createWuiUrlProvider(baseUrl).getUserUrlProvider(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * @param model - * - * @return - */ - @Override - protected String getExpectedAllUrl(String baseUrl, String model) - { - return createModelBaseUrl(baseUrl, model); - } - - /** - * Method description - * - * - * @param baseUrl - * @param model - * @param item - * - * @return - */ - @Override - protected String getExpectedDetailUrl(String baseUrl, String model, - String item) - { - return createModelBaseUrl(baseUrl, model).concat( - WUIUrlBuilder.SEPARATOR).concat(item); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * @param model - * - * @return - */ - private String createModelBaseUrl(String baseUrl, String model) - { - return createWuiUrl(baseUrl, modelMap.get(model)); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Map modelMap; -} diff --git a/scm-core/src/test/java/sonia/scm/url/WUIRepositoryUrlProviderTest.java b/scm-core/src/test/java/sonia/scm/url/WUIRepositoryUrlProviderTest.java deleted file mode 100644 index 91a1a8fb8a..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/WUIRepositoryUrlProviderTest.java +++ /dev/null @@ -1,218 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.util.HttpUtil; - -/** - * - * @author Sebastian Sdorra - */ -public class WUIRepositoryUrlProviderTest extends RepositoryUrlProviderTestBase -{ - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected RepositoryUrlProvider createRepositoryUrlProvider(String baseUrl) - { - return UrlProviderFactory.createUrlProvider(baseUrl, - UrlProviderFactory.TYPE_WUI).getRepositoryUrlProvider(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - protected String getExpectedBlameUrl(String baseUrl, String repositoryId, - String path, String revision) - { - return createRepositoryWuiUrl(baseUrl, "contentPanel", repositoryId).concat( - ";").concat(revision).concat(";").concat(path).concat(";blame"); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - protected String getExpectedBrowseUrl(String baseUrl, String repositoryId, - String path, String revision) - { - return createRepositoryWuiUrl( - baseUrl, "repositoryBrowser", repositoryId).concat(";").concat( - revision).concat(";").concat(path); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * @param start - * @param limit - * - * @return - */ - @Override - protected String getExpectedChangesetUrl(String baseUrl, String repositoryId, - String path, String revision, int start, int limit) - { - return createRepositoryWuiUrl(baseUrl, "contentPanel", repositoryId).concat( - ";").concat(revision).concat(";").concat(path).concat(";history"); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param start - * @param limit - * - * @return - */ - @Override - protected String getExpectedChangesetUrl(String baseUrl, String repositoryId, - int start, int limit) - { - return createRepositoryWuiUrl( - baseUrl, "repositoryChangesetViewerPanel", repositoryId).concat( - ";").concat(String.valueOf(start)).concat(";").concat( - String.valueOf(limit)); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param path - * @param revision - * - * @return - */ - @Override - protected String getExpectedContentUrl(String baseUrl, String repositoryId, - String path, String revision) - { - return createRepositoryWuiUrl(baseUrl, "contentPanel", repositoryId).concat( - ";").concat(revision).concat(";").concat(path).concat(";history"); - } - - /** - * Method description - * - * - * @param baseUrl - * @param type - * @param name - * - * @return - */ - @Override - protected String getExpectedDetailUrl(String baseUrl, String type, - String name) - { - return createRepositoryWuiUrl( - baseUrl, "repositoryPanel", - type.concat(HttpUtil.SEPARATOR_PATH).concat(name)); - } - - /** - * Method description - * - * - * @param baseUrl - * @param repositoryId - * @param revision - * - * @return - */ - @Override - protected String getExpectedDiffUrl(String baseUrl, String repositoryId, - String revision) - { - return createRepositoryWuiUrl(baseUrl, "diffPanel", - repositoryId).concat(";").concat(revision); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * @param component - * @param repository - * - * @return - */ - private String createRepositoryWuiUrl(String baseUrl, String component, - String repository) - { - return createWuiUrl(baseUrl, component).concat( - WUIUrlBuilder.SEPARATOR).concat(repository); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/WUIUrlBuilderTest.java b/scm-core/src/test/java/sonia/scm/url/WUIUrlBuilderTest.java deleted file mode 100644 index 4fdefc5fb1..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/WUIUrlBuilderTest.java +++ /dev/null @@ -1,114 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Test; - -import sonia.scm.util.HttpUtil; - -import static org.junit.Assert.*; - -/** - * - * @author Sebastian Sdorra - */ -public class WUIUrlBuilderTest -{ - - /** Field description */ - private static final String BASEURL = - "http://scm.scm-manager.org/scm/index.html"; - - /** Field description */ - private static final String COMPONENT = "testCmp"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Test - public void testStringAppend() - { - WUIUrlBuilder builder = createBuilder(); - - builder.append("testParam"); - assertEquals(createBaseWuiUrl().concat(";testParam"), builder.toString()); - builder = createBuilder(); - builder.append("param1").append("param2").append("param3"); - assertEquals(createBaseWuiUrl().concat(";param1;param2;param3"), - builder.toString()); - } - - /** - * Method description - * - */ - @Test - public void testIntAppend() - { - WUIUrlBuilder builder = createBuilder(); - - builder.append(3); - assertEquals(createBaseWuiUrl().concat(";3"), builder.toString()); - builder = createBuilder(); - builder.append(1).append(2).append(3); - assertEquals(createBaseWuiUrl().concat(";1;2;3"), - builder.toString()); - } - - /** - * Method description - * - * - * @return - */ - private String createBaseWuiUrl() - { - return BASEURL.concat(HttpUtil.SEPARATOR_HASH).concat(COMPONENT); - } - - /** - * Method description - * - * - * @return - */ - private WUIUrlBuilder createBuilder() - { - return new WUIUrlBuilder(BASEURL, COMPONENT); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/WUIUrlProviderTest.java b/scm-core/src/test/java/sonia/scm/url/WUIUrlProviderTest.java deleted file mode 100644 index 19ec89835a..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/WUIUrlProviderTest.java +++ /dev/null @@ -1,102 +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.url; - -//~--- non-JDK imports -------------------------------------------------------- - -/** - * - * @author Sebastian Sdorra - */ -public class WUIUrlProviderTest extends UrlProviderTestBase -{ - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected UrlProvider createUrlProvider(String baseUrl) - { - return UrlProviderFactory.createUrlProvider(baseUrl, - UrlProviderFactory.TYPE_WUI); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected String getExpectedAuthenticationUrl(String baseUrl) - { - return createWuiUrl(baseUrl); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected String getExpectedConfigUrl(String baseUrl) - { - return createWuiUrl(baseUrl, "scmConfig"); - } - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected String getExpectedStateUrl(String baseUrl) - { - return createWuiUrl(baseUrl); - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/XMLRestModelUrlProviderTest.java b/scm-core/src/test/java/sonia/scm/url/XMLRestModelUrlProviderTest.java deleted file mode 100644 index 7017e0551a..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/XMLRestModelUrlProviderTest.java +++ /dev/null @@ -1,70 +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.url; - -/** - * - * @author Sebastian Sdorra - */ -public class XMLRestModelUrlProviderTest extends RestModelUrlProviderTestBase -{ - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected UrlProvider createUrlProvider(String baseUrl) - { - return UrlProviderFactory.createUrlProvider(baseUrl, - UrlProviderFactory.TYPE_RESTAPI_XML); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - protected String getExtension() - { - return EXTENSION_XML; - } -} diff --git a/scm-core/src/test/java/sonia/scm/url/XMLRestUrlProviderTest.java b/scm-core/src/test/java/sonia/scm/url/XMLRestUrlProviderTest.java deleted file mode 100644 index 33e19aa258..0000000000 --- a/scm-core/src/test/java/sonia/scm/url/XMLRestUrlProviderTest.java +++ /dev/null @@ -1,70 +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.url; - -/** - * - * @author Sebastian Sdorra - */ -public class XMLRestUrlProviderTest extends RestUrlProviderTestBase -{ - - /** - * Method description - * - * - * @param baseUrl - * - * @return - */ - @Override - protected UrlProvider createUrlProvider(String baseUrl) - { - return UrlProviderFactory.createUrlProvider(baseUrl, - UrlProviderFactory.TYPE_RESTAPI_XML); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - protected String getExtension() - { - return EXTENSION_XML; - } -} diff --git a/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java b/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java new file mode 100644 index 0000000000..50ae2254e6 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java @@ -0,0 +1,57 @@ +package sonia.scm.util; + +import org.junit.jupiter.api.Test; + +import java.util.Comparator; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ComparablesTest { + + @Test + void shouldCompare() { + One a = new One("a"); + One b = new One("b"); + + Comparator comparable = Comparables.comparator(One.class, "value"); + assertThat(comparable.compare(a, b)).isEqualTo(-1); + } + + @Test + void shouldThrowAnExceptionForNonExistingField() { + assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "awesome")); + } + + @Test + void shouldThrowAnExceptionForNonComparableField() { + assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "nonComparable")); + } + + @Test + void shouldThrowAnExceptionIfTheFieldHasNoGetter() { + assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "incredible")); + } + + private static class One { + + private String value; + private String incredible; + private NonComparable nonComparable; + + One(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public NonComparable getNonComparable() { + return nonComparable; + } + } + + private static class NonComparable {} + +} diff --git a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java index b85221f8c8..5e6fa3e10e 100644 --- a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java @@ -54,6 +54,18 @@ import javax.servlet.http.HttpServletRequest; public class HttpUtilTest { + @Test + public void concatenateTest() { + assertEquals( + "/scm/git/hitchhiker/tricia", + HttpUtil.concatenate("/scm", "git", "hitchhiker", "tricia") + ); + assertEquals( + "scm/git/hitchhiker/tricia", + HttpUtil.concatenate("scm", "git", "hitchhiker", "tricia") + ); + } + /** * Method description * @@ -63,19 +75,19 @@ public class HttpUtilTest { //J- assertEquals( - "http://www.scm-manager/scm/test", + "http://www.scm-manager/scm/test", HttpUtil.append("http://www.scm-manager/scm/", "test") ); assertEquals( - "http://www.scm-manager/scm/test", + "http://www.scm-manager/scm/test", HttpUtil.append("http://www.scm-manager/scm", "test") ); assertEquals( - "http://www.scm-manager/scm/test", + "http://www.scm-manager/scm/test", HttpUtil.append("http://www.scm-manager/scm", "/test") ); assertEquals( - "http://www.scm-manager/scm/test", + "http://www.scm-manager/scm/test", HttpUtil.append("http://www.scm-manager/scm/", "/test") ); //J+ diff --git a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java index 3eaddf2d36..51ff906b8f 100644 --- a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java @@ -110,10 +110,10 @@ public class ValidationUtilTest assertTrue(ValidationUtil.isNameValid("Test123-git")); assertTrue(ValidationUtil.isNameValid("Test_user-123.git")); assertTrue(ValidationUtil.isNameValid("test@scm-manager.de")); - assertTrue(ValidationUtil.isNameValid("test 123")); assertTrue(ValidationUtil.isNameValid("t")); // false + assertFalse(ValidationUtil.isNameValid("test 123")); assertFalse(ValidationUtil.isNameValid(" test 123")); assertFalse(ValidationUtil.isNameValid(" test 123 ")); assertFalse(ValidationUtil.isNameValid("test 123 ")); @@ -143,51 +143,25 @@ public class ValidationUtilTest assertFalse(ValidationUtil.isNotContaining("test", "t")); } - /** - * Method description - * - */ @Test - public void testIsRepositoryNameValid() - { - assertTrue(ValidationUtil.isRepositoryNameValid("scm")); - assertTrue(ValidationUtil.isRepositoryNameValid("scm/main")); - assertTrue(ValidationUtil.isRepositoryNameValid("scm/plugins/git-plugin")); - assertTrue(ValidationUtil.isRepositoryNameValid("s")); - assertTrue(ValidationUtil.isRepositoryNameValid("sc")); - assertTrue(ValidationUtil.isRepositoryNameValid(".scm/plugins")); - - // issue 142 - assertFalse(ValidationUtil.isRepositoryNameValid(".")); - assertFalse(ValidationUtil.isRepositoryNameValid("/")); - assertFalse(ValidationUtil.isRepositoryNameValid("scm/plugins/.")); - assertFalse(ValidationUtil.isRepositoryNameValid("scm/../plugins")); - assertFalse(ValidationUtil.isRepositoryNameValid("scm/main/")); - assertFalse(ValidationUtil.isRepositoryNameValid("/scm/main/")); - - // issue 144 - assertFalse(ValidationUtil.isRepositoryNameValid("scm/./main")); - assertFalse(ValidationUtil.isRepositoryNameValid("scm//main")); - - // issue 148 - //J- + public void testIsRepositoryNameValid() { String[] validPaths = { "scm", - "scm/main", - "scm/plugins/git-plugin", + "scm-", + "scm_", + "s_cm", + "s-cm", "s", "sc", - ".scm/plugins", ".hiddenrepo", "b.", "...", "..c", "d..", - "a/b..", - "a/..b", - "a..c", + "a..c" }; - + + // issue 142, 144 and 148 String[] invalidPaths = { ".", "/", @@ -228,17 +202,23 @@ public class ValidationUtilTest "abc)abc", "abc[abc", "abc]abc", - "abc|abc" + "abc|abc", + "scm/main", + "scm/plugins/git-plugin", + ".scm/plugins", + "a/b..", + "a/..b", + "scm/main", + "scm/plugins/git-plugin", + "_scm", + "-scm" }; - //J+ - for (String path : validPaths) - { + for (String path : validPaths) { assertTrue(ValidationUtil.isRepositoryNameValid(path)); } - for (String path : invalidPaths) - { + for (String path : invalidPaths) { assertFalse(ValidationUtil.isRepositoryNameValid(path)); } } diff --git a/scm-core/src/test/java/sonia/scm/util/ValidationUtil_IllegalCharactersTest.java b/scm-core/src/test/java/sonia/scm/util/ValidationUtil_IllegalCharactersTest.java new file mode 100644 index 0000000000..5c6431ea3a --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/util/ValidationUtil_IllegalCharactersTest.java @@ -0,0 +1,47 @@ +package sonia.scm.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertFalse; +import static sonia.scm.util.ValidationUtil.REGEX_NAME; + +@RunWith(Parameterized.class) +public class ValidationUtil_IllegalCharactersTest { + + private static final List ACCEPTED_CHARS = asList('@', '_', '-', '.'); + + private final Pattern userGroupPattern=Pattern.compile(REGEX_NAME); + + private final String expression; + + public ValidationUtil_IllegalCharactersTest(String expression) { + this.expression = expression; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection createParameters() { + return Stream.concat(IntStream.range(0x20, 0x2f).mapToObj(i -> (char) i), // chars before '0' + Stream.concat(IntStream.range(0x3a, 0x40).mapToObj(i -> (char) i), // chars between '9' and 'A' + Stream.concat(IntStream.range(0x5b, 0x60).mapToObj(i -> (char) i), // chars between 'Z' and 'a' + IntStream.range(0x7b, 0xff).mapToObj(i -> (char) i)))) // chars after 'z' + .filter(c -> !ACCEPTED_CHARS.contains(c)) + .flatMap(c -> Stream.of("abc" + c + "xyz", "@" + c, c + "tail")) + .map(c -> new String[] {c}) + .collect(Collectors.toList()); + } + + @Test + public void shouldNotAcceptSpecialCharacters() { + assertFalse(userGroupPattern.matcher(expression).matches()); + } +} diff --git a/scm-core/src/test/java/sonia/scm/web/AbstractRepositoryJsonEnricherTest.java b/scm-core/src/test/java/sonia/scm/web/AbstractRepositoryJsonEnricherTest.java new file mode 100644 index 0000000000..2c8ef76464 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/web/AbstractRepositoryJsonEnricherTest.java @@ -0,0 +1,107 @@ +package sonia.scm.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Resources; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.api.v2.resources.ScmPathInfoStore; + +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.net.URI; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class AbstractRepositoryJsonEnricherTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private AbstractRepositoryJsonEnricher linkEnricher; + private JsonNode rootNode; + + @BeforeEach + void globalSetUp() { + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + + linkEnricher = new AbstractRepositoryJsonEnricher(objectMapper) { + @Override + protected void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name) { + addLink(repositoryNode, "new-link", "/somewhere"); + } + }; + } + + @Test + void shouldEnrichRepositories() throws IOException { + URL resource = Resources.getResource("sonia/scm/repository/repository-001.json"); + rootNode = objectMapper.readTree(resource); + + JsonEnricherContext context = new JsonEnricherContext( + URI.create("/"), + MediaType.valueOf(VndMediaType.REPOSITORY), + rootNode + ); + + linkEnricher.enrich(context); + + String configLink = context.getResponseEntity() + .get("_links") + .get("new-link") + .get("href") + .asText(); + + assertThat(configLink).isEqualTo("/somewhere"); + } + + @Test + void shouldEnrichAllRepositories() throws IOException { + URL resource = Resources.getResource("sonia/scm/repository/repository-collection-001.json"); + rootNode = objectMapper.readTree(resource); + + JsonEnricherContext context = new JsonEnricherContext( + URI.create("/"), + MediaType.valueOf(VndMediaType.REPOSITORY_COLLECTION), + rootNode + ); + + linkEnricher.enrich(context); + + context.getResponseEntity() + .get("_embedded") + .withArray("repositories") + .elements() + .forEachRemaining(node -> { + String configLink = node + .get("_links") + .get("new-link") + .get("href") + .asText(); + + assertThat(configLink).isEqualTo("/somewhere"); + }); + } + + @Test + void shouldNotModifyObjectsWithUnsupportedMediaType() throws IOException { + URL resource = Resources.getResource("sonia/scm/repository/repository-001.json"); + rootNode = objectMapper.readTree(resource); + JsonEnricherContext context = new JsonEnricherContext( + URI.create("/"), + MediaType.valueOf(VndMediaType.USER), + rootNode + ); + + linkEnricher.enrich(context); + + boolean hasNewPullRequestLink = context.getResponseEntity() + .get("_links") + .has("new-link"); + + assertThat(hasNewPullRequestLink).isFalse(); + } +} diff --git a/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java new file mode 100644 index 0000000000..2f53ae8102 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java @@ -0,0 +1,59 @@ +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 testResultHasMediaTypeWithCamelCaseMediaType() { + String mediaType = "application/hitchhikersGuideToTheGalaxy"; + JsonEnricherContext context = new JsonEnricherContext(null, MediaType.valueOf(mediaType), null); + + assertThat(enricher.resultHasMediaType(mediaType, context)).isTrue(); + } + + @Test + public void testAppendLink() { + ObjectNode root = objectMapper.createObjectNode(); + ObjectNode links = objectMapper.createObjectNode(); + root.set("_links", links); + JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, root); + enricher.enrich(context); + + assertThat(links.get("awesome").get("href").asText()).isEqualTo("/my/awesome/link"); + } + + private static class TestJsonEnricher extends JsonEnricherBase { + + public TestJsonEnricher(ObjectMapper objectMapper) { + super(objectMapper); + } + + @Override + public void enrich(JsonEnricherContext context) { + JsonNode gitConfigRefNode = createObject(singletonMap("href", value("/my/awesome/link"))); + + addPropertyNode(context.getResponseEntity().get("_links"), "awesome", gitConfigRefNode); + } + } + +} diff --git a/scm-core/src/test/java/sonia/scm/web/UserAgentParserTest.java b/scm-core/src/test/java/sonia/scm/web/UserAgentParserTest.java index d3f917932c..eb1de0f499 100644 --- a/scm-core/src/test/java/sonia/scm/web/UserAgentParserTest.java +++ b/scm-core/src/test/java/sonia/scm/web/UserAgentParserTest.java @@ -43,7 +43,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; diff --git a/scm-core/src/test/java/sonia/scm/web/filter/AuthenticationFilterTest.java b/scm-core/src/test/java/sonia/scm/web/filter/AuthenticationFilterTest.java index 052f6e1417..30b42fda21 100644 --- a/scm-core/src/test/java/sonia/scm/web/filter/AuthenticationFilterTest.java +++ b/scm-core/src/test/java/sonia/scm/web/filter/AuthenticationFilterTest.java @@ -37,33 +37,28 @@ package sonia.scm.web.filter; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; - import com.google.common.collect.ImmutableSet; - import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; - import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; - +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.config.ScmConfiguration; import sonia.scm.web.WebTokenGenerator; -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.verify; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -109,26 +104,6 @@ public class AuthenticationFilterTest "Authorization Required"); } - /** - * Method description - * - * - * @throws IOException - * @throws ServletException - */ - @Test - public void testDoFilterWithAnonymousAccess() - throws IOException, ServletException - { - configuration.setAnonymousAccessEnabled(true); - - AuthenticationFilter filter = createAuthenticationFilter(); - - filter.doFilter(request, response, chain); - verify(chain).doFilter(any(HttpServletRequest.class), - any(HttpServletResponse.class)); - } - /** * Method description * diff --git a/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java new file mode 100644 index 0000000000..1f9b4fad07 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java @@ -0,0 +1,72 @@ +package sonia.scm.web.filter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.util.HttpUtil; +import sonia.scm.web.UserAgent; +import sonia.scm.web.UserAgentParser; +import sonia.scm.web.WebTokenGenerator; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class HttpProtocolServletAuthenticationFilterBaseTest { + + private ScmConfiguration configuration = new ScmConfiguration(); + + private Set tokenGenerators = Collections.emptySet(); + + @Mock + private UserAgentParser userAgentParser; + + private HttpProtocolServletAuthenticationFilterBase authenticationFilter; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private FilterChain filterChain; + + private UserAgent nonBrowser = UserAgent.builder("i'm not a browser").browser(false).build(); + private UserAgent browser = UserAgent.builder("i am a browser").browser(true).build(); + + @BeforeEach + void setUpObjectUnderTest() { + authenticationFilter = new HttpProtocolServletAuthenticationFilterBase(configuration, tokenGenerators, userAgentParser); + } + + @Test + void shouldSendUnauthorized() throws IOException, ServletException { + when(userAgentParser.parse(request)).thenReturn(nonBrowser); + + authenticationFilter.handleUnauthorized(request, response, filterChain); + + verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, HttpUtil.STATUS_UNAUTHORIZED_MESSAGE); + } + + @Test + void shouldCallFilterChain() throws IOException, ServletException { + when(userAgentParser.parse(request)).thenReturn(browser); + + authenticationFilter.handleUnauthorized(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java b/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java new file mode 100644 index 0000000000..9fa65d51b8 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java @@ -0,0 +1,74 @@ +package sonia.scm.web.filter; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.Repository; +import sonia.scm.repository.spi.ScmProviderHttpServlet; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@SubjectAware(configuration = "classpath:sonia/scm/shiro.ini") +public class PermissionFilterTest { + + public static final Repository REPOSITORY = new Repository("1", "git", "space", "name"); + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ScmProviderHttpServlet delegateServlet = mock(ScmProviderHttpServlet.class); + + private final PermissionFilter permissionFilter = new PermissionFilter(new ScmConfiguration(), delegateServlet) { + @Override + protected boolean isWriteRequest(HttpServletRequest request) { + return writeRequest; + } + }; + + private final HttpServletRequest request = mock(HttpServletRequest.class); + private final HttpServletResponse response = mock(HttpServletResponse.class); + + private boolean writeRequest = false; + + @Test + @SubjectAware(username = "reader", password = "secret") + public void shouldPassForReaderOnReadRequest() throws IOException, ServletException { + writeRequest = false; + + permissionFilter.service(request, response, REPOSITORY); + + verify(delegateServlet).service(request, response, REPOSITORY); + } + + @Test + @SubjectAware(username = "reader", password = "secret") + public void shouldBlockForReaderOnWriteRequest() throws IOException, ServletException { + writeRequest = true; + + permissionFilter.service(request, response, REPOSITORY); + + verify(response).sendError(eq(401), anyString()); + verify(delegateServlet, never()).service(request, response, REPOSITORY); + } + + @Test + @SubjectAware(username = "writer", password = "secret") + public void shouldPassForWriterOnWriteRequest() throws IOException, ServletException { + writeRequest = true; + + permissionFilter.service(request, response, REPOSITORY); + + verify(delegateServlet).service(request, response, REPOSITORY); + } +} diff --git a/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java b/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java index ecfdd06a0f..16c4278793 100644 --- a/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java +++ b/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java @@ -89,7 +89,7 @@ public class IndentXMLStreamWriterTest StringBuilder buffer = new StringBuilder(""); buffer.append(IndentXMLStreamWriter.LINE_SEPARATOR); buffer.append("").append(IndentXMLStreamWriter.LINE_SEPARATOR); - buffer.append(" Hello"); + buffer.append(" Hello"); buffer.append(IndentXMLStreamWriter.LINE_SEPARATOR); buffer.append("").append(IndentXMLStreamWriter.LINE_SEPARATOR); diff --git a/scm-core/src/test/java/sonia/scm/xml/XmlInstantAdapterTest.java b/scm-core/src/test/java/sonia/scm/xml/XmlInstantAdapterTest.java new file mode 100644 index 0000000000..eb1ea86aee --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/xml/XmlInstantAdapterTest.java @@ -0,0 +1,47 @@ +package sonia.scm.xml; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; + +import javax.xml.bind.JAXB; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.nio.file.Path; +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(TempDirectory.class) +class XmlInstantAdapterTest { + + @Test + void shouldMarshalAndUnmarshalInstant(@TempDirectory.TempDir Path tempDirectory) { + Path path = tempDirectory.resolve("instant.xml"); + + Instant instant = Instant.now(); + InstantObject object = new InstantObject(instant); + JAXB.marshal(object, path.toFile()); + + InstantObject unmarshaled = JAXB.unmarshal(path.toFile(), InstantObject.class); + assertEquals(instant, unmarshaled.instant); + } + + @XmlRootElement(name = "instant-object") + @XmlAccessorType(XmlAccessType.FIELD) + public static class InstantObject { + + @XmlJavaTypeAdapter(XmlInstantAdapter.class) + private Instant instant; + + public InstantObject() { + } + + InstantObject(Instant instant) { + this.instant = instant; + } + } + +} diff --git a/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/scm-core/src/test/resources/sonia/scm/repository/repository-001.json b/scm-core/src/test/resources/sonia/scm/repository/repository-001.json new file mode 100644 index 0000000000..43ea136942 --- /dev/null +++ b/scm-core/src/test/resources/sonia/scm/repository/repository-001.json @@ -0,0 +1,42 @@ +{ + "creationDate": "2018-11-09T09:48:32.732Z", + "description": "Handling static webresources made easy", + "healthCheckFailures": [], + "lastModified": "2018-11-09T09:49:20.973Z", + "namespace": "scmadmin", + "name": "web-resources", + "archived": false, + "type": "git", + "_links": { + "self": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "delete": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "update": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "permissions": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/" + }, + "protocol": [ + { + "href": "http://localhost:8081/scm/repo/scmadmin/web-resources", + "name": "http" + } + ], + "tags": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/" + }, + "branches": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/" + }, + "changesets": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/" + }, + "sources": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/" + } + } +} diff --git a/scm-core/src/test/resources/sonia/scm/repository/repository-collection-001.json b/scm-core/src/test/resources/sonia/scm/repository/repository-collection-001.json new file mode 100644 index 0000000000..f4eeb24bbc --- /dev/null +++ b/scm-core/src/test/resources/sonia/scm/repository/repository-collection-001.json @@ -0,0 +1,106 @@ +{ + "page": 0, + "pageTotal": 1, + "_links": { + "self": { + "href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10" + }, + "first": { + "href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10" + }, + "last": { + "href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10" + }, + "create": { + "href": "http://localhost:8081/scm/api/v2/repositories/" + } + }, + "_embedded": { + "repositories": [ + { + "creationDate": "2018-11-09T09:48:32.732Z", + "description": "Handling static webresources made easy", + "healthCheckFailures": [], + "lastModified": "2018-11-09T09:49:20.973Z", + "namespace": "scmadmin", + "name": "web-resources", + "archived": false, + "type": "git", + "_links": { + "self": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "delete": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "update": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "permissions": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/" + }, + "protocol": [ + { + "href": "http://localhost:8081/scm/repo/scmadmin/web-resources", + "name": "http" + } + ], + "tags": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/" + }, + "branches": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/" + }, + "changesets": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/" + }, + "sources": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/" + } + } + }, + { + "creationDate": "2018-11-09T09:48:32.732Z", + "description": "Handling static webresources made easy", + "healthCheckFailures": [], + "lastModified": "2018-11-09T09:49:20.973Z", + "namespace": "scmadmin", + "name": "web-resources", + "archived": false, + "type": "git", + "_links": { + "self": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "delete": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "update": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "permissions": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/" + }, + "protocol": [ + { + "href": "http://localhost:8081/scm/repo/scmadmin/web-resources", + "name": "http" + } + ], + "tags": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/" + }, + "branches": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/" + }, + "changesets": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/" + }, + "sources": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/" + } + } + } + ] + } +} diff --git a/scm-core/src/test/resources/sonia/scm/shiro.ini b/scm-core/src/test/resources/sonia/scm/shiro.ini index e87c81b097..fda268ec83 100644 --- a/scm-core/src/test/resources/sonia/scm/shiro.ini +++ b/scm-core/src/test/resources/sonia/scm/shiro.ini @@ -1,6 +1,12 @@ [users] trillian = secret, user +admin = secret, admin +writer = secret, repo_write +reader = secret, repo_read +unpriv = secret [roles] admin = * -user = something:* \ No newline at end of file +user = something:* +repo_read = "repository:read,pull:1" +repo_write = "repository:read,write,pull,push:1" diff --git a/scm-dao-xml/pom.xml b/scm-dao-xml/pom.xml index dcb8b940b0..5424d38f26 100644 --- a/scm-dao-xml/pom.xml +++ b/scm-dao-xml/pom.xml @@ -8,7 +8,8 @@ scm 2.0.0-SNAPSHOT - + + sonia.scm scm-dao-xml 2.0.0-SNAPSHOT scm-dao-xml diff --git a/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java b/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java index 577d732317..d6b65b41bd 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java @@ -64,9 +64,11 @@ public class XmlGroupDAO extends AbstractXmlDAO * @param storeFactory */ @Inject - public XmlGroupDAO(ConfigurationStoreFactory storeFactory) - { - super(storeFactory.getStore(XmlGroupDatabase.class, STORE_NAME)); + public XmlGroupDAO(ConfigurationStoreFactory storeFactory) { + super(storeFactory + .withType(XmlGroupDatabase.class) + .withName(STORE_NAME) + .build()); } //~--- methods -------------------------------------------------------------- diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java new file mode 100644 index 0000000000..f22180ff9a --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java @@ -0,0 +1,55 @@ +package sonia.scm.repository.xml; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ContextEntry; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.store.StoreConstants; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import java.nio.file.Path; + +public class MetadataStore implements UpdateStepRepositoryMetadataAccess { + + private static final Logger LOG = LoggerFactory.getLogger(MetadataStore.class); + + private final JAXBContext jaxbContext; + + public MetadataStore() { + try { + jaxbContext = JAXBContext.newInstance(Repository.class); + } catch (JAXBException ex) { + throw new IllegalStateException("failed to create jaxb context for repository", ex); + } + } + + public Repository read(Path path) { + LOG.trace("read repository metadata from {}", path); + try { + return (Repository) jaxbContext.createUnmarshaller().unmarshal(resolveDataPath(path).toFile()); + } catch (JAXBException ex) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Path.class, path.toString()).build(), "failed read repository metadata", ex + ); + } + } + + void write(Path path, Repository repository) { + LOG.trace("write repository metadata of {} to {}", repository.getNamespaceAndName(), path); + try { + Marshaller marshaller = jaxbContext.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.marshal(repository, resolveDataPath(path).toFile()); + } catch (JAXBException ex) { + throw new InternalRepositoryException(repository, "failed write repository metadata", ex); + } + } + + private Path resolveDataPath(Path repositoryPath) { + return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java new file mode 100644 index 0000000000..8f667b0e65 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -0,0 +1,174 @@ +package sonia.scm.repository.xml; + +import sonia.scm.SCMContextProvider; +import sonia.scm.io.FileSystem; +import sonia.scm.repository.BasicRepositoryLocationResolver; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.store.StoreConstants; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.nio.file.Path; +import java.time.Clock; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +/** + * A Location Resolver for File based Repository Storage. + *

+ * WARNING: The Locations provided with this class may not be used from the plugins to store any plugin specific files. + *

+ * Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data
+ * Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files
+ * Please use the {@link sonia.scm.store.ConfigurationStoreFactory} and the {@link sonia.scm.store.ConfigurationStore} classes to store configurations + * + * @since 2.0.0 + */ +@Singleton +public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver { + + public static final String STORE_NAME = "repository-paths"; + + private final SCMContextProvider contextProvider; + private final InitialRepositoryLocationResolver initialRepositoryLocationResolver; + private final FileSystem fileSystem; + + private final PathDatabase pathDatabase; + private final Map pathById; + + private final Clock clock; + + private Long creationTime; + private Long lastModified; + + @Inject + public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem) { + this(contextProvider, initialRepositoryLocationResolver, fileSystem, Clock.systemUTC()); + } + + PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem, Clock clock) { + super(Path.class); + this.contextProvider = contextProvider; + this.initialRepositoryLocationResolver = initialRepositoryLocationResolver; + this.fileSystem = fileSystem; + this.pathById = new ConcurrentHashMap<>(); + + this.clock = clock; + + this.creationTime = clock.millis(); + pathDatabase = new PathDatabase(resolveStorePath()); + + read(); + } + + @Override + protected RepositoryLocationResolverInstance create(Class type) { + return new RepositoryLocationResolverInstance() { + @Override + public T getLocation(String repositoryId) { + if (pathById.containsKey(repositoryId)) { + return (T) contextProvider.resolve(pathById.get(repositoryId)); + } else { + throw new IllegalStateException("location for repository " + repositoryId + " does not exist"); + } + } + + @Override + public T createLocation(String repositoryId) { + if (pathById.containsKey(repositoryId)) { + throw new IllegalStateException("location for repository " + repositoryId + " already exists"); + } else { + return (T) create(repositoryId); + } + } + + @Override + public void setLocation(String repositoryId, T location) { + if (pathById.containsKey(repositoryId)) { + throw new IllegalStateException("location for repository " + repositoryId + " already exists"); + } else { + PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath()); + } + } + + @Override + public void forAllLocations(BiConsumer consumer) { + pathById.forEach((id, path) -> consumer.accept(id, (T) contextProvider.resolve(path))); + } + }; + } + + Path create(String repositoryId) { + Path path = initialRepositoryLocationResolver.getPath(repositoryId); + setLocation(repositoryId, path); + Path resolvedPath = contextProvider.resolve(path); + try { + fileSystem.create(resolvedPath.toFile()); + } catch (Exception e) { + throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e); + } + return resolvedPath; + } + + Path remove(String repositoryId) { + Path removedPath = pathById.remove(repositoryId); + writePathDatabase(); + return contextProvider.resolve(removedPath); + } + + void updateModificationDate() { + this.writePathDatabase(); + } + + private void writePathDatabase() { + lastModified = clock.millis(); + pathDatabase.write(creationTime, lastModified, pathById); + } + + private void read() { + Path storePath = resolveStorePath(); + + // Files.exists is slow on java 8 + if (storePath.toFile().exists()) { + pathDatabase.read(this::onLoadDates, this::onLoadRepository); + } + } + + private void onLoadDates(Long creationTime, Long lastModified) { + this.creationTime = creationTime; + this.lastModified = lastModified; + } + + public Long getCreationTime() { + return creationTime; + } + + public Long getLastModified() { + return lastModified; + } + + + private void onLoadRepository(String id, Path repositoryPath) { + pathById.put(id, repositoryPath); + } + + private Path resolveStorePath() { + return contextProvider.getBaseDirectory() + .toPath() + .resolve(StoreConstants.CONFIG_DIRECTORY_NAME) + .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); + } + + private void setLocation(String repositoryId, Path repositoryBasePath) { + pathById.put(repositoryId, repositoryBasePath); + writePathDatabase(); + } + + public void refresh() { + this.read(); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java new file mode 100644 index 0000000000..70698aed59 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java @@ -0,0 +1,146 @@ +package sonia.scm.repository.xml; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ContextEntry; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.xml.IndentXMLStreamWriter; +import sonia.scm.xml.XmlStreams; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +class PathDatabase { + + private static final Logger LOG = LoggerFactory.getLogger(PathDatabase.class); + + private static final String ENCODING = "UTF-8"; + private static final String VERSION = "1.0"; + + private static final String ELEMENT_REPOSITORIES = "repositories"; + private static final String ATTRIBUTE_CREATION_TIME = "creation-time"; + private static final String ATTRIBUTE_LAST_MODIFIED = "last-modified"; + + private static final String ELEMENT_REPOSITORY = "repository"; + private static final String ATTRIBUTE_ID = "id"; + + private final Path storePath; + + PathDatabase(Path storePath){ + this.storePath = storePath; + } + + void write(Long creationTime, Long lastModified, Map pathDatabase) { + ensureParentDirectoryExists(); + LOG.trace("write repository path database to {}", storePath); + + try (IndentXMLStreamWriter writer = XmlStreams.createWriter(storePath)) { + writer.writeStartDocument(ENCODING, VERSION); + + writeRepositoriesStart(writer, creationTime, lastModified); + for (Map.Entry e : pathDatabase.entrySet()) { + writeRepository(writer, e.getKey(), e.getValue()); + } + writer.writeEndElement(); + + writer.writeEndDocument(); + } catch (XMLStreamException | IOException ex) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(), + "failed to write repository path database", + ex + ); + } + } + + private void ensureParentDirectoryExists() { + Path parent = storePath.getParent(); + // Files.exists is slow on java 8 + if (!parent.toFile().exists()) { + try { + Files.createDirectories(parent); + } catch (IOException ex) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Path.class, parent.toString()).build(), + "failed to create parent directory", + ex + ); + } + } + } + + private void writeRepositoriesStart(XMLStreamWriter writer, Long creationTime, Long lastModified) throws XMLStreamException { + writer.writeStartElement(ELEMENT_REPOSITORIES); + writer.writeAttribute(ATTRIBUTE_CREATION_TIME, String.valueOf(creationTime)); + writer.writeAttribute(ATTRIBUTE_LAST_MODIFIED, String.valueOf(lastModified)); + } + + private void writeRepository(XMLStreamWriter writer, String id, Path value) throws XMLStreamException { + writer.writeStartElement(ELEMENT_REPOSITORY); + writer.writeAttribute(ATTRIBUTE_ID, id); + writer.writeCharacters(value.toString()); + writer.writeEndElement(); + } + + void read(OnRepositories onRepositories, OnRepository onRepository) { + LOG.trace("read repository path database from {}", storePath); + XMLStreamReader reader = null; + try { + reader = XmlStreams.createReader(storePath); + + while (reader.hasNext()) { + int eventType = reader.next(); + + if (eventType == XMLStreamReader.START_ELEMENT) { + String element = reader.getLocalName(); + if (ELEMENT_REPOSITORIES.equals(element)) { + readRepositories(reader, onRepositories); + } else if (ELEMENT_REPOSITORY.equals(element)) { + readRepository(reader, onRepository); + } + } + } + } catch (XMLStreamException | IOException ex) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(), + "failed to read repository path database", + ex + ); + } finally { + XmlStreams.close(reader); + } + } + + private void readRepository(XMLStreamReader reader, OnRepository onRepository) throws XMLStreamException { + String id = reader.getAttributeValue(null, ATTRIBUTE_ID); + Path path = Paths.get(reader.getElementText()); + onRepository.handle(id, path); + } + + private void readRepositories(XMLStreamReader reader, OnRepositories onRepositories) { + String creationTime = reader.getAttributeValue(null, ATTRIBUTE_CREATION_TIME); + String lastModified = reader.getAttributeValue(null, ATTRIBUTE_LAST_MODIFIED); + onRepositories.handle(Long.parseLong(creationTime), Long.parseLong(lastModified)); + } + + @FunctionalInterface + interface OnRepositories { + + void handle(Long creationTime, Long lastModified); + + } + + @FunctionalInterface + interface OnRepository { + + void handle(String id, Path path); + + } + +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java new file mode 100644 index 0000000000..f963047624 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java @@ -0,0 +1,17 @@ +package sonia.scm.repository.xml; + +import sonia.scm.repository.RepositoryLocationResolver; + +import javax.inject.Inject; +import java.nio.file.Path; +import java.util.function.BiConsumer; + +public class SingleRepositoryUpdateProcessor { + + @Inject + private RepositoryLocationResolver locationResolver; + + public void doUpdate(BiConsumer forEachRepository) { + locationResolver.forClass(Path.class).forAllLocations(forEachRepository); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java index 7c17c365aa..1242c99641 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

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

* http://bitbucket.org/sdorra/scm-manager - * */ @@ -34,100 +33,170 @@ package sonia.scm.repository.xml; //~--- non-JDK imports -------------------------------------------------------- -import com.google.inject.Inject; +import com.google.common.collect.ImmutableList; import com.google.inject.Singleton; - +import sonia.scm.io.FileSystem; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; -import sonia.scm.xml.AbstractXmlDAO; -import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.repository.RepositoryLocationResolver; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** - * * @author Sebastian Sdorra */ @Singleton -public class XmlRepositoryDAO - extends AbstractXmlDAO - implements RepositoryDAO -{ +public class XmlRepositoryDAO implements RepositoryDAO { - /** Field description */ - public static final String STORE_NAME = "repositories"; - //~--- constructors --------------------------------------------------------- + private final MetadataStore metadataStore = new MetadataStore(); + + private final PathBasedRepositoryLocationResolver repositoryLocationResolver; + private final FileSystem fileSystem; + + private final Map byId; + private final Map byNamespaceAndName; - /** - * Constructs ... - * - * - * @param storeFactory - */ @Inject - public XmlRepositoryDAO(ConfigurationStoreFactory storeFactory) - { - super(storeFactory.getStore(XmlRepositoryDatabase.class, STORE_NAME)); + public XmlRepositoryDAO(PathBasedRepositoryLocationResolver repositoryLocationResolver, FileSystem fileSystem) { + this.repositoryLocationResolver = repositoryLocationResolver; + this.fileSystem = fileSystem; + + this.byId = new ConcurrentHashMap<>(); + this.byNamespaceAndName = new ConcurrentHashMap<>(); + + init(); } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * - * @return - */ - @Override - public boolean contains(String type, String name) - { - return db.contains(type, name); + private void init() { + RepositoryLocationResolver.RepositoryLocationResolverInstance pathRepositoryLocationResolverInstance = repositoryLocationResolver.create(Path.class); + pathRepositoryLocationResolverInstance.forAllLocations((repositoryId, repositoryPath) -> { + Repository repository = metadataStore.read(repositoryPath); + byNamespaceAndName.put(repository.getNamespaceAndName(), repository); + byId.put(repositoryId, repository); + }); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * - * @return - */ @Override - public Repository get(String type, String name) - { - return db.get(type, name); + public String getType() { + return "xml"; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * - * @return - */ @Override - protected Repository clone(Repository repository) - { - return repository.clone(); + public void add(Repository repository) { + add(repository, repositoryLocationResolver.create(repository.getId())); + } + + public void add(Repository repository, Object location) { + if (!(location instanceof Path)) { + throw new IllegalArgumentException("can only handle locations of type " + Path.class.getName() + ", not of type " + location.getClass().getName()); + } + Repository clone = repository.clone(); + + synchronized (this) { + Path repositoryPath = (Path) location; + + try { + metadataStore.write(repositoryPath, repository); + } catch (Exception e) { + repositoryLocationResolver.remove(repository.getId()); + throw new InternalRepositoryException(repository, "failed to create filesystem", e); + } + + byId.put(repository.getId(), clone); + byNamespaceAndName.put(repository.getNamespaceAndName(), clone); + } } - /** - * Method description - * - * - * @return - */ @Override - protected XmlRepositoryDatabase createNewDatabase() - { - return new XmlRepositoryDatabase(); + public boolean contains(Repository repository) { + return byId.containsKey(repository.getId()); + } + + @Override + public boolean contains(NamespaceAndName namespaceAndName) { + return byNamespaceAndName.containsKey(namespaceAndName); + } + + @Override + public boolean contains(String id) { + return byId.containsKey(id); + } + + @Override + public Repository get(NamespaceAndName namespaceAndName) { + return byNamespaceAndName.get(namespaceAndName); + } + + @Override + public Repository get(String id) { + return byId.get(id); + } + + @Override + public Collection getAll() { + return ImmutableList.copyOf(byNamespaceAndName.values()); + } + + @Override + public void modify(Repository repository) { + Repository clone = repository.clone(); + + synchronized (this) { + // remove old namespaceAndName from map, in case of rename + Repository prev = byId.put(clone.getId(), clone); + if (prev != null) { + byNamespaceAndName.remove(prev.getNamespaceAndName()); + } + byNamespaceAndName.put(clone.getNamespaceAndName(), clone); + } + + Path repositoryPath = repositoryLocationResolver + .create(Path.class) + .getLocation(repository.getId()); + repositoryLocationResolver.updateModificationDate(); + metadataStore.write(repositoryPath, clone); + } + + @Override + public void delete(Repository repository) { + Path path; + synchronized (this) { + Repository prev = byId.remove(repository.getId()); + if (prev != null) { + byNamespaceAndName.remove(prev.getNamespaceAndName()); + } + path = repositoryLocationResolver.remove(repository.getId()); + } + + try { + fileSystem.destroy(path.toFile()); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "failed to destroy filesystem", e); + } + } + + @Override + public Long getCreationTime() { + return repositoryLocationResolver.getCreationTime(); + } + + @Override + public Long getLastModified() { + return repositoryLocationResolver.getLastModified(); + } + + public void refresh() { + repositoryLocationResolver.refresh(); + byNamespaceAndName.clear(); + byId.clear(); + init(); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDatabase.java deleted file mode 100644 index 8f065a5ca2..0000000000 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDatabase.java +++ /dev/null @@ -1,302 +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.xml; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.Repository; -import sonia.scm.xml.XmlDatabase; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "repository-db") -@XmlAccessorType(XmlAccessType.FIELD) -public class XmlRepositoryDatabase implements XmlDatabase -{ - - /** - * Constructs ... - * - */ - public XmlRepositoryDatabase() - { - long c = System.currentTimeMillis(); - - creationTime = c; - lastModified = c; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * - * @return - */ - static String createKey(String type, String name) - { - return type.concat(":").concat(name); - } - - /** - * Method description - * - * - * @param repository - * - * @return - */ - static String createKey(Repository repository) - { - return createKey(repository.getType(), repository.getName()); - } - - /** - * Method description - * - * - * @param repository - */ - @Override - public void add(Repository repository) - { - repositoryMap.put(createKey(repository), repository); - } - - /** - * Method description - * - * - * - * @param type - * @param name - * - * @return - */ - public boolean contains(String type, String name) - { - return repositoryMap.containsKey(createKey(type, name)); - } - - /** - * Method description - * - * - * @param id - * - * @return - */ - @Override - public boolean contains(String id) - { - return get(id) != null; - } - - /** - * Method description - * - * - * @param repository - * - * @return - */ - public boolean contains(Repository repository) - { - return repositoryMap.containsKey(createKey(repository)); - } - - /** - * Method description - * - * - * @param repository - */ - public void remove(Repository repository) - { - repositoryMap.remove(createKey(repository)); - } - - /** - * Method description - * - * - * @param id - * - * @return - */ - @Override - public Repository remove(String id) - { - Repository r = get(id); - - remove(r); - - return r; - } - - /** - * Method description - * - * - * @return - */ - @Override - public Collection values() - { - return repositoryMap.values(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * - * @return - */ - public Repository get(String type, String name) - { - return repositoryMap.get(createKey(type, name)); - } - - /** - * Method description - * - * - * @param id - * - * @return - */ - @Override - public Repository get(String id) - { - Repository repository = null; - - for (Repository r : values()) - { - if (r.getId().equals(id)) - { - repository = r; - - break; - } - } - - return repository; - } - - /** - * Method description - * - * - * @return - */ - @Override - public long getCreationTime() - { - return creationTime; - } - - /** - * Method description - * - * - * @return - */ - @Override - public long getLastModified() - { - return lastModified; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param creationTime - */ - @Override - public void setCreationTime(long creationTime) - { - this.creationTime = creationTime; - } - - /** - * Method description - * - * - * @param lastModified - */ - @Override - public void setLastModified(long lastModified) - { - this.lastModified = lastModified; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Long creationTime; - - /** Field description */ - private Long lastModified; - - /** Field description */ - @XmlJavaTypeAdapter(XmlRepositoryMapAdapter.class) - @XmlElement(name = "repositories") - private Map repositoryMap = new LinkedHashMap<>(); -} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryList.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryList.java deleted file mode 100644 index 085d65476a..0000000000 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryList.java +++ /dev/null @@ -1,123 +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.xml; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Map; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "repositories") -@XmlAccessorType(XmlAccessType.FIELD) -public class XmlRepositoryList implements Iterable -{ - - /** - * Constructs ... - * - */ - public XmlRepositoryList() {} - - /** - * Constructs ... - * - * - * - * @param repositoryMap - */ - public XmlRepositoryList(Map repositoryMap) - { - this.repositories = new LinkedList<>(repositoryMap.values()); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public Iterator iterator() - { - return repositories.iterator(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public LinkedList getRepositories() - { - return repositories; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param repositories - */ - public void setRepositories(LinkedList repositories) - { - this.repositories = repositories; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "repository") - private LinkedList repositories; -} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryMapAdapter.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryMapAdapter.java deleted file mode 100644 index c31a0cd26d..0000000000 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryMapAdapter.java +++ /dev/null @@ -1,96 +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.xml; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.LinkedHashMap; -import java.util.Map; - -import javax.xml.bind.annotation.adapters.XmlAdapter; - -/** - * - * @author Sebastian Sdorra - */ -public class XmlRepositoryMapAdapter - extends XmlAdapter> -{ - - /** - * Method description - * - * - * @param repositoryMap - * - * @return - * - * @throws Exception - */ - @Override - public XmlRepositoryList marshal(Map repositoryMap) - throws Exception - { - return new XmlRepositoryList(repositoryMap); - } - - /** - * Method description - * - * - * @param repositories - * - * @return - * - * @throws Exception - */ - @Override - public Map unmarshal(XmlRepositoryList repositories) - throws Exception - { - Map repositoryMap = new LinkedHashMap<>(); - - for (Repository repository : repositories) - { - repositoryMap.put(XmlRepositoryDatabase.createKey(repository), - repository); - } - - return repositoryMap; - } -} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java new file mode 100644 index 0000000000..0eb860a134 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDAO.java @@ -0,0 +1,42 @@ +package sonia.scm.repository.xml; + +import com.google.inject.Inject; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleDAO; +import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.xml.AbstractXmlDAO; + +import javax.inject.Singleton; +import java.util.List; + +@Singleton +public class XmlRepositoryRoleDAO extends AbstractXmlDAO + implements RepositoryRoleDAO { + + public static final String STORE_NAME = "repositoryRoles"; + + @Inject + public XmlRepositoryRoleDAO(ConfigurationStoreFactory storeFactory) { + super(storeFactory + .withType(XmlRepositoryRoleDatabase.class) + .withName(STORE_NAME) + .build()); + } + + @Override + protected RepositoryRole clone(RepositoryRole role) + { + return role.clone(); + } + + @Override + protected XmlRepositoryRoleDatabase createNewDatabase() + { + return new XmlRepositoryRoleDatabase(); + } + + @Override + public List getAll() { + return (List) super.getAll(); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java new file mode 100644 index 0000000000..c7665316e2 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleDatabase.java @@ -0,0 +1,77 @@ +package sonia.scm.repository.xml; + +import sonia.scm.repository.RepositoryRole; +import sonia.scm.xml.XmlDatabase; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +@XmlRootElement(name = "user-db") +@XmlAccessorType(XmlAccessType.FIELD) +public class XmlRepositoryRoleDatabase implements XmlDatabase { + + private Long creationTime; + private Long lastModified; + + @XmlJavaTypeAdapter(XmlRepositoryRoleMapAdapter.class) + @XmlElement(name = "roles") + private Map roleMap = new LinkedHashMap<>(); + + public XmlRepositoryRoleDatabase() { + long c = System.currentTimeMillis(); + + creationTime = c; + lastModified = c; + } + + @Override + public void add(RepositoryRole role) { + roleMap.put(role.getName(), role); + } + + @Override + public boolean contains(String name) { + return roleMap.containsKey(name); + } + + @Override + public RepositoryRole remove(String name) { + return roleMap.remove(name); + } + + @Override + public Collection values() { + return roleMap.values(); + } + + @Override + public RepositoryRole get(String name) { + return roleMap.get(name); + } + + @Override + public long getCreationTime() { + return creationTime; + } + + @Override + public long getLastModified() { + return lastModified; + } + + @Override + public void setCreationTime(long creationTime) { + this.creationTime = creationTime; + } + + @Override + public void setLastModified(long lastModified) { + this.lastModified = lastModified; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleList.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleList.java new file mode 100644 index 0000000000..7910d4300a --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleList.java @@ -0,0 +1,74 @@ +/** + * 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.xml; + +import sonia.scm.repository.RepositoryRole; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +@XmlRootElement(name = "roles") +@XmlAccessorType(XmlAccessType.FIELD) +public class XmlRepositoryRoleList implements Iterable { + + public XmlRepositoryRoleList() {} + + public XmlRepositoryRoleList(Map roleMap) { + this.roles = new LinkedList(roleMap.values()); + } + + @Override + public Iterator iterator() + { + return roles.iterator(); + } + + public LinkedList getRoles() + { + return roles; + } + + public void setRoles(LinkedList roles) + { + this.roles = roles; + } + + @XmlElement(name = "role") + private LinkedList roles; +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleMapAdapter.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleMapAdapter.java new file mode 100644 index 0000000000..959eff331a --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryRoleMapAdapter.java @@ -0,0 +1,60 @@ +/** + * 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.xml; + +import sonia.scm.repository.RepositoryRole; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.util.LinkedHashMap; +import java.util.Map; + +public class XmlRepositoryRoleMapAdapter + extends XmlAdapter> { + + @Override + public XmlRepositoryRoleList marshal(Map roleMap) { + return new XmlRepositoryRoleList(roleMap); + } + + @Override + public Map unmarshal(XmlRepositoryRoleList roles) { + Map roleMap = new LinkedHashMap<>(); + + for (RepositoryRole role : roles) { + roleMap.put(role.getName(), role); + } + + return roleMap; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java b/scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java new file mode 100644 index 0000000000..d22a76c79d --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java @@ -0,0 +1,68 @@ +package sonia.scm.store; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.BlobDirectoryAccess; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class DefaultBlobDirectoryAccess implements BlobDirectoryAccess { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultBlobDirectoryAccess.class); + + private final SCMContextProvider contextProvider; + private final RepositoryLocationResolver locationResolver; + + @Inject + public DefaultBlobDirectoryAccess(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { + this.contextProvider = contextProvider; + this.locationResolver = locationResolver; + } + + @Override + public void forBlobDirectories(BlobDirectoryConsumer blobDirectoryConsumer) throws IOException { + Path v1blobDir = computeV1BlobDir(); + if (Files.exists(v1blobDir) && Files.isDirectory(v1blobDir)) { + try (Stream fileStream = Files.list(v1blobDir)) { + fileStream.filter(p -> Files.isDirectory(p)).forEach(p -> { + try { + blobDirectoryConsumer.accept(p); + } catch (IOException e) { + throw new RuntimeException("could not call consumer for blob directory " + p, e); + } + }); + } + } + } + + @Override + public void moveToRepositoryBlobStore(Path blobDirectory, String newDirectoryName, String repositoryId) throws IOException { + Path repositoryLocation; + try { + repositoryLocation = locationResolver + .forClass(Path.class) + .getLocation(repositoryId); + } catch (IllegalStateException e) { + LOG.info("ignoring blob directory {} because there is no repository location for repository id {}", blobDirectory, repositoryId); + return; + } + Path target = repositoryLocation + .resolve(Store.BLOB.getRepositoryStoreDirectory()); + IOUtil.mkdirs(target.toFile()); + Path resolvedSourceDirectory = computeV1BlobDir().resolve(blobDirectory); + Path resolvedTargetDirectory = target.resolve(newDirectoryName); + LOG.trace("moving directory {} to {}", resolvedSourceDirectory, resolvedTargetDirectory); + Files.move(resolvedSourceDirectory, resolvedTargetDirectory); + } + + private Path computeV1BlobDir() { + return contextProvider.getBaseDirectory().toPath().resolve("var").resolve("blob"); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java index 5bfc4f34b9..967aa8fb1c 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java @@ -31,18 +31,21 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- + import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ import java.io.File; +import java.nio.file.Path; + +//~--- JDK imports ------------------------------------------------------------ /** * Abstract store factory for file based stores. - * + * * @author Sebastian Sdorra */ public abstract class FileBasedStoreFactory { @@ -51,39 +54,54 @@ public abstract class FileBasedStoreFactory { * the logger for FileBasedStoreFactory */ private static final Logger LOG = LoggerFactory.getLogger(FileBasedStoreFactory.class); + private SCMContextProvider contextProvider; + private RepositoryLocationResolver repositoryLocationResolver; + private Store store; - private static final String BASE_DIRECTORY = "var"; - - private final SCMContextProvider context; - - private final String dataDirectoryName; - - private File dataDirectory; - - protected FileBasedStoreFactory(SCMContextProvider context, - String dataDirectoryName) { - this.context = context; - this.dataDirectoryName = dataDirectoryName; + protected FileBasedStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, Store store) { + this.contextProvider = contextProvider; + this.repositoryLocationResolver = repositoryLocationResolver; + this.store = store; } - //~--- get methods ---------------------------------------------------------- - /** - * Returns data directory for given name. - * - * @param name name of data directory - * - * @return data directory - */ - protected File getDirectory(String name) { - if (dataDirectory == null) { - dataDirectory = new File(context.getBaseDirectory(), - BASE_DIRECTORY.concat(File.separator).concat(dataDirectoryName)); - LOG.debug("create data directory {}", dataDirectory); - } + protected File getStoreLocation(StoreParameters storeParameters) { + return getStoreLocation(storeParameters.getName(), null, storeParameters.getRepositoryId()); + } - File storeDirectory = new File(dataDirectory, name); + protected File getStoreLocation(TypedStoreParameters storeParameters) { + return getStoreLocation(storeParameters.getName(), storeParameters.getType(), storeParameters.getRepositoryId()); + } + + protected File getStoreLocation(String name, Class type, String repositoryId) { + File storeDirectory; + if (repositoryId != null) { + LOG.debug("create store with type: {}, name: {} and repository id: {}", type, name, repositoryId); + storeDirectory = this.getStoreDirectory(store, repositoryId); + } else { + LOG.debug("create store with type: {} and name: {} ", type, name); + storeDirectory = this.getStoreDirectory(store); + } IOUtil.mkdirs(storeDirectory); - return storeDirectory; + return new File(storeDirectory, name); + } + + /** + * Get the store directory of a specific repository + * @param store the type of the store + * @param repositoryId the id of the repossitory + * @return the store directory of a specific repository + */ + private File getStoreDirectory(Store store, String repositoryId) { + return new File(repositoryLocationResolver.forClass(Path.class).getLocation(repositoryId).toFile(), store.getRepositoryStoreDirectory()); + } + + /** + * Get the global store directory + * @param store the type of the store + * @return the global store directory + */ + private File getStoreDirectory(Store store) { + return new File(contextProvider.getBaseDirectory(), store.getGlobalStoreDirectory()); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java index 8cc5b34ac2..4a7c9fc713 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java @@ -31,14 +31,17 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- + import com.google.inject.Inject; import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.security.KeyGenerator; +import sonia.scm.util.IOUtil; + +import java.io.File; /** * File based store factory. @@ -48,8 +51,6 @@ import sonia.scm.security.KeyGenerator; @Singleton public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobStoreFactory { - private static final String DIRECTORY_NAME = "blob"; - /** * the logger for FileBlobStoreFactory */ @@ -60,21 +61,22 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS /** * Constructs a new instance. * - * @param context scm context + * @param repositoryLocationResolver location resolver * @param keyGenerator key generator */ @Inject - public FileBlobStoreFactory(SCMContextProvider context, - KeyGenerator keyGenerator) { - super(context, DIRECTORY_NAME); + public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) { + super(contextProvider, repositoryLocationResolver, Store.BLOB); this.keyGenerator = keyGenerator; } @Override - public BlobStore getBlobStore(String name) { - LOG.debug("create new blob with name {}", name); - - return new FileBlobStore(keyGenerator, getDirectory(name)); + @SuppressWarnings("unchecked") + public BlobStore getStore(StoreParameters storeParameters) { + File storeLocation = getStoreLocation(storeParameters); + IOUtil.mkdirs(storeLocation); + return new FileBlobStore(keyGenerator, storeLocation); } + } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java index f1f3040883..40cf03c8a8 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java @@ -35,32 +35,14 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Charsets; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Maps; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.security.KeyGenerator; import sonia.scm.xml.IndentXMLStreamWriter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Map.Entry; +import sonia.scm.xml.XmlStreams; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; @@ -68,11 +50,14 @@ import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.namespace.QName; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; -import javax.xml.stream.XMLStreamWriter; +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -255,74 +240,6 @@ public class JAXBConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore je = new JAXBElement<>(QName.valueOf(TAG_VALUE), type, - e.getValue()); + JAXBElement je = new JAXBElement(QName.valueOf(TAG_VALUE), type, + e.getValue()); m.marshal(je, writer); @@ -453,10 +359,6 @@ public class JAXBConfigurationEntryStore implements ConfigurationEntryStore * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + *

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

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,97 +24,44 @@ * 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.store; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.Inject; import com.google.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.security.KeyGenerator; -import sonia.scm.util.IOUtil; //~--- JDK imports ------------------------------------------------------------ -import java.io.File; - /** * * @author Sebastian Sdorra */ @Singleton -public class JAXBConfigurationEntryStoreFactory - implements ConfigurationEntryStoreFactory -{ +public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory + implements ConfigurationEntryStoreFactory { - /** - * the logger for JAXBConfigurationEntryStoreFactory - */ - private static final Logger logger = - LoggerFactory.getLogger(JAXBConfigurationEntryStoreFactory.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param keyGenerator - * @param context - */ - @Inject - public JAXBConfigurationEntryStoreFactory(KeyGenerator keyGenerator, - SCMContextProvider context) - { - this.keyGenerator = keyGenerator; - directory = new File(context.getBaseDirectory(), - StoreConstants.CONFIGDIRECTORY_NAME); - IOUtil.mkdirs(directory); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * @param - * - * @return - */ - @Override - public ConfigurationEntryStore getStore(Class type, String name) - { - logger.debug("create new configuration store for type {} with name {}", - type, name); - - //J- - return new JAXBConfigurationEntryStore<>( - new File(directory, name.concat(StoreConstants.FILE_EXTENSION)), - keyGenerator, - type - ); - //J+ - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private File directory; - - /** Field description */ private KeyGenerator keyGenerator; + + @Inject + public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) { + super(contextProvider, repositoryLocationResolver, Store.CONFIG); + this.keyGenerator = keyGenerator; + } + + @Override + public ConfigurationEntryStore getStore(TypedStoreParameters storeParameters) { + return new JAXBConfigurationEntryStore<>( + getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepositoryId()), + keyGenerator, + storeParameters.getType()); + } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java index 4a87ea57f6..ac1477d7ea 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java @@ -61,7 +61,7 @@ public class JAXBConfigurationStore extends AbstractStore { private JAXBContext context; - JAXBConfigurationStore(Class type, File configFile) { + public JAXBConfigurationStore(Class type, File configFile) { this.type = type; try { diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java index 70fd962254..e66394cb5d 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java @@ -32,55 +32,33 @@ package sonia.scm.store; import com.google.inject.Inject; import com.google.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; -import sonia.scm.util.IOUtil; - -import java.io.File; +import sonia.scm.repository.RepositoryLocationResolver; /** - * JAXB implementation of {@link JAXBConfigurationStoreFactory}. + * JAXB implementation of {@link ConfigurationStoreFactory}. * * @author Sebastian Sdorra */ @Singleton -public class JAXBConfigurationStoreFactory implements ConfigurationStoreFactory { - - /** - * the logger for JAXBConfigurationStoreFactory - */ - private static final Logger LOG = LoggerFactory.getLogger(JAXBConfigurationStoreFactory.class); - - private final File configDirectory; +public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory implements ConfigurationStoreFactory { /** * Constructs a new instance. * - * @param context scm context + * @param repositoryLocationResolver Resolver to get the repository Directory */ @Inject - public JAXBConfigurationStoreFactory(SCMContextProvider context) { - configDirectory = new File(context.getBaseDirectory(), StoreConstants.CONFIGDIRECTORY_NAME); - IOUtil.mkdirs(configDirectory); + public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver) { + super(contextProvider, repositoryLocationResolver, Store.CONFIG); } @Override - public JAXBConfigurationStore getStore(Class type, String name) { - if (configDirectory == null) { - throw new IllegalStateException("store factory is not initialized"); - } - - File configFile = new File(configDirectory, name.concat(StoreConstants.FILE_EXTENSION)); - - if (LOG.isDebugEnabled()) { - LOG.debug("create store for {} at {}", type.getName(), - configFile.getPath()); - } - - return new JAXBConfigurationStore<>(type, configFile); + public JAXBConfigurationStore getStore(TypedStoreParameters storeParameters) { + return new JAXBConfigurationStore<>( + storeParameters.getType(), + getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), + storeParameters.getType(), + storeParameters.getRepositoryId())); } - } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java index 732b8c675b..579ef75b71 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java @@ -37,11 +37,12 @@ package sonia.scm.store; import com.google.inject.Inject; import com.google.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.security.KeyGenerator; +import sonia.scm.util.IOUtil; + +import java.io.File; /** * @@ -49,57 +50,20 @@ import sonia.scm.security.KeyGenerator; */ @Singleton public class JAXBDataStoreFactory extends FileBasedStoreFactory - implements DataStoreFactory -{ + implements DataStoreFactory { - /** Field description */ - private static final String DIRECTORY_NAME = "data"; + private KeyGenerator keyGenerator; - /** - * the logger for JAXBDataStoreFactory - */ - private static final Logger logger = - LoggerFactory.getLogger(JAXBDataStoreFactory.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param context - * @param keyGenerator - */ @Inject - public JAXBDataStoreFactory(SCMContextProvider context, - KeyGenerator keyGenerator) - { - super(context, DIRECTORY_NAME); + public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) { + super(contextProvider, repositoryLocationResolver, Store.DATA); this.keyGenerator = keyGenerator; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param type - * @param name - * @param - * - * @return - */ @Override - public DataStore getStore(Class type, String name) - { - logger.debug("create new store for type {} with name {}", type, name); - - return new JAXBDataStore<>(keyGenerator, type, getDirectory(name)); + public DataStore getStore(TypedStoreParameters storeParameters) { + File storeLocation = getStoreLocation(storeParameters); + IOUtil.mkdirs(storeLocation); + return new JAXBDataStore<>(keyGenerator, storeParameters.getType(), storeLocation); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private KeyGenerator keyGenerator; } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java new file mode 100644 index 0000000000..ca4aeea468 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java @@ -0,0 +1,90 @@ +package sonia.scm.store; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.PropertyFileAccess; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class JAXBPropertyFileAccess implements PropertyFileAccess { + + private static final Logger LOG = LoggerFactory.getLogger(JAXBPropertyFileAccess.class); + + private final SCMContextProvider contextProvider; + private final RepositoryLocationResolver locationResolver; + + @Inject + public JAXBPropertyFileAccess(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { + this.contextProvider = contextProvider; + this.locationResolver = locationResolver; + } + + @Override + public Target renameGlobalConfigurationFrom(String oldName) { + return newName -> { + Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME); + Path oldConfigFile = configDir.resolve(oldName + StoreConstants.FILE_EXTENSION); + Path newConfigFile = configDir.resolve(newName + StoreConstants.FILE_EXTENSION); + Files.move(oldConfigFile, newConfigFile); + }; + } + + @Override + public StoreFileTools forStoreName(String storeName) { + return new StoreFileTools() { + @Override + public void forStoreFiles(FileConsumer storeFileConsumer) throws IOException { + Path v1storeDir = computeV1StoreDir(); + if (Files.exists(v1storeDir) && Files.isDirectory(v1storeDir)) { + try (Stream fileStream = Files.list(v1storeDir)) { + fileStream.filter(p -> p.toString().endsWith(StoreConstants.FILE_EXTENSION)).forEach(p -> { + try { + String storeName = extractStoreName(p); + storeFileConsumer.accept(p, storeName); + } catch (IOException e) { + throw new RuntimeException("could not call consumer for store file " + p + " with name " + storeName, e); + } + }); + } + } + } + + @Override + public void moveAsRepositoryStore(Path storeFile, String repositoryId) throws IOException { + Path repositoryLocation; + try { + repositoryLocation = locationResolver + .forClass(Path.class) + .getLocation(repositoryId); + } catch (IllegalStateException e) { + LOG.info("ignoring store file {} because there is no repository location for repository id {}", storeFile, repositoryId); + return; + } + Path target = repositoryLocation + .resolve(Store.DATA.getRepositoryStoreDirectory()) + .resolve(storeName); + IOUtil.mkdirs(target.toFile()); + Path resolvedSourceFile = computeV1StoreDir().resolve(storeFile); + Path resolvedTargetFile = target.resolve(storeFile.getFileName()); + LOG.trace("moving file {} to {}", resolvedSourceFile, resolvedTargetFile); + Files.move(resolvedSourceFile, resolvedTargetFile); + } + + private Path computeV1StoreDir() { + return contextProvider.getBaseDirectory().toPath().resolve("var").resolve("data").resolve(storeName); + } + + private String extractStoreName(Path p) { + String fileName = p.getFileName().toString(); + return fileName.substring(0, fileName.length() - StoreConstants.FILE_EXTENSION.length()); + } + }; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/Store.java b/scm-dao-xml/src/main/java/sonia/scm/store/Store.java new file mode 100644 index 0000000000..511ef8323e --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/Store.java @@ -0,0 +1,50 @@ +package sonia.scm.store; + +import java.io.File; + +public enum Store { + CONFIG("config"), + DATA("data"), + BLOB("blob"); + + private static final String GLOBAL_STORE_BASE_DIRECTORY = "var"; + private static final String STORE_DIRECTORY = "store"; + + private String directory; + + Store(String directory) { + + this.directory = directory; + } + + /** + * Get the relative store directory path to be stored in the repository root + *

+ * The repository store directories are: + * repo_base_dir/store/config/ + * repo_base_dir/store/blob/ + * repo_base_dir/store/data/ + * + * @return the relative store directory path to be stored in the repository root + */ + public String getRepositoryStoreDirectory() { + return STORE_DIRECTORY + File.separator + directory; + } + + /** + * Get the relative store directory path to be stored in the global root + *

+ * The global store directories are: + * base_dir/config/ + * base_dir/var/blob/ + * base_dir/var/data/ + * + * @return the relative store directory path to be stored in the global root + */ + public String getGlobalStoreDirectory() { + if (this.equals(CONFIG)) { + return directory; + } + return GLOBAL_STORE_BASE_DIRECTORY + File.separator + directory; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/StoreConstants.java b/scm-dao-xml/src/main/java/sonia/scm/store/StoreConstants.java index 0bd9864cd7..cf24d125c2 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/StoreConstants.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/StoreConstants.java @@ -37,12 +37,14 @@ package sonia.scm.store; * * @author Sebastian Sdorra */ -public interface StoreConstants +public class StoreConstants { - /** Field description */ - public static final String CONFIGDIRECTORY_NAME = "config"; + private StoreConstants() { } + + public static final String CONFIG_DIRECTORY_NAME = "config"; + + public static final String REPOSITORY_METADATA = "metadata"; - /** Field description */ public static final String FILE_EXTENSION = ".xml"; } diff --git a/scm-dao-xml/src/main/java/sonia/scm/update/xml/XmlV1PropertyDAO.java b/scm-dao-xml/src/main/java/sonia/scm/update/xml/XmlV1PropertyDAO.java new file mode 100644 index 0000000000..5bdee89c68 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/update/xml/XmlV1PropertyDAO.java @@ -0,0 +1,30 @@ +package sonia.scm.update.xml; + +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.update.V1Properties; +import sonia.scm.update.V1PropertyDAO; +import sonia.scm.update.V1PropertyReader; + +import javax.inject.Inject; +import java.util.Map; + +public class XmlV1PropertyDAO implements V1PropertyDAO { + + private final ConfigurationEntryStoreFactory configurationEntryStoreFactory; + + @Inject + public XmlV1PropertyDAO(ConfigurationEntryStoreFactory configurationEntryStoreFactory) { + this.configurationEntryStoreFactory = configurationEntryStoreFactory; + } + + @Override + public V1PropertyReader.Instance getProperties(V1PropertyReader reader) { + ConfigurationEntryStore propertyStore = configurationEntryStoreFactory + .withType(V1Properties.class) + .withName(reader.getStoreName()) + .build(); + Map all = propertyStore.getAll(); + return reader.createInstance(all); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java b/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java index 1bfd877f44..ea7f18fbba 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java @@ -36,11 +36,10 @@ package sonia.scm.user.xml; import com.google.inject.Inject; import com.google.inject.Singleton; - +import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.user.User; import sonia.scm.user.UserDAO; import sonia.scm.xml.AbstractXmlDAO; -import sonia.scm.store.ConfigurationStoreFactory; /** * @@ -65,7 +64,10 @@ public class XmlUserDAO extends AbstractXmlDAO @Inject public XmlUserDAO(ConfigurationStoreFactory storeFactory) { - super(storeFactory.getStore(XmlUserDatabase.class, STORE_NAME)); + super(storeFactory + .withType(XmlUserDatabase.class) + .withName(STORE_NAME) + .build()); } //~--- methods -------------------------------------------------------------- diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java index 6b74fce7ca..5b24096eb5 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

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

* http://bitbucket.org/sdorra/scm-manager - * */ @@ -37,15 +36,13 @@ package sonia.scm.xml; import com.google.common.collect.ImmutableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.GenericDAO; import sonia.scm.ModelObject; -import sonia.scm.group.xml.XmlGroupDAO; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.store.ConfigurationStore; import java.util.Collection; -import sonia.scm.store.ConfigurationStore; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -55,7 +52,7 @@ import sonia.scm.store.ConfigurationStore; * @param */ public abstract class AbstractXmlDAO> implements GenericDAO + T extends XmlDatabase> implements GenericDAO { /** Field description */ @@ -65,7 +62,7 @@ public abstract class AbstractXmlDAO store; + protected final ConfigurationStore store; /** Field description */ protected T db; diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java b/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java new file mode 100644 index 0000000000..4b3d9b0f28 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java @@ -0,0 +1,71 @@ +package sonia.scm.xml; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class XmlStreams { + + private static final Logger LOG = LoggerFactory.getLogger(XmlStreams.class); + + private XmlStreams() { + } + + public static void close(XMLStreamWriter writer) { + if (writer != null) { + try { + writer.close(); + } catch (XMLStreamException ex) { + LOG.error("could not close writer", ex); + } + } + } + + public static void close(XMLStreamReader reader) { + if (reader != null) { + try { + reader.close(); + } catch (XMLStreamException ex) { + LOG.error("could not close reader", ex); + } + } + } + + public static XMLStreamReader createReader(Path path) throws IOException, XMLStreamException { + return createReader(Files.newBufferedReader(path, StandardCharsets.UTF_8)); + } + + public static XMLStreamReader createReader(File file) throws IOException, XMLStreamException { + return createReader(file.toPath()); + } + + private static XMLStreamReader createReader(Reader reader) throws XMLStreamException { + return XMLInputFactory.newInstance().createXMLStreamReader(reader); + } + + + public static IndentXMLStreamWriter createWriter(Path path) throws IOException, XMLStreamException { + return createWriter(Files.newBufferedWriter(path, StandardCharsets.UTF_8)); + } + + public static IndentXMLStreamWriter createWriter(File file) throws IOException, XMLStreamException { + return createWriter(file.toPath()); + } + + private static IndentXMLStreamWriter createWriter(Writer writer) throws XMLStreamException { + return new IndentXMLStreamWriter(XMLOutputFactory.newFactory().createXMLStreamWriter(writer)); + } + +} diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java new file mode 100644 index 0000000000..1c5a337abc --- /dev/null +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java @@ -0,0 +1,176 @@ +package sonia.scm.repository.xml; + +import com.google.common.base.Charsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import sonia.scm.SCMContextProvider; +import sonia.scm.io.DefaultFileSystem; +import sonia.scm.io.FileSystem; +import sonia.scm.repository.InitialRepositoryLocationResolver; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class PathBasedRepositoryLocationResolverTest { + + private static final long CREATION_TIME = 42; + + @Mock + private SCMContextProvider contextProvider; + + @Mock + private InitialRepositoryLocationResolver initialRepositoryLocationResolver; + + @Mock + private Clock clock; + + private final FileSystem fileSystem = new DefaultFileSystem(); + + private Path basePath; + + private PathBasedRepositoryLocationResolver resolver; + + @BeforeEach + void beforeEach(@TempDirectory.TempDir Path temp) { + this.basePath = temp; + when(contextProvider.getBaseDirectory()).thenReturn(temp.toFile()); + when(contextProvider.resolve(any(Path.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(initialRepositoryLocationResolver.getPath(anyString())).thenAnswer(invocation -> temp.resolve(invocation.getArgument(0).toString())); + when(clock.millis()).thenReturn(CREATION_TIME); + resolver = createResolver(); + } + + @Test + void shouldCreateInitialDirectory() { + Path path = resolver.forClass(Path.class).createLocation("newId"); + + assertThat(path).isEqualTo(basePath.resolve("newId")); + assertThat(path).isDirectory(); + } + + @Test + void shouldPersistInitialDirectory() { + resolver.forClass(Path.class).createLocation("newId"); + + String content = getXmlFileContent(); + + assertThat(content).contains("newId"); + assertThat(content).contains(basePath.resolve("newId").toString()); + } + + @Test + void shouldPersistWithCreationDate() { + long now = CREATION_TIME + 100; + when(clock.millis()).thenReturn(now); + + resolver.forClass(Path.class).createLocation("newId"); + + assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); + + String content = getXmlFileContent(); + assertThat(content).contains("creation-time=\"" + CREATION_TIME + "\""); + } + + @Test + void shouldUpdateWithModifiedDate() { + long now = CREATION_TIME + 100; + when(clock.millis()).thenReturn(now); + + resolver.forClass(Path.class).createLocation("newId"); + + assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); + assertThat(resolver.getLastModified()).isEqualTo(now); + + String content = getXmlFileContent(); + assertThat(content).contains("creation-time=\"" + CREATION_TIME + "\""); + assertThat(content).contains("last-modified=\"" + now + "\""); + } + + @Nested + class WithExistingData { + + private PathBasedRepositoryLocationResolver resolverWithExistingData; + + @BeforeEach + void createExistingDatabase() { + resolver.forClass(Path.class).createLocation("existingId_1"); + resolver.forClass(Path.class).createLocation("existingId_2"); + resolverWithExistingData = createResolver(); + } + + @Test + void shouldInitWithExistingData() { + Map foundRepositories = new HashMap<>(); + resolverWithExistingData.forClass(Path.class).forAllLocations( + foundRepositories::put + ); + assertThat(foundRepositories) + .containsKeys("existingId_1", "existingId_2"); + } + + @Test + void shouldRemoveFromFile() { + resolverWithExistingData.remove("existingId_1"); + + assertThat(getXmlFileContent()).doesNotContain("existingId_1"); + } + + @Test + void shouldNotUpdateModificationDateForExistingDirectoryMapping() { + long now = CREATION_TIME + 100; + Path path = resolverWithExistingData.create(Path.class).getLocation("existingId_1"); + + assertThat(path).isEqualTo(basePath.resolve("existingId_1")); + + String content = getXmlFileContent(); + assertThat(content).doesNotContain("last-modified=\"" + now + "\""); + } + + @Test + void shouldNotCreateDirectoryForExistingMapping() throws IOException { + Files.delete(basePath.resolve("existingId_1")); + + Path path = resolverWithExistingData.create(Path.class).getLocation("existingId_1"); + + assertThat(path).doesNotExist(); + } + } + + private String getXmlFileContent() { + Path storePath = basePath.resolve("config").resolve("repository-paths.xml"); + + assertThat(storePath).isRegularFile(); + return content(storePath); + } + + private PathBasedRepositoryLocationResolver createResolver() { + return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, fileSystem, clock); + } + + private String content(Path storePath) { + try { + return new String(Files.readAllBytes(storePath), Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java new file mode 100644 index 0000000000..f5571441e7 --- /dev/null +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -0,0 +1,359 @@ +package sonia.scm.repository.xml; + + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import sonia.scm.io.DefaultFileSystem; +import sonia.scm.io.FileSystem; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryPermission; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith({MockitoExtension.class, TempDirectory.class}) +@MockitoSettings(strictness = Strictness.LENIENT) +class XmlRepositoryDAOTest { + + private final Repository REPOSITORY = createRepository("42"); + + @Mock + private PathBasedRepositoryLocationResolver locationResolver; + private Consumer> triggeredOnForAllLocations = none -> {}; + + private FileSystem fileSystem = new DefaultFileSystem(); + + private XmlRepositoryDAO dao; + + @BeforeEach + void createDAO(@TempDirectory.TempDir Path basePath) { + when(locationResolver.create(Path.class)).thenReturn( + new RepositoryLocationResolver.RepositoryLocationResolverInstance() { + @Override + public Path getLocation(String repositoryId) { + return locationResolver.create(repositoryId); + } + + @Override + public Path createLocation(String repositoryId) { + return locationResolver.create(repositoryId); + } + + @Override + public void setLocation(String repositoryId, Path location) { + } + + @Override + public void forAllLocations(BiConsumer consumer) { + triggeredOnForAllLocations.accept(consumer); + } + } + ); + when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation)); + when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString())); + } + + private Path createMockedRepoPath(@TempDirectory.TempDir Path basePath, InvocationOnMock invocation) { + Path resolvedPath = basePath.resolve(invocation.getArgument(0).toString()); + try { + Files.createDirectories(resolvedPath); + } catch (IOException e) { + fail(e); + } + return resolvedPath; + } + + @Nested + class WithEmptyDatabase { + + @BeforeEach + void createDAO() { + dao = new XmlRepositoryDAO(locationResolver, fileSystem); + } + + @Test + void shouldReturnXmlType() { + assertThat(dao.getType()).isEqualTo("xml"); + } + + @Test + void shouldReturnCreationTimeOfLocationResolver() { + long now = 42L; + when(locationResolver.getCreationTime()).thenReturn(now); + assertThat(dao.getCreationTime()).isEqualTo(now); + } + + @Test + void shouldReturnLasModifiedOfLocationResolver() { + long now = 42L; + when(locationResolver.getLastModified()).thenReturn(now); + assertThat(dao.getLastModified()).isEqualTo(now); + } + + @Test + void shouldReturnTrueForEachContainsMethod() { + dao.add(REPOSITORY); + + assertThat(dao.contains(REPOSITORY)).isTrue(); + assertThat(dao.contains(REPOSITORY.getId())).isTrue(); + assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isTrue(); + } + + @Test + void shouldPersistRepository() { + dao.add(REPOSITORY); + + String content = getXmlFileContent(REPOSITORY.getId()); + + assertThat(content).contains("42"); + } + + @Test + void shouldDeleteDataFile() { + dao.add(REPOSITORY); + dao.delete(REPOSITORY); + + assertThat(metadataFile(REPOSITORY.getId())).doesNotExist(); + } + + @Test + void shouldModifyRepository() { + dao.add(REPOSITORY); + Repository changedRepository = REPOSITORY.clone(); + changedRepository.setContact("change"); + + dao.modify(changedRepository); + + String content = getXmlFileContent(REPOSITORY.getId()); + + assertThat(content).contains("change"); + } + + @Test + void shouldReturnFalseForEachContainsMethod() { + assertThat(dao.contains(REPOSITORY)).isFalse(); + assertThat(dao.contains(REPOSITORY.getId())).isFalse(); + assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isFalse(); + } + + @Test + void shouldReturnNullForEachGetMethod() { + assertThat(dao.get("42")).isNull(); + assertThat(dao.get(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isNull(); + } + + @Test + void shouldReturnRepository() { + dao.add(REPOSITORY); + + assertThat(dao.get("42")).isEqualTo(REPOSITORY); + assertThat(dao.get(new NamespaceAndName("space", "42"))).isEqualTo(REPOSITORY); + } + + @Test + void shouldNotReturnTheSameInstance() { + dao.add(REPOSITORY); + + Repository repository = dao.get("42"); + assertThat(repository).isNotSameAs(REPOSITORY); + } + + @Test + void shouldReturnAllRepositories() { + dao.add(REPOSITORY); + + Repository secondRepository = createRepository("23"); + dao.add(secondRepository); + + Collection repositories = dao.getAll(); + assertThat(repositories) + .containsExactlyInAnyOrder(REPOSITORY, secondRepository); + } + + @Test + void shouldModifyRepositoryTwice() { + REPOSITORY.setDescription("HeartOfGold"); + dao.add(REPOSITORY); + assertThat(dao.get("42").getDescription()).isEqualTo("HeartOfGold"); + + Repository heartOfGold = createRepository("42"); + heartOfGold.setDescription("Heart of Gold"); + dao.modify(heartOfGold); + + assertThat(dao.get("42").getDescription()).isEqualTo("Heart of Gold"); + } + + @Test + void shouldRemoveRepository() { + dao.add(REPOSITORY); + assertThat(dao.contains("42")).isTrue(); + + dao.delete(REPOSITORY); + assertThat(dao.contains("42")).isFalse(); + assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isFalse(); + + Path storePath = metadataFile(REPOSITORY.getId()); + + assertThat(storePath).doesNotExist(); + } + + @Test + void shouldRenameTheRepository() { + dao.add(REPOSITORY); + + REPOSITORY.setNamespace("hg2tg"); + REPOSITORY.setName("hog"); + + dao.modify(REPOSITORY); + + Repository repository = dao.get("42"); + assertThat(repository.getNamespace()).isEqualTo("hg2tg"); + assertThat(repository.getName()).isEqualTo("hog"); + + assertThat(dao.contains(new NamespaceAndName("hg2tg", "hog"))).isTrue(); + assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); + + String content = getXmlFileContent(REPOSITORY.getId()); + assertThat(content).contains("hog"); + } + + @Test + void shouldDeleteRepositoryEvenWithChangedNamespace() { + dao.add(REPOSITORY); + + REPOSITORY.setNamespace("hg2tg"); + REPOSITORY.setName("hog"); + + dao.delete(REPOSITORY); + + assertThat(dao.contains(new NamespaceAndName("space", "42"))).isFalse(); + } + + @Test + void shouldRemoveRepositoryDirectoryAfterDeletion() { + dao.add(REPOSITORY); + + Path path = locationResolver.create(REPOSITORY.getId()); + assertThat(path).isDirectory(); + + dao.delete(REPOSITORY); + assertThat(path).doesNotExist(); + } + + @Test + void shouldPersistPermissions() { + REPOSITORY.setPermissions(asList(new RepositoryPermission("trillian", asList("read", "write"), false), new RepositoryPermission("vogons", singletonList("delete"), true))); + dao.add(REPOSITORY); + + String content = getXmlFileContent(REPOSITORY.getId()); + assertThat(content).containsSubsequence("trillian", "read", "write"); + assertThat(content).containsSubsequence("vogons", "delete"); + } + + @Test + void shouldUpdateRepositoryPathDatabse() { + dao.add(REPOSITORY); + + verify(locationResolver, never()).updateModificationDate(); + + dao.modify(REPOSITORY); + + verify(locationResolver).updateModificationDate(); + } + + private String getXmlFileContent(String id) { + Path storePath = metadataFile(id); + + assertThat(storePath).isRegularFile(); + return content(storePath); + } + + private Path metadataFile(String id) { + return locationResolver.create(id).resolve("metadata.xml"); + } + + private String content(Path storePath) { + try { + return new String(Files.readAllBytes(storePath), Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @Nested + class WithExistingRepositories { + + private Path repositoryPath; + + @BeforeEach + void createMetadataFileForRepository(@TempDirectory.TempDir Path basePath) throws IOException { + repositoryPath = basePath.resolve("existing"); + + Files.createDirectories(repositoryPath); + URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml"); + Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml")); + } + + @Test + void shouldReadExistingRepositoriesFromPathDatabase() { + // given + mockExistingPath(); + + // when + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + + // then + assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue(); + } + + @Test + void shouldRefreshWithExistingRepositoriesFromPathDatabase() { + // given + mockExistingPath(); + + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + + // when + dao.refresh(); + + // then + verify(locationResolver).refresh(); + assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue(); + } + + private void mockExistingPath() { + triggeredOnForAllLocations = consumer -> consumer.accept("existing", repositoryPath); + } + } + + private Repository createRepository(String id) { + return new Repository(id, "xml", "space", id); + } +} diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java index cae872538d..3ec16baa57 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java @@ -34,8 +34,15 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- +import org.junit.Test; +import sonia.scm.repository.Repository; import sonia.scm.security.UUIDKeyGenerator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNotNull; + /** * * @author Sebastian Sdorra @@ -52,6 +59,24 @@ public class FileBlobStoreTest extends BlobStoreTestBase @Override protected BlobStoreFactory createBlobStoreFactory() { - return new FileBlobStoreFactory(contextProvider, new UUIDKeyGenerator()); + return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator()); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldStoreAndLoadInRepository() { + BlobStore store = createBlobStoreFactory() + .withName("test") + .forRepository(new Repository("id", "git", "ns", "n")) + .build(); + + Blob createdBlob = store.create("abc"); + List storedBlobs = store.getAll(); + + assertNotNull(createdBlob); + assertThat(storedBlobs) + .isNotNull() + .hasSize(1) + .usingElementComparatorOnFields("id").containsExactly(createdBlob); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java index d0f17fc313..3d9fa3f283 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java @@ -37,25 +37,22 @@ package sonia.scm.store; import com.google.common.io.Closeables; import com.google.common.io.Resources; - import org.junit.Test; - import sonia.scm.security.AssignedPermission; import sonia.scm.security.UUIDKeyGenerator; -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; - import java.net.URL; - import java.util.UUID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -132,19 +129,29 @@ public class JAXBConfigurationEntryStoreTest public void testStoreAndLoad() throws IOException { String name = UUID.randomUUID().toString(); - ConfigurationEntryStore store = - createPermissionStore(RESOURCE_FIXED, name); + ConfigurationEntryStore store = createPermissionStore(RESOURCE_FIXED, name); store.put("a45", new AssignedPermission("tuser4", "repository:create")); - store = - createConfigurationStoreFactory().getStore(AssignedPermission.class, - name); + store = createConfigurationStoreFactory() + .withType(AssignedPermission.class) + .withName(name) + .build(); AssignedPermission ap = store.get("a45"); assertNotNull(ap); assertEquals("tuser4", ap.getName()); - assertEquals("repository:create", ap.getPermission()); + assertEquals("repository:create", ap.getPermission().getValue()); + } + + @Test + public void shouldStoreAndLoadInRepository() throws IOException + { + repoStore.put("abc", new StoreObject("abc_value")); + StoreObject storeObject = repoStore.get("abc"); + + assertNotNull(storeObject); + assertEquals("abc_value", storeObject.getValue()); } /** @@ -154,10 +161,9 @@ public class JAXBConfigurationEntryStoreTest * @return */ @Override - protected ConfigurationEntryStoreFactory createConfigurationStoreFactory() + protected ConfigurationEntryStoreFactory createConfigurationStoreFactory() { - return new JAXBConfigurationEntryStoreFactory(new UUIDKeyGenerator(), - contextProvider); + return new JAXBConfigurationEntryStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator()); } /** @@ -225,8 +231,9 @@ public class JAXBConfigurationEntryStoreTest } copy(resource, name); - - return createConfigurationStoreFactory().getStore(AssignedPermission.class, - name); + return createConfigurationStoreFactory() + .withType(AssignedPermission.class) + .withName(name) + .build(); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java index 4151a6ca20..802f193340 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java @@ -32,9 +32,15 @@ package sonia.scm.store; +import org.junit.Test; +import sonia.scm.repository.Repository; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * Unit tests for {@link JAXBConfigurationStore}. - * + * * @author Sebastian Sdorra */ public class JAXBConfigurationStoreTest extends StoreTestBase { @@ -42,6 +48,24 @@ public class JAXBConfigurationStoreTest extends StoreTestBase { @Override protected ConfigurationStoreFactory createStoreFactory() { - return new JAXBConfigurationStoreFactory(contextProvider); + return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver); + } + + + @Test + @SuppressWarnings("unchecked") + public void shouldStoreAndLoadInRepository() + { + ConfigurationStore store = createStoreFactory() + .withType(StoreObject.class) + .withName("test") + .forRepository(new Repository("id", "git", "ns", "n")) + .build(); + + store.set(new StoreObject("value")); + StoreObject storeObject = store.get(); + + assertNotNull(storeObject); + assertEquals("value", storeObject.getValue()); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java index 9834a48916..04d86aa625 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java @@ -34,14 +34,18 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- +import org.junit.Test; +import sonia.scm.repository.Repository; import sonia.scm.security.UUIDKeyGenerator; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * * @author Sebastian Sdorra */ -public class JAXBDataStoreTest extends DataStoreTestBase -{ +public class JAXBDataStoreTest extends DataStoreTestBase { /** * Method description @@ -52,6 +56,33 @@ public class JAXBDataStoreTest extends DataStoreTestBase @Override protected DataStoreFactory createDataStoreFactory() { - return new JAXBDataStoreFactory(contextProvider, new UUIDKeyGenerator()); + return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator()); + } + + @Override + protected DataStore getDataStore(Class type, Repository repository) { + return createDataStoreFactory() + .withType(type) + .withName("test") + .forRepository(repository) + .build(); + } + + @Override + protected DataStore getDataStore(Class type) { + return createDataStoreFactory() + .withType(type) + .withName("test") + .build(); + } + + @Test + public void shouldStoreAndLoadInRepository() + { + repoStore.put("abc", new StoreObject("abc_value")); + StoreObject storeObject = repoStore.get("abc"); + + assertNotNull(storeObject); + assertEquals("abc_value", storeObject.getValue()); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBPropertyFileAccessTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBPropertyFileAccessTest.java new file mode 100644 index 0000000000..629453e6c4 --- /dev/null +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBPropertyFileAccessTest.java @@ -0,0 +1,119 @@ +package sonia.scm.store; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; +import sonia.scm.io.DefaultFileSystem; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; +import sonia.scm.update.PropertyFileAccess; +import sonia.scm.util.IOUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +@ExtendWith(TempDirectory.class) +@ExtendWith(MockitoExtension.class) +class JAXBPropertyFileAccessTest { + + public static final String REPOSITORY_ID = "repoId"; + public static final String STORE_NAME = "test"; + + @Mock + SCMContextProvider contextProvider; + + RepositoryLocationResolver locationResolver; + + JAXBPropertyFileAccess fileAccess; + + @BeforeEach + void initTempDir(@TempDirectory.TempDir Path tempDir) { + lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); + lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString())); + + locationResolver = new PathBasedRepositoryLocationResolver(contextProvider, new InitialRepositoryLocationResolver(), new DefaultFileSystem()); + + fileAccess = new JAXBPropertyFileAccess(contextProvider, locationResolver); + } + + @Test + void shouldRenameGlobalConfigFile() throws IOException { + Path baseDirectory = contextProvider.getBaseDirectory().toPath(); + Path configDirectory = baseDirectory.resolve(StoreConstants.CONFIG_DIRECTORY_NAME); + + Files.createDirectories(configDirectory); + + Path oldPath = configDirectory.resolve("old" + StoreConstants.FILE_EXTENSION); + Files.createFile(oldPath); + + fileAccess.renameGlobalConfigurationFrom("old").to("new"); + + Path newPath = configDirectory.resolve("new" + StoreConstants.FILE_EXTENSION); + assertThat(oldPath).doesNotExist(); + assertThat(newPath).exists(); + } + + @Nested + class ForExistingRepository { + + + @BeforeEach + void createRepositoryLocation() { + locationResolver.forClass(Path.class).createLocation(REPOSITORY_ID); + } + + @Test + void shouldMoveStoreFileToRepositoryBasedLocation(@TempDirectory.TempDir Path tempDir) throws IOException { + createV1StoreFile(tempDir, "myStore.xml"); + + fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID); + + assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("myStore.xml")).exists(); + } + + @Test + void shouldMoveAllStoreFilesToRepositoryBasedLocations(@TempDirectory.TempDir Path tempDir) throws IOException { + locationResolver.forClass(Path.class).createLocation("repoId2"); + + createV1StoreFile(tempDir, REPOSITORY_ID + ".xml"); + createV1StoreFile(tempDir, "repoId2.xml"); + + PropertyFileAccess.StoreFileTools statisticStoreAccess = fileAccess.forStoreName(STORE_NAME); + statisticStoreAccess.forStoreFiles(statisticStoreAccess::moveAsRepositoryStore); + + assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("repoId.xml")).exists(); + assertThat(tempDir.resolve("repositories").resolve("repoId2").resolve("store").resolve("data").resolve(STORE_NAME).resolve("repoId2.xml")).exists(); + } + } + + private void createV1StoreFile(@TempDirectory.TempDir Path tempDir, String name) throws IOException { + Path v1Dir = tempDir.resolve("var").resolve("data").resolve(STORE_NAME); + IOUtil.mkdirs(v1Dir.toFile()); + Files.createFile(v1Dir.resolve(name)); + } + + @Nested + class ForMissingRepository { + + @Test + void shouldIgnoreStoreFile(@TempDirectory.TempDir Path tempDir) throws IOException { + createV1StoreFile(tempDir, "myStore.xml"); + + fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID); + + assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("myStore.xml")).doesNotExist(); + } + } +} diff --git a/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml b/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml new file mode 100644 index 0000000000..a9e84994dc --- /dev/null +++ b/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml @@ -0,0 +1,9 @@ + + + + existing + space + existing + false + xml + diff --git a/scm-it/pom.xml b/scm-it/pom.xml new file mode 100644 index 0000000000..c38c73ce3e --- /dev/null +++ b/scm-it/pom.xml @@ -0,0 +1,242 @@ + + + + 4.0.0 + + + sonia.scm + scm + 2.0.0-SNAPSHOT + + + sonia.scm + scm-it + + war + 2.0.0-SNAPSHOT + scm-it + + + + sonia.scm + scm-core + 2.0.0-SNAPSHOT + + + + sonia.scm + scm-test + 2.0.0-SNAPSHOT + + + + sonia.scm.plugins + scm-git-plugin + 2.0.0-SNAPSHOT + test + + + + sonia.scm.plugins + scm-git-plugin + 2.0.0-SNAPSHOT + tests + test + + + + sonia.scm.plugins + scm-hg-plugin + 2.0.0-SNAPSHOT + test + + + + sonia.scm.plugins + scm-hg-plugin + 2.0.0-SNAPSHOT + tests + test + + + + sonia.scm.plugins + scm-svn-plugin + 2.0.0-SNAPSHOT + test + + + + sonia.scm.plugins + scm-svn-plugin + 2.0.0-SNAPSHOT + tests + test + + + + io.rest-assured + rest-assured + 3.1.0 + test + + + + org.glassfish + javax.json + 1.0.4 + runtime + + + + + + + + org.apache.maven.plugins + maven-war-plugin + + false + + + + + com.mycila.maven-license-plugin + maven-license-plugin + 1.9.0 + +

http://download.scm-manager.org/licenses/mvn-license.txt
+ + src/** + **/test/** + + + target/** + .hg/** + + true + + + + + + + + + it + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.12 + + + sonia/scm/it/*ITCase.java + + + + + integration-test + + integration-test + + + + verify + + verify + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.10 + + + + sonia.scm + scm-webapp + ${project.version} + war + ${project.build.outputDirectory} + scm-webapp.war + + + + + + copy-war + pre-integration-test + + copy + + + + + + + org.eclipse.jetty + jetty-maven-plugin + ${jetty.maven.version} + + 8085 + STOP + + + scm.home + ${scm.home} + + + scm.stage + ${scm.stage} + + + java.awt.headless + true + + + + /scm + + ${project.basedir}/src/main/conf/jetty.xml + ${project.build.outputDirectory}/scm-webapp.war + 0 + true + + + + start-jetty + pre-integration-test + + deploy-war + + + + stop-jetty + post-integration-test + + stop + + + + + + + + + + DEVELOPMENT + target/scm-it + + + + + + + diff --git a/scm-it/src/main/conf/jetty.xml b/scm-it/src/main/conf/jetty.xml new file mode 100644 index 0000000000..ec7ac555c8 --- /dev/null +++ b/scm-it/src/main/conf/jetty.xml @@ -0,0 +1,72 @@ + + + + + + + + + 16384 + 16384 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java new file mode 100644 index 0000000000..1b48da792b --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java @@ -0,0 +1,170 @@ +package sonia.scm.it; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junitpioneer.jupiter.TempDirectory; +import sonia.scm.it.utils.RepositoryUtil; +import sonia.scm.it.utils.RestUtil; +import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.ScmTypes; +import sonia.scm.it.utils.TestData; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientException; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.stream.Stream; + +import static java.util.Collections.emptyMap; +import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static sonia.scm.it.utils.TestData.JSON_BUILDER; +import static sonia.scm.it.utils.TestData.USER_ANONYMOUS; +import static sonia.scm.it.utils.TestData.WRITE; +import static sonia.scm.it.utils.TestData.getDefaultRepositoryUrl; + +@ExtendWith(TempDirectory.class) +class AnonymousAccessITCase { + + @Test + void shouldAccessIndexResourceWithoutAuthentication() { + ScmRequests.start() + .requestIndexResource() + .assertStatusCode(200); + } + + @Test + void shouldRejectRepositoryResourceWithoutAuthentication() { + assertEquals(401, RestAssured.given() + .when() + .get(RestUtil.REST_BASE_URL.resolve("repositories/")) + .statusCode()); + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class WithAnonymousAccess { + @BeforeAll + void enableAnonymousAccess() { + setAnonymousAccess(true); + } + + @BeforeEach + void createRepository() { + TestData.createDefault(); + } + + @Test + void shouldGrantAnonymousAccessToRepositoryList() { + assertEquals(200, RestAssured.given() + .when() + .get(RestUtil.REST_BASE_URL.resolve("repositories")) + .statusCode()); + } + + @Nested + class WithoutAnonymousAccessForRepository { + + @ParameterizedTest + @ArgumentsSource(ScmTypes.class) + void shouldGrantAnonymousAccessToRepository(String type) { + assertEquals(401, RestAssured.given() + .when() + .get(getDefaultRepositoryUrl(type)) + .statusCode()); + } + + @ParameterizedTest + @ArgumentsSource(ScmTypes.class) + void shouldNotCloneRepository(String type, @TempDirectory.TempDir Path temporaryFolder) { + assertThrows(RepositoryClientException.class, () -> RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile())); + } + } + + @Nested + class WithAnonymousAccessForRepository { + + @BeforeEach + void grantAnonymousAccessToRepo() { + ScmTypes.availableScmTypes().stream().forEach(type -> TestData.createUserPermission(USER_ANONYMOUS, WRITE, type)); + } + + @ParameterizedTest + @ArgumentsSource(ScmTypes.class) + void shouldGrantAnonymousAccessToRepository(String type) { + assertEquals(200, RestAssured.given() + .when() + .get(getDefaultRepositoryUrl(type)) + .statusCode()); + } + + @ParameterizedTest + @ArgumentsSource(ScmTypes.class) + void shouldCloneRepository(String type, @TempDirectory.TempDir Path temporaryFolder) throws IOException { + RepositoryClient client = RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile()); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + } + + @AfterAll + void disableAnonymousAccess() { + setAnonymousAccess(false); + } + } + + private static void setAnonymousAccess(boolean anonymousAccessEnabled) { + RestUtil.given("application/vnd.scmm-config+json;v=2") + .body(createConfig(anonymousAccessEnabled)) + + .when() + .put(RestUtil.REST_BASE_URL.toASCIIString() + "config") + + .then() + .statusCode(HttpServletResponse.SC_NO_CONTENT); + } + + private static String createConfig(boolean anonymousAccessEnabled) { + JsonArray emptyArray = Json.createBuilderFactory(emptyMap()).createArrayBuilder().build(); + return JSON_BUILDER + .add("adminGroups", emptyArray) + .add("adminUsers", emptyArray) + .add("anonymousAccessEnabled", anonymousAccessEnabled) + .add("baseUrl", "https://next-scm.cloudogu.com/scm") + .add("dateFormat", "YYYY-MM-DD HH:mm:ss") + .add("disableGroupingGrid", false) + .add("enableProxy", false) + .add("enabledXsrfProtection", true) + .add("forceBaseUrl", false) + .add("loginAttemptLimit", 100) + .add("loginAttemptLimitTimeout", 300) + .add("loginInfoUrl", "https://login-info.scm-manager.org/api/v1/login-info") + .add("namespaceStrategy", "UsernameNamespaceStrategy") + .add("pluginUrl", "https://oss.cloudogu.com/jenkins/job/scm-manager/job/scm-manager-bitbucket/job/plugin-snapshot/job/master/lastSuccessfulBuild/artifact/plugins/plugin-center.json") + .add("proxyExcludes", emptyArray) + .addNull("proxyPassword") + .add("proxyPort", 8080) + .add("proxyServer", "proxy.mydomain.com") + .addNull("proxyUser") + .add("realmDescription", "SONIA :: SCM Manager") + .add("skipFailedAuthenticators", false) + .build().toString(); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/AutoCompleteITCase.java b/scm-it/src/test/java/sonia/scm/it/AutoCompleteITCase.java new file mode 100644 index 0000000000..f343f322a3 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/AutoCompleteITCase.java @@ -0,0 +1,73 @@ +package sonia.scm.it; + +import org.junit.Before; +import org.junit.Test; +import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.TestData; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AutoCompleteITCase { + + + public static final String CREATED_USER_PREFIX = "user_"; + public static final String CREATED_GROUP_PREFIX = "group_"; + + @Before + public void init() { + TestData.cleanup(); + } + + @Test + public void adminShouldAutoComplete() { + shouldAutocomplete(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN); + } + + @Test + public void userShouldAutoComplete() { + String username = "nonAdmin"; + String password = "pass"; + TestData.createUser(username, password, false, "xml", "email@e.de"); + shouldAutocomplete(username, password); + } + + public void shouldAutocomplete(String username, String password) { + createUsers(); + createGroups(); + ScmRequests.start() + .requestIndexResource(username, password) + .assertStatusCode(200) + .requestAutoCompleteGroups("group*") + .assertStatusCode(200) + .assertAutoCompleteResults(assertAutoCompleteResult(CREATED_GROUP_PREFIX)) + .returnToPrevious() + .requestAutoCompleteUsers("user*") + .assertStatusCode(200) + .assertAutoCompleteResults(assertAutoCompleteResult(CREATED_USER_PREFIX)); + } + + @SuppressWarnings("unchecked") + private Consumer> assertAutoCompleteResult(String id) { + return autoCompleteDtos -> { + IntStream.range(0, 5).forEach(i -> { + assertThat(autoCompleteDtos).as("return maximum 5 entries").hasSize(5); + assertThat(autoCompleteDtos.get(i)).containsEntry("id", id + (i + 1)); + assertThat(autoCompleteDtos.get(i)).containsEntry("displayName", id + (i + 1)); + }); + }; + } + + private void createUsers() { + IntStream.range(0, 6).forEach(i -> TestData.createUser(CREATED_USER_PREFIX + (i + 1), "pass", false, "xml", CREATED_USER_PREFIX + (i + 1) + "@scm-manager.org")); + } + + private void createGroups() { + IntStream.range(0, 6).forEach(i -> TestData.createGroup(CREATED_GROUP_PREFIX + (i + 1), CREATED_GROUP_PREFIX + (i + 1))); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/DiffITCase.java b/scm-it/src/test/java/sonia/scm/it/DiffITCase.java new file mode 100644 index 0000000000..a9d99deab2 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/DiffITCase.java @@ -0,0 +1,264 @@ +package sonia.scm.it; + +import org.apache.http.HttpStatus; +import org.assertj.core.api.AbstractCharSequenceAssert; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.it.utils.RepositoryUtil; +import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.TestData; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.client.api.RepositoryClient; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD; +import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME; + +public class DiffITCase { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + private RepositoryClient svnRepositoryClient; + private RepositoryClient gitRepositoryClient; + private RepositoryClient hgRepositoryClient; + private ScmRequests.RepositoryResponse svnRepositoryResponse; + private ScmRequests.RepositoryResponse hgRepositoryResponse; + private ScmRequests.RepositoryResponse gitRepositoryResponse; + private File svnFolder; + private File gitFolder; + private File hgFolder; + + @Before + public void init() throws IOException { + TestData.createDefault(); + String namespace = ADMIN_USERNAME; + String repo = TestData.getDefaultRepoName("svn"); + svnRepositoryResponse = + ScmRequests.start() + .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD) + .requestRepository(namespace, repo) + .assertStatusCode(HttpStatus.SC_OK); + svnFolder = tempFolder.newFolder("svn"); + svnRepositoryClient = RepositoryUtil.createRepositoryClient("svn", svnFolder); + + repo = TestData.getDefaultRepoName("git"); + gitRepositoryResponse = + ScmRequests.start() + .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD) + .requestRepository(namespace, repo) + .assertStatusCode(HttpStatus.SC_OK); + gitFolder = tempFolder.newFolder("git"); + gitRepositoryClient = RepositoryUtil.createRepositoryClient("git", gitFolder); + + repo = TestData.getDefaultRepoName("hg"); + hgRepositoryResponse = + ScmRequests.start() + .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD) + .requestRepository(namespace, repo) + .assertStatusCode(HttpStatus.SC_OK); + hgFolder = tempFolder.newFolder("hg"); + hgRepositoryClient = RepositoryUtil.createRepositoryClient("hg", hgFolder); + } + + @Test + public void shouldFindDiffsInGitFormat() throws IOException { + String svnDiff = getDiff(RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), svnRepositoryResponse); + String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse); + String hgDiff = getDiff(RepositoryUtil.createAndCommitFile(hgRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), hgRepositoryResponse); + + assertThat(Lists.newArrayList(svnDiff, gitDiff, hgDiff)) + .allSatisfy(diff -> assertThat(diff) + .contains("diff --git ")); + } + + @Test + public void svnAddFileDiffShouldBeConvertedToGitDiff() throws IOException { + String svnDiff = getDiff(RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), svnRepositoryResponse); + String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse); + + String expected = getGitDiffWithoutIndexLine(gitDiff); + assertDiffsAreEqual(svnDiff, expected); + } + + @Test + public void svnDeleteFileDiffShouldBeConvertedToGitDiff() throws IOException { + RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"); + RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"); + + String svnDiff = getDiff(RepositoryUtil.removeAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt"), svnRepositoryResponse); + String gitDiff = getDiff(RepositoryUtil.removeAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt"), gitRepositoryResponse); + + String expected = getGitDiffWithoutIndexLine(gitDiff); + assertDiffsAreEqual(svnDiff, expected); + } + + @Test + public void svnUpdateFileDiffShouldBeConvertedToGitDiff() throws IOException { + RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"); + RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"); + + String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "the updated content of a"), svnRepositoryResponse); + String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "the updated content of a"), gitRepositoryResponse); + + String expected = getGitDiffWithoutIndexLine(gitDiff); + assertDiffsAreEqual(svnDiff, expected); + } + + @Test + public void svnMultipleChangesDiffShouldBeConvertedToGitDiff() throws IOException { + String svnDiff = getDiff(applyMultipleChanges(svnRepositoryClient, "fileToBeDeleted.txt", "fileToBeUpdated.txt", "addedFile.txt"), svnRepositoryResponse); + String gitDiff = getDiff(applyMultipleChanges(gitRepositoryClient, "fileToBeDeleted.txt", "fileToBeUpdated.txt", "addedFile.txt"), gitRepositoryResponse); + + String endOfDiffPart = "\\ No newline at end of file\n"; + String[] gitDiffs = gitDiff.split(endOfDiffPart); + List expected = Arrays.stream(gitDiffs) + .map(this::getGitDiffWithoutIndexLine) + .collect(Collectors.toList()); + assertThat(svnDiff.split(endOfDiffPart)) + .containsExactlyInAnyOrderElementsOf(expected); + } + + @Test + public void svnMultipleSubFolderChangesDiffShouldBeConvertedToGitDiff() throws IOException { + String svnDiff = getDiff(applyMultipleChanges(svnRepositoryClient, "a/b/fileToBeDeleted.txt", "a/c/fileToBeUpdated.txt", "a/d/addedFile.txt"), svnRepositoryResponse); + String gitDiff = getDiff(applyMultipleChanges(gitRepositoryClient, "a/b/fileToBeDeleted.txt", "a/c/fileToBeUpdated.txt", "a/d/addedFile.txt"), gitRepositoryResponse); + + String endOfDiffPart = "\\ No newline at end of file\n"; + String[] gitDiffs = gitDiff.split(endOfDiffPart); + List expected = Arrays.stream(gitDiffs) + .map(this::getGitDiffWithoutIndexLine) + .collect(Collectors.toList()); + assertThat(svnDiff.split(endOfDiffPart)) + .containsExactlyInAnyOrderElementsOf(expected); + } + + @Test + public void svnLargeChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException { + String fileName = "SvnDiffGenerator_forTest"; + RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, ""); + RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, ""); + + String fileContent = getFileContent("/diff/largefile/original/SvnDiffGenerator_forTest"); + String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse); + String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse); + assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff)); + + fileContent = getFileContent("/diff/largefile/modified/v1/SvnDiffGenerator_forTest"); + svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse); + gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse); + assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff)); + + fileContent = getFileContent("/diff/largefile/modified/v2/SvnDiffGenerator_forTest"); + svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse); + gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse); + assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff)); + } + + /** + * FIXME: the binary Git Diff output is not GIT conform + */ + @Test + @Ignore + @SuppressWarnings("squid:S1607") + public void svnBinaryChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException { + String fileName = "binary"; + File file = new File(svnRepositoryClient.getWorkingCopy(), fileName); + Files.copy(Paths.get(getClass().getResource("/diff/binaryfile/echo").toURI()), Paths.get(file.toURI())); + Changeset commit = RepositoryUtil.addFileAndCommit(svnRepositoryClient, fileName, ADMIN_USERNAME, ""); + + file = new File(gitRepositoryClient.getWorkingCopy(), fileName); + Files.copy(Paths.get(getClass().getResource("/diff/binaryfile/echo").toURI()), Paths.get(file.toURI())); + + Changeset commit1 = RepositoryUtil.addFileAndCommit(gitRepositoryClient, fileName, ADMIN_USERNAME, ""); + String svnDiff = getDiff(commit, svnRepositoryResponse); + String gitDiff = getDiff(commit1, gitRepositoryResponse); + assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff)); + + } + + @Test + public void svnRenameChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException { + String fileName = "a.txt"; + RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, "content of a"); + RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, "content of a"); + + String newFileName = "renamed_a.txt"; + File file = new File(svnRepositoryClient.getWorkingCopy(), fileName); + file.renameTo(new File(svnRepositoryClient.getWorkingCopy(), newFileName)); + + String svnDiff = getDiff(RepositoryUtil.addFileAndCommit(svnRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), svnRepositoryResponse); + + file = new File(gitRepositoryClient.getWorkingCopy(), fileName); + file.renameTo(new File(gitRepositoryClient.getWorkingCopy(), newFileName)); + String gitDiff = getDiff(RepositoryUtil.addFileAndCommit(gitRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), gitRepositoryResponse); + + String expected = getGitDiffWithoutIndexLine(gitDiff); + assertDiffsAreEqual(svnDiff, expected); + } + + public String getFileContent(String name) throws URISyntaxException, IOException { + Path path; + path = Paths.get(getClass().getResource(name).toURI()); + Stream lines = Files.lines(path); + String data = lines.collect(Collectors.joining("\n")); + lines.close(); + return data; + } + + /** + * The index line is not provided from the svn git formatter and it is not needed in the ui diff view + * for more details about the git diff format: https://git-scm.com/docs/git-diff + * + * @param gitDiff + * @return diff without the index line + */ + private String getGitDiffWithoutIndexLine(String gitDiff) { + return gitDiff.replaceAll(".*(index.*\n)", ""); + } + + private void assertDiffsAreEqual(String svnDiff, String gitDiff) { + assertThat(svnDiff) + .as("diffs are different\n\nsvn:\n==================================================\n\n%s\n\ngit:\n==================================================\n\n%s)", svnDiff, gitDiff) + .isEqualTo(gitDiff); + } + + private String getDiff(Changeset svnChangeset, ScmRequests.RepositoryResponse svnRepositoryResponse) { + return svnRepositoryResponse.requestChangesets() + .requestDiffInGitFormat(svnChangeset.getId()) + .getResponse() + .body() + .asString(); + } + + private Changeset applyMultipleChanges(RepositoryClient repositoryClient, String fileToBeDeleted, final String fileToBeUpdated, final String addedFile) throws IOException { + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileToBeDeleted, "file to be deleted"); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileToBeUpdated, "file to be updated"); + Map addedFiles = new HashMap() {{ + put(addedFile, "content"); + }}; + Map modifiedFiles = new HashMap() {{ + put(fileToBeUpdated, "the updated content"); + }}; + ArrayList removedFiles = Lists.newArrayList(fileToBeDeleted); + return RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java b/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java new file mode 100644 index 0000000000..1490a917df --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.it; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.eclipse.jgit.api.CommitCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.eclipse.jgit.transport.RemoteRefUpdate.Status; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.it.utils.RestUtil; +import sonia.scm.it.utils.TestData; +import sonia.scm.web.VndMediaType; + +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static sonia.scm.it.utils.RestUtil.given; + +/** + * Integration Tests for Git with non fast-forward pushes. + */ +public class GitNonFastForwardITCase { + + private File workingCopy; + private Git git; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Before + public void createAndCloneTestRepository() throws IOException, GitAPIException { + TestData.createDefault(); + this.workingCopy = tempFolder.newFolder(); + + this.git = clone(RestUtil.BASE_URL.toASCIIString() + "repo/scmadmin/HeartOfGold-git"); + } + + @After + public void cleanup() { + TestData.cleanup(); + } + + /** + * Ensures that the normal behaviour (non fast-forward is allowed), is restored after the tests are executed. + */ + @AfterClass + public static void allowNonFastForward() { + setNonFastForwardDisallowed(false); + } + + @Test + public void testGitPushAmendWithoutForce() throws IOException, GitAPIException { + setNonFastForwardDisallowed(false); + + addTestFileToWorkingCopyAndCommit("a"); + pushAndAssert(false, Status.OK); + + addTestFileToWorkingCopyAndCommitAmend("c"); + pushAndAssert(false, Status.REJECTED_NONFASTFORWARD); + } + + @Test + public void testGitPushAmendWithForce() throws IOException, GitAPIException { + setNonFastForwardDisallowed(false); + + addTestFileToWorkingCopyAndCommit("a"); + pushAndAssert(false, Status.OK); + + addTestFileToWorkingCopyAndCommitAmend("c"); + pushAndAssert(true, Status.OK); + } + + @Test + public void testGitPushAmendForceWithDisallowNonFastForward() throws GitAPIException, IOException { + setNonFastForwardDisallowed(true); + + addTestFileToWorkingCopyAndCommit("a"); + pushAndAssert(false, Status.OK); + + addTestFileToWorkingCopyAndCommitAmend("c"); + pushAndAssert(true, Status.REJECTED_OTHER_REASON); + + setNonFastForwardDisallowed(false); + } + + private CredentialsProvider createCredentialProvider() { + return new UsernamePasswordCredentialsProvider( + RestUtil.ADMIN_USERNAME, RestUtil.ADMIN_PASSWORD + ); + } + + private Git clone(String url) throws GitAPIException { + return Git.cloneRepository() + .setDirectory(workingCopy) + .setURI(url) + .setCredentialsProvider(createCredentialProvider()) + .call(); + } + + private void addTestFileToWorkingCopyAndCommit(String name) throws IOException, GitAPIException { + addTestFile(name); + prepareCommit() + .setMessage("added ".concat(name)) + .call(); + } + + private void addTestFile(String name) throws IOException, GitAPIException { + String filename = name.concat(".txt"); + Files.write(name, new File(workingCopy, filename), Charsets.UTF_8); + git.add().addFilepattern(filename).call(); + } + + private CommitCommand prepareCommit() { + return git.commit() + .setAuthor("Trillian McMillian", "trillian@hitchhiker.com"); + } + + private void pushAndAssert(boolean force, Status expectedStatus) throws GitAPIException { + Iterable results = push(force); + assertStatus(results, expectedStatus); + } + + private Iterable push(boolean force) throws GitAPIException { + return git.push() + .setRemote("origin") + .add("master") + .setForce(force) + .setCredentialsProvider(createCredentialProvider()) + .call(); + } + + private void assertStatus(Iterable results, Status expectedStatus) { + for ( PushResult pushResult : results ) { + assertStatus(pushResult, expectedStatus); + } + } + + private void assertStatus(PushResult pushResult, Status expectedStatus) { + for ( RemoteRefUpdate remoteRefUpdate : pushResult.getRemoteUpdates() ) { + assertEquals(expectedStatus, remoteRefUpdate.getStatus()); + } + } + + private void addTestFileToWorkingCopyAndCommitAmend(String name) throws IOException, GitAPIException { + addTestFile(name); + prepareCommit() + .setMessage("amend commit, because of missing ".concat(name)) + .setAmend(true) + .call(); + } + + private static void setNonFastForwardDisallowed(boolean nonFastForwardDisallowed) { + String config = String.format("{'disabled': false, 'gcExpression': null, 'nonFastForwardDisallowed': %s}", nonFastForwardDisallowed) + .replace('\'', '"'); + + given(VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX) + .body(config) + + .when() + .put(RestUtil.REST_BASE_URL.toASCIIString() + "config/git" ) + + .then() + .statusCode(HttpServletResponse.SC_NO_CONTENT); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/I18nServletITCase.java b/scm-it/src/test/java/sonia/scm/it/I18nServletITCase.java new file mode 100644 index 0000000000..6597880665 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/I18nServletITCase.java @@ -0,0 +1,19 @@ +package sonia.scm.it; + +import org.junit.Test; +import sonia.scm.it.utils.ScmRequests; + +import static org.assertj.core.api.Assertions.assertThat; + +public class I18nServletITCase { + + @Test + public void shouldGetCollectedPluginTranslations() { + ScmRequests.start() + .requestPluginTranslations("de") + .assertStatusCode(200) + .assertSingleProperty(value -> assertThat(value).isNotNull(), "scm-git-plugin") + .assertSingleProperty(value -> assertThat(value).isNotNull(), "scm-hg-plugin") + .assertSingleProperty(value -> assertThat(value).isNotNull(), "scm-svn-plugin"); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/IndexITCase.java b/scm-it/src/test/java/sonia/scm/it/IndexITCase.java new file mode 100644 index 0000000000..4a621d962f --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/IndexITCase.java @@ -0,0 +1,48 @@ +package sonia.scm.it; + +import io.restassured.RestAssured; +import org.apache.http.HttpStatus; +import org.junit.Test; +import sonia.scm.it.utils.RestUtil; +import sonia.scm.web.VndMediaType; + +import static sonia.scm.it.utils.RegExMatcher.matchesPattern; +import static sonia.scm.it.utils.RestUtil.given; + +public class IndexITCase { + + @Test + public void shouldLinkEverythingForAdmin() { + given(VndMediaType.INDEX) + + .when() + .get(RestUtil.createResourceUrl("")) + + .then() + .statusCode(HttpStatus.SC_OK) + .body( + "_links.repositories.href", matchesPattern(".+/repositories/"), + "_links.users.href", matchesPattern(".+/users/"), + "_links.groups.href", matchesPattern(".+/groups/"), + "_links.config.href", matchesPattern(".+/config"), + "_links.gitConfig.href", matchesPattern(".+/config/git"), + "_links.hgConfig.href", matchesPattern(".+/config/hg"), + "_links.svnConfig.href", matchesPattern(".+/config/svn") + ); + } + + @Test + public void shouldCreateLoginLinksForAnonymousAccess() { + RestAssured.given() // do not specify user credentials + + .when() + .get(RestUtil.createResourceUrl("")) + + .then() + .statusCode(HttpStatus.SC_OK) + .body( + "_links.login.href", matchesPattern(".+/auth/.+") + ); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/MeITCase.java b/scm-it/src/test/java/sonia/scm/it/MeITCase.java new file mode 100644 index 0000000000..89c6eeb7b8 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/MeITCase.java @@ -0,0 +1,67 @@ +package sonia.scm.it; + +import org.junit.Before; +import org.junit.Test; +import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.TestData; + +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() + .requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) + .requestMe() + .assertStatusCode(200) + .requestChangePassword(TestData.USER_SCM_ADMIN, newPassword) + .assertStatusCode(204); + // assert password is changed -> login with the new Password than undo changes + ScmRequests.start() + .requestIndexResource(TestData.USER_SCM_ADMIN, newPassword) + .requestMe() + .assertStatusCode(200) + .requestChangePassword(newPassword, TestData.USER_SCM_ADMIN) + .assertStatusCode(204); + } + + @Test + public void nonAdminUserShouldChangeOwnPassword() { + String newPassword = "pass1"; + String username = "user1"; + String password = "pass"; + TestData.createUser(username, password,false,"xml", "em@l.de"); + // user change the own password + ScmRequests.start() + .requestIndexResource(username, password) + .requestMe() + .assertStatusCode(200) + .requestChangePassword(password, newPassword) + .assertStatusCode(204); + // assert password is changed -> login with the new Password than undo changes + ScmRequests.start() + .requestIndexResource(username, newPassword) + .requestMe() + .assertStatusCode(200); + + } + + @Test + public void shouldHidePasswordLinkIfUserTypeIsNotXML() { + String newUser = "user"; + String password = "pass"; + String type = "not XML Type"; + TestData.createUser(newUser, password, true, type, "user@scm-manager.org"); + ScmRequests.start() + .requestIndexResource(newUser, password) + .requestMe() + .assertStatusCode(200) + .assertPasswordLinkDoesNotExists(); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java new file mode 100644 index 0000000000..926be5459f --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + */ + + +package sonia.scm.it; + +import org.apache.http.HttpStatus; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import sonia.scm.it.utils.RepositoryUtil; +import sonia.scm.it.utils.TestData; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientException; +import sonia.scm.web.VndMediaType; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.ScmTypes.availableScmTypes; +import static sonia.scm.it.utils.TestData.OWNER; +import static sonia.scm.it.utils.TestData.READ; +import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN; +import static sonia.scm.it.utils.TestData.WRITE; +import static sonia.scm.it.utils.TestData.callRepository; + +@RunWith(Parameterized.class) +public class PermissionsITCase { + + public static final String USER_READ = "user_read"; + public static final String USER_PASS = "pass"; + private static final String USER_WRITE = "user_write"; + private static final String USER_OWNER = "user_owner"; + private static final String USER_OTHER = "user_other"; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final String repositoryType; + private Collection createdPermissions; + + + public PermissionsITCase(String repositoryType) { + this.repositoryType = repositoryType; + } + + @Parameters(name = "{0}") + public static Collection createParameters() { + return availableScmTypes(); + } + + @Before + public void prepareEnvironment() { + TestData.createDefault(); + TestData.createNotAdminUser(USER_READ, USER_PASS); + TestData.createUserPermission(USER_READ, READ, repositoryType); + TestData.createNotAdminUser(USER_WRITE, USER_PASS); + TestData.createUserPermission(USER_WRITE, WRITE, repositoryType); + TestData.createNotAdminUser(USER_OWNER, USER_PASS); + TestData.createUserPermission(USER_OWNER, OWNER, repositoryType); + TestData.createNotAdminUser(USER_OTHER, USER_PASS); + createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER); + } + + @Test + public void readUserShouldNotSeePermissions() { + assertNull(callRepository(USER_READ, USER_PASS, repositoryType, HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href")); + } + + @Test + public void readUserShouldNotSeeBruteForcePermissions() { + given(VndMediaType.REPOSITORY_PERMISSION, USER_READ, USER_PASS) + .when() + .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + public void writeUserShouldNotSeePermissions() { + assertNull(callRepository(USER_WRITE, USER_PASS, repositoryType, HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href")); + } + + @Test + public void writeUserShouldNotSeeBruteForcePermissions() { + given(VndMediaType.REPOSITORY_PERMISSION, USER_WRITE, USER_PASS) + .when() + .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + public void ownerShouldSeePermissions() { + List userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType); + Assertions.assertThat(userPermissions).extracting(e -> e.get("name")).containsAll(createdPermissions); + } + + @Test + public void otherUserShouldNotSeeRepository() { + callRepository(USER_OTHER, USER_PASS, repositoryType, HttpStatus.SC_FORBIDDEN); + } + + @Test + public void otherUserShouldNotSeeBruteForcePermissions() { + given(VndMediaType.REPOSITORY_PERMISSION, USER_OTHER, USER_PASS) + .when() + .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + public void readUserShouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_READ, USER_PASS); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + + @Test + public void writeUserShouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_WRITE, USER_PASS); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + + @Test + public void ownerShouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_OWNER, USER_PASS); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + + @Test + public void otherUserShouldNotCloneRepository() { + TestData.callRepository(USER_OTHER, USER_PASS, repositoryType, HttpStatus.SC_FORBIDDEN); + } + + @Test(expected = RepositoryClientException.class) + public void userWithReadPermissionShouldBeNotAuthorizedToCommit() throws IOException { + createAndCommit(USER_READ); + } + + @Test + public void userWithOwnerPermissionShouldBeAuthorizedToCommit() throws IOException { + createAndCommit(USER_OWNER); + } + + @Test + public void userWithWritePermissionShouldBeAuthorizedToCommit() throws IOException { + createAndCommit(USER_WRITE); + } + + private void createAndCommit(String username) throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), username, PermissionsITCase.USER_PASS); + addAndCommitRandomFile(client, username); + } + + @Test + public void userWithOwnerPermissionShouldBeAuthorizedToDeleteRepository(){ + assertDeleteRepositoryOperation(HttpStatus.SC_NO_CONTENT, HttpStatus.SC_NOT_FOUND, USER_OWNER, repositoryType); + } + + @Test + public void userWithReadPermissionShouldNotBeAuthorizedToDeleteRepository(){ + assertDeleteRepositoryOperation(HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_READ, repositoryType); + } + + @Test + public void userWithWritePermissionShouldNotBeAuthorizedToDeleteRepository(){ + assertDeleteRepositoryOperation(HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_WRITE, repositoryType); + } + + private void assertDeleteRepositoryOperation(int expectedDeleteStatus, int expectedGetStatus, String user, String repositoryType) { + given(VndMediaType.REPOSITORY, user, PermissionsITCase.USER_PASS) + + .when() + .delete(TestData.getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedDeleteStatus); + + given(VndMediaType.REPOSITORY, user, PermissionsITCase.USER_PASS) + + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedGetStatus); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java new file mode 100644 index 0000000000..c49a65bea2 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + */ + + +package sonia.scm.it; + +//~--- non-JDK imports -------------------------------------------------------- + +import org.apache.http.HttpStatus; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import sonia.scm.it.utils.RepositoryUtil; +import sonia.scm.it.utils.TestData; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.web.VndMediaType; + +import java.io.IOException; +import java.util.Collection; +import java.util.Objects; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static sonia.scm.it.utils.RegExMatcher.matchesPattern; +import static sonia.scm.it.utils.RestUtil.createResourceUrl; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.ScmTypes.availableScmTypes; +import static sonia.scm.it.utils.TestData.repositoryJson; + +@RunWith(Parameterized.class) +public class RepositoriesITCase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final String repositoryType; + + private String repositoryUrl; + + public RepositoriesITCase(String repositoryType) { + this.repositoryType = repositoryType; + this.repositoryUrl = TestData.getDefaultRepositoryUrl(repositoryType); + } + + @Parameters(name = "{0}") + public static Collection createParameters() { + return availableScmTypes(); + } + + @Before + public void createRepository() { + TestData.createDefault(); + } + + @Test + public void shouldCreateSuccessfully() { + given(VndMediaType.REPOSITORY) + + .when() + .get(repositoryUrl) + + .then() + .statusCode(HttpStatus.SC_OK) + .body( + "name", equalTo("HeartOfGold-" + repositoryType), + "type", equalTo(repositoryType), + "creationDate", matchesPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+Z"), + "lastModified", is(nullValue()), + "_links.self.href", equalTo(repositoryUrl) + ); + } + + @Test + public void shouldDeleteSuccessfully() { + given(VndMediaType.REPOSITORY) + + .when() + .delete(repositoryUrl) + + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + + given(VndMediaType.REPOSITORY) + + .when() + .get(repositoryUrl) + + .then() + .statusCode(HttpStatus.SC_NOT_FOUND); + } + + @Test + public void shouldRejectMultipleCreations() { + String repositoryJson = repositoryJson(repositoryType); + given(VndMediaType.REPOSITORY) + .body(repositoryJson) + + .when() + .post(createResourceUrl("repositories")) + + .then() + .statusCode(HttpStatus.SC_CONFLICT); + } + + @Test + public void shouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.getRoot()); + assertEquals("expected metadata dir", 1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + + @Test + public void shouldCommitFiles() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "scmadmin", "scmadmin"); + String name = RepositoryUtil.addAndCommitRandomFile(client, "scmadmin"); + RepositoryClient checkClient = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "scmadmin", "scmadmin"); + Assertions.assertThat(checkClient.getWorkingCopy().list()).contains(name); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java new file mode 100644 index 0000000000..83baa89463 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -0,0 +1,429 @@ +package sonia.scm.it; + +import groovy.util.logging.Slf4j; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.apache.http.HttpStatus; +import org.assertj.core.util.Lists; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import sonia.scm.it.utils.RepositoryUtil; +import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.TestData; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.client.api.ClientCommand; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.web.VndMediaType; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.lang.Thread.sleep; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertNotNull; +import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD; +import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.ScmTypes.availableScmTypes; + +@RunWith(Parameterized.class) +@Slf4j +public class RepositoryAccessITCase { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private final String repositoryType; + private File folder; + private ScmRequests.RepositoryResponse repositoryResponse; + + public RepositoryAccessITCase(String repositoryType) { + this.repositoryType = repositoryType; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection createParameters() { + return availableScmTypes(); + } + + @Before + public void init() { + TestData.createDefault(); + folder = tempFolder.getRoot(); + String namespace = ADMIN_USERNAME; + String repo = TestData.getDefaultRepoName(repositoryType); + repositoryResponse = + ScmRequests.start() + .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD) + .requestRepository(namespace, repo) + .assertStatusCode(HttpStatus.SC_OK); + } + + @Test + public void shouldFindBranches() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + + Assume.assumeTrue("There are no branches for " + repositoryType, repositoryClient.isCommandSupported(ClientCommand.BRANCH)); + + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a"); + + String branchesUrl = given() + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.branches.href"); + + Object branchName = given() + .when() + .get(branchesUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_embedded.branches[0].name"); + + assertNotNull(branchName); + } + + @Test + public void shouldFindTags() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + + Assume.assumeTrue("There are no tags for " + repositoryType, repositoryClient.isCommandSupported(ClientCommand.TAG)); + + Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "a.txt", "a"); + + String tagName = "v1.0"; + String repositoryUrl = TestData.getDefaultRepositoryUrl(repositoryType); + String tagsUrl = given() + .when() + .get(repositoryUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.tags.href"); + + ExtractableResponse response = given(VndMediaType.TAG_COLLECTION, ADMIN_USERNAME, ADMIN_PASSWORD) + .when() + .get(tagsUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract(); + + assertThat(response).isNotNull(); + assertThat(response.body()).isNotNull(); + assertThat(response.body().asString()) + .isNotNull() + .isNotBlank(); + + RepositoryUtil.addTag(repositoryClient, changeset.getId(), tagName); + response = given(VndMediaType.TAG_COLLECTION, ADMIN_USERNAME, ADMIN_PASSWORD) + .when() + .get(tagsUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract(); + + assertThat(response).isNotNull(); + assertThat(response.body()).isNotNull(); + assertThat(response.body().asString()) + .isNotNull() + .isNotBlank(); + + assertThat(response.body().jsonPath().getString("_links.self.href")) + .as("assert tags self link") + .isNotNull() + .contains(repositoryUrl + "/tags/"); + + assertThat(response.body().jsonPath().getList("_embedded.tags")) + .as("assert tag size") + .isNotNull() + .size() + .isGreaterThan(0); + + assertThat(response.body().jsonPath().getMap("_embedded.tags.find{it.name=='" + tagName + "'}")) + .as("assert tag name and revision") + .isNotNull() + .hasSize(3) + .containsEntry("name", tagName) + .containsEntry("revision", changeset.getId()); + + assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.self.href")) + .as("assert single tag self link") + .isNotNull() + .contains(String.format("%s/tags/%s", repositoryUrl, tagName)); + + assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.sources.href")) + .as("assert single tag source link") + .isNotNull() + .contains(String.format("%s/sources/%s", repositoryUrl, changeset.getId())); + + assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.changeset.href")) + .as("assert single tag changesets link") + .isNotNull() + .contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId())); + } + + @Test + @SuppressWarnings("squid:S2925") + public void shouldReadContent() throws IOException, InterruptedException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a"); + tempFolder.newFolder("subfolder"); + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "subfolder/a.txt", "sub-a"); + + sleep(1000); + + String sourcesUrl = given() + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.sources.href"); + + String rootContentUrl = given() + .when() + .get(sourcesUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_embedded.children.find{it.name=='a.txt'}._links.self.href"); + + given() + .when() + .get(rootContentUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("a")); + + String subfolderSourceUrl = given() + .when() + .get(sourcesUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_embedded.children.find{it.name=='subfolder'}._links.self.href"); + String selfOfSubfolderUrl = given() + .when() + .get(subfolderSourceUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.self.href"); + assertThat(subfolderSourceUrl).isEqualTo(selfOfSubfolderUrl); + String subfolderContentUrl = given() + .when() + .get(subfolderSourceUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_embedded.children[0]._links.self.href"); + given() + .when() + .get(subfolderContentUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("sub-a")); + } + + @Test + public void shouldFindChangesets() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a"); + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "b.txt", "b"); + + String changesetsUrl = given() + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.changesets.href"); + + List changesets = given() + .when() + .get(changesetsUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_embedded.changesets.id"); + + assertThat(changesets).size().isBetween(2, 3); // svn has an implicit root revision '0' that is extra to the two commits + } + + + @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"); + repositoryResponse + .requestSources() + .requestSelf("folder") + .requestSelf("subfolder") + .requestFileHistory("a.txt") + .assertStatusCode(HttpStatus.SC_OK) + .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(); + repositoryResponse + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .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(); + repositoryResponse + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .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(); + repositoryResponse + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) + .assertModified(modifiedFiles -> assertThat(modifiedFiles) + .hasSize(1) + .containsExactly(fileName)) + .assertRemoved(removedFiles -> assertThat(removedFiles) + .hasSize(0)) + .assertAdded(addedFiles -> assertThat(addedFiles) + .hasSize(0)); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldFindMultipleModifications() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "b.txt", "b"); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "c.txt", "c"); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "d.txt", "d"); + Map addedFiles = new HashMap() {{ + put("a.txt", "bla bla"); + }}; + Map modifiedFiles = new HashMap() {{ + put("b.txt", "new content"); + }}; + ArrayList removedFiles = Lists.newArrayList("c.txt", "d.txt"); + Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); + + String revision = changeset.getId(); + repositoryResponse + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) + .assertAdded(a -> assertThat(a) + .hasSize(1) + .containsExactly("a.txt")) + .assertModified(m -> assertThat(m) + .hasSize(1) + .containsExactly("b.txt")) + .assertRemoved(r -> assertThat(r) + .hasSize(2) + .containsExactly("c.txt", "d.txt")); + } + + @Test + @SuppressWarnings("unchecked") + public void svnShouldCreateOneModificationPerFolder() throws IOException { + Assume.assumeThat(repositoryType, equalTo("svn")); + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "bbb/bb/b.txt", "b"); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "ccc/cc/c.txt", "c"); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "ddd/dd/d.txt", "d"); + Map addedFiles = new HashMap() + {{ + put("aaa/aa/a.txt", "bla bla"); + }}; + Map modifiedFiles = new HashMap() + {{ + put("bbb/bb/b.txt", "new content"); + }}; + ArrayList removedFiles = Lists.newArrayList("ccc/cc/c.txt", "ddd/dd/d.txt"); + Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); + + String revision = changeset.getId(); + repositoryResponse + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) + .assertAdded(a -> assertThat(a) + .hasSize(3) + .containsExactly("aaa/aa/a.txt", "aaa", "aaa/aa")) + .assertModified(m-> assertThat(m) + .hasSize(1) + .containsExactly("bbb/bb/b.txt")) + .assertRemoved(r -> assertThat(r) + .hasSize(2) + .containsExactly("ccc/cc/c.txt", "ddd/dd/d.txt")); + } +} + diff --git a/scm-it/src/test/java/sonia/scm/it/RoleITCase.java b/scm-it/src/test/java/sonia/scm/it/RoleITCase.java new file mode 100644 index 0000000000..331a251d23 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RoleITCase.java @@ -0,0 +1,77 @@ +package sonia.scm.it; + +import org.apache.http.HttpStatus; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.TestData; +import sonia.scm.web.VndMediaType; + +import static org.junit.Assert.assertNotNull; +import static sonia.scm.it.PermissionsITCase.USER_PASS; +import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD; +import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN; +import static sonia.scm.it.utils.TestData.callRepository; + +public class RoleITCase { + + private static final String USER = "user"; + public static final String ROLE_NAME = "permission-role"; + + @Before + public void init() { + TestData.createDefault(); + TestData.createNotAdminUser(USER, USER_PASS); + } + + @Test + public void userShouldSeePermissionsAfterAddingRoleToUser() { + callRepository(USER, USER_PASS, "git", HttpStatus.SC_FORBIDDEN); + + String repositoryRolesUrl = new ScmRequests() + .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD) + .getUrl("repositoryRoles"); + + given() + .when() + .delete(repositoryRolesUrl + ROLE_NAME) + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + + given(VndMediaType.REPOSITORY_ROLE) + .when() + .content("{" + + "\"name\": \"" + ROLE_NAME + "\"," + + "\"verbs\": [\"read\",\"permissionRead\"]" + + "}") + .post(repositoryRolesUrl) + .then() + .statusCode(HttpStatus.SC_CREATED); + + String permissionUrl = given(VndMediaType.REPOSITORY, USER_SCM_ADMIN, USER_SCM_ADMIN) + .when() + .get(TestData.getDefaultRepositoryUrl("git")) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href"); + + given(VndMediaType.REPOSITORY_PERMISSION) + .when() + .content("{\n" + + "\t\"role\": \"" + ROLE_NAME + "\",\n" + + "\t\"name\": \"" + USER + "\",\n" + + "\t\"groupPermission\": false\n" + + "\t\n" + + "}") + .post(permissionUrl) + .then() + .statusCode(HttpStatus.SC_CREATED); + + assertNotNull(callRepository(USER, USER_PASS, "git", HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href")); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/UserITCase.java b/scm-it/src/test/java/sonia/scm/it/UserITCase.java new file mode 100644 index 0000000000..231527e9fc --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/UserITCase.java @@ -0,0 +1,100 @@ +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", "user@scm-manager.org"); + String newPassword = "new_password"; + // admin change the own password + ScmRequests.start() + .requestIndexResource(newUser, password) + .assertStatusCode(200) + .requestUser(newUser) + .assertStatusCode(200) + .assertPassword(Assert::assertNull) + .requestChangePassword(newPassword) + .assertStatusCode(204); + // assert password is changed -> login with the new Password + ScmRequests.start() + .requestIndexResource(newUser, newPassword) + .assertStatusCode(200) + .requestUser(newUser) + .assertPassword(Assert::assertNull); + } + + @Test + public void adminShouldChangePasswordOfOtherUser() { + String newUser = "user"; + String password = "pass"; + TestData.createUser(newUser, password, true, "xml", "user@scm-manager.org"); + String newPassword = "new_password"; + // admin change the password of the user + ScmRequests.start() + .requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) + .assertStatusCode(200) + .requestUser(newUser) + .assertStatusCode(200) + .assertPassword(Assert::assertNull) + .requestChangePassword(newPassword) // the oldPassword is not needed in the user resource + .assertStatusCode(204); + // assert password is changed + ScmRequests.start() + .requestIndexResource(newUser, newPassword) + .assertStatusCode(200) + .requestUser(newUser) + .assertStatusCode(200); + + } + + @Test + public void nonAdminUserShouldNotChangePasswordOfOtherUser() { + String user = "user"; + String password = "pass"; + TestData.createUser(user, password, false, "xml", "em@l.de"); + String user2 = "user2"; + TestData.createUser(user2, password, false, "xml", "em@l.de"); + ScmRequests.start() + .requestIndexResource(user, password) + .assertUsersLinkDoesNotExists(); + // use the users/ endpoint bypassed the index resource + ScmRequests.start() + .requestUser(user, password, user2) + .assertStatusCode(403); + // use the users/password endpoint bypassed the index and users resources + ScmRequests.start() + .requestUserChangePassword(user, password, user2, "newPassword") + .assertStatusCode(403); + } + + @Test + public void shouldHidePasswordLinkIfUserTypeIsNotXML() { + String newUser = "user"; + String password = "pass"; + String type = "not XML Type"; + TestData.createUser(newUser, password, true, type, "user@scm-manager.org"); + ScmRequests.start() + .requestIndexResource(newUser, password) + .assertStatusCode(200) + .requestUser(newUser) + .assertStatusCode(200) + .assertPassword(Assert::assertNull) + .assertType(s -> assertThat(s).isEqualTo(type)) + .assertPasswordLinkDoesNotExists(); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/utils/NullAwareJsonObjectBuilder.java b/scm-it/src/test/java/sonia/scm/it/utils/NullAwareJsonObjectBuilder.java new file mode 100644 index 0000000000..31a12f1969 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/utils/NullAwareJsonObjectBuilder.java @@ -0,0 +1,95 @@ +package sonia.scm.it.utils; + +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonValue; +import java.math.BigDecimal; +import java.math.BigInteger; + +public class NullAwareJsonObjectBuilder implements JsonObjectBuilder { + public static JsonObjectBuilder wrap(JsonObjectBuilder builder) { + if (builder == null) { + throw new IllegalArgumentException("Can't wrap nothing."); + } + return new NullAwareJsonObjectBuilder(builder); + } + + private final JsonObjectBuilder builder; + + private NullAwareJsonObjectBuilder(JsonObjectBuilder builder) { + this.builder = builder; + } + + public JsonObjectBuilder add(String name, JsonValue value) { + return builder.add(name, (value == null) ? JsonValue.NULL : value); + } + + @Override + public JsonObjectBuilder add(String name, String value) { + if (value != null){ + return builder.add(name, value ); + }else{ + return builder.addNull(name); + } + } + + @Override + public JsonObjectBuilder add(String name, BigInteger value) { + if (value != null){ + return builder.add(name, value ); + }else{ + return builder.addNull(name); + } + } + + @Override + public JsonObjectBuilder add(String name, BigDecimal value) { + if (value != null){ + return builder.add(name, value ); + }else{ + return builder.addNull(name); + } + } + + @Override + public JsonObjectBuilder add(String s, int i) { + return builder.add(s, i); + } + + @Override + public JsonObjectBuilder add(String s, long l) { + return builder.add(s, l); + } + + @Override + public JsonObjectBuilder add(String s, double v) { + return builder.add(s, v); + } + + @Override + public JsonObjectBuilder add(String s, boolean b) { + return builder.add(s, b); + } + + @Override + public JsonObjectBuilder addNull(String s) { + return builder.addNull(s); + } + + @Override + public JsonObjectBuilder add(String s, JsonObjectBuilder jsonObjectBuilder) { + return builder.add(s, jsonObjectBuilder); + } + + @Override + public JsonObjectBuilder add(String s, JsonArrayBuilder jsonArrayBuilder) { + return builder.add(s, jsonArrayBuilder); + } + + @Override + public JsonObject build() { + return builder.build(); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java b/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java new file mode 100644 index 0000000000..8fb9fdf798 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java @@ -0,0 +1,29 @@ +package sonia.scm.it.utils; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import java.util.regex.Pattern; + +public class RegExMatcher extends BaseMatcher { + public static Matcher matchesPattern(String pattern) { + return new RegExMatcher(pattern); + } + + private final String pattern; + + private RegExMatcher(String pattern) { + this.pattern = pattern; + } + + @Override + public void describeTo(Description description) { + description.appendText("matching to regex pattern \"" + pattern + "\""); + } + + @Override + public boolean matches(Object o) { + return o != null && Pattern.compile(pattern).matcher(o.toString()).matches(); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java new file mode 100644 index 0000000000..752be88dbc --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java @@ -0,0 +1,152 @@ +package sonia.scm.it.utils; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Person; +import sonia.scm.repository.Tag; +import sonia.scm.repository.client.api.ClientCommand; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientFactory; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class RepositoryUtil { + + private static final Logger LOG = LoggerFactory.getLogger(RepositoryUtil.class); + + private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); + + public static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException { + return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin"); + } + + public static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException { + String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK) + .extract() + .path("_links.protocol.find{it.name=='http'}.href"); + + return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder); + } + + public static RepositoryClient createAnonymousRepositoryClient(String repositoryType, File folder) throws IOException { + String httpProtocolUrl = TestData.callRepository("scmadmin", "scmadmin", repositoryType, HttpStatus.SC_OK) + .extract() + .path("_links.protocol.find{it.name=='http'}.href"); + + return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, folder); + } + + public static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException { + String uuid = UUID.randomUUID().toString(); + String name = "file-" + uuid + ".uuid"; + createAndCommitFile(client, username, name, uuid); + return name; + } + + public static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { + writeAndAddFile(repositoryClient, fileName, content); + return commit(repositoryClient, username, "added " + fileName); + } + + /** + * Bundle multiple File modification in one changeset + * + * @param repositoryClient + * @param username + * @param addedFiles map.key: path of the file, value: the file content + * @param modifiedFiles map.key: path of the file, value: the file content + * @param removedFiles list of file paths to be removed + * @return the changeset with all modifications + * @throws IOException + */ + public static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map addedFiles, Map modifiedFiles, List removedFiles) throws IOException { + for (String fileName : addedFiles.keySet()) { + writeAndAddFile(repositoryClient, fileName, addedFiles.get(fileName)); + } + for (String fileName : modifiedFiles.keySet()) { + writeAndAddFile(repositoryClient, fileName, modifiedFiles.get(fileName)); + } + for (String fileName : removedFiles) { + deleteFileAndApplyRemoveCommand(repositoryClient, fileName); + } + return commit(repositoryClient, username, "multiple file modifications" ); + } + + private static File writeAndAddFile(RepositoryClient repositoryClient, String fileName, String content) throws IOException { + File file = new File(repositoryClient.getWorkingCopy(), fileName); + Files.createParentDirs(file); + Files.write(content, file, Charsets.UTF_8); + addWithParentDirectories(repositoryClient, file); + return file; + } + + public static Changeset updateAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { + writeAndAddFile(repositoryClient, fileName, content); + return commit(repositoryClient, username, "updated " + fileName); + } + + 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 { + File parent = file.getParentFile(); + String thisName = file.getName(); + String path; + if (!repositoryClient.getWorkingCopy().equals(parent)) { + path = addWithParentDirectories(repositoryClient, parent) + File.separator + thisName; + } else { + path = thisName; + } + addFile(repositoryClient, path); + return path; + } + + public static Changeset addFileAndCommit(RepositoryClient repositoryClient, String path, String username, String message) throws IOException { + repositoryClient.getAddCommand().add(path); + return commit(repositoryClient, username, message); + } + + + public static void addFile(RepositoryClient repositoryClient, String path) throws IOException { + repositoryClient.getAddCommand().add(path); + } + + public static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException { + LOG.info("user: {} try to commit with message: {}", username, message); + Changeset changeset = repositoryClient.getCommitCommand().commit(new Person(username, username + "@scm-manager.org"), message); + if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { + repositoryClient.getPushCommand().push(); + } + return changeset; + } + + public static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException { + if (repositoryClient.isCommandSupported(ClientCommand.TAG)) { + Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN); + if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { + repositoryClient.getPushCommand().pushTags(); + } + return tag; + } + + return null; + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java new file mode 100644 index 0000000000..34c28a2a46 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java @@ -0,0 +1,41 @@ +package sonia.scm.it.utils; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import java.net.URI; + +import static java.net.URI.create; + +public class RestUtil { + + public static final URI BASE_URL = create("http://localhost:8081/scm/"); + public static final URI REST_BASE_URL = BASE_URL.resolve("api/v2/"); + + public static URI createResourceUrl(String path) { + return REST_BASE_URL.resolve(path); + } + + public static final String ADMIN_USERNAME = "scmadmin"; + public static final String ADMIN_PASSWORD = "scmadmin"; + + public static RequestSpecification given() { + return RestAssured.given() + .auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD); + } + + public static RequestSpecification given(String mediaType) { + return given(mediaType, ADMIN_USERNAME, ADMIN_PASSWORD); + } + + public static RequestSpecification given(String mediaType, String username, String password) { + return givenAnonymous(mediaType) + .auth().preemptive().basic(username, password); + } + + public static RequestSpecification givenAnonymous(String mediaType) { + return RestAssured.given() + .contentType(mediaType) + .accept(mediaType); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java new file mode 100644 index 0000000000..69b9940f70 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java @@ -0,0 +1,439 @@ +package sonia.scm.it.utils; + +import io.restassured.RestAssured; +import io.restassured.response.Response; +import org.junit.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.web.VndMediaType; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static org.hamcrest.Matchers.is; +import static sonia.scm.it.utils.TestData.createPasswordChangeJson; + + +/** + * Encapsulate rest requests of a repository in builder pattern + *

+ * A Get Request can be applied with the methods request*() + * These methods return a AppliedGet*Request object + * This object can be used to apply general assertions over the rest Assured response + * In the AppliedGet*Request classes there is a using*Response() method + * that return the *Response class containing specific operations related to the specific response + * the *Response class contains also the request*() method to apply the next GET request from a link in the response. + */ +public class ScmRequests { + + private static final Logger LOG = LoggerFactory.getLogger(ScmRequests.class); + + private String username; + private String password; + + public static ScmRequests start() { + return new ScmRequests(); + } + + public IndexResponse requestIndexResource() { + return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString())); + } + + public IndexResponse requestIndexResource(String username, String password) { + setUsername(username); + setPassword(password); + return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString())); + } + + public UserResponse requestUser(String username, String password, String pathParam) { + setUsername(username); + setPassword(password); + return new UserResponse<>(applyGETRequest(RestUtil.REST_BASE_URL.resolve("users/"+pathParam).toString()), null); + } + + public ChangePasswordResponse requestUserChangePassword(String username, String password, String userPathParam, String newPassword) { + setUsername(username); + setPassword(password); + return new ChangePasswordResponse<>(applyPUTRequest(RestUtil.REST_BASE_URL.resolve("users/"+userPathParam+"/password").toString(), VndMediaType.PASSWORD_OVERWRITE, TestData.createPasswordChangeJson(password,newPassword)), null); + } + + @SuppressWarnings("unchecked") + public ModelResponse requestPluginTranslations(String language) { + Response response = applyGETRequest(RestUtil.BASE_URL.resolve("locales/" + language + "/plugins.json").toString()); + return new ModelResponse(response, null); + } + + /** + * 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 applyGETRequestFromLinkWithParams(response, linkPropertyName, ""); + } + + /** + * 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 + * @param params query params eg. ?q=xyz&count=12 or path params eg. namespace/name + * @return the response of the GET request using the given link + */ + private Response applyGETRequestFromLinkWithParams(Response response, String linkPropertyName, String params) { + String url = response + .then() + .extract() + .path(linkPropertyName); + Assert.assertNotNull("no url found for link " + linkPropertyName, url); + return applyGETRequestWithQueryParams(url, params); + } + + /** + * Apply a GET Request to the given url and return the response. + * + * @param url the url of the GET request + * @param params query params eg. ?q=xyz&count=12 or path params eg. namespace/name + * @return the response of the GET request using the given url + */ + private Response applyGETRequestWithQueryParams(String url, String params) { + LOG.info("GET {}", url); + if (username == null || password == null){ + return RestAssured.given() + .when() + .get(url + params); + } + return RestAssured.given() + .auth().preemptive().basic(username, password) + .when() + .get(url + params); + } + + /** + * Apply a GET Request to the given url and return the response. + * + * @param url the url of the GET request + * @return the response of the GET request using the given url + **/ + private Response applyGETRequest(String url) { + return applyGETRequestWithQueryParams(url, ""); + } + + + /** + * Apply a PUT Request to the extracted url from the given link + * + * @param response the response containing the link + * @param linkPropertyName the property name of link + * @param body + * @return the response of the PUT request using the given link + */ + private Response applyPUTRequestFromLink(Response response, String linkPropertyName, String content, String body) { + return applyPUTRequest(response + .then() + .extract() + .path(linkPropertyName), content, body); + } + + + /** + * Apply a PUT Request to the given url and return the response. + * + * @param url the url of the PUT request + * @param mediaType + * @param body + * @return the response of the PUT request using the given url + */ + private Response applyPUTRequest(String url, String mediaType, String body) { + LOG.info("PUT {}", url); + return RestAssured.given() + .auth().preemptive().basic(username, password) + .when() + .contentType(mediaType) + .accept(mediaType) + .body(body) + .put(url); + } + + private void setUsername(String username) { + this.username = username; + } + + private void setPassword(String password) { + this.password = password; + } + + public class IndexResponse extends ModelResponse { + public static final String LINK_AUTOCOMPLETE_USERS = "_links.autocomplete.find{it.name=='users'}.href"; + public static final String LINK_AUTOCOMPLETE_GROUPS = "_links.autocomplete.find{it.name=='groups'}.href"; + public static final String LINK_REPOSITORIES = "_links.repositories.href"; + private static final String LINK_ME = "_links.me.href"; + private static final String LINK_USERS = "_links.users.href"; + + public IndexResponse(Response response) { + super(response, null); + } + + public AutoCompleteResponse requestAutoCompleteUsers(String q) { + return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_USERS, "?q=" + q), this); + } + + public AutoCompleteResponse requestAutoCompleteGroups(String q) { + return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_GROUPS, "?q=" + q), this); + } + + public RepositoryResponse requestRepository(String namespace, String name) { + return new RepositoryResponse<>(applyGETRequestFromLinkWithParams(response, LINK_REPOSITORIES, namespace + "/" + name), this); + } + + public MeResponse requestMe() { + return new MeResponse<>(applyGETRequestFromLink(response, LINK_ME), this); + } + + public UserResponse requestUser(String username) { + return new UserResponse<>(applyGETRequestFromLinkWithParams(response, LINK_USERS, username), this); + } + + public IndexResponse assertUsersLinkDoesNotExists() { + return super.assertPropertyPathDoesNotExists(LINK_USERS); + } + + public String getUrl(String linkName) { + return response + .then() + .extract() + .path("_links." + linkName + ".href"); + } + } + + public class RepositoryResponse extends ModelResponse, PREV> { + + + public static final String LINKS_SOURCES = "_links.sources.href"; + public static final String LINKS_CHANGESETS = "_links.changesets.href"; + + public RepositoryResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + + public SourcesResponse requestSources() { + return new SourcesResponse<>(applyGETRequestFromLink(response, LINKS_SOURCES), this); + } + + public ChangesetsResponse requestChangesets() { + return new ChangesetsResponse<>(applyGETRequestFromLink(response, LINKS_CHANGESETS), this); + } + + } + + public class ChangesetsResponse extends ModelResponse, PREV> { + + public ChangesetsResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + + public ChangesetsResponse assertChangesets(Consumer> changesetsConsumer) { + List changesets = response.then().extract().path("_embedded.changesets"); + changesetsConsumer.accept(changesets); + return this; + } + + public DiffResponse requestDiffInGitFormat(String revision) { + return new DiffResponse<>(applyGETRequestFromLinkWithParams(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href", "?format=GIT"), this); + } + + public ModificationsResponse requestModifications(String revision) { + return new ModificationsResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"), this); + } + } + + + public class SourcesResponse extends ModelResponse, PREV> { + + public SourcesResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + + public SourcesResponse assertRevision(Consumer assertRevision) { + String revision = response.then().extract().path("revision"); + assertRevision.accept(revision); + return this; + } + + public SourcesResponse assertFiles(Consumer assertFiles) { + List files = response.then().extract().path("files"); + assertFiles.accept(files); + return this; + } + + public ChangesetsResponse requestFileHistory(String fileName) { + return new ChangesetsResponse<>(applyGETRequestFromLink(response, "_embedded.children.find{it.name=='" + fileName + "'}._links.history.href"), this); + } + + public SourcesResponse requestSelf(String fileName) { + return new SourcesResponse<>(applyGETRequestFromLink(response, "_embedded.children.find{it.name=='" + fileName + "'}._links.self.href"), this); + } + } + + public class ModificationsResponse extends ModelResponse, PREV> { + + public ModificationsResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + + public ModificationsResponse assertRevision(Consumer assertRevision) { + String revision = response.then().extract().path("revision"); + assertRevision.accept(revision); + return this; + } + + public ModificationsResponse assertAdded(Consumer> assertAdded) { + List added = response.then().extract().path("added"); + assertAdded.accept(added); + return this; + } + + public ModificationsResponse assertRemoved(Consumer> assertRemoved) { + List removed = response.then().extract().path("removed"); + assertRemoved.accept(removed); + return this; + } + + public ModificationsResponse assertModified(Consumer> assertModified) { + List modified = response.then().extract().path("modified"); + assertModified.accept(modified); + return this; + } + + } + + public class MeResponse extends ModelResponse, PREV> { + + public static final String LINKS_PASSWORD_HREF = "_links.password.href"; + + public MeResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + + public MeResponse assertPasswordLinkDoesNotExists() { + return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF); + } + + public ChangePasswordResponse requestChangePassword(String oldPassword, String newPassword) { + return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this); + } + } + + public class UserResponse extends ModelResponse, PREV> { + + public static final String LINKS_PASSWORD_HREF = "_links.password.href"; + + public UserResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + + public UserResponse assertPassword(Consumer assertPassword) { + return super.assertSingleProperty(assertPassword, "password"); + } + + public UserResponse assertType(Consumer assertType) { + return assertSingleProperty(assertType, "type"); + } + + public UserResponse assertPasswordLinkDoesNotExists() { + return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF); + } + + public ChangePasswordResponse requestChangePassword(String newPassword) { + return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_OVERWRITE, createPasswordChangeJson(null, newPassword)), this); + } + } + + + /** + * encapsulate standard assertions over model properties + */ + public class ModelResponse, PREV extends ModelResponse> { + + protected PREV previousResponse; + protected Response response; + + public ModelResponse(Response response, PREV previousResponse) { + this.response = response; + this.previousResponse = previousResponse; + } + + public Response getResponse(){ + return response; + } + + public PREV returnToPrevious() { + return previousResponse; + } + + public SELF assertSingleProperty(Consumer 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 assertProperties, String propertyJsonPath) { + List properties = response.then().extract().path(propertyJsonPath); + assertProperties.accept(properties); + 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 AutoCompleteResponse extends ModelResponse, PREV> { + + public AutoCompleteResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + + public AutoCompleteResponse assertAutoCompleteResults(Consumer> checker) { + List result = response.then().extract().path(""); + checker.accept(result); + return this; + } + + } + + + public class DiffResponse extends ModelResponse, PREV> { + + public DiffResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + } + + public class ChangePasswordResponse extends ModelResponse, PREV> { + + public ChangePasswordResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java new file mode 100644 index 0000000000..4b48c89bbc --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java @@ -0,0 +1,30 @@ +package sonia.scm.it.utils; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import sonia.scm.util.IOUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Stream; + +public class ScmTypes implements ArgumentsProvider { + public static Collection availableScmTypes() { + Collection params = new ArrayList<>(); + + params.add("git"); + params.add("svn"); + + if (IOUtil.search("hg") != null) { + params.add("hg"); + } + + return params; + } + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return availableScmTypes().stream().map(Arguments::of); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java new file mode 100644 index 0000000000..cfefc1171a --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -0,0 +1,276 @@ +package sonia.scm.it.utils; + +import io.restassured.response.ValidatableResponse; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.web.VndMediaType; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import java.net.URI; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static sonia.scm.it.utils.RestUtil.createResourceUrl; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.RestUtil.givenAnonymous; +import static sonia.scm.it.utils.ScmTypes.availableScmTypes; + +public class TestData { + + private static final Logger LOG = LoggerFactory.getLogger(TestData.class); + + public static final String USER_SCM_ADMIN = "scmadmin"; + public static final String USER_ANONYMOUS = "_anonymous"; + + public static final Collection READ = asList("read", "pull"); + public static final Collection WRITE = asList("read", "write", "pull", "push"); + public static final Collection OWNER = asList("*"); + + private static final List PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); + + private static Map DEFAULT_REPOSITORIES = new HashMap<>(); + public static final JsonObjectBuilder JSON_BUILDER = NullAwareJsonObjectBuilder.wrap(Json.createObjectBuilder()); + + public static void createDefault() { + cleanup(); + createDefaultRepositories(); + } + + public static void cleanup() { + LOG.info("start to clean up to integration tests"); + cleanupRepositories(); + cleanupGroups(); + cleanupUsers(); + } + + public static String getDefaultRepositoryUrl(String repositoryType) { + return DEFAULT_REPOSITORIES.get(repositoryType); + } + + public static void createNotAdminUser(String username, String password) { + createUser(username, password, false, "xml", "user1@scm-manager.org"); + } + + public static void createUser(String username, String password, boolean isAdmin, String type, final String email) { + LOG.info("create user with username: {}", username); + String admin = isAdmin ? "true" : "false"; + given(VndMediaType.USER) + .when() + .content(new StringBuilder() + .append(" {\n") + .append(" \"active\": true,\n") + .append(" \"admin\": ").append(admin).append(",\n") + .append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n") + .append(" \"displayName\": \"").append(username).append("\",\n") + .append(" \"mail\": \"" + email + "\",\n") + .append(" \"name\": \"").append(username).append("\",\n") + .append(" \"password\": \"").append(password).append("\",\n") + .append(" \"type\": \"").append(type).append("\"\n") + .append(" }").toString()) + .post(getUsersUrl()) + .then() + .statusCode(HttpStatus.SC_CREATED); + + if (isAdmin) { + assignAdminPermissions(username); + } + } + + public static void assignAdminPermissions(String username) { + LOG.info("assign admin permissions to user {}", username); + given(VndMediaType.PERMISSION_COLLECTION) + .when() + .body("{'permissions': ['*']}".replaceAll("'", "\"")) + .put(getPermissionUrl(username)) + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + } + + private static URI getPermissionUrl(String username) { + return RestUtil.createResourceUrl(String.format("users/%s/permissions", username)); + } + + public static void createGroup(String groupName, String desc) { + LOG.info("create group with group name: {} and description {}", groupName, desc); + given(VndMediaType.GROUP) + .when() + .content(getGroupJson(groupName,desc)) + .post(getGroupsUrl()) + .then() + .statusCode(HttpStatus.SC_CREATED) + ; + } + + public static void createUserPermission(String username, Collection verbs, String repositoryType) { + String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); + LOG.info("create permission with name {} and verbs {} using the endpoint: {}", username, verbs, defaultPermissionUrl); + given(VndMediaType.REPOSITORY_PERMISSION) + .when() + .content("{\n" + + "\t\"verbs\": " + verbs.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" + + "\t\"name\": \"" + username + "\",\n" + + "\t\"groupPermission\": false\n" + + "\t\n" + + "}") + .post(defaultPermissionUrl) + .then() + .statusCode(HttpStatus.SC_CREATED) + ; + } + + public static List getUserPermissions(String username, String password, String repositoryType) { + return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList("_embedded.permissions"); + } + + public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { + return given(VndMediaType.REPOSITORY_PERMISSION, username, password) + .when() + .get(TestData.getDefaultPermissionUrl(username, password, repositoryType)) + .then() + .statusCode(expectedStatusCode); + } + + public static ValidatableResponse callRepository(String username, String password, String repositoryType, int expectedStatusCode) { + return given(VndMediaType.REPOSITORY, username, password) + + .when() + .get(getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedStatusCode); + } + + public static ValidatableResponse callAnonymousRepository(String repositoryType, int expectedStatusCode) { + return givenAnonymous(VndMediaType.REPOSITORY) + + .when() + .get(getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedStatusCode); + } + + public static String getDefaultPermissionUrl(String username, String password, String repositoryType) { + return given(VndMediaType.REPOSITORY, username, password) + .when() + .get(getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href"); + } + + + private static void cleanupRepositories() { + LOG.info("clean up repository"); + List repositories = given(VndMediaType.REPOSITORY_COLLECTION) + .when() + .get(createResourceUrl("repositories")) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList("_embedded.repositories._links.self.href"); + LOG.info("about to delete {} repositories", repositories.size()); + repositories.forEach(TestData::delete); + DEFAULT_REPOSITORIES.clear(); + } + + private static void cleanupGroups() { + List groups = given(VndMediaType.GROUP_COLLECTION) + .when() + .get(createResourceUrl("groups")) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList("_embedded.groups._links.self.href"); + LOG.info("about to delete {} groups", groups.size()); + groups.forEach(TestData::delete); + } + + private static void cleanupUsers() { + List users = given(VndMediaType.USER_COLLECTION) + .when() + .get(createResourceUrl("users")) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList("_embedded.users._links.self.href"); + LOG.info("about to delete {} users", users.size()); + users.stream().filter(url -> PROTECTED_USERS.stream().noneMatch(url::contains)).forEach(TestData::delete); + } + + private static void delete(String url) { + given(VndMediaType.REPOSITORY) + .when() + .delete(url) + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + LOG.info("deleted {}", url); + } + + private static void createDefaultRepositories() { + LOG.info("create default repositories"); + for (String repositoryType : availableScmTypes()) { + String url = given(VndMediaType.REPOSITORY) + .body(repositoryJson(repositoryType)) + + .when() + .post(createResourceUrl("repositories")) + + .then() + .statusCode(HttpStatus.SC_CREATED) + .extract() + .header("location"); + LOG.info("a {} repository is created: {}", repositoryType, url); + DEFAULT_REPOSITORIES.put(repositoryType, url); + } + } + + public static String repositoryJson(String repositoryType) { + return JSON_BUILDER + .add("contact", "zaphod.beeblebrox@hitchhiker.com") + .add("description", "Heart of Gold") + .add("name", getDefaultRepoName(repositoryType)) + .add("type", repositoryType) + .build().toString(); + } + + public static String getDefaultRepoName(String repositoryType) { + return "HeartOfGold-" + repositoryType; + } + + public static String getGroupJson(String groupname , String desc) { + return JSON_BUILDER + .add("name", groupname) + .add("description", desc) + .build().toString(); + } + + public static URI getGroupsUrl() { + return RestUtil.createResourceUrl("groups/"); + } + + public static URI getUsersUrl() { + return RestUtil.createResourceUrl("users/"); + } + + 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) { + cleanup(); + } + +} diff --git a/scm-it/src/test/resources/diff/binaryfile/echo b/scm-it/src/test/resources/diff/binaryfile/echo new file mode 100755 index 0000000000..11bc2152e4 Binary files /dev/null and b/scm-it/src/test/resources/diff/binaryfile/echo differ diff --git a/scm-it/src/test/resources/diff/largefile/modified/v1/SvnDiffGenerator_forTest b/scm-it/src/test/resources/diff/largefile/modified/v1/SvnDiffGenerator_forTest new file mode 100644 index 0000000000..f714a955c7 --- /dev/null +++ b/scm-it/src/test/resources/diff/largefile/modified/v1/SvnDiffGenerator_forTest @@ -0,0 +1,1230 @@ +package sonia.scm.repository.spi; + +import de.regnis.q.sequence.line.diff.QDiffGenerator; +import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory; +import de.regnis.q.sequence.line.diff.QDiffManager; +import de.regnis.q.sequence.line.diff.QDiffUniGenerator; +import org.tmatesoft.svn.core.SVNErrorCode; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNMergeRangeList; +import org.tmatesoft.svn.core.SVNProperties; +import org.tmatesoft.svn.core.SVNProperty; +import org.tmatesoft.svn.core.SVNPropertyValue; +import org.tmatesoft.svn.core.internal.util.SVNHashMap; +import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; +import org.tmatesoft.svn.core.internal.util.SVNPathUtil; +import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; +import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback; +import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; +import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; +import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback; +import org.tmatesoft.svn.core.wc.ISVNOptions; +import org.tmatesoft.svn.core.wc.SVNDiffOptions; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import org.tmatesoft.svn.util.SVNLogType; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +public class SCMSvnDiffGenerator implements ISvnDiffGenerator { + + protected static final String WC_REVISION_LABEL = "(working copy)"; + protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________"; + protected static final String HEADER_SEPARATOR = "==================================================================="; + protected static final String HEADER_ENCODING = "UTF-8"; + + private SvnTarget originalTarget1; + private SvnTarget originalTarget2; + private SvnTarget baseTarget; + private SvnTarget relativeToTarget; + private SvnTarget repositoryRoot; + private String encoding; + private byte[] eol; + private boolean useGitFormat; + private boolean forcedBinaryDiff; + + private boolean diffDeleted; + private boolean diffAdded; + private List rawDiffOptions; + private boolean forceEmpty; + + private Set visitedPaths; + private String externalDiffCommand; + private SVNDiffOptions diffOptions; + private boolean fallbackToAbsolutePath; + private ISVNOptions options; + private boolean propertiesOnly; + private boolean ignoreProperties; + + private String getDisplayPath(SvnTarget target) { + String relativePath; + if (baseTarget == null) { + relativePath = null; + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = baseTarget.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) { + String relativePath; + if (repositoryRoot == null) { + relativePath = null; + } else { + if (repositoryRoot.isFile() == target.isFile()) { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = repositoryRoot.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = new File("").getAbsolutePath(); + relativePath = getRelativePath(targetString, baseTargetString); + } + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativePath(String targetString, String baseTargetString) { + if (targetString != null) { + targetString = targetString.replace(File.separatorChar, '/'); + } + if (baseTargetString != null) { + baseTargetString = baseTargetString.replace(File.separatorChar, '/'); + } + + final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString); + if (pathAsChild != null) { + return pathAsChild; + } + if (targetString.equals(baseTargetString)) { + return ""; + } + return null; + } + + private String getChildPath(String path, String relativeToPath) { + if (relativeToTarget == null) { + return null; + } + + String relativePath = getRelativePath(path, relativeToPath); + if (relativePath == null) { + return path; + } + + if (relativePath.length() > 0) { + return relativePath; + } + + if (relativeToPath.equals(path)) { + return "."; + } + + return null; + } + + public SCMSvnDiffGenerator() { + this.originalTarget1 = null; + this.originalTarget2 = null; + this.visitedPaths = new HashSet(); + this.diffDeleted = true; + this.diffAdded = true; + } + + public void setBaseTarget(SvnTarget baseTarget) { + this.baseTarget = baseTarget; + } + + public void setUseGitFormat(boolean useGitFormat) { + this.useGitFormat = useGitFormat; + } + + public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) { + this.originalTarget1 = originalTarget1; + this.originalTarget2 = originalTarget2; + } + + public void setRelativeToTarget(SvnTarget relativeToTarget) { + this.relativeToTarget = relativeToTarget; + } + + public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) { + //anchors are not used + } + + public void setRepositoryRoot(SvnTarget repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + public void setForceEmpty(boolean forceEmpty) { + this.forceEmpty = forceEmpty; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return encoding; + } + + public String getGlobalEncoding() { + ISVNOptions options = getOptions(); + + if (options != null && options instanceof DefaultSVNOptions) { + DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options; + return defaultOptions.getGlobalCharset(); + } + return null; + } + + public void setEOL(byte[] eol) { + this.eol = eol; + } + + public byte[] getEOL() { + return eol; + } + + public boolean isForcedBinaryDiff() { + return forcedBinaryDiff; + } + + public void setForcedBinaryDiff(boolean forcedBinaryDiff) { + this.forcedBinaryDiff = forcedBinaryDiff; + } + + public boolean isPropertiesOnly() { + return propertiesOnly; + } + + public void setPropertiesOnly(boolean propertiesOnly) { + this.propertiesOnly = propertiesOnly; + } + + public boolean isIgnoreProperties() { + return ignoreProperties; + } + + public void setIgnoreProperties(boolean ignoreProperties) { + this.ignoreProperties = ignoreProperties; + } + + public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isIgnoreProperties()) { + return; + } + if (dirWasAdded && !isDiffAdded()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (displayPath == null || displayPath.length() == 0) { + displayPath = "."; + } + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + boolean showDiffHeader = !visitedPaths.contains(displayPath); + if (showDiffHeader) { + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified); + visitedPaths.add(displayPath); + if (useGitFormat) { + displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + if (shouldStopDisplaying) { + return; + } + +// if (useGitFormat) { +// String copyFromPath = null; +// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified; +// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1); +// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2); +// displayGitDiffHeader(outputStream, operationKind, +// getRelativeToRootPath(target, originalTarget1), +// getRelativeToRootPath(target, originalTarget2), +// copyFromPath); +// } + + if (useGitFormat) { + displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null); + } else { + displayHeaderFields(outputStream, label1, label2); + } + } + + displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream); + + displayPropDiffValues(outputStream, propChanges, originalProps); + } + + private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''", + displayPath, relativeToPath); + SVNErrorManager.error(errorMessage, SVNLogType.CLIENT); + } + + private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + + try { + displayString(outputStream, "--- "); + displayFirstGitLabelPath(outputStream, path1, revision1, operation); + displayEOL(outputStream); + displayString(outputStream, "+++ "); + displaySecondGitLabelPath(outputStream, path2, revision2, operation); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private String adjustRelativeToReposRoot(String targetString) { + if (repositoryRoot != null) { + String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString(); + String relativePath = getRelativePath(targetString, repositoryRootString); + return relativePath == null ? "" : relativePath; + } + return targetString; + } + + private String computeLabel(String targetString, String originalTargetString) { + if (originalTargetString.length() == 0) { + return targetString; + } else if (originalTargetString.charAt(0) == '/') { + return targetString + "\t(..." + originalTargetString + ")"; + } else { + return targetString + "\t(.../" + originalTargetString + ")"; + } + } + + public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isPropertiesOnly()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean leftIsBinary = false; + boolean rightIsBinary = false; + + if (mimeType1 != null) { + leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1); + } + if (mimeType2 != null) { + rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2); + } + + if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + + displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary); + + return; + } + + final String diffCommand = getExternalDiffCommand(); + if (diffCommand != null) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2); + } else { + internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2); + } + } + + private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException { + displayCannotDisplayFileMarkedBinary(outputStream); + + if (leftIsBinary && !rightIsBinary) { + displayMimeType(outputStream, mimeType1); + } else if (!leftIsBinary && rightIsBinary) { + displayMimeType(outputStream, mimeType2); + } else if (leftIsBinary && rightIsBinary) { + if (mimeType1.equals(mimeType2)) { + displayMimeType(outputStream, mimeType1); + } else { + displayMimeTypes(outputStream, mimeType1, mimeType2); + } + } + } + + private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException { + String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath); + if (file2 == null && !isDiffDeleted()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + if (file1 == null && !isDiffAdded()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath); + + RandomAccessFile is1 = null; + RandomAccessFile is2 = null; + try { + is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1); + is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2); + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + final String diffHeader; + if (forceEmpty || useGitFormat) { + displayString(outputStream, header); + diffHeader = headerFields; + + visitedPaths.add(displayPath); + } else { + diffHeader = header + headerFields; + } + QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader); + EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream); + QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator); + if (emptyDetectionOutputStream.isSomethingWritten()) { + visitedPaths.add(displayPath); + } + emptyDetectionOutputStream.flush(); + } catch (IOException e) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage()); + SVNErrorManager.error(err, e, SVNLogType.DEFAULT); + } finally { + SVNFileUtil.closeFile(is1); + SVNFileUtil.closeFile(is2); + } + } + + private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (useGitFormat) { + displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath); + } else { + displayHeaderFields(byteArrayOutputStream, label1, label2); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + boolean stopDisplaying = displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation); + if (useGitFormat) { + displayGitDiffHeader(byteArrayOutputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + copyFromPath); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException { + final List args = new ArrayList(); + args.add(diffCommand); + if (rawDiffOptions != null) { + args.addAll(rawDiffOptions); + } else { + Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection(); + args.addAll(svnDiffOptionsCollection); + args.add("-u"); + } + + if (label1 != null) { + args.add("-L"); + args.add(label1); + } + + if (label2 != null) { + args.add("-L"); + args.add(label2); + } + + boolean tmpFile1 = false; + boolean tmpFile2 = false; + if (file1 == null) { + file1 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile1 = true; + } + if (file2 == null) { + file2 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile2 = true; + } + + String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/'); + String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/'); + + args.add(file1Path); + args.add(file2Path); + try { + final Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + + SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true, + new ISVNReturnValueCallback() { + + public void handleReturnValue(int returnValue) throws SVNException { + if (returnValue != 0 && returnValue != 1) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, + "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)}); + SVNErrorManager.error(err, SVNLogType.DEFAULT); + } + } + + public void handleChar(char ch) throws SVNException { + try { + writer.write(ch); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } + } + + public boolean isHandleProgramOutput() { + return true; + } + }); + + writer.flush(); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } finally { + try { + if (tmpFile1) { + SVNFileUtil.deleteFile(file1); + } + if (tmpFile2) { + SVNFileUtil.deleteFile(file2); + } + } catch (SVNException e) { + // skip + } + } + } + + private String getExternalDiffCommand() { + return externalDiffCommand; + } + + private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = "); + displayString(outputStream, mimeType); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = ("); + displayString(outputStream, mimeType1); + displayString(outputStream, ", "); + displayString(outputStream, mimeType2); + displayString(outputStream, ")"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException { + try { + displayString(outputStream, "Cannot display: file marked as a binary type."); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void ensureEncodingAndEOLSet() { + if (getEOL() == null) { + setEOL(SVNProperty.EOL_LF_BYTES); + } + if (getEncoding() == null) { + final ISVNOptions options = getOptions(); + if (options != null && options.getNativeCharset() != null) { + setEncoding(options.getNativeCharset()); + } else { + setEncoding("UTF-8"); + } + } + } + + private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException { + for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) { + String name = (String) changedPropNames.next(); + SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null; + SVNPropertyValue newValue = diff.getSVNPropertyValue(name); + String headerFormat = null; + + if (originalValue == null) { + headerFormat = "Added: "; + } else if (newValue == null) { + headerFormat = "Deleted: "; + } else { + headerFormat = "Modified: "; + } + + try { + displayString(outputStream, (headerFormat + name)); + displayEOL(outputStream); + if (SVNProperty.MERGE_INFO.equals(name)) { + displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString()); + continue; + } + + byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding()); + byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding()); + + if (originalValueBytes == null) { + originalValueBytes = new byte[0]; + } else { + originalValueBytes = maybeAppendEOL(originalValueBytes); + } + + boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 && + (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] || + newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]); + + if (newValueBytes == null) { + newValueBytes = new byte[0]; + } else { + newValueBytes = maybeAppendEOL(newValueBytes); + } + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##"); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + QDiffGenerator generator = new QDiffUniGenerator(properties, ""); + Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes), + null, writer, generator); + writer.flush(); + if (!newValueHadEol) { + displayString(outputStream, "\\ No newline at end of property"); + displayEOL(outputStream); + } + } catch (IOException e) { + wrapException(e); + } + } + + } + + private byte[] maybeAppendEOL(byte[] buffer) { + if (buffer.length == 0) { + return buffer; + } + + byte lastByte = buffer[buffer.length - 1]; + if (lastByte == SVNProperty.EOL_CR_BYTES[0]) { + return buffer; + } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) { + final byte[] newBuffer = new byte[buffer.length + getEOL().length]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length); + return newBuffer; + } else { + return buffer; + } + } + + private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("a/" + copyFromPath, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("a/" + copyFromPath, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("b/" + path2, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath); + } + } + + private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "new file mode 10644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "deleted file mode 10644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "copy from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "copy to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "rename from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "rename to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException { + displayGitPath(outputStream, path1, "a/", false); + } + + private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException { + displayGitPath(outputStream, path2, "b/", false); + } + + private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "a/"; + if (operation == SvnDiffCallback.OperationKind.Added) { + path1 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix, true); + } + + private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "b/"; + if (operation == SvnDiffCallback.OperationKind.Deleted) { + path2 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix, true); + } + + private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix, boolean label) throws IOException { +// if (!label && path1.length() == 0) { +// displayString(outputStream, "."); +// } else { + displayString(outputStream, pathPrefix); + displayString(outputStream, path1); +// } + } + + private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) { + String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor); + return getLabel(adjustedPath, revision); + } + + private String getAdjustedPath(String displayPath, String path1, String commonAncestor) { + String adjustedPath = getRelativePath(path1, commonAncestor); + + if (adjustedPath == null || adjustedPath.length() == 0) { + adjustedPath = displayPath; + } else if (adjustedPath.charAt(0) == '/') { + adjustedPath = displayPath + "\t(..." + adjustedPath + ")"; + } else { + adjustedPath = displayPath + "\t(.../" + adjustedPath + ")"; + } + return adjustedPath; + //TODO: respect relativeToDir + } + + protected String getLabel(String path, String revToken) { + revToken = revToken == null ? WC_REVISION_LABEL : revToken; + return path + "\t" + revToken; + } + + protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException { + try { + if (deleted && !isDiffDeleted()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (deleted)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + if (added && !isDiffAdded()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (added)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + displayString(os, "Index: "); + displayString(os, path); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return false; + } catch (IOException e) { + wrapException(e); + } + return false; + } + + protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException { + try { + displayString(os, "--- "); + displayString(os, label1); + displayEOL(os); + displayString(os, "+++ "); + displayString(os, label2); + displayEOL(os); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException { + try { + displayEOL(outputStream); + displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path))); + displayEOL(outputStream); + displayString(outputStream, PROPERTIES_SEPARATOR); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) { + if (value == null) { + return null; + } + if (value.isString()) { + try { + return value.getString().getBytes(encoding); + } catch (UnsupportedEncodingException e) { + return value.getString().getBytes(); + } + } + return value.getBytes(); + } + + private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException { + Map oldMergeInfo = null; + Map newMergeInfo = null; + if (oldValue != null) { + oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null); + } + if (newValue != null) { + newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null); + } + + Map deleted = new TreeMap(); + Map added = new TreeMap(); + SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true); + + for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path); + displayString(outputStream, (" Reverse-merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + + for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path); + displayString(outputStream, (" Merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + } + + private boolean useLocalFileSeparatorChar() { + return true; + } + + public boolean isDiffDeleted() { + return diffDeleted; + } + + public boolean isDiffAdded() { + return diffAdded; + } + + private void wrapException(IOException e) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e); + SVNErrorManager.error(errorMessage, e, SVNLogType.WC); + } + + private void displayString(OutputStream outputStream, String s) throws IOException { + outputStream.write(s.getBytes(HEADER_ENCODING)); + } + + private void displayEOL(OutputStream os) throws IOException { + os.write(getEOL()); + } + + public SVNDiffOptions getDiffOptions() { + if (diffOptions == null) { + diffOptions = new SVNDiffOptions(); + } + return diffOptions; + } + + public void setExternalDiffCommand(String externalDiffCommand) { + this.externalDiffCommand = externalDiffCommand; + } + + public void setRawDiffOptions(List rawDiffOptions) { + this.rawDiffOptions = rawDiffOptions; + } + + public void setDiffOptions(SVNDiffOptions diffOptions) { + this.diffOptions = diffOptions; + } + + public void setDiffDeleted(boolean diffDeleted) { + this.diffDeleted = diffDeleted; + } + + public void setDiffAdded(boolean diffAdded) { + this.diffAdded = diffAdded; + } + + public void setBasePath(File absoluteFile) { + setBaseTarget(SvnTarget.fromFile(absoluteFile)); + } + + public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) { + this.fallbackToAbsolutePath = fallbackToAbsolutePath; + } + + public void setOptions(ISVNOptions options) { + this.options = options; + } + + public ISVNOptions getOptions() { + return options; + } + + private class EmptyDetectionOutputStream extends OutputStream { + + private final OutputStream outputStream; + private boolean somethingWritten; + + public EmptyDetectionOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + this.somethingWritten = false; + } + + public boolean isSomethingWritten() { + return somethingWritten; + } + + @Override + public void write(int c) throws IOException { + somethingWritten = true; + outputStream.write(c); + } + + @Override + public void write(byte[] bytes) throws IOException { + somethingWritten = bytes.length > 0; + outputStream.write(bytes); + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + somethingWritten = length > 0; + outputStream.write(bytes, offset, length); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + } +} diff --git a/scm-it/src/test/resources/diff/largefile/modified/v2/SvnDiffGenerator_forTest b/scm-it/src/test/resources/diff/largefile/modified/v2/SvnDiffGenerator_forTest new file mode 100644 index 0000000000..c46ce5f534 --- /dev/null +++ b/scm-it/src/test/resources/diff/largefile/modified/v2/SvnDiffGenerator_forTest @@ -0,0 +1,1234 @@ +package sonia.scm.repository.spi; + +import de.regnis.q.sequence.line.diff.QDiffGenerator; +import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory; +import de.regnis.q.sequence.line.diff.QDiffManager; +import de.regnis.q.sequence.line.diff.QDiffUniGenerator; +import org.tmatesoft.svn.core.SVNErrorCode; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNMergeRangeList; +import org.tmatesoft.svn.core.SVNProperties; +import org.tmatesoft.svn.core.SVNProperty; +import org.tmatesoft.svn.core.SVNPropertyValue; +import org.tmatesoft.svn.core.internal.util.SVNHashMap; +import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; +import org.tmatesoft.svn.core.internal.util.SVNPathUtil; +import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; +import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback; +import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; +import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; +import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback; +import org.tmatesoft.svn.core.wc.ISVNOptions; +import org.tmatesoft.svn.core.wc.SVNDiffOptions; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import org.tmatesoft.svn.util.SVNLogType; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +public class SCMSvnDiffGenerator implements ISvnDiffGenerator { + + protected static final String WC_REVISION_LABEL = "(working copy)"; + protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________"; + protected static final String HEADER_SEPARATOR = "==================================================================="; + protected static final String HEADER_ENCODING = "UTF-8"; + + private SvnTarget originalTarget1; + private SvnTarget originalTarget2; + private SvnTarget baseTarget; + private SvnTarget relativeToTarget; + private SvnTarget repositoryRoot; + private String encoding; + private byte[] eol; + private boolean useGitFormat; + private boolean forcedBinaryDiff; + + private boolean diffDeleted; + private boolean diffAdded; + private List rawDiffOptions; + private boolean forceEmpty; + + private Set visitedPaths; + private String externalDiffCommand; + private SVNDiffOptions diffOptions; + private boolean fallbackToAbsolutePath; + private ISVNOptions options; + private boolean propertiesOnly; + private boolean ignoreProperties; + + private String getDisplayPath(SvnTarget target) { + String relativePath; + if (baseTarget == null) { + relativePath = null; + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = baseTarget.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) { + String relativePath; + if (repositoryRoot == null) { + relativePath = null; + } else { + if (repositoryRoot.isFile() == target.isFile()) { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = repositoryRoot.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = new File("").getAbsolutePath(); + relativePath = getRelativePath(targetString, baseTargetString); + } + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativePath(String targetString, String baseTargetString) { + if (targetString != null) { + targetString = targetString.replace(File.separatorChar, '/'); + } + if (baseTargetString != null) { + baseTargetString = baseTargetString.replace(File.separatorChar, '/'); + } + + final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString); + if (pathAsChild != null) { + return pathAsChild; + } + if (targetString.equals(baseTargetString)) { + return ""; + } + return null; + } + + private String getChildPath(String path, String relativeToPath) { + if (relativeToTarget == null) { + return null; + } + + String relativePath = getRelativePath(path, relativeToPath); + if (relativePath == null) { + return path; + } + + if (relativePath.length() > 0) { + return relativePath; + } + + if (relativeToPath.equals(path)) { + return "."; + } + + return null; + } + + public SCMSvnDiffGenerator() { + this.originalTarget1 = null; + this.originalTarget2 = null; + this.visitedPaths = new HashSet(); + this.diffDeleted = true; + this.diffAdded = true; + } + + public void setBaseTarget(SvnTarget baseTarget) { + this.baseTarget = baseTarget; + } + + public void setUseGitFormat(boolean useGitFormat) { + this.useGitFormat = useGitFormat; + } + + public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) { + this.originalTarget1 = originalTarget1; + this.originalTarget2 = originalTarget2; + } + + public void setRelativeToTarget(SvnTarget relativeToTarget) { + this.relativeToTarget = relativeToTarget; + } + + public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) { + //anchors are not used + } + + public void setRepositoryRoot(SvnTarget repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + public void setForceEmpty(boolean forceEmpty) { + this.forceEmpty = forceEmpty; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return encoding; + } + + public String getGlobalEncoding() { + ISVNOptions options = getOptions(); + + if (options != null && options instanceof DefaultSVNOptions) { + DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options; + return defaultOptions.getGlobalCharset(); + } + return null; + } + + public void setEOL(byte[] eol) { + this.eol = eol; + } + + public byte[] getEOL() { + return eol; + } + + public boolean isForcedBinaryDiff() { + return forcedBinaryDiff; + } + + public void setForcedBinaryDiff(boolean forcedBinaryDiff) { + this.forcedBinaryDiff = forcedBinaryDiff; + } + + public boolean isPropertiesOnly() { + return propertiesOnly; + } + + public void setPropertiesOnly(boolean propertiesOnly) { + this.propertiesOnly = propertiesOnly; + } + + public boolean isIgnoreProperties() { + return ignoreProperties; + } + + public void setIgnoreProperties(boolean ignoreProperties) { + this.ignoreProperties = ignoreProperties; + } + + public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isIgnoreProperties()) { + return; + } + if (dirWasAdded && !isDiffAdded()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (displayPath == null || displayPath.length() == 0) { + displayPath = "."; + } + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + boolean showDiffHeader = !visitedPaths.contains(displayPath); + if (showDiffHeader) { + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified); + visitedPaths.add(displayPath); + if (useGitFormat) { + displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + if (shouldStopDisplaying) { + return; + } + +// if (useGitFormat) { +// String copyFromPath = null; +// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified; +// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1); +// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2); +// displayGitDiffHeader(outputStream, operationKind, +// getRelativeToRootPath(target, originalTarget1), +// getRelativeToRootPath(target, originalTarget2), +// copyFromPath); +// } + + if (useGitFormat) { + displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null); + } else { + displayHeaderFields(outputStream, label1, label2); + } + } + + displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream); + + displayPropDiffValues(outputStream, propChanges, originalProps); + } + + private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''", + displayPath, relativeToPath); + SVNErrorManager.error(errorMessage, SVNLogType.CLIENT); + } + + private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + + try { + displayString(outputStream, "--- "); + displayFirstGitLabelPath(outputStream, path1, revision1, operation); + displayEOL(outputStream); + displayString(outputStream, "+++ "); + displaySecondGitLabelPath(outputStream, path2, revision2, operation); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private String adjustRelativeToReposRoot(String targetString) { + if (repositoryRoot != null) { + String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString(); + String relativePath = getRelativePath(targetString, repositoryRootString); + return relativePath == null ? "" : relativePath; + } + return targetString; + } + + private String computeLabel(String targetString, String originalTargetString) { + if (originalTargetString.length() == 0) { + return targetString; + } else if (originalTargetString.charAt(0) == '/') { + return targetString + "\t(..." + originalTargetString + ")"; + } else { + return targetString + "\t(.../" + originalTargetString + ")"; + } + } + + public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isPropertiesOnly()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean leftIsBinary = false; + boolean rightIsBinary = false; + + if (mimeType1 != null) { + leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1); + } + if (mimeType2 != null) { + rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2); + } + + if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + + displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary); + + return; + } + + final String diffCommand = getExternalDiffCommand(); + if (diffCommand != null) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2); + } else { + internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2); + } + } + + private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException { + displayCannotDisplayFileMarkedBinary(outputStream); + + if (leftIsBinary && !rightIsBinary) { + displayMimeType(outputStream, mimeType1); + } else if (!leftIsBinary && rightIsBinary) { + displayMimeType(outputStream, mimeType2); + } else if (leftIsBinary && rightIsBinary) { + if (mimeType1.equals(mimeType2)) { + displayMimeType(outputStream, mimeType1); + } else { + displayMimeTypes(outputStream, mimeType1, mimeType2); + } + } + } + + private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException { + String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath); + if (file2 == null && !isDiffDeleted()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + if (file1 == null && !isDiffAdded()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath); + + RandomAccessFile is1 = null; + RandomAccessFile is2 = null; + try { + is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1); + is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2); + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + final String diffHeader; + if (forceEmpty || useGitFormat) { + displayString(outputStream, header); + diffHeader = headerFields; + + visitedPaths.add(displayPath); + } else { + diffHeader = header + headerFields; + } + QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader); + EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream); + QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator); + if (emptyDetectionOutputStream.isSomethingWritten()) { + visitedPaths.add(displayPath); + } + emptyDetectionOutputStream.flush(); + } catch (IOException e) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage()); + SVNErrorManager.error(err, e, SVNLogType.DEFAULT); + } finally { + SVNFileUtil.closeFile(is1); + SVNFileUtil.closeFile(is2); + } + } + + private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (useGitFormat) { + displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath); + } else { + displayHeaderFields(byteArrayOutputStream, label1, label2); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (!useGitFormat) { + displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation); + } else { + displayGitDiffHeader(byteArrayOutputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + copyFromPath); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException { + final List args = new ArrayList(); + args.add(diffCommand); + if (rawDiffOptions != null) { + args.addAll(rawDiffOptions); + } else { + Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection(); + args.addAll(svnDiffOptionsCollection); + args.add("-u"); + } + + if (label1 != null) { + args.add("-L"); + args.add(label1); + } + + if (label2 != null) { + args.add("-L"); + args.add(label2); + } + + boolean tmpFile1 = false; + boolean tmpFile2 = false; + if (file1 == null) { + file1 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile1 = true; + } + if (file2 == null) { + file2 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile2 = true; + } + + String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/'); + String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/'); + + args.add(file1Path); + args.add(file2Path); + try { + final Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + + SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true, + new ISVNReturnValueCallback() { + + public void handleReturnValue(int returnValue) throws SVNException { + if (returnValue != 0 && returnValue != 1) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, + "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)}); + SVNErrorManager.error(err, SVNLogType.DEFAULT); + } + } + + public void handleChar(char ch) throws SVNException { + try { + writer.write(ch); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } + } + + public boolean isHandleProgramOutput() { + return true; + } + }); + + writer.flush(); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } finally { + try { + if (tmpFile1) { + SVNFileUtil.deleteFile(file1); + } + if (tmpFile2) { + SVNFileUtil.deleteFile(file2); + } + } catch (SVNException e) { + // skip + } + } + } + + private String getExternalDiffCommand() { + return externalDiffCommand; + } + + private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = "); + displayString(outputStream, mimeType); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = ("); + displayString(outputStream, mimeType1); + displayString(outputStream, ", "); + displayString(outputStream, mimeType2); + displayString(outputStream, ")"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException { + try { + displayString(outputStream, "Cannot display: file marked as a binary type."); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void ensureEncodingAndEOLSet() { + if (getEOL() == null) { + setEOL(SVNProperty.EOL_LF_BYTES); + } + if (getEncoding() == null) { + final ISVNOptions options = getOptions(); + if (options != null && options.getNativeCharset() != null) { + setEncoding(options.getNativeCharset()); + } else { + setEncoding("UTF-8"); + } + } + } + + private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException { + for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) { + String name = (String) changedPropNames.next(); + SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null; + SVNPropertyValue newValue = diff.getSVNPropertyValue(name); + String headerFormat = null; + + if (originalValue == null) { + headerFormat = "Added: "; + } else if (newValue == null) { + headerFormat = "Deleted: "; + } else { + headerFormat = "Modified: "; + } + + try { + displayString(outputStream, (headerFormat + name)); + displayEOL(outputStream); + if (SVNProperty.MERGE_INFO.equals(name)) { + displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString()); + continue; + } + + byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding()); + byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding()); + + if (originalValueBytes == null) { + originalValueBytes = new byte[0]; + } else { + originalValueBytes = maybeAppendEOL(originalValueBytes); + } + + boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 && + (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] || + newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]); + + if (newValueBytes == null) { + newValueBytes = new byte[0]; + } else { + newValueBytes = maybeAppendEOL(newValueBytes); + } + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##"); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + QDiffGenerator generator = new QDiffUniGenerator(properties, ""); + Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes), + null, writer, generator); + writer.flush(); + if (!newValueHadEol) { + displayString(outputStream, "\\ No newline at end of property"); + displayEOL(outputStream); + } + } catch (IOException e) { + wrapException(e); + } + } + + } + + private byte[] maybeAppendEOL(byte[] buffer) { + if (buffer.length == 0) { + return buffer; + } + + byte lastByte = buffer[buffer.length - 1]; + if (lastByte == SVNProperty.EOL_CR_BYTES[0]) { + return buffer; + } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) { + final byte[] newBuffer = new byte[buffer.length + getEOL().length]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length); + return newBuffer; + } else { + return buffer; + } + } + + private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("a/" + copyFromPath, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("a/" + copyFromPath, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("b/" + path2, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath); + } + } + + private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "new file mode 100644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "deleted file mode 100644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "copy from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "copy to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "rename from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "rename to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException { + displayGitPath(outputStream, path1, "a/", false); + } + + private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException { + displayGitPath(outputStream, path2, "b/", false); + } + + private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "a/"; + if (operation == SvnDiffCallback.OperationKind.Added) { + path1 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix, true); + } + + private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "b/"; + if (operation == SvnDiffCallback.OperationKind.Deleted) { + path2 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix, true); + } + + private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix, boolean label) throws IOException { +// if (!label && path1.length() == 0) { +// displayString(outputStream, "."); +// } else { + displayString(outputStream, pathPrefix); + displayString(outputStream, path1); +// } + } + + private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) { + String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor); + return getLabel(adjustedPath, revision); + } + + private String getAdjustedPath(String displayPath, String path1, String commonAncestor) { + String adjustedPath = getRelativePath(path1, commonAncestor); + + if (adjustedPath == null || adjustedPath.length() == 0) { + adjustedPath = displayPath; + } else if (adjustedPath.charAt(0) == '/') { + adjustedPath = displayPath + "\t(..." + adjustedPath + ")"; + } else { + adjustedPath = displayPath + "\t(.../" + adjustedPath + ")"; + } + return adjustedPath; + //TODO: respect relativeToDir + } + + protected String getLabel(String path, String revToken) { + if (useGitFormat){ + return path; + } + revToken = revToken == null ? WC_REVISION_LABEL : revToken; + return path + "\t" + revToken; + } + + protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException { + try { + if (deleted && !isDiffDeleted()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (deleted)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + if (added && !isDiffAdded()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (added)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + displayString(os, "Index: "); + displayString(os, path); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return false; + } catch (IOException e) { + wrapException(e); + } + return false; + } + + protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException { + try { + displayString(os, "--- "); + displayString(os, label1); + displayEOL(os); + displayString(os, "+++ "); + displayString(os, label2); + displayEOL(os); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException { + try { + displayEOL(outputStream); + displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path))); + displayEOL(outputStream); + displayString(outputStream, PROPERTIES_SEPARATOR); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) { + if (value == null) { + return null; + } + if (value.isString()) { + try { + return value.getString().getBytes(encoding); + } catch (UnsupportedEncodingException e) { + return value.getString().getBytes(); + } + } + return value.getBytes(); + } + + private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException { + Map oldMergeInfo = null; + Map newMergeInfo = null; + if (oldValue != null) { + oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null); + } + if (newValue != null) { + newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null); + } + + Map deleted = new TreeMap(); + Map added = new TreeMap(); + SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true); + + for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path); + displayString(outputStream, (" Reverse-merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + + for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path); + displayString(outputStream, (" Merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + } + + private boolean useLocalFileSeparatorChar() { + return true; + } + + public boolean isDiffDeleted() { + return diffDeleted; + } + + public boolean isDiffAdded() { + return diffAdded; + } + + private void wrapException(IOException e) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e); + SVNErrorManager.error(errorMessage, e, SVNLogType.WC); + } + + private void displayString(OutputStream outputStream, String s) throws IOException { + outputStream.write(s.getBytes(HEADER_ENCODING)); + } + + private void displayEOL(OutputStream os) throws IOException { + os.write(getEOL()); + } + + public SVNDiffOptions getDiffOptions() { + if (diffOptions == null) { + diffOptions = new SVNDiffOptions(); + } + return diffOptions; + } + + public void setExternalDiffCommand(String externalDiffCommand) { + this.externalDiffCommand = externalDiffCommand; + } + + public void setRawDiffOptions(List rawDiffOptions) { + this.rawDiffOptions = rawDiffOptions; + } + + public void setDiffOptions(SVNDiffOptions diffOptions) { + this.diffOptions = diffOptions; + } + + public void setDiffDeleted(boolean diffDeleted) { + this.diffDeleted = diffDeleted; + } + + public void setDiffAdded(boolean diffAdded) { + this.diffAdded = diffAdded; + } + + public void setBasePath(File absoluteFile) { + setBaseTarget(SvnTarget.fromFile(absoluteFile)); + } + + public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) { + this.fallbackToAbsolutePath = fallbackToAbsolutePath; + } + + public void setOptions(ISVNOptions options) { + this.options = options; + } + + public ISVNOptions getOptions() { + return options; + } + + private class EmptyDetectionOutputStream extends OutputStream { + + private final OutputStream outputStream; + private boolean somethingWritten; + + public EmptyDetectionOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + this.somethingWritten = false; + } + + public boolean isSomethingWritten() { + return somethingWritten; + } + + @Override + public void write(int c) throws IOException { + somethingWritten = true; + outputStream.write(c); + } + + @Override + public void write(byte[] bytes) throws IOException { + somethingWritten = bytes.length > 0; + outputStream.write(bytes); + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + somethingWritten = length > 0; + outputStream.write(bytes, offset, length); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + } +} diff --git a/scm-it/src/test/resources/diff/largefile/original/SvnDiffGenerator_forTest b/scm-it/src/test/resources/diff/largefile/original/SvnDiffGenerator_forTest new file mode 100644 index 0000000000..d0cf749024 --- /dev/null +++ b/scm-it/src/test/resources/diff/largefile/original/SvnDiffGenerator_forTest @@ -0,0 +1,1240 @@ +package sonia.scm.repository.spi; + +import de.regnis.q.sequence.line.diff.QDiffGenerator; +import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory; +import de.regnis.q.sequence.line.diff.QDiffManager; +import de.regnis.q.sequence.line.diff.QDiffUniGenerator; +import org.tmatesoft.svn.core.SVNErrorCode; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNMergeRangeList; +import org.tmatesoft.svn.core.SVNProperties; +import org.tmatesoft.svn.core.SVNProperty; +import org.tmatesoft.svn.core.SVNPropertyValue; +import org.tmatesoft.svn.core.internal.util.SVNHashMap; +import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; +import org.tmatesoft.svn.core.internal.util.SVNPathUtil; +import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; +import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback; +import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; +import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; +import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback; +import org.tmatesoft.svn.core.wc.ISVNOptions; +import org.tmatesoft.svn.core.wc.SVNDiffOptions; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import org.tmatesoft.svn.util.SVNLogType; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +public class SCMSvnDiffGenerator implements ISvnDiffGenerator { + + protected static final String WC_REVISION_LABEL = "(working copy)"; + protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________"; + protected static final String HEADER_SEPARATOR = "==================================================================="; + protected static final String HEADER_ENCODING = "UTF-8"; + + private SvnTarget originalTarget1; + private SvnTarget originalTarget2; + private SvnTarget baseTarget; + private SvnTarget relativeToTarget; + private SvnTarget repositoryRoot; + private String encoding; + private byte[] eol; + private boolean useGitFormat; + private boolean forcedBinaryDiff; + + private boolean diffDeleted; + private boolean diffAdded; + private List rawDiffOptions; + private boolean forceEmpty; + + private Set visitedPaths; + private String externalDiffCommand; + private SVNDiffOptions diffOptions; + private boolean fallbackToAbsolutePath; + private ISVNOptions options; + private boolean propertiesOnly; + private boolean ignoreProperties; + + private String getDisplayPath(SvnTarget target) { + String relativePath; + if (baseTarget == null) { + relativePath = null; + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = baseTarget.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) { + String relativePath; + if (repositoryRoot == null) + { + relativePath = null; + } + else + { + if (repositoryRoot.isFile() == target.isFile()) + { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = repositoryRoot.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + else + { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = new File("").getAbsolutePath(); + relativePath = getRelativePath(targetString, baseTargetString); + } + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativePath(String targetString, String baseTargetString) { + if (targetString != null) + { + targetString = targetString.replace(File.separatorChar, '/'); + } + if (baseTargetString != null) + { + baseTargetString = baseTargetString.replace(File.separatorChar, '/'); + } + + final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString); + if (pathAsChild != null) + { + return pathAsChild; + } + if (targetString.equals(baseTargetString)) + { + return ""; + } + return null; + } + + private String getChildPath(String path, String relativeToPath) { + if (relativeToTarget == null) { + return null; + } + + String relativePath = getRelativePath(path, relativeToPath); + if (relativePath == null) { + return path; + } + + if (relativePath.length() > 0) { + return relativePath; + } + + if (relativeToPath.equals(path)) { + return "."; + } + + return null; + } + + public SCMSvnDiffGenerator() { + this.originalTarget1 = null; + this.originalTarget2 = null; + this.visitedPaths = new HashSet(); + this.diffDeleted = true; + this.diffAdded = true; + } + + public void setBaseTarget(SvnTarget baseTarget) { + this.baseTarget = baseTarget; + } + + public void setUseGitFormat(boolean useGitFormat) { + this.useGitFormat = useGitFormat; + } + + public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) { + this.originalTarget1 = originalTarget1; + this.originalTarget2 = originalTarget2; + } + + public void setRelativeToTarget(SvnTarget relativeToTarget) { + this.relativeToTarget = relativeToTarget; + } + + public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) { + //anchors are not used + } + + public void setRepositoryRoot(SvnTarget repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + public void setForceEmpty(boolean forceEmpty) { + this.forceEmpty = forceEmpty; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return encoding; + } + + public String getGlobalEncoding() { + ISVNOptions options = getOptions(); + + if (options != null && options instanceof DefaultSVNOptions) { + DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options; + return defaultOptions.getGlobalCharset(); + } + return null; + } + + public void setEOL(byte[] eol) { + this.eol = eol; + } + + public byte[] getEOL() { + return eol; + } + + public boolean isForcedBinaryDiff() { + return forcedBinaryDiff; + } + + public void setForcedBinaryDiff(boolean forcedBinaryDiff) { + this.forcedBinaryDiff = forcedBinaryDiff; + } + + public boolean isPropertiesOnly() { + return propertiesOnly; + } + + public void setPropertiesOnly(boolean propertiesOnly) { + this.propertiesOnly = propertiesOnly; + } + + public boolean isIgnoreProperties() { + return ignoreProperties; + } + + public void setIgnoreProperties(boolean ignoreProperties) { + this.ignoreProperties = ignoreProperties; + } + + public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isIgnoreProperties()) { + return; + } + if (dirWasAdded && !isDiffAdded()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (displayPath == null || displayPath.length() == 0) { + displayPath = "."; + } + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + boolean showDiffHeader = !visitedPaths.contains(displayPath); + if (showDiffHeader) { + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified); + visitedPaths.add(displayPath); + if (useGitFormat) { + displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + if (shouldStopDisplaying) { + return; + } + +// if (useGitFormat) { +// String copyFromPath = null; +// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified; +// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1); +// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2); +// displayGitDiffHeader(outputStream, operationKind, +// getRelativeToRootPath(target, originalTarget1), +// getRelativeToRootPath(target, originalTarget2), +// copyFromPath); +// } + + if (useGitFormat) { + displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null); + } else { + displayHeaderFields(outputStream, label1, label2); + } + } + + displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream); + + displayPropDiffValues(outputStream, propChanges, originalProps); + } + + private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''", + displayPath, relativeToPath); + SVNErrorManager.error(errorMessage, SVNLogType.CLIENT); + } + + private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + + try { + displayString(outputStream, "--- "); + displayFirstGitLabelPath(outputStream, path1, revision1, operation); + displayEOL(outputStream); + displayString(outputStream, "+++ "); + displaySecondGitLabelPath(outputStream, path2, revision2, operation); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private String adjustRelativeToReposRoot(String targetString) { + if (repositoryRoot != null) { + String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString(); + String relativePath = getRelativePath(targetString, repositoryRootString); + return relativePath == null ? "" : relativePath; + } + return targetString; + } + + private String computeLabel(String targetString, String originalTargetString) { + if (originalTargetString.length() == 0) { + return targetString; + } else if (originalTargetString.charAt(0) == '/') { + return targetString + "\t(..." + originalTargetString + ")"; + } else { + return targetString + "\t(.../" + originalTargetString + ")"; + } + } + + public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isPropertiesOnly()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean leftIsBinary = false; + boolean rightIsBinary = false; + + if (mimeType1 != null) { + leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1); + } + if (mimeType2 != null) { + rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2); + } + + if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + + displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary); + + return; + } + + final String diffCommand = getExternalDiffCommand(); + if (diffCommand != null) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2); + } else { + internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2); + } + } + + private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException { + displayCannotDisplayFileMarkedBinary(outputStream); + + if (leftIsBinary && !rightIsBinary) { + displayMimeType(outputStream, mimeType1); + } else if (!leftIsBinary && rightIsBinary) { + displayMimeType(outputStream, mimeType2); + } else if (leftIsBinary && rightIsBinary) { + if (mimeType1.equals(mimeType2)) { + displayMimeType(outputStream, mimeType1); + } else { + displayMimeTypes(outputStream, mimeType1, mimeType2); + } + } + } + + private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException { + String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath); + if (file2 == null && !isDiffDeleted()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + if (file1 == null && !isDiffAdded()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath); + + RandomAccessFile is1 = null; + RandomAccessFile is2 = null; + try { + is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1); + is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2); + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + final String diffHeader; + if (forceEmpty || useGitFormat) { + displayString(outputStream, header); + diffHeader = headerFields; + + visitedPaths.add(displayPath); + } else { + diffHeader = header + headerFields; + } + QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader); + EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream); + QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator); + if (emptyDetectionOutputStream.isSomethingWritten()) { + visitedPaths.add(displayPath); + } + emptyDetectionOutputStream.flush(); + } catch (IOException e) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage()); + SVNErrorManager.error(err, e, SVNLogType.DEFAULT); + } finally { + SVNFileUtil.closeFile(is1); + SVNFileUtil.closeFile(is2); + } + } + + private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (useGitFormat) { + displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath); + } else { + displayHeaderFields(byteArrayOutputStream, label1, label2); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + boolean stopDisplaying = displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation); + if (useGitFormat) { + displayGitDiffHeader(byteArrayOutputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + copyFromPath); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException { + final List args = new ArrayList(); + args.add(diffCommand); + if (rawDiffOptions != null) { + args.addAll(rawDiffOptions); + } else { + Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection(); + args.addAll(svnDiffOptionsCollection); + args.add("-u"); + } + + if (label1 != null) { + args.add("-L"); + args.add(label1); + } + + if (label2 != null) { + args.add("-L"); + args.add(label2); + } + + boolean tmpFile1 = false; + boolean tmpFile2 = false; + if (file1 == null) { + file1 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile1 = true; + } + if (file2 == null) { + file2 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile2 = true; + } + + String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/'); + String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/'); + + args.add(file1Path); + args.add(file2Path); + try { + final Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + + SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true, + new ISVNReturnValueCallback() { + + public void handleReturnValue(int returnValue) throws SVNException { + if (returnValue != 0 && returnValue != 1) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, + "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)}); + SVNErrorManager.error(err, SVNLogType.DEFAULT); + } + } + + public void handleChar(char ch) throws SVNException { + try { + writer.write(ch); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } + } + + public boolean isHandleProgramOutput() { + return true; + } + }); + + writer.flush(); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } finally { + try { + if (tmpFile1) { + SVNFileUtil.deleteFile(file1); + } + if (tmpFile2) { + SVNFileUtil.deleteFile(file2); + } + } catch (SVNException e) { + // skip + } + } + } + + private String getExternalDiffCommand() { + return externalDiffCommand; + } + + private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = "); + displayString(outputStream, mimeType); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = ("); + displayString(outputStream, mimeType1); + displayString(outputStream, ", "); + displayString(outputStream, mimeType2); + displayString(outputStream, ")"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException { + try { + displayString(outputStream, "Cannot display: file marked as a binary type."); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void ensureEncodingAndEOLSet() { + if (getEOL() == null) { + setEOL(SVNProperty.EOL_LF_BYTES); + } + if (getEncoding() == null) { + final ISVNOptions options = getOptions(); + if (options != null && options.getNativeCharset() != null) { + setEncoding(options.getNativeCharset()); + } else { + setEncoding("UTF-8"); + } + } + } + + private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException { + for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) { + String name = (String) changedPropNames.next(); + SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null; + SVNPropertyValue newValue = diff.getSVNPropertyValue(name); + String headerFormat = null; + + if (originalValue == null) { + headerFormat = "Added: "; + } else if (newValue == null) { + headerFormat = "Deleted: "; + } else { + headerFormat = "Modified: "; + } + + try { + displayString(outputStream, (headerFormat + name)); + displayEOL(outputStream); + if (SVNProperty.MERGE_INFO.equals(name)) { + displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString()); + continue; + } + + byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding()); + byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding()); + + if (originalValueBytes == null) { + originalValueBytes = new byte[0]; + } else { + originalValueBytes = maybeAppendEOL(originalValueBytes); + } + + boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 && + (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] || + newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]); + + if (newValueBytes == null) { + newValueBytes = new byte[0]; + } else { + newValueBytes = maybeAppendEOL(newValueBytes); + } + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##"); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + QDiffGenerator generator = new QDiffUniGenerator(properties, ""); + Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes), + null, writer, generator); + writer.flush(); + if (!newValueHadEol) { + displayString(outputStream, "\\ No newline at end of property"); + displayEOL(outputStream); + } + } catch (IOException e) { + wrapException(e); + } + } + + } + + private byte[] maybeAppendEOL(byte[] buffer) { + if (buffer.length == 0) { + return buffer; + } + + byte lastByte = buffer[buffer.length - 1]; + if (lastByte == SVNProperty.EOL_CR_BYTES[0]) { + return buffer; + } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) { + final byte[] newBuffer = new byte[buffer.length + getEOL().length]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length); + return newBuffer; + } else { + return buffer; + } + } + + private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("a/" + copyFromPath, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("a/" + copyFromPath, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("b/" + path2, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath); + } + } + + private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "new file mode 10644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "deleted file mode 10644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "copy from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "copy to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "rename from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "rename to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException { + displayGitPath(outputStream, path1, "a/", false); + } + + private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException { + displayGitPath(outputStream, path2, "b/", false); + } + + private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "a/"; + if (operation == SvnDiffCallback.OperationKind.Added) { + path1 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix, true); + } + + private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "b/"; + if (operation == SvnDiffCallback.OperationKind.Deleted) { + path2 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix, true); + } + + private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix, boolean label) throws IOException { +// if (!label && path1.length() == 0) { +// displayString(outputStream, "."); +// } else { + displayString(outputStream, pathPrefix); + displayString(outputStream, path1); +// } + } + + private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) { + String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor); + return getLabel(adjustedPath, revision); + } + + private String getAdjustedPath(String displayPath, String path1, String commonAncestor) { + String adjustedPath = getRelativePath(path1, commonAncestor); + + if (adjustedPath == null || adjustedPath.length() == 0) { + adjustedPath = displayPath; + } else if (adjustedPath.charAt(0) == '/') { + adjustedPath = displayPath + "\t(..." + adjustedPath + ")"; + } else { + adjustedPath = displayPath + "\t(.../" + adjustedPath + ")"; + } + return adjustedPath; + //TODO: respect relativeToDir + } + + protected String getLabel(String path, String revToken) { + revToken = revToken == null ? WC_REVISION_LABEL : revToken; + return path + "\t" + revToken; + } + + protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException { + try { + if (deleted && !isDiffDeleted()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (deleted)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + if (added && !isDiffAdded()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (added)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + displayString(os, "Index: "); + displayString(os, path); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return false; + } catch (IOException e) { + wrapException(e); + } + return false; + } + + protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException { + try { + displayString(os, "--- "); + displayString(os, label1); + displayEOL(os); + displayString(os, "+++ "); + displayString(os, label2); + displayEOL(os); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException { + try { + displayEOL(outputStream); + displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path))); + displayEOL(outputStream); + displayString(outputStream, PROPERTIES_SEPARATOR); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) { + if (value == null) { + return null; + } + if (value.isString()) { + try { + return value.getString().getBytes(encoding); + } catch (UnsupportedEncodingException e) { + return value.getString().getBytes(); + } + } + return value.getBytes(); + } + + private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException { + Map oldMergeInfo = null; + Map newMergeInfo = null; + if (oldValue != null) { + oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null); + } + if (newValue != null) { + newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null); + } + + Map deleted = new TreeMap(); + Map added = new TreeMap(); + SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true); + + for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path); + displayString(outputStream, (" Reverse-merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + + for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path); + displayString(outputStream, (" Merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + } + + private boolean useLocalFileSeparatorChar() { + return true; + } + + public boolean isDiffDeleted() { + return diffDeleted; + } + + public boolean isDiffAdded() { + return diffAdded; + } + + private void wrapException(IOException e) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e); + SVNErrorManager.error(errorMessage, e, SVNLogType.WC); + } + + private void displayString(OutputStream outputStream, String s) throws IOException { + outputStream.write(s.getBytes(HEADER_ENCODING)); + } + + private void displayEOL(OutputStream os) throws IOException { + os.write(getEOL()); + } + + public SVNDiffOptions getDiffOptions() { + if (diffOptions == null) { + diffOptions = new SVNDiffOptions(); + } + return diffOptions; + } + + public void setExternalDiffCommand(String externalDiffCommand) { + this.externalDiffCommand = externalDiffCommand; + } + + public void setRawDiffOptions(List rawDiffOptions) { + this.rawDiffOptions = rawDiffOptions; + } + + public void setDiffOptions(SVNDiffOptions diffOptions) { + this.diffOptions = diffOptions; + } + + public void setDiffDeleted(boolean diffDeleted) { + this.diffDeleted = diffDeleted; + } + + public void setDiffAdded(boolean diffAdded) { + this.diffAdded = diffAdded; + } + + public void setBasePath(File absoluteFile) { + setBaseTarget(SvnTarget.fromFile(absoluteFile)); + } + + public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) { + this.fallbackToAbsolutePath = fallbackToAbsolutePath; + } + + public void setOptions(ISVNOptions options) { + this.options = options; + } + + public ISVNOptions getOptions() { + return options; + } + + private class EmptyDetectionOutputStream extends OutputStream { + + private final OutputStream outputStream; + private boolean somethingWritten; + + public EmptyDetectionOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + this.somethingWritten = false; + } + + public boolean isSomethingWritten() { + return somethingWritten; + } + + @Override + public void write(int c) throws IOException { + somethingWritten = true; + outputStream.write(c); + } + + @Override + public void write(byte[] bytes) throws IOException { + somethingWritten = bytes.length > 0; + outputStream.write(bytes); + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + somethingWritten = length > 0; + outputStream.write(bytes, offset, length); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + } +} diff --git a/scm-plugin-backend/pom.xml b/scm-plugin-backend/pom.xml deleted file mode 100644 index c3c62e7c8f..0000000000 --- a/scm-plugin-backend/pom.xml +++ /dev/null @@ -1,164 +0,0 @@ - - - - 4.0.0 - - - scm - sonia.scm - 1.55-SNAPSHOT - - - scm-plugin-backend - war - 1.55-SNAPSHOT - ${project.artifactId} - - - - - javax.servlet - servlet-api - ${servlet.version} - provided - - - - - - javax.transaction - jta - 1.1 - provided - - - - - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - - - - org.slf4j - log4j-over-slf4j - ${slf4j.version} - - - - ch.qos.logback - logback-classic - ${logback.version} - - - - org.freemarker - freemarker - ${freemarker.version} - - - - sonia.scm - scm-core - 1.55-SNAPSHOT - - - - com.sun.jersey.contribs - jersey-guice - ${jersey.version} - - - - rome - rome - 1.0 - - - - net.sf.ehcache - ehcache-core - ${ehcache.version} - - - - org.imgscalr - imgscalr-lib - 4.2 - - - - - - org.apache.shiro - shiro-web - ${shiro.version} - - - - org.apache.shiro - shiro-guice - ${shiro.version} - - - - org.apache.shiro - shiro-ehcache - ${shiro.version} - - - - - - src/main/webapp/template/** - - - - - - - - com.mycila.maven-license-plugin - maven-license-plugin - 1.9.0 - -

http://download.scm-manager.org/licenses/mvn-license.txt
- - src/** - **/test/** - - - target/** - .hg/** - **/html5.js - **/*.html - **/fancybox/** - - true - - - - - org.mortbay.jetty - jetty-maven-plugin - ${jetty.version} - - 8004 - STOP - - /scm-plugin-backend - - ${project.build.javaLevel} - ${project.build.javaLevel} - ${project.build.sourceEncoding} - 0 - - - - - - scm-plugin-backend - - - diff --git a/scm-plugin-backend/src/main/java/sonia/scm/plugin/AdminAccountConfiguration.java b/scm-plugin-backend/src/main/java/sonia/scm/plugin/AdminAccountConfiguration.java deleted file mode 100644 index 46bbc06c64..0000000000 --- a/scm-plugin-backend/src/main/java/sonia/scm/plugin/AdminAccountConfiguration.java +++ /dev/null @@ -1,231 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm.plugin; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Objects; - -import org.apache.shiro.authc.SaltedAuthenticationInfo; -import org.apache.shiro.codec.Base64; -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.SimplePrincipalCollection; -import org.apache.shiro.util.ByteSource; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "admin-account") -@XmlAccessorType(XmlAccessType.FIELD) -public class AdminAccountConfiguration implements SaltedAuthenticationInfo -{ - - /** Field description */ - private static final long serialVersionUID = -8678832281151044462L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public AdminAccountConfiguration() {} - - /** - * Constructs ... - * - * - * @param username - * @param salt - * @param password - */ - public AdminAccountConfiguration(String username, String salt, - String password) - { - this.username = username; - this.salt = salt; - this.password = password; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param obj - * - * @return - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final AdminAccountConfiguration other = (AdminAccountConfiguration) obj; - - return Objects.equal(username, other.username) - && Objects.equal(salt, other.salt) - && Objects.equal(password, other.password); - } - - /** - * Method description - * - * - * @return - */ - @Override - public int hashCode() - { - return Objects.hashCode(username, salt, password); - } - - /** - * Method description - * - * - * @return - */ - @Override - @SuppressWarnings("squid:S2068") - public String toString() - { - //J- - return Objects.toStringHelper(this) - .add("username", username) - .add("salt", "xxx") - .add("password", "xxx") - .toString(); - //J+ - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public Object getCredentials() - { - return password; - } - - /** - * Method description - * - * - * @return - */ - @Override - public ByteSource getCredentialsSalt() - { - return ByteSource.Util.bytes(Base64.decode(salt)); - } - - /** - * Method description - * - * - * @return - */ - public String getPassword() - { - return password; - } - - /** - * Method description - * - * - * @return - */ - @Override - public PrincipalCollection getPrincipals() - { - - // TODO - return new SimplePrincipalCollection(username, "scm-backend"); - } - - /** - * Method description - * - * - * @return - */ - public String getSalt() - { - return salt; - } - - /** - * Method description - * - * - * @return - */ - public String getUsername() - { - return username; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String password; - - /** Field description */ - private String salt; - - /** Field description */ - private String username; -} diff --git a/scm-plugin-backend/src/main/java/sonia/scm/plugin/security/SecurityModule.java b/scm-plugin-backend/src/main/java/sonia/scm/plugin/security/SecurityModule.java deleted file mode 100644 index 9be130375e..0000000000 --- a/scm-plugin-backend/src/main/java/sonia/scm/plugin/security/SecurityModule.java +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm.plugin.security; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.name.Named; -import com.google.inject.name.Names; - -import org.apache.shiro.authc.credential.CredentialsMatcher; -import org.apache.shiro.authc.credential.HashedCredentialsMatcher; -import org.apache.shiro.cache.CacheManager; -import org.apache.shiro.crypto.RandomNumberGenerator; -import org.apache.shiro.crypto.SecureRandomNumberGenerator; -import org.apache.shiro.crypto.hash.SimpleHash; -import org.apache.shiro.guice.web.ShiroWebModule; -import org.apache.shiro.util.ByteSource; - -import sonia.scm.plugin.Roles; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.servlet.ServletContext; - -import javax.swing.JOptionPane; - -/** - * - * @author Sebastian Sdorra - */ -public class SecurityModule extends ShiroWebModule -{ - - /** Field description */ - private static final String ATTRIBUTE_FAILURE = "shiroLoginFailure"; - - /** Field description */ - private static final String HASH_ALGORITHM = "SHA-256"; - - /** Field description */ - private static final int HASH_ITERATIONS = 1024; - - /** Field description */ - private static final String PAGE_LOGIN = "/page/login.html"; - - /** Field description */ - private static final String PAGE_SUCCESS = "/admin/index.html"; - - /** Field description */ - private static final String PAGE_UNAUTHORIZED = "/error/unauthorized.html"; - - /** Field description */ - @SuppressWarnings("squid:S2068") - private static final String PARAM_PASSWORD = "password"; - - /** Field description */ - private static final String PARAM_REMEMBERME = "rememberme"; - - /** Field description */ - private static final String PARAM_USERNAME = "username"; - - /** Field description */ - private static final String PATTERN_ADMIN = "/admin/**"; - - /** Field description */ - private static final Named NAMED_USERNAMEPARAM = - Names.named("shiro.usernameParam"); - - /** Field description */ - private static final Named NAMED_UNAUTHORIZEDURL = - Names.named("shiro.unauthorizedUrl"); - - /** Field description */ - private static final Named NAMED_SUCCESSURL = Names.named("shiro.successUrl"); - - /** Field description */ - private static final Named NAMED_REMEMBERMEPARAM = - Names.named("shiro.rememberMeParam"); - - /** Field description */ - private static final Named NAMED_PASSWORDPARAM = - Names.named("shiro.passwordParam"); - - /** Field description */ - private static final Named NAMED_LOGINURL = Names.named("shiro.loginUrl"); - - /** Field description */ - private static final Named NAMED_FAILUREKEYATTRIBUTE = - Names.named("shiro.failureKeyAttribute"); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param servletContext - */ - public SecurityModule(ServletContext servletContext) - { - super(servletContext); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param args - */ - public static void main(String[] args) - { - String value = JOptionPane.showInputDialog("Password"); - RandomNumberGenerator rng = new SecureRandomNumberGenerator(); - ByteSource salt = rng.nextBytes(); - SimpleHash hash = new SimpleHash(HASH_ALGORITHM, value, salt, - HASH_ITERATIONS); - - System.out.append("Salt: ").println(salt.toBase64()); - System.out.append("Hash: ").println(hash.toBase64()); - } - - /** - * Method description - * - */ - @Override - protected void configureShiroWeb() - { - bindConstants(); - bindCredentialsMatcher(); - - // bind cache manager - bind(CacheManager.class).toProvider(CacheManagerProvider.class); - - // bind realm - bindRealm().to(DefaultAdminRealm.class); - - // add filters - addFilterChain(PAGE_LOGIN, AUTHC); - addFilterChain(PATTERN_ADMIN, AUTHC, config(ROLES, Roles.ADMIN)); - } - - /** - * Method description - * - */ - private void bindConstants() - { - bindConstant().annotatedWith(NAMED_LOGINURL).to(PAGE_LOGIN); - bindConstant().annotatedWith(NAMED_USERNAMEPARAM).to(PARAM_USERNAME); - bindConstant().annotatedWith(NAMED_PASSWORDPARAM).to(PARAM_PASSWORD); - bindConstant().annotatedWith(NAMED_REMEMBERMEPARAM).to(PARAM_REMEMBERME); - bindConstant().annotatedWith(NAMED_SUCCESSURL).to(PAGE_SUCCESS); - bindConstant().annotatedWith(NAMED_UNAUTHORIZEDURL).to(PAGE_UNAUTHORIZED); - bindConstant().annotatedWith(NAMED_FAILUREKEYATTRIBUTE).to( - ATTRIBUTE_FAILURE); - } - - /** - * Method description - * - */ - private void bindCredentialsMatcher() - { - HashedCredentialsMatcher matcher = - new HashedCredentialsMatcher(HASH_ALGORITHM); - - matcher.setHashIterations(HASH_ITERATIONS); - matcher.setStoredCredentialsHexEncoded(false); - bind(CredentialsMatcher.class).toInstance(matcher); - } -} diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index 5b61882815..1611633449 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -21,7 +21,7 @@ scm-svn-plugin scm-legacy-plugin - + @@ -30,23 +30,37 @@ ${servlet.version} provided - + sonia.scm scm-core 2.0.0-SNAPSHOT provided - - - + + + sonia.scm scm-annotation-processor 2.0.0-SNAPSHOT provided - + + + + org.mapstruct + mapstruct-processor + provided + + + + + org.projectlombok + lombok + provided + + @@ -56,6 +70,18 @@ test + + org.jboss.resteasy + resteasy-jaxrs + test + + + + org.jboss.resteasy + resteasy-jackson2-provider + test + + @@ -80,60 +106,118 @@
- + - + sonia.scm.maven smp-maven-plugin - 1.0.0-alpha-2 true - - true - - - - fix-descriptor - process-resources - - fix-descriptor - - - - append-dependencies - process-classes - - append-dependencies - - - - + + + com.github.sdorra + buildfrontend-maven-plugin + + + ${nodejs.version} + + + YARN + ${yarn.version} + + false + + + - release + plugin-doc + - sonia.maven - web-compressor - 1.4 + org.apache.maven.plugins + maven-resources-plugin + copy-enunciate-configuration compile - compress-directory + copy-resources + + ${project.build.directory} + + + src/main/doc + true + + **/enunciate.xml + + + + + + + + + + com.webcohesion.enunciate + enunciate-maven-plugin + + + + docs + + compile - true - ${project.build.directory}/classes + ${project.build.directory}/enunciate.xml + ${project.build.directory} + restdocs + + + com.webcohesion.enunciate + enunciate-top + ${enunciate.version} + + + com.webcohesion.enunciate + enunciate-swagger + + + + + com.webcohesion.enunciate + enunciate-lombok + ${enunciate.version} + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + src/main/doc/assembly.xml + + + + + package + + single + + + diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json new file mode 100644 index 0000000000..8ca5d633e4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/package.json @@ -0,0 +1,25 @@ +{ + "name": "@scm-manager/scm-git-plugin", + "private": true, + "version": "2.0.0-SNAPSHOT", + "license": "BSD-3-Clause", + "main": "./src/main/js/index.ts", + "scripts": { + "build": "ui-scripts plugin", + "watch": "ui-scripts plugin-watch", + "test": "jest", + "typecheck": "tsc" + }, + "babel": { + "presets": [ + "@scm-manager/babel-preset" + ] + }, + "jest": { + "preset": "@scm-manager/jest-preset" + }, + "prettier": "@scm-manager/prettier-config", + "dependencies": { + "@scm-manager/ui-plugins": "^2.0.0-SNAPSHOT" + } +} diff --git a/scm-plugins/scm-git-plugin/pom.xml b/scm-plugins/scm-git-plugin/pom.xml index c2e37510d8..0cace5e94d 100644 --- a/scm-plugins/scm-git-plugin/pom.xml +++ b/scm-plugins/scm-git-plugin/pom.xml @@ -1,6 +1,6 @@ - + 4.0.0 @@ -10,13 +10,12 @@ scm-git-plugin - 2.0.0-SNAPSHOT - scm-git-plugin smp https://bitbucket.org/sdorra/scm-manager Plugin for the version control system Git + sonia.jgit org.eclipse.jgit @@ -40,13 +39,29 @@ commons-lang 2.6 + + + org.jboss.resteasy + resteasy-jackson2-provider + ${resteasy.version} + + - - - + - + + + sonia.scm.maven + smp-maven-plugin + true + + true + + + + + org.apache.maven.plugins maven-jar-plugin @@ -59,20 +74,20 @@ - + - + - + - + maven.scm-manager.org scm-manager release repository http://maven.scm-manager.org/nexus/content/groups/public - + diff --git a/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java b/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java index 88c988537d..8e1a6e5ef3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java +++ b/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java @@ -48,6 +48,7 @@ import org.eclipse.jgit.lib.RepositoryCache; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.spi.HookEventFacade; +import sonia.scm.web.CollectingPackParserListener; import sonia.scm.web.GitReceiveHook; //~--- JDK imports ------------------------------------------------------------ @@ -64,10 +65,10 @@ public class ScmTransportProtocol extends TransportProtocol { /** Field description */ - private static final String NAME = "scm"; + public static final String NAME = "scm"; /** Field description */ - private static final Set SCHEMES = ImmutableSet.of("scm"); + private static final Set SCHEMES = ImmutableSet.of(NAME); //~--- constructors --------------------------------------------------------- @@ -136,7 +137,7 @@ public class ScmTransportProtocol extends TransportProtocol */ @Override public Transport open(URIish uri, Repository local, String remoteName) - throws NotSupportedException, TransportException + throws TransportException { File localDirectory = local.getDirectory(); File path = local.getFS().resolve(localDirectory, uri.getPath()); @@ -150,7 +151,7 @@ public class ScmTransportProtocol extends TransportProtocol //J- return new TransportLocalWithHooks( hookEventFacadeProvider.get(), - repositoryHandlerProvider.get(), + repositoryHandlerProvider.get(), local, uri, gitDir ); //J+ @@ -234,6 +235,8 @@ public class ScmTransportProtocol extends TransportProtocol pack.setPreReceiveHook(hook); pack.setPostReceiveHook(hook); + + CollectingPackParserListener.set(pack); } return pack; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/rest/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/rest/resources/GitConfigResource.java deleted file mode 100644 index 4ecc2f07e7..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/rest/resources/GitConfigResource.java +++ /dev/null @@ -1,124 +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.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import sonia.scm.repository.GitConfig; -import sonia.scm.repository.GitRepositoryHandler; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; - -/** - * - * @author Sebastian Sdorra - */ -@Singleton -@Path("config/repositories/git") -@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) -public class GitConfigResource -{ - - /** - * Constructs ... - * - * - * - * @param repositoryHandler - */ - @Inject - public GitConfigResource(GitRepositoryHandler repositoryHandler) - { - this.repositoryHandler = repositoryHandler; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @GET - public GitConfig getConfig() - { - GitConfig config = repositoryHandler.getConfig(); - - if (config == null) - { - config = new GitConfig(); - repositoryHandler.setConfig(config); - } - - return config; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param uriInfo - * @param config - * - * @return - */ - @POST - @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public Response setConfig(@Context UriInfo uriInfo, GitConfig config) - { - repositoryHandler.setConfig(config); - repositoryHandler.storeConfig(); - - return Response.created(uriInfo.getRequestUri()).build(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private GitRepositoryHandler repositoryHandler; -} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java new file mode 100644 index 0000000000..08602e2af1 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java @@ -0,0 +1,25 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@Getter +@Setter +public class GitConfigDto extends HalRepresentation { + + private boolean disabled = false; + + private String gcExpression; + + private boolean nonFastForwardDisallowed; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapper.java new file mode 100644 index 0000000000..74ba684a24 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapper.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import sonia.scm.repository.GitConfig; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class GitConfigDtoToGitConfigMapper { + public abstract GitConfig map(GitConfigDto dto); +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigInIndexResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigInIndexResource.java new file mode 100644 index 0000000000..e6d7546ff4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigInIndexResource.java @@ -0,0 +1,41 @@ +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.repository.GitConfig; +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; + + @Inject + public GitConfigInIndexResource(Provider scmPathInfoStore, ObjectMapper objectMapper) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.read(GitConfig.PERMISSION).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); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java new file mode 100644 index 0000000000..7cda4bc9d3 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -0,0 +1,100 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.GitConfig; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.web.GitVndMediaType; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +/** + * RESTful Web Service Resource to manage the configuration of the git plugin. + */ +@Path(GitConfigResource.GIT_CONFIG_PATH_V2) +public class GitConfigResource { + + static final String GIT_CONFIG_PATH_V2 = "v2/config/git"; + private final GitConfigDtoToGitConfigMapper dtoToConfigMapper; + private final GitConfigToGitConfigDtoMapper configToDtoMapper; + private final GitRepositoryHandler repositoryHandler; + private final Provider gitRepositoryConfigResource; + + @Inject + public GitConfigResource(GitConfigDtoToGitConfigMapper dtoToConfigMapper, GitConfigToGitConfigDtoMapper configToDtoMapper, + GitRepositoryHandler repositoryHandler, Provider gitRepositoryConfigResource) { + this.dtoToConfigMapper = dtoToConfigMapper; + this.configToDtoMapper = configToDtoMapper; + this.repositoryHandler = repositoryHandler; + this.gitRepositoryConfigResource = gitRepositoryConfigResource; + } + + /** + * Returns the git config. + */ + @GET + @Path("") + @Produces(GitVndMediaType.GIT_CONFIG) + @TypeHint(GitConfigDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:git\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get() { + + GitConfig config = repositoryHandler.getConfig(); + + if (config == null) { + config = new GitConfig(); + repositoryHandler.setConfig(config); + } + + ConfigurationPermissions.read(config).check(); + + return Response.ok(configToDtoMapper.map(config)).build(); + } + + /** + * Modifies the git config. + * + * @param configDto new configuration object + */ + @PUT + @Path("") + @Consumes(GitVndMediaType.GIT_CONFIG) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:git\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response update(GitConfigDto configDto) { + + GitConfig config = dtoToConfigMapper.map(configDto); + + ConfigurationPermissions.write(config).check(); + + repositoryHandler.setConfig(config); + repositoryHandler.storeConfig(); + + return Response.noContent().build(); + } + + @Path("{namespace}/{name}") + public GitRepositoryConfigResource getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) { + return gitRepositoryConfigResource.get(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java new file mode 100644 index 0000000000..7607b31faf --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java @@ -0,0 +1,41 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.GitConfig; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper { + + @Inject + private ScmPathInfoStore scmPathInfoStore; + + @AfterMapping + void appendLinks(GitConfig config, @MappingTarget GitConfigDto target) { + Links.Builder linksBuilder = linkingTo().self(self()); + if (ConfigurationPermissions.write(config).isPermitted()) { + linksBuilder.single(link("update", update())); + } + target.add(linksBuilder.build()); + } + + private String self() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class); + return linkBuilder.method("get").parameters().href(); + } + + private String update() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class); + return linkBuilder.method("update").parameters().href(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigChangeClearRepositoryCacheListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigChangeClearRepositoryCacheListener.java new file mode 100644 index 0000000000..df93fa4886 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigChangeClearRepositoryCacheListener.java @@ -0,0 +1,19 @@ +package sonia.scm.api.v2.resources; + +import com.github.legman.Subscribe; +import sonia.scm.EagerSingleton; +import sonia.scm.event.ScmEventBus; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.ClearRepositoryCacheEvent; + +import java.util.Objects; + +@EagerSingleton @Extension +public class GitRepositoryConfigChangeClearRepositoryCacheListener { + @Subscribe + public void sendClearRepositoryCacheEvent(GitRepositoryConfigChangedEvent event) { + if (!Objects.equals(event.getOldConfig().getDefaultBranch(), event.getNewConfig().getDefaultBranch())) { + ScmEventBus.getInstance().post(new ClearRepositoryCacheEvent(event.getRepository())); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigChangedEvent.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigChangedEvent.java new file mode 100644 index 0000000000..eaf575a610 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigChangedEvent.java @@ -0,0 +1,30 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.event.Event; +import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.repository.Repository; + +@Event +public class GitRepositoryConfigChangedEvent { + private final Repository repository; + private final GitRepositoryConfig oldConfig; + private final GitRepositoryConfig newConfig; + + public GitRepositoryConfigChangedEvent(Repository repository, GitRepositoryConfig oldConfig, GitRepositoryConfig newConfig) { + this.repository = repository; + this.oldConfig = oldConfig; + this.newConfig = newConfig; + } + + public Repository getRepository() { + return repository; + } + + public GitRepositoryConfig getOldConfig() { + return oldConfig; + } + + public GitRepositoryConfig getNewConfig() { + return newConfig; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigDto.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigDto.java new file mode 100644 index 0000000000..d22d6c194e --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigDto.java @@ -0,0 +1,24 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuppressWarnings("squid:S2160") // there is no proper semantic for equals on this dto +public class GitRepositoryConfigDto extends HalRepresentation { + + private String defaultBranch; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricher.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricher.java new file mode 100644 index 0000000000..2566fd82f7 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricher.java @@ -0,0 +1,37 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.web.AbstractRepositoryJsonEnricher; + +import javax.inject.Inject; +import javax.inject.Provider; + +@Extension +public class GitRepositoryConfigEnricher extends AbstractRepositoryJsonEnricher { + + private final Provider scmPathInfoStore; + private final RepositoryManager manager; + + @Inject + public GitRepositoryConfigEnricher(Provider scmPathInfoStore, ObjectMapper objectMapper, RepositoryManager manager) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + this.manager = manager; + } + + @Override + protected void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name) { + if (GitRepositoryHandler.TYPE_NAME.equals(manager.get(new NamespaceAndName(namespace, name)).getType())) { + String repositoryConfigLink = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class) + .method("getRepositoryConfig") + .parameters(namespace, name) + .href(); + addLink(repositoryNode, "configuration", repositoryConfigLink); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigMapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigMapper.java new file mode 100644 index 0000000000..9fde8f7d98 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigMapper.java @@ -0,0 +1,46 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class GitRepositoryConfigMapper { + + @Inject + private ScmPathInfoStore scmPathInfoStore; + + public abstract GitRepositoryConfigDto map(GitRepositoryConfig config, @Context Repository repository); + public abstract GitRepositoryConfig map(GitRepositoryConfigDto dto); + + @AfterMapping + void appendLinks(@MappingTarget GitRepositoryConfigDto target, @Context Repository repository) { + Links.Builder linksBuilder = linkingTo().self(self()); + if (RepositoryPermissions.custom("git", repository).isPermitted()) { + linksBuilder.single(link("update", update())); + } + target.add(linksBuilder.build()); + } + + private String self() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class); + return linkBuilder.method("get").parameters().href(); + } + + private String update() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class); + return linkBuilder.method("update").parameters().href(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java new file mode 100644 index 0000000000..175caf8840 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java @@ -0,0 +1,93 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.web.GitVndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +public class GitRepositoryConfigResource { + + private static final Logger LOG = LoggerFactory.getLogger(GitRepositoryConfigResource.class); + + private final GitRepositoryConfigMapper repositoryConfigMapper; + private final RepositoryManager repositoryManager; + private final GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider; + + @Inject + public GitRepositoryConfigResource(GitRepositoryConfigMapper repositoryConfigMapper, RepositoryManager repositoryManager, GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider) { + this.repositoryConfigMapper = repositoryConfigMapper; + this.repositoryManager = repositoryManager; + this.gitRepositoryConfigStoreProvider = gitRepositoryConfigStoreProvider; + } + + @GET + @Path("/") + @Produces(GitVndMediaType.GIT_REPOSITORY_CONFIG) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository config"), + @ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) { + Repository repository = getRepository(namespace, name); + RepositoryPermissions.read(repository).check(); + ConfigurationStore repositoryConfigStore = getStore(repository); + GitRepositoryConfig config = repositoryConfigStore.get(); + GitRepositoryConfigDto dto = repositoryConfigMapper.map(config, repository); + return Response.ok(dto).build(); + } + + @PUT + @Path("/") + @Consumes(GitVndMediaType.GIT_REPOSITORY_CONFIG) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the privilege to change this repositories config"), + @ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response setRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name, GitRepositoryConfigDto dto) { + Repository repository = getRepository(namespace, name); + RepositoryPermissions.custom("git", repository).check(); + ConfigurationStore repositoryConfigStore = getStore(repository); + GitRepositoryConfig config = repositoryConfigMapper.map(dto); + repositoryConfigStore.set(config); + LOG.info("git default branch of repository {} has changed, sending clear cache event", repository.getNamespaceAndName()); + return Response.noContent().build(); + } + + private Repository getRepository(@PathParam("namespace") String namespace, @PathParam("name") String name) { + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); + Repository repository = repositoryManager.get(namespaceAndName); + if (repository == null) { + throw notFound(entity(namespaceAndName)); + } + return repository; + } + + private ConfigurationStore getStore(Repository repository) { + return gitRepositoryConfigStoreProvider.get(repository); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigStoreProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigStoreProvider.java new file mode 100644 index 0000000000..ce37fb65f4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigStoreProvider.java @@ -0,0 +1,50 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.repository.Repository; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreFactory; + +import javax.inject.Inject; + +public class GitRepositoryConfigStoreProvider { + + private final ConfigurationStoreFactory configurationStoreFactory; + + @Inject + public GitRepositoryConfigStoreProvider(ConfigurationStoreFactory configurationStoreFactory) { + this.configurationStoreFactory = configurationStoreFactory; + } + + public ConfigurationStore get(Repository repository) { + return new StoreWrapper(configurationStoreFactory.withType(GitRepositoryConfig.class).withName("gitConfig").forRepository(repository).build(), repository); + } + + private static class StoreWrapper implements ConfigurationStore { + + private final ConfigurationStore delegate; + private final Repository repository; + + private StoreWrapper(ConfigurationStore delegate, Repository repository) { + this.delegate = delegate; + this.repository = repository; + } + + @Override + public GitRepositoryConfig get() { + GitRepositoryConfig config = delegate.get(); + if (config == null) { + return new GitRepositoryConfig(); + } + return config; + } + + @Override + public void set(GitRepositoryConfig newConfig) { + GitRepositoryConfig oldConfig = get(); + delegate.set(newConfig); + ScmEventBus.getInstance().post(new GitRepositoryConfigChangedEvent(repository, oldConfig, newConfig)); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java new file mode 100644 index 0000000000..4fc3a5415c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java @@ -0,0 +1,42 @@ +package sonia.scm.protocolcommand.git; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.spi.HookEventFacade; +import sonia.scm.web.CollectingPackParserListener; +import sonia.scm.web.GitReceiveHook; + +public abstract class BaseReceivePackFactory implements ReceivePackFactory { + + private final GitRepositoryHandler handler; + private final GitReceiveHook hook; + + protected BaseReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { + this.handler = handler; + this.hook = new GitReceiveHook(hookEventFacade, handler); + } + + @Override + public final ReceivePack create(T connection, Repository repository) throws ServiceNotAuthorizedException, ServiceNotEnabledException { + ReceivePack receivePack = createBasicReceivePack(connection, repository); + receivePack.setAllowNonFastForwards(isNonFastForwardAllowed()); + + receivePack.setPreReceiveHook(hook); + receivePack.setPostReceiveHook(hook); + // apply collecting listener, to be able to check which commits are new + CollectingPackParserListener.set(receivePack); + + return receivePack; + } + + protected abstract ReceivePack createBasicReceivePack(T request, Repository repository) + throws ServiceNotEnabledException, ServiceNotAuthorizedException; + + private boolean isNonFastForwardAllowed() { + return ! handler.getConfig().isNonFastForwardDisallowed(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreter.java new file mode 100644 index 0000000000..d4e6f8edbb --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreter.java @@ -0,0 +1,32 @@ +package sonia.scm.protocolcommand.git; + +import sonia.scm.protocolcommand.CommandInterpreter; +import sonia.scm.protocolcommand.RepositoryContextResolver; +import sonia.scm.protocolcommand.ScmCommandProtocol; + +class GitCommandInterpreter implements CommandInterpreter { + private final GitRepositoryContextResolver gitRepositoryContextResolver; + private final GitCommandProtocol gitCommandProtocol; + private final String[] args; + + GitCommandInterpreter(GitRepositoryContextResolver gitRepositoryContextResolver, GitCommandProtocol gitCommandProtocol, String[] args) { + this.gitRepositoryContextResolver = gitRepositoryContextResolver; + this.gitCommandProtocol = gitCommandProtocol; + this.args = args; + } + + @Override + public String[] getParsedArgs() { + return args; + } + + @Override + public ScmCommandProtocol getProtocolHandler() { + return gitCommandProtocol; + } + + @Override + public RepositoryContextResolver getRepositoryContextResolver() { + return gitRepositoryContextResolver; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreterFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreterFactory.java new file mode 100644 index 0000000000..bd38dc01db --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreterFactory.java @@ -0,0 +1,37 @@ +package sonia.scm.protocolcommand.git; + +import sonia.scm.plugin.Extension; +import sonia.scm.protocolcommand.CommandInterpreter; +import sonia.scm.protocolcommand.CommandInterpreterFactory; + +import javax.inject.Inject; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +@Extension +public class GitCommandInterpreterFactory implements CommandInterpreterFactory { + private final GitCommandProtocol gitCommandProtocol; + private final GitRepositoryContextResolver gitRepositoryContextResolver; + + @Inject + public GitCommandInterpreterFactory(GitCommandProtocol gitCommandProtocol, GitRepositoryContextResolver gitRepositoryContextResolver) { + this.gitCommandProtocol = gitCommandProtocol; + this.gitRepositoryContextResolver = gitRepositoryContextResolver; + } + + @Override + public Optional canHandle(String command) { + try { + String[] args = GitCommandParser.parse(command); + if (args[0].startsWith("git")) { + return of(new GitCommandInterpreter(gitRepositoryContextResolver, gitCommandProtocol, args)); + } else { + return empty(); + } + } catch (IllegalArgumentException e) { + return empty(); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandParser.java new file mode 100644 index 0000000000..624b951d42 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandParser.java @@ -0,0 +1,88 @@ +package sonia.scm.protocolcommand.git; + +import java.util.ArrayList; +import java.util.List; + +class GitCommandParser { + + private GitCommandParser() { + } + + static String[] parse(String command) { + List strs = parseDelimitedString(command, " ", true); + String[] args = strs.toArray(new String[strs.size()]); + for (int i = 0; i < args.length; i++) { + String argVal = args[i]; + if (argVal.startsWith("'") && argVal.endsWith("'")) { + args[i] = argVal.substring(1, argVal.length() - 1); + argVal = args[i]; + } + if (argVal.startsWith("\"") && argVal.endsWith("\"")) { + args[i] = argVal.substring(1, argVal.length() - 1); + } + } + + if (args.length != 2) { + throw new IllegalArgumentException("Invalid git command line (no arguments): " + command); + } + return args; + } + + private static List parseDelimitedString(String value, String delim, boolean trim) { + if (value == null) { + value = ""; + } + + List list = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + int expecting = 7; + boolean isEscaped = false; + + for(int i = 0; i < value.length(); ++i) { + char c = value.charAt(i); + boolean isDelimiter = delim.indexOf(c) >= 0; + if (!isEscaped && c == '\\') { + isEscaped = true; + } else { + if (isEscaped) { + sb.append(c); + } else if (isDelimiter && (expecting & 2) != 0) { + if (trim) { + String str = sb.toString(); + list.add(str.trim()); + } else { + list.add(sb.toString()); + } + + sb.delete(0, sb.length()); + expecting = 7; + } else if (c == '"' && (expecting & 4) != 0) { + sb.append(c); + expecting = 9; + } else if (c == '"' && (expecting & 8) != 0) { + sb.append(c); + expecting = 7; + } else { + if ((expecting & 1) == 0) { + throw new IllegalArgumentException("Invalid delimited string: " + value); + } + + sb.append(c); + } + + isEscaped = false; + } + } + + if (sb.length() > 0) { + if (trim) { + String str = sb.toString(); + list.add(str.trim()); + } else { + list.add(sb.toString()); + } + } + + return list; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandProtocol.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandProtocol.java new file mode 100644 index 0000000000..b11ea80cfe --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandProtocol.java @@ -0,0 +1,74 @@ +package sonia.scm.protocolcommand.git; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.UploadPack; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.plugin.Extension; +import sonia.scm.protocolcommand.CommandContext; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.protocolcommand.ScmCommandProtocol; +import sonia.scm.repository.RepositoryPermissions; + +import javax.inject.Inject; +import java.io.IOException; + +@Extension +public class GitCommandProtocol implements ScmCommandProtocol { + + private static final Logger LOG = LoggerFactory.getLogger(GitCommandProtocol.class); + + private ScmUploadPackFactory uploadPackFactory; + private ScmReceivePackFactory receivePackFactory; + + @Inject + public GitCommandProtocol(ScmUploadPackFactory uploadPackFactory, ScmReceivePackFactory receivePackFactory) { + this.uploadPackFactory = uploadPackFactory; + this.receivePackFactory = receivePackFactory; + } + + @Override + public void handle(CommandContext commandContext, RepositoryContext repositoryContext) throws IOException { + String subCommand = commandContext.getArgs()[0]; + + if (RemoteConfig.DEFAULT_UPLOAD_PACK.equals(subCommand)) { + LOG.trace("got upload pack"); + upload(commandContext, repositoryContext); + } else if (RemoteConfig.DEFAULT_RECEIVE_PACK.equals(subCommand)) { + LOG.trace("got receive pack"); + receive(commandContext, repositoryContext); + } else { + throw new IllegalArgumentException("Unknown git command: " + commandContext.getCommand()); + } + } + + private void receive(CommandContext commandContext, RepositoryContext repositoryContext) throws IOException { + RepositoryPermissions.push(repositoryContext.getRepository()).check(); + try (Repository repository = open(repositoryContext)) { + ReceivePack receivePack = receivePackFactory.create(repositoryContext, repository); + receivePack.receive(commandContext.getInputStream(), commandContext.getOutputStream(), commandContext.getErrorStream()); + } catch (ServiceNotEnabledException | ServiceNotAuthorizedException e) { + throw new IOException("error creating receive pack for ssh", e); + } + } + + private void upload(CommandContext commandContext, RepositoryContext repositoryContext) throws IOException { + RepositoryPermissions.pull(repositoryContext.getRepository()).check(); + try (Repository repository = open(repositoryContext)) { + UploadPack uploadPack = uploadPackFactory.create(repositoryContext, repository); + uploadPack.upload(commandContext.getInputStream(), commandContext.getOutputStream(), commandContext.getErrorStream()); + } + } + + private Repository open(RepositoryContext repositoryContext) throws IOException { + RepositoryCache.FileKey key = RepositoryCache.FileKey.lenient(repositoryContext.getDirectory().toFile(), FS.DETECTED); + return key.open(true); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java new file mode 100644 index 0000000000..737776f3ac --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java @@ -0,0 +1,44 @@ +package sonia.scm.protocolcommand.git; + +import com.google.common.base.Splitter; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.protocolcommand.RepositoryContextResolver; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Inject; +import java.nio.file.Path; +import java.util.Iterator; + +public class GitRepositoryContextResolver implements RepositoryContextResolver { + + private RepositoryManager repositoryManager; + private RepositoryLocationResolver locationResolver; + + @Inject + public GitRepositoryContextResolver(RepositoryManager repositoryManager, RepositoryLocationResolver locationResolver) { + this.repositoryManager = repositoryManager; + this.locationResolver = locationResolver; + } + + public RepositoryContext resolve(String[] args) { + NamespaceAndName namespaceAndName = extractNamespaceAndName(args); + Repository repository = repositoryManager.get(namespaceAndName); + Path path = locationResolver.forClass(Path.class).getLocation(repository.getId()).resolve("data"); + return new RepositoryContext(repository, path); + } + + private NamespaceAndName extractNamespaceAndName(String[] args) { + String path = args[args.length - 1]; + Iterator it = Splitter.on('/').omitEmptyStrings().split(path).iterator(); + String type = it.next(); + if ("repo".equals(type)) { + String ns = it.next(); + String name = it.next(); + return new NamespaceAndName(ns, name); + } + return null; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java new file mode 100644 index 0000000000..105a04ccdb --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java @@ -0,0 +1,21 @@ +package sonia.scm.protocolcommand.git; + +import com.google.inject.Inject; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceivePack; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.spi.HookEventFacade; + +public class ScmReceivePackFactory extends BaseReceivePackFactory { + + @Inject + public ScmReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { + super(handler, hookEventFacade); + } + + @Override + protected ReceivePack createBasicReceivePack(RepositoryContext repositoryContext, Repository repository) { + return new ReceivePack(repository); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmUploadPackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmUploadPackFactory.java new file mode 100644 index 0000000000..962437a59b --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmUploadPackFactory.java @@ -0,0 +1,13 @@ +package sonia.scm.protocolcommand.git; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.UploadPack; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import sonia.scm.protocolcommand.RepositoryContext; + +public class ScmUploadPackFactory implements UploadPackFactory { + @Override + public UploadPack create(RepositoryContext repositoryContext, Repository repository) { + return new UploadPack(repository); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java index 4838f8ec23..2275fbcfd0 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java @@ -37,31 +37,23 @@ package sonia.scm.repository; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; - -import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.treewalk.EmptyTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Closeable; import java.io.IOException; - import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.Set; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -137,27 +129,9 @@ public class GitChangesetConverter implements Closeable * * @throws IOException */ - public Changeset createChangeset(RevCommit commit) throws IOException + public Changeset createChangeset(RevCommit commit) { - List branches = Lists.newArrayList(); - Set refs = repository.getAllRefsByPeeledObjectId().get(commit.getId()); - - if (Util.isNotEmpty(refs)) - { - - for (Ref ref : refs) - { - String branch = GitUtil.getBranch(ref); - - if (branch != null) - { - branches.add(branch); - } - } - - } - - return createChangeset(commit, branches); + return createChangeset(commit, Collections.emptyList()); } /** @@ -172,7 +146,6 @@ public class GitChangesetConverter implements Closeable * @throws IOException */ public Changeset createChangeset(RevCommit commit, String branch) - throws IOException { return createChangeset(commit, Lists.newArrayList(branch)); } @@ -190,7 +163,6 @@ public class GitChangesetConverter implements Closeable * @throws IOException */ public Changeset createChangeset(RevCommit commit, List branches) - throws IOException { String id = commit.getId().name(); List parentList = null; @@ -198,7 +170,7 @@ public class GitChangesetConverter implements Closeable if (Util.isNotEmpty(parents)) { - parentList = new ArrayList<>(); + parentList = new ArrayList(); for (RevCommit parent : parents) { @@ -224,13 +196,6 @@ public class GitChangesetConverter implements Closeable changeset.setParents(parentList); } - Modifications modifications = createModifications(treeWalk, commit); - - if (modifications != null) - { - changeset.setModifications(modifications); - } - Collection tagCollection = tags.get(commit.getId()); if (Util.isNotEmpty(tagCollection)) @@ -245,108 +210,7 @@ public class GitChangesetConverter implements Closeable 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 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 --------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java index 80fe8907ac..8f4de9aa30 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java @@ -39,6 +39,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; /** * @@ -46,14 +47,37 @@ import javax.xml.bind.annotation.XmlRootElement; */ @XmlRootElement(name = "config") @XmlAccessorType(XmlAccessType.FIELD) -public class GitConfig extends SimpleRepositoryConfig { - +public class GitConfig extends RepositoryConfig { + + @SuppressWarnings("WeakerAccess") // This might be needed for permission checking + public static final String PERMISSION = "git"; + @XmlElement(name = "gc-expression") private String gcExpression; - public String getGcExpression() - { + @XmlElement(name = "disallow-non-fast-forward") + private boolean nonFastForwardDisallowed; + + public String getGcExpression() { return gcExpression; } - + + public void setGcExpression(String gcExpression) { + this.gcExpression = gcExpression; + } + + public boolean isNonFastForwardDisallowed() { + return nonFastForwardDisallowed; + } + + public void setNonFastForwardDisallowed(boolean nonFastForwardDisallowed) { + this.nonFastForwardDisallowed = nonFastForwardDisallowed; + } + + @Override + @XmlTransient // Only for permission checks, don't serialize to XML + public String getId() { + // Don't change this without migrating SCM permission configuration! + return PERMISSION; + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java new file mode 100644 index 0000000000..a978295166 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java @@ -0,0 +1,21 @@ +package sonia.scm.repository; + +import org.eclipse.jgit.lib.StoredConfig; + +import java.io.IOException; + +public class GitConfigHelper { + + private static final String CONFIG_SECTION_SCMM = "scmm"; + private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; + + public void createScmmConfig(Repository repository, org.eclipse.jgit.lib.Repository gitRepository) throws IOException { + StoredConfig config = gitRepository.getConfig(); + config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId()); + config.save(); + } + + public String getRepositoryId(StoredConfig gitConfig) { + return gitConfig.getString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConstants.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConstants.java deleted file mode 100644 index 6d833577e1..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConstants.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2014, 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; - -/** - * Constants for Git. - * - * @author Sebastian Sdorra - * @since 1.50 - */ -public final class GitConstants { - - /** - * Default branch repository property. - */ - public static final String PROPERTY_DEFAULT_BRANCH = "git.default-branch"; - - private GitConstants() { - } - -} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java index 248eae92d1..9a30cc9f3e 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java @@ -121,7 +121,7 @@ public class GitGcTask implements Runnable { } private void gc(Repository repository){ - File file = repositoryHandler.getDirectory(repository); + File file = repositoryHandler.getDirectory(repository.getId()); Git git = null; try { git = open(file); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHeadModifier.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHeadModifier.java new file mode 100644 index 0000000000..b82e8bdd3c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHeadModifier.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2014, 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; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +/** + * The GitHeadModifier is able to modify the head of a git repository. + * + * @author Sebastian Sdorra + * @since 1.61 + */ +public class GitHeadModifier { + + private static final Logger LOG = LoggerFactory.getLogger(GitHeadModifier.class); + + private final GitRepositoryHandler repositoryHandler; + + @Inject + public GitHeadModifier(GitRepositoryHandler repositoryHandler) { + this.repositoryHandler = repositoryHandler; + } + + /** + * Ensures that the repositories head points to the given branch. The method will return {@code false} if the + * repositories head points already to the given branch. + * + * @param repository repository to modify + * @param newHead branch which should be the new head of the repository + * + * @return {@code true} if the head has changed + */ + public boolean ensure(Repository repository, String newHead) { + try (org.eclipse.jgit.lib.Repository gitRepository = open(repository)) { + String currentHead = resolve(gitRepository); + if (!Objects.equals(currentHead, newHead)) { + return modify(gitRepository, newHead); + } + } catch (IOException ex) { + LOG.warn("failed to change head of repository", ex); + } + return false; + } + + private String resolve(org.eclipse.jgit.lib.Repository gitRepository) throws IOException { + Ref ref = gitRepository.getRefDatabase().getRef(Constants.HEAD); + if ( ref.isSymbolic() ) { + ref = ref.getTarget(); + } + return GitUtil.getBranch(ref); + } + + private boolean modify(org.eclipse.jgit.lib.Repository gitRepository, String newHead) throws IOException { + RefUpdate refUpdate = gitRepository.getRefDatabase().newUpdate(Constants.HEAD, true); + refUpdate.setForceUpdate(true); + RefUpdate.Result result = refUpdate.link(Constants.R_HEADS + newHead); + return result == RefUpdate.Result.FORCED; + } + + private org.eclipse.jgit.lib.Repository open(Repository repository) throws IOException { + File directory = repositoryHandler.getDirectory(repository.getId()); + return GitUtil.open(directory); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfig.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfig.java new file mode 100644 index 0000000000..a0136c8ea6 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryConfig.java @@ -0,0 +1,27 @@ +package sonia.scm.repository; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "config") +@XmlAccessorType(XmlAccessType.FIELD) +public class GitRepositoryConfig { + + public GitRepositoryConfig() { + } + + public GitRepositoryConfig(String defaultBranch) { + this.defaultBranch = defaultBranch; + } + + private String defaultBranch; + + public String getDefaultBranch() { + return defaultBranch; + } + + public void setDefaultBranch(String defaultBranch) { + this.defaultBranch = defaultBranch; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 2338cc3b46..e07b3d0d83 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -38,26 +38,22 @@ package sonia.scm.repository; import com.google.common.base.Strings; import com.google.inject.Inject; import com.google.inject.Singleton; - +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; - -import sonia.scm.Type; -import sonia.scm.io.FileSystem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.spi.GitRepositoryServiceProvider; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.schedule.Scheduler; +import sonia.scm.schedule.Task; +import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; import java.io.IOException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.SCMContextProvider; -import sonia.scm.schedule.Scheduler; -import sonia.scm.schedule.Task; -import sonia.scm.store.ConfigurationStoreFactory; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -88,31 +84,30 @@ public class GitRepositoryHandler private static final Logger logger = LoggerFactory.getLogger(GitRepositoryHandler.class); /** Field description */ - public static final Type TYPE = new RepositoryType(TYPE_NAME, + public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, GitRepositoryServiceProvider.COMMANDS); private static final Object LOCK = new Object(); - + private final Scheduler scheduler; - + + private final GitWorkdirFactory workdirFactory; + private Task task; - + //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param storeFactory - * @param fileSystem - * @param scheduler - */ @Inject - public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler) + public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, + Scheduler scheduler, + RepositoryLocationResolver repositoryLocationResolver, + GitWorkdirFactory workdirFactory, + PluginLoader pluginLoader) { - super(storeFactory, fileSystem); + super(storeFactory, repositoryLocationResolver, pluginLoader); this.scheduler = scheduler; + this.workdirFactory = workdirFactory; } //~--- get methods ---------------------------------------------------------- @@ -121,17 +116,17 @@ public class GitRepositoryHandler public void init(SCMContextProvider context) { super.init(context); - scheduleGc(); + scheduleGc(getConfig().getGcExpression()); } @Override public void setConfig(GitConfig config) { + scheduleGc(config.getGcExpression()); super.setConfig(config); - scheduleGc(); } - - private void scheduleGc() + + private void scheduleGc(String expression) { synchronized (LOCK){ if ( task != null ){ @@ -139,15 +134,14 @@ public class GitRepositoryHandler task.cancel(); task = null; } - String exp = getConfig().getGcExpression(); - if (!Strings.isNullOrEmpty(exp)) + if (!Strings.isNullOrEmpty(expression)) { - logger.info("schedule git gc task with expression {}", exp); - task = scheduler.schedule(exp, GitGcTask.class); + logger.info("schedule git gc task with expression {}", expression); + task = scheduler.schedule(expression, GitGcTask.class); } } } - + /** * Method description * @@ -167,7 +161,7 @@ public class GitRepositoryHandler * @return */ @Override - public Type getType() + public RepositoryType getType() { return TYPE; } @@ -184,27 +178,24 @@ public class GitRepositoryHandler return getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION); } + public GitWorkdirFactory getWorkdirFactory() { + return workdirFactory; + } + + public String getRepositoryId(StoredConfig gitConfig) { + return new GitConfigHelper().getRepositoryId(gitConfig); + } + //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param repository - * @param directory - * - * @throws IOException - * @throws RepositoryException - */ @Override - protected void create(Repository repository, File directory) - throws RepositoryException, IOException - { + protected void create(Repository repository, File directory) throws IOException { try (org.eclipse.jgit.lib.Repository gitRepository = build(directory)) { gitRepository.create(true); + new GitConfigHelper().createScmmConfig(repository, gitRepository); } } - + private org.eclipse.jgit.lib.Repository build(File directory) throws IOException { return new FileRepositoryBuilder() .setGitDir(directory) @@ -238,18 +229,4 @@ public class GitRepositoryHandler { return GitConfig.class; } - - /** - * Method description - * - * - * @param directory - * - * @return - */ - @Override - protected boolean isRepository(File directory) - { - return new File(directory, DIRECTORY_REFS).exists(); - } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryModifyListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryModifyListener.java index db2b7b09ec..a16b34f6be 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryModifyListener.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryModifyListener.java @@ -30,68 +30,46 @@ */ package sonia.scm.repository; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Objects; -import com.google.common.eventbus.Subscribe; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.github.legman.Subscribe; import sonia.scm.EagerSingleton; -import sonia.scm.HandlerEventType; -import sonia.scm.event.ScmEventBus; +import sonia.scm.api.v2.resources.GitRepositoryConfigChangedEvent; +import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.plugin.Extension; +import javax.inject.Inject; + /** * Repository listener which handles git related repository events. - * + * * @author Sebastian Sdorra * @since 1.50 */ @Extension @EagerSingleton public class GitRepositoryModifyListener { - - /** - * the logger for GitRepositoryModifyListener - */ - private static final Logger logger = LoggerFactory.getLogger(GitRepositoryModifyListener.class); - + + private final GitHeadModifier headModifier; + private final GitRepositoryConfigStoreProvider storeProvider; + + @Inject + public GitRepositoryModifyListener(GitHeadModifier headModifier, GitRepositoryConfigStoreProvider storeProvider) { + this.headModifier = headModifier; + this.storeProvider = storeProvider; + } + /** * Receives {@link RepositoryModificationEvent} and fires a {@link ClearRepositoryCacheEvent} if * the default branch of a git repository was modified. - * + * * @param event repository modification event */ @Subscribe - public void handleEvent(RepositoryModificationEvent event){ - Repository repository = event.getItem(); - - if ( isModifyEvent(event) && - isGitRepository(event.getItem()) && - hasDefaultBranchChanged(event.getItemBeforeModification(), repository)) - { - logger.info("git default branch of repository {} has changed, sending clear cache event", repository.getId()); - sendClearRepositoryCacheEvent(repository); + public void handleEvent(GitRepositoryConfigChangedEvent event){ + Repository repository = event.getRepository(); + + String defaultBranch = storeProvider.get(repository).get().getDefaultBranch(); + if (defaultBranch != null) { + headModifier.ensure(repository, defaultBranch); } } - - @VisibleForTesting - protected void sendClearRepositoryCacheEvent(Repository repository) { - ScmEventBus.getInstance().post(new ClearRepositoryCacheEvent(repository)); - } - - private boolean isModifyEvent(RepositoryEvent event) { - return event.getEventType() == HandlerEventType.MODIFY; - } - - private boolean isGitRepository(Repository repository) { - return GitRepositoryHandler.TYPE_NAME.equals(repository.getType()); - } - - private boolean hasDefaultBranchChanged(Repository old, Repository current) { - return !Objects.equal( - old.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH), - current.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH) - ); - } - } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitSubModuleParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitSubModuleParser.java index d5ed9cb500..82e2919996 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitSubModuleParser.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitSubModuleParser.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,95 +24,49 @@ * 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 sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.util.HashMap; import java.util.Map; import java.util.Scanner; /** - * * @author Sebastian Sdorra */ -public final class GitSubModuleParser -{ +public final class GitSubModuleParser { - /** - * Constructs ... - * - */ - private GitSubModuleParser() {} + private GitSubModuleParser() { + } - //~--- methods -------------------------------------------------------------- - - /** - * //~--- methods -------------------------------------------------------------- - * - * - * Method description - * - * - * @param content - * - * @return - */ - public static Map parse(String content) - { + public static Map parse(String content) { Map subRepositories = new HashMap<>(); - Scanner scanner = new Scanner(content); - SubRepository repository = null; - - while (scanner.hasNextLine()) - { - String line = scanner.nextLine(); - - if (Util.isNotEmpty(line)) - { - line = line.trim(); - - if (line.startsWith("[") && line.endsWith("]")) - { - repository = new SubRepository(); - } - else if (line.startsWith("path")) - { - subRepositories.put(getValue(line), repository); - } - else if (line.startsWith("url")) - { - repository.setRepositoryUrl(getValue(line)); + try (Scanner scanner = new Scanner(content)) { + SubRepository repository = null; + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (Util.isNotEmpty(line)) { + line = line.trim(); + if (line.startsWith("[") && line.endsWith("]")) { + repository = new SubRepository(); + } else if (line.startsWith("path")) { + subRepositories.put(getValue(line), repository); + } else if (line.startsWith("url")) { + repository.setRepositoryUrl(getValue(line)); + } } } } - return subRepositories; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param line - * - * @return - */ - private static String getValue(String line) - { + private static String getValue(String line) { return line.split("=")[1].trim(); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index 1538269ee1..d726b992ca 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -39,38 +39,45 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; - import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.attributes.Attribute; +import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.lfs.LfsPointer; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.FS; - +import org.eclipse.jgit.util.LfsFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import sonia.scm.ContextEntry; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; -import java.io.IOException; - -import java.util.Map; -import java.util.concurrent.TimeUnit; +import sonia.scm.web.GitUserAgentProvider; import javax.servlet.http.HttpServletRequest; -import sonia.scm.web.GitUserAgentProvider; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -78,7 +85,7 @@ import sonia.scm.web.GitUserAgentProvider; */ public final class GitUtil { - + private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider(); /** Field description */ @@ -192,22 +199,7 @@ public final class GitUtil return tags; } - /** - * Method description - * - * - * @param git - * @param directory - * @param remoteRepository - * - * @return - * - * @throws RepositoryException - */ - public static FetchResult fetch(Git git, File directory, - Repository remoteRepository) - throws RepositoryException - { + public static FetchResult fetch(Git git, File directory, Repository remoteRepository) { try { FetchCommand fetch = git.fetch(); @@ -220,7 +212,7 @@ public final class GitUtil } catch (GitAPIException ex) { - throw new RepositoryException("could not fetch", ex); + throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Remote", directory.toString()).in(remoteRepository), "could not fetch", ex); } } @@ -294,7 +286,7 @@ public final class GitUtil { if (walk != null) { - walk.close();; + walk.close(); } } @@ -339,14 +331,14 @@ public final class GitUtil return branch; } - + /** * Returns {@code true} if the provided reference name is a branch name. - * + * * @param refName reference name - * + * * @return {@code true} if the name is a branch name - * + * * @since 1.50 */ public static boolean isBranch(String refName) @@ -365,12 +357,11 @@ public final class GitUtil * * @throws IOException */ - public static ObjectId getBranchId(org.eclipse.jgit.lib.Repository repo, + public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo, String branchName) throws IOException { - ObjectId branchId = null; - + Ref ref = null; if (!branchName.startsWith(REF_HEAD)) { branchName = PREFIX_HEADS.concat(branchName); @@ -380,24 +371,19 @@ public final class GitUtil try { - Ref ref = repo.findRef(branchName); + ref = repo.findRef(branchName); - if (ref != null) - { - branchId = ref.getObjectId(); - } - else if (logger.isWarnEnabled()) + if (ref == null) { logger.warn("could not find branch for {}", branchName); } - } catch (IOException ex) { logger.warn("error occured during resolve of branch id", ex); } - return branchId; + return ref; } /** @@ -462,7 +448,7 @@ public final class GitUtil * * @return */ - public static String getId(ObjectId objectId) + public static String getId(AnyObjectId objectId) { String id = Util.EMPTY_STRING; @@ -519,68 +505,48 @@ public final class GitUtil return ref; } - /** - * Method description - * - * - * @param repo - * - * @return - * - * @throws IOException - */ - public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo) - throws IOException - { - ObjectId id = null; - String head = null; - Map refs = repo.getAllRefs(); + public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo) { + return getRepositoryHeadRef(repo).map(Ref::getObjectId).orElse(null); + } - for (Map.Entry e : refs.entrySet()) - { - String key = e.getKey(); + public static Optional getRepositoryHeadRef(org.eclipse.jgit.lib.Repository repo) { + Optional foundRef = findMostAppropriateHead(repo.getAllRefs()); - if (REF_HEAD.equals(key)) - { - head = REF_HEAD; - id = e.getValue().getObjectId(); - - break; - } - else if (key.startsWith(REF_HEAD_PREFIX)) - { - id = e.getValue().getObjectId(); - head = key.substring(REF_HEAD_PREFIX.length()); - - if (REF_MASTER.equals(head)) - { - break; - } + if (foundRef.isPresent()) { + if (logger.isDebugEnabled()) { + logger.debug("use {}:{} as repository head for directory {}", + foundRef.map(GitUtil::getBranch).orElse(null), + foundRef.map(Ref::getObjectId).map(ObjectId::name).orElse(null), + repo.getDirectory()); } + } else { + logger.warn("could not find repository head in directory {}", repo.getDirectory()); } - if (id == null) - { - id = repo.resolve(Constants.HEAD); + return foundRef; + } + + private static Optional findMostAppropriateHead(Map refs) { + Ref refHead = refs.get(REF_HEAD); + if (refHead != null && refHead.isSymbolic() && isBranch(refHead.getTarget().getName())) { + return of(refHead.getTarget()); } - if (logger.isDebugEnabled()) - { - if ((head != null) && (id != null)) - { - logger.debug("use {}:{} as repository head", head, id.name()); - } - else if (id != null) - { - logger.debug("use {} as repository head", id.name()); - } - else - { - logger.warn("could not find repository head"); - } + Ref master = refs.get(REF_HEAD_PREFIX + REF_MASTER); + if (master != null) { + return of(master); } - return id; + Ref develop = refs.get(REF_HEAD_PREFIX + "develop"); + if (develop != null) { + return of(develop); + } + + return refs.entrySet() + .stream() + .filter(e -> e.getKey().startsWith(REF_HEAD_PREFIX)) + .map(Map.Entry::getValue) + .findFirst(); } /** @@ -651,11 +617,11 @@ public final class GitUtil /** * Returns the name of the tag or {@code null} if the the ref is not a tag. - * + * * @param refName ref name - * + * * @return name of tag or {@link null} - * + * * @since 1.50 */ public static String getTagName(String refName) @@ -668,7 +634,7 @@ public final class GitUtil return tagName; } - + /** * Method description * @@ -728,7 +694,7 @@ public final class GitUtil { //J- return fs.resolve(dir, DIRECTORY_OBJETCS).exists() - && fs.resolve(dir, DIRECTORY_REFS).exists() + && fs.resolve(dir, DIRECTORY_REFS).exists() &&!fs.resolve(dir, DIRECTORY_DOTGIT).exists(); //J+ } @@ -759,6 +725,37 @@ public final class GitUtil return (id != null) &&!id.equals(ObjectId.zeroId()); } + /** + * Computes the first common ancestor of two revisions, aka merge base. + */ + public static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { + try (RevWalk mergeBaseWalk = new RevWalk(repository)) { + mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE); + mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(revision1)); + mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision2)); + RevCommit ancestor = mergeBaseWalk.next(); + if (ancestor == null) { + String msg = "revisions %s and %s are not related and therefore do not have a common ancestor"; + throw new NoCommonHistoryException(String.format(msg, revision1.name(), revision2.name())); + } + return ancestor.getId(); + } + } + + public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) throws IOException { + Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit); + + Attribute filter = attributes.get("filter"); + if (filter != null && "lfs".equals(filter.getValue())) { + ObjectId blobId = treeWalk.getObjectId(0); + try (InputStream is = repo.open(blobId, Constants.OBJ_BLOB).openStream()) { + return of(LfsPointer.parseLfsPointer(is)); + } + } else { + return empty(); + } + } + //~--- methods -------------------------------------------------------------- /** diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkQueueShutdownListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkQueueShutdownListener.java new file mode 100644 index 0000000000..e3dcd7e564 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkQueueShutdownListener.java @@ -0,0 +1,26 @@ +package sonia.scm.repository; + +import org.eclipse.jgit.lib.internal.WorkQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.plugin.Extension; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +@Extension +public class GitWorkQueueShutdownListener implements ServletContextListener { + + private static final Logger LOG = LoggerFactory.getLogger(GitWorkQueueShutdownListener.class); + + @Override + public void contextInitialized(ServletContextEvent sce) { + + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + LOG.warn("shutdown jGit WorkQueue executor"); + WorkQueue.getExecutor().shutdown(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java new file mode 100644 index 0000000000..9b2be467e8 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java @@ -0,0 +1,8 @@ +package sonia.scm.repository; + +import org.eclipse.jgit.lib.Repository; +import sonia.scm.repository.spi.GitContext; +import sonia.scm.repository.util.WorkdirFactory; + +public interface GitWorkdirFactory extends WorkdirFactory { +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java index e7a75a0ff4..bcc2dc8a18 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java @@ -68,17 +68,40 @@ public class GitHookTagProvider implements HookTagProvider { if (Strings.isNullOrEmpty(tag)){ logger.debug("received ref name {} is not a tag", refName); - } else if (rc.getType() == ReceiveCommand.Type.CREATE) { - createdTagBuilder.add(new Tag(tag, GitUtil.getId(rc.getNewId()))); - } else if (rc.getType() == ReceiveCommand.Type.DELETE){ - deletedTagBuilder.add(new Tag(tag, GitUtil.getId(rc.getOldId()))); + } else if (isCreate(rc)) { + createdTagBuilder.add(createTagFromNewId(rc, tag)); + } else if (isDelete(rc)){ + deletedTagBuilder.add(createTagFromOldId(rc, tag)); + } else if (isUpdate(rc)) { + createdTagBuilder.add(createTagFromNewId(rc, tag)); + deletedTagBuilder.add(createTagFromOldId(rc, tag)); } } createdTags = createdTagBuilder.build(); deletedTags = deletedTagBuilder.build(); } - + + private Tag createTagFromNewId(ReceiveCommand rc, String tag) { + return new Tag(tag, GitUtil.getId(rc.getNewId())); + } + + private Tag createTagFromOldId(ReceiveCommand rc, String tag) { + return new Tag(tag, GitUtil.getId(rc.getOldId())); + } + + private boolean isUpdate(ReceiveCommand rc) { + return rc.getType() == ReceiveCommand.Type.UPDATE || rc.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD; + } + + private boolean isDelete(ReceiveCommand rc) { + return rc.getType() == ReceiveCommand.Type.DELETE; + } + + private boolean isCreate(ReceiveCommand rc) { + return rc.getType() == ReceiveCommand.Type.CREATE; + } + @Override public List getCreatedTags() { return createdTags; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index 7591d5c7a7..adf7878221 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -35,14 +35,34 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Strings; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.Status; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.repository.GitConstants; import sonia.scm.repository.GitUtil; +import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.user.User; import java.io.IOException; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; //~--- JDK imports ------------------------------------------------------------ @@ -50,7 +70,7 @@ import java.io.IOException; * * @author Sebastian Sdorra */ -public class AbstractGitCommand +class AbstractGitCommand { /** @@ -65,7 +85,7 @@ public class AbstractGitCommand * @param context * @param repository */ - protected AbstractGitCommand(GitContext context, + AbstractGitCommand(GitContext context, sonia.scm.repository.Repository repository) { this.repository = repository; @@ -82,12 +102,12 @@ public class AbstractGitCommand * * @throws IOException */ - protected Repository open() throws IOException + Repository open() throws IOException { return context.open(); } - protected ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException { + ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException { ObjectId commit; if ( Strings.isNullOrEmpty(requestedCommit) ) { commit = getDefaultBranch(gitRepository); @@ -96,27 +116,165 @@ public class AbstractGitCommand } return commit; } - - protected ObjectId getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { - ObjectId head; - if ( Strings.isNullOrEmpty(requestedBranch) ) { - head = getDefaultBranch(gitRepository); + + ObjectId getDefaultBranch(Repository gitRepository) throws IOException { + Ref ref = getBranchOrDefault(gitRepository, null); + if (ref == null) { + return null; } else { - head = GitUtil.getBranchId(gitRepository, requestedBranch); + return ref.getObjectId(); } - return head; } - - protected ObjectId getDefaultBranch(Repository gitRepository) throws IOException { - ObjectId head; - String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH); - if (!Strings.isNullOrEmpty(defaultBranchName)) { - head = GitUtil.getBranchId(gitRepository, defaultBranchName); + + Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { + if ( Strings.isNullOrEmpty(requestedBranch) ) { + String defaultBranchName = context.getConfig().getDefaultBranch(); + if (!Strings.isNullOrEmpty(defaultBranchName)) { + return GitUtil.getBranchId(gitRepository, defaultBranchName); + } else { + logger.trace("no default branch configured, use repository head as default"); + Optional repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository); + return repositoryHeadRef.orElse(null); + } } else { - logger.trace("no default branch configured, use repository head as default"); - head = GitUtil.getRepositoryHead(gitRepository); + return GitUtil.getBranchId(gitRepository, requestedBranch); + } + } + + > R inClone(Function workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) { + try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, initialBranch)) { + Repository repository = workingCopy.getWorkingRepository(); + logger.debug("cloned repository to folder {}", repository.getWorkTree()); + return workerSupplier.apply(new Git(repository)).run(); + } catch (IOException e) { + throw new InternalRepositoryException(context.getRepository(), "could not clone repository", e); + } + } + + ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision) throws IOException { + sonia.scm.repository.Repository scmRepository = context.getRepository(); + return resolveRevisionOrThrowNotFound(repository, revision, scmRepository); + } + + static ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision, sonia.scm.repository.Repository scmRepository) throws IOException { + ObjectId resolved = repository.resolve(revision); + if (resolved == null) { + throw notFound(entity("Revision", revision).in(scmRepository)); + } else { + return resolved; + } + } + + abstract static class GitCloneWorker { + private final Git clone; + private final GitContext context; + private final sonia.scm.repository.Repository repository; + + GitCloneWorker(Git clone, GitContext context, sonia.scm.repository.Repository repository) { + this.clone = clone; + this.context = context; + this.repository = repository; + } + + abstract R run() throws IOException; + + Git getClone() { + return clone; + } + + GitContext getContext() { + return context; + } + + void checkOutBranch(String branchName) throws IOException { + try { + clone.checkout().setName(branchName).call(); + } catch (RefNotFoundException e) { + logger.trace("could not checkout branch {} directly; trying to create local branch", branchName, e); + checkOutTargetAsNewLocalBranch(branchName); + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not checkout branch: " + branchName, e); + } + } + + private void checkOutTargetAsNewLocalBranch(String branchName) throws IOException { + try { + ObjectId targetRevision = resolveRevision(branchName); + clone.checkout().setStartPoint(targetRevision.getName()).setName(branchName).setCreateBranch(true).call(); + } catch (RefNotFoundException e) { + logger.debug("could not checkout branch {} as local branch", branchName, e); + throw notFound(entity("Revision", branchName).in(context.getRepository())); + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not checkout branch as local branch: " + branchName, e); + } + } + + ObjectId resolveRevision(String revision) throws IOException { + ObjectId resolved = clone.getRepository().resolve(revision); + if (resolved == null) { + return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision, context.getRepository()); + } else { + return resolved; + } + } + + void failIfNotChanged(Supplier doThrow) { + try { + if (clone.status().call().isClean()) { + throw doThrow.get(); + } + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not read status of repository", e); + } + } + + Optional doCommit(String message, Person author) { + Person authorToUse = determineAuthor(author); + try { + Status status = clone.status().call(); + if (!status.isClean() || isInMerge()) { + return of(clone.commit() + .setAuthor(authorToUse.getName(), authorToUse.getMail()) + .setMessage(message) + .call()); + } else { + return empty(); + } + } catch (GitAPIException | IOException e) { + throw new InternalRepositoryException(context.getRepository(), "could not commit changes", e); + } + } + + private boolean isInMerge() throws IOException { + return clone.getRepository().readMergeHeads() != null && !clone.getRepository().readMergeHeads().isEmpty(); + } + + void push() { + try { + clone.push().call(); + } catch (GitAPIException e) { + throw new IntegrateChangesFromWorkdirException(repository, + "could not push changes into central repository", e); + } + logger.debug("pushed changes"); + } + + Ref getCurrentRevision() throws IOException { + return getClone().getRepository().getRefDatabase().findRef("HEAD"); + } + + private Person determineAuthor(Person author) { + if (author == null) { + Subject subject = SecurityUtils.getSubject(); + User user = subject.getPrincipals().oneByType(User.class); + String name = user.getDisplayName(); + String email = user.getMail(); + logger.debug("no author set; using logged in user: {} <{}>", name, email); + return new Person(name, email); + } else { + return author; + } } - return head; } //~--- fields --------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java index aed1917110..348203af92 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java @@ -36,28 +36,25 @@ package sonia.scm.repository.spi; import com.google.common.collect.Lists; import com.google.common.io.Closeables; - import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; - import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; - import java.util.List; import java.util.Map.Entry; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -117,32 +114,17 @@ public abstract class AbstractGitIncomingOutgoingCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - protected ChangesetPagingResult getIncomingOrOutgoingChangesets( - PagedRemoteCommandRequest request) - throws IOException, RepositoryException - { + protected ChangesetPagingResult getIncomingOrOutgoingChangesets(PagedRemoteCommandRequest request) throws IOException { Repository remoteRepository = request.getRemoteRepository(); Git git = Git.wrap(open()); - GitUtil.fetch(git, handler.getDirectory(remoteRepository), remoteRepository); + GitUtil.fetch(git, handler.getDirectory(remoteRepository.getId()), remoteRepository); ObjectId localId = getDefaultBranch(git.getRepository()); ObjectId remoteId = null; - Ref remoteBranch = getRemoteBranch(git.getRepository(), localId, - remoteRepository); + Ref remoteBranch = getRemoteBranch(git.getRepository(), localId, remoteRepository); if (remoteBranch != null) { @@ -178,7 +160,7 @@ public abstract class AbstractGitIncomingOutgoingCommand } catch (Exception ex) { - throw new RepositoryException("could not execute incoming command", ex); + throw new InternalRepositoryException(repository, "could not execute incoming command", ex); } finally { @@ -191,23 +173,7 @@ public abstract class AbstractGitIncomingOutgoingCommand return new ChangesetPagingResult(changesets.size(), changesets); } - /** - * Method description - * - * - * @param repository - * @param local - * @param remoteRepository - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private Ref getRemoteBranch(org.eclipse.jgit.lib.Repository repository, - ObjectId local, Repository remoteRepository) - throws IOException, RepositoryException - { + private Ref getRemoteBranch(org.eclipse.jgit.lib.Repository repository, ObjectId local, Repository remoteRepository) throws IOException { Ref ref = null; if (local != null) @@ -234,13 +200,7 @@ public abstract class AbstractGitIncomingOutgoingCommand { if (e.getKey().startsWith(prefix)) { - if (ref != null) - { - throw new RepositoryException("could not find remote branch"); - } - ref = e.getValue(); - break; } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java index 764dde47f3..95215607fe 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java @@ -37,28 +37,24 @@ package sonia.scm.repository.spi; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; - import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RemoteRefUpdate; - +import org.eclipse.jgit.transport.ScmTransportProtocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitUtil; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.InternalRepositoryException; import java.io.File; -import java.io.IOException; - import java.util.Collection; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -67,7 +63,7 @@ public abstract class AbstractGitPushOrPullCommand extends AbstractGitCommand { /** Field description */ - private static final String SCHEME = "scm://"; + private static final String SCHEME = ScmTransportProtocol.NAME + "://"; /** * the logger for AbstractGitPushOrPullCommand @@ -94,20 +90,7 @@ public abstract class AbstractGitPushOrPullCommand extends AbstractGitCommand //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * @param source - * @param remoteUrl - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - protected long push(Repository source, String remoteUrl) - throws IOException, RepositoryException - { + protected long push(Repository source, String remoteUrl) { Git git = Git.wrap(source); org.eclipse.jgit.api.PushCommand push = git.push(); @@ -132,7 +115,7 @@ public abstract class AbstractGitPushOrPullCommand extends AbstractGitCommand } catch (Exception ex) { - throw new RepositoryException("could not execute push/pull command", ex); + throw new InternalRepositoryException(repository, "could not execute push/pull command", ex); } return counter; @@ -185,7 +168,7 @@ public abstract class AbstractGitPushOrPullCommand extends AbstractGitCommand } else { - throw new IllegalArgumentException("repository or url is requiered"); + throw new IllegalArgumentException("repository or url is required"); } return url; @@ -214,7 +197,7 @@ public abstract class AbstractGitPushOrPullCommand extends AbstractGitCommand */ protected String getRemoteUrl(sonia.scm.repository.Repository repository) { - return getRemoteUrl(handler.getDirectory(repository)); + return getRemoteUrl(handler.getDirectory(repository.getId())); } //~--- methods -------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java new file mode 100644 index 0000000000..0204ca4e3c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java @@ -0,0 +1,115 @@ +package sonia.scm.repository.spi; + +import com.google.common.base.Strings; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import sonia.scm.repository.GitUtil; +import sonia.scm.util.Util; + +import java.io.IOException; +import java.util.List; + +final class Differ implements AutoCloseable { + + private final RevWalk walk; + private final TreeWalk treeWalk; + private final RevCommit commit; + + private Differ(RevCommit commit, RevWalk walk, TreeWalk treeWalk) { + this.commit = commit; + this.walk = walk; + this.treeWalk = treeWalk; + } + + static Diff diff(Repository repository, DiffCommandRequest request) throws IOException { + try (Differ differ = create(repository, request)) { + return differ.diff(); + } + } + + private static Differ create(Repository repository, DiffCommandRequest request) throws IOException { + RevWalk walk = new RevWalk(repository); + + ObjectId revision = repository.resolve(request.getRevision()); + RevCommit commit = walk.parseCommit(revision); + + walk.markStart(commit); + commit = walk.next(); + TreeWalk treeWalk = new TreeWalk(repository); + treeWalk.reset(); + treeWalk.setRecursive(true); + + if (Util.isNotEmpty(request.getPath())) + { + treeWalk.setFilter(PathFilter.create(request.getPath())); + } + + + if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) + { + ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); + ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision); + RevTree tree = walk.parseCommit(ancestorId).getTree(); + treeWalk.addTree(tree); + } + else if (commit.getParentCount() > 0) + { + RevTree tree = commit.getParent(0).getTree(); + + if (tree != null) + { + treeWalk.addTree(tree); + } + else + { + treeWalk.addTree(new EmptyTreeIterator()); + } + } + else + { + treeWalk.addTree(new EmptyTreeIterator()); + } + + treeWalk.addTree(commit.getTree()); + + return new Differ(commit, walk, treeWalk); + } + + private Diff diff() throws IOException { + List entries = DiffEntry.scan(treeWalk); + return new Diff(commit, entries); + } + + @Override + public void close() { + GitUtil.release(walk); + GitUtil.release(treeWalk); + } + + public static class Diff { + + private final RevCommit commit; + private final List entries; + + private Diff(RevCommit commit, List entries) { + this.commit = commit; + this.entries = entries; + } + + public RevCommit getCommit() { + return commit; + } + + public List getEntries() { + return entries; + } + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java new file mode 100644 index 0000000000..8d445e1c44 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java @@ -0,0 +1,20 @@ +package sonia.scm.repository.spi; + +public class FileRange { + + private final int start; + private final int lineCount; + + public FileRange(int start, int lineCount) { + this.start = start; + this.lineCount = lineCount; + } + + public int getStart() { + return start; + } + + public int getLineCount() { + return lineCount; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java index 51f6974daa..5787842b9a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java @@ -37,30 +37,28 @@ package sonia.scm.repository.spi; import com.google.common.base.Preconditions; import com.google.common.base.Strings; - import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.revwalk.RevCommit; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.BlameLine; import sonia.scm.repository.BlameResult; import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Person; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; - import java.util.ArrayList; import java.util.List; +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -76,15 +74,6 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * - * @param context - * @param repository - * @param repositoryDirectory - */ public GitBlameCommand(GitContext context, Repository repository) { super(context, repository); @@ -92,20 +81,9 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override public BlameResult getBlameResult(BlameCommandRequest request) - throws IOException, RepositoryException + throws IOException { if (logger.isDebugEnabled()) { @@ -132,12 +110,11 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand if (gitBlameResult == null) { - throw new RepositoryException( - "could not create blame result for path ".concat( - request.getPath())); + throw new InternalRepositoryException(entity("Path", request.getPath()).in(repository), + "could not create blame result for path"); } - List blameLines = new ArrayList<>(); + List blameLines = new ArrayList(); int total = gitBlameResult.getResultContents().size(); int i = 0; @@ -174,7 +151,7 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand } catch (GitAPIException ex) { - throw new RepositoryException("could not create blame view", ex); + throw new InternalRepositoryException(repository, "could not create blame view", ex); } return result; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java new file mode 100644 index 0000000000..fd3c25cf39 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java @@ -0,0 +1,152 @@ +/** + * 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.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.CannotDeleteCurrentBranchException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Ref; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.Branch; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.PreReceiveRepositoryHookEvent; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.RepositoryHookType; +import sonia.scm.repository.api.BranchRequest; +import sonia.scm.repository.api.HookBranchProvider; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.repository.api.HookFeature; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +public class GitBranchCommand extends AbstractGitCommand implements BranchCommand { + + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; + + GitBranchCommand(GitContext context, Repository repository, HookContextFactory hookContextFactory, ScmEventBus eventBus) { + super(context, repository); + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; + } + + @Override + public Branch branch(BranchRequest request) { + try (Git git = new Git(context.open())) { + RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.createHookEvent(request.getNewBranch())); + eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent)); + Ref ref = git.branchCreate().setStartPoint(request.getParentBranch()).setName(request.getNewBranch()).call(); + eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent)); + return Branch.normalBranch(request.getNewBranch(), GitUtil.getId(ref.getObjectId())); + } catch (GitAPIException | IOException ex) { + throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex); + } + } + + @Override + public void deleteOrClose(String branchName) { + try (Git gitRepo = new Git(context.open())) { + RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.deleteHookEvent(branchName)); + eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent)); + gitRepo + .branchDelete() + .setBranchNames(branchName) + .setForce(true) + .call(); + eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent)); + } catch (CannotDeleteCurrentBranchException e) { + throw new CannotDeleteDefaultBranchException(context.getRepository(), branchName); + } catch (GitAPIException | IOException ex) { + throw new InternalRepositoryException(entity(context.getRepository()), String.format("Could not delete branch: %s", branchName)); + } + } + + private RepositoryHookEvent createBranchHookEvent(BranchHookContextProvider hookEvent) { + HookContext context = hookContextFactory.createContext(hookEvent, this.context.getRepository()); + return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE); + } + + private static class BranchHookContextProvider extends HookContextProvider { + private final List newBranches; + private final List deletedBranches; + + private BranchHookContextProvider(List newBranches, List deletedBranches) { + this.newBranches = newBranches; + this.deletedBranches = deletedBranches; + } + + static BranchHookContextProvider createHookEvent(String newBranch) { + return new BranchHookContextProvider(singletonList(newBranch), emptyList()); + } + + static BranchHookContextProvider deleteHookEvent(String deletedBranch) { + return new BranchHookContextProvider(emptyList(), singletonList(deletedBranch)); + } + + @Override + public Set getSupportedFeatures() { + return singleton(HookFeature.BRANCH_PROVIDER); + } + + @Override + public HookBranchProvider getBranchProvider() { + return new HookBranchProvider() { + @Override + public List getCreatedOrModified() { + return newBranches; + } + + @Override + public List getDeletedOrClosed() { + return deletedBranches; + } + }; + } + + @Override + public HookChangesetProvider getChangesetProvider() { + return r -> new HookChangesetResponse(emptyList()); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java index 984f3e37cf..ed0ac772f1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java @@ -34,17 +34,23 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.collect.Lists; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Ref; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.repository.Branch; import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import java.io.IOException; import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; //~--- JDK imports ------------------------------------------------------------ @@ -52,17 +58,10 @@ import java.util.List; * * @author Sebastian Sdorra */ -public class GitBranchesCommand extends AbstractGitCommand - implements BranchesCommand -{ +public class GitBranchesCommand extends AbstractGitCommand implements BranchesCommand { + + private static final Logger LOG = LoggerFactory.getLogger(GitBranchesCommand.class); - /** - * Constructs ... - * - * - * @param context - * @param repository - */ public GitBranchesCommand(GitContext context, Repository repository) { super(context, repository); @@ -70,44 +69,56 @@ public class GitBranchesCommand extends AbstractGitCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override - public List getBranches() throws RepositoryException, IOException - { - List branches = null; + public List getBranches() throws IOException { + Git git = createGit(); - Git git = new Git(open()); - - try - { - List refs = git.branchList().call(); - - branches = Lists.transform(refs, ref -> { - Branch branch = null; - String branchName = GitUtil.getBranch(ref); - - if (branchName != null) - { - branch = new Branch(branchName, GitUtil.getId(ref.getObjectId())); - } - - return branch; - }); + String defaultBranchName = determineDefaultBranchName(git); + try { + return git + .branchList() + .call() + .stream() + .map(ref -> createBranchObject(defaultBranchName, ref)) + .collect(Collectors.toList()); + } catch (GitAPIException ex) { + throw new InternalRepositoryException(repository, "could not read branches", ex); } - catch (GitAPIException ex) - { - throw new RepositoryException("could not read branches", ex); - } + } - return branches; + @VisibleForTesting + Git createGit() throws IOException { + return new Git(open()); + } + + @Nullable + private Branch createBranchObject(String defaultBranchName, Ref ref) { + String branchName = GitUtil.getBranch(ref); + + if (branchName == null) { + LOG.warn("could not determine branch name for branch name {} at revision {}", ref.getName(), ref.getObjectId()); + return null; + } else { + if (branchName.equals(defaultBranchName)) { + return Branch.defaultBranch(branchName, GitUtil.getId(ref.getObjectId())); + } else { + return Branch.normalBranch(branchName, GitUtil.getId(ref.getObjectId())); + } + } + } + + private String determineDefaultBranchName(Git git) { + String defaultBranchName = context.getConfig().getDefaultBranch(); + if (Strings.isNullOrEmpty(defaultBranchName)) { + return getRepositoryHeadRef(git).map(GitUtil::getBranch).orElse(null); + } else { + return defaultBranchName; + } + } + + Optional getRepositoryHeadRef(Git git) { + return GitUtil.getRepositoryHeadRef(git.getRepository()); } } + diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index feb773af21..2048d13dea 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -35,10 +35,10 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; - -import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lfs.LfsPointer; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -49,28 +49,31 @@ import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import sonia.scm.NotFoundException; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.GitSubModuleParser; import sonia.scm.repository.GitUtil; -import sonia.scm.repository.PathNotFoundException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.SubRepository; +import sonia.scm.store.Blob; +import sonia.scm.store.BlobStore; import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; - import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -88,41 +91,32 @@ public class GitBrowseCommand extends AbstractGitCommand */ private static final Logger logger = LoggerFactory.getLogger(GitBrowseCommand.class); + private final LfsBlobStoreFactory lfsBlobStoreFactory; //~--- constructors --------------------------------------------------------- /** * Constructs ... - * - * @param context + * @param context * @param repository + * @param lfsBlobStoreFactory */ - public GitBrowseCommand(GitContext context, Repository repository) + public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) { super(context, repository); + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override @SuppressWarnings("unchecked") public BrowserResult getBrowserResult(BrowseCommandRequest request) - throws IOException, RepositoryException - { + throws IOException { logger.debug("try to create browse result for {}", request); BrowserResult result; + org.eclipse.jgit.lib.Repository repo = open(); ObjectId revId; @@ -137,21 +131,21 @@ public class GitBrowseCommand extends AbstractGitCommand if (revId != null) { - result = getResult(repo, request, revId); + result = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); } else { if (Util.isNotEmpty(request.getRevision())) { logger.error("could not find revision {}", request.getRevision()); + throw notFound(entity("Revision", request.getRevision()).in(this.repository)); } else if (logger.isWarnEnabled()) { - logger.warn("coul not find head of repository, empty?"); + logger.warn("could not find head of repository, empty?"); } - result = new BrowserResult(Constants.HEAD, null, null, - Collections.EMPTY_LIST); + result = new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot()); } return result; @@ -159,6 +153,14 @@ public class GitBrowseCommand extends AbstractGitCommand //~--- methods -------------------------------------------------------------- + private FileObject createEmtpyRoot() { + FileObject fileObject = new FileObject(); + fileObject.setName(""); + fileObject.setPath(""); + fileObject.setDirectory(true); + return fileObject; + } + /** * Method description * @@ -172,71 +174,69 @@ public class GitBrowseCommand extends AbstractGitCommand * @throws IOException */ private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, - BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) - throws IOException, RepositoryException - { - FileObject file; + BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) + throws IOException { - try + FileObject file = new FileObject(); + + String path = treeWalk.getPathString(); + + file.setName(treeWalk.getNameString()); + file.setPath(path); + + SubRepository sub = null; + + if (!request.isDisableSubRepositoryDetection()) { - file = new FileObject(); + sub = getSubRepository(repo, revId, path); + } - String path = treeWalk.getPathString(); + if (sub != null) + { + logger.trace("{} seems to be a sub repository", path); + file.setDirectory(true); + file.setSubRepository(sub); + } + else + { + ObjectLoader loader = repo.open(treeWalk.getObjectId(0)); - file.setName(treeWalk.getNameString()); - file.setPath(path); + file.setDirectory(loader.getType() == Constants.OBJ_TREE); - SubRepository sub = null; - - if (!request.isDisableSubRepositoryDetection()) + // don't show message and date for directories to improve performance + if (!file.isDirectory() &&!request.isDisableLastCommit()) { - sub = getSubRepository(repo, revId, path); - } + logger.trace("fetch last commit for {} at {}", path, revId.getName()); + RevCommit commit = getLatestCommit(repo, revId, path); - if (sub != null) - { - logger.trace("{} seems to be a sub repository", path); - file.setDirectory(true); - file.setSubRepository(sub); - } - else - { - ObjectLoader loader = repo.open(treeWalk.getObjectId(0)); + Optional lfsPointer = GitUtil.getLfsPointer(repo, path, commit, treeWalk); - file.setDirectory(loader.getType() == Constants.OBJ_TREE); - file.setLength(loader.getSize()); + if (lfsPointer.isPresent()) { + BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); + String oid = lfsPointer.get().getOid().getName(); + Blob blob = lfsBlobStore.get(oid); + if (blob == null) { + logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName()); + file.setLength(-1); + } else { + file.setLength(blob.getSize()); + } + } else { + file.setLength(loader.getSize()); + } - // don't show message and date for directories to improve performance - if (!file.isDirectory() &&!request.isDisableLastCommit()) + if (commit != null) { - logger.trace("fetch last commit for {} at {}", path, revId.getName()); - - RevCommit commit = getLatestCommit(repo, revId, path); - - if (commit != null) - { - file.setLastModified(GitUtil.getCommitTime(commit)); - file.setDescription(commit.getShortMessage()); - } - else if (logger.isWarnEnabled()) - { - logger.warn("could not find latest commit for {} on {}", path, - revId); - } + file.setLastModified(GitUtil.getCommitTime(commit)); + file.setDescription(commit.getShortMessage()); + } + else if (logger.isWarnEnabled()) + { + logger.warn("could not find latest commit for {} on {}", path, + revId); } } } - catch (MissingObjectException ex) - { - file = null; - logger.error("could not fetch object for id {}", revId); - - if (logger.isTraceEnabled()) - { - logger.trace("could not fetch object", ex); - } - } - return file; } @@ -254,7 +254,7 @@ public class GitBrowseCommand extends AbstractGitCommand * @return */ private RevCommit getLatestCommit(org.eclipse.jgit.lib.Repository repo, - ObjectId revId, String path) + ObjectId revId, String path) { RevCommit result = null; RevWalk walk = null; @@ -282,37 +282,19 @@ public class GitBrowseCommand extends AbstractGitCommand return result; } - /** - * Method description - * - * - * @param repo - * @param request - * @param revId - * @param path - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private BrowserResult getResult(org.eclipse.jgit.lib.Repository repo, - BrowseCommandRequest request, ObjectId revId) - throws IOException, RepositoryException - { - BrowserResult result = null; + private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException { RevWalk revWalk = null; TreeWalk treeWalk = null; - try - { - if (logger.isDebugEnabled()) - { - logger.debug("load repository browser for revision {}", revId.name()); - } + FileObject result; + + try { + logger.debug("load repository browser for revision {}", revId.name()); treeWalk = new TreeWalk(repo); - treeWalk.setRecursive(request.isRecursive()); + if (!isRootRequest(request)) { + treeWalk.setFilter(PathFilter.create(request.getPath())); + } revWalk = new RevWalk(repo); RevTree tree = revWalk.parseTree(revId); @@ -323,65 +305,20 @@ public class GitBrowseCommand extends AbstractGitCommand } else { - logger.error("could not find tree for {}", revId.name()); + throw new IllegalStateException("could not find tree for " + revId.name()); } - result = new BrowserResult(); - - List files = Lists.newArrayList(); - - String path = request.getPath(); - - if (Util.isEmpty(path)) - { - while (treeWalk.next()) - { - FileObject fo = createFileObject(repo, request, revId, treeWalk); - - if (fo != null) - { - files.add(fo); - } - } - } - else - { - String[] parts = path.split("/"); - int current = 0; - int limit = parts.length; - - while (treeWalk.next()) - { - String name = treeWalk.getNameString(); - - if (current >= limit) - { - String p = treeWalk.getPathString(); - - if (p.split("/").length > limit) - { - FileObject fo = createFileObject(repo, request, revId, treeWalk); - - if (fo != null) - { - files.add(fo); - } - } - } - else if (name.equalsIgnoreCase(parts[current])) - { - current++; - - if (!request.isRecursive()) - { - treeWalk.enterSubtree(); - } - } + if (isRootRequest(request)) { + result = createEmtpyRoot(); + findChildren(result, repo, request, revId, treeWalk); + } else { + result = findFirstMatch(repo, request, revId, treeWalk); + if ( result.isDirectory() ) { + treeWalk.enterSubtree(); + findChildren(result, repo, request, revId, treeWalk); } } - result.setFiles(files); - result.setRevision(revId.getName()); } finally { @@ -392,24 +329,65 @@ public class GitBrowseCommand extends AbstractGitCommand return result; } - /** - * Method description - * - * - * @param repo - * @param revision - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ + private boolean isRootRequest(BrowseCommandRequest request) { + return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath()); + } + + private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { + List files = Lists.newArrayList(); + while (treeWalk.next()) + { + + FileObject fileObject = createFileObject(repo, request, revId, treeWalk); + if (!fileObject.getPath().startsWith(parent.getPath())) { + parent.setChildren(files); + return fileObject; + } + + files.add(fileObject); + + if (request.isRecursive() && fileObject.isDirectory()) { + treeWalk.enterSubtree(); + FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk); + if (rc != null) { + files.add(rc); + } + } + } + + parent.setChildren(files); + + return null; + } + + private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo, + BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { + String[] pathElements = request.getPath().split("/"); + int currentDepth = 0; + int limit = pathElements.length; + + while (treeWalk.next()) { + String name = treeWalk.getNameString(); + + if (name.equalsIgnoreCase(pathElements[currentDepth])) { + currentDepth++; + + if (currentDepth >= limit) { + return createFileObject(repo, request, revId, treeWalk); + } else { + treeWalk.enterSubtree(); + } + } + } + + throw notFound(entity("File", request.getPath()).in("Revision", revId.getName()).in(this.repository)); + } + @SuppressWarnings("unchecked") private Map getSubRepositories(org.eclipse.jgit.lib.Repository repo, - ObjectId revision) - throws IOException, RepositoryException - { + ObjectId revision) + throws IOException { if (logger.isDebugEnabled()) { logger.debug("read submodules of {} at {}", repository.getName(), @@ -419,11 +397,11 @@ public class GitBrowseCommand extends AbstractGitCommand Map subRepositories; try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) { - new GitCatCommand(context, repository).getContent(repo, revision, + new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revision, PATH_MODULES, baos); subRepositories = GitSubModuleParser.parse(baos.toString()); } - catch (PathNotFoundException ex) + catch (NotFoundException ex) { logger.trace("could not find .gitmodules", ex); subRepositories = Collections.EMPTY_MAP; @@ -433,9 +411,8 @@ public class GitBrowseCommand extends AbstractGitCommand } private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo, - ObjectId revId, String path) - throws IOException, RepositoryException - { + ObjectId revId, String path) + throws IOException { Map subRepositories = subrepositoryCache.get(revId); if (subRepositories == null) @@ -455,7 +432,7 @@ public class GitBrowseCommand extends AbstractGitCommand } //~--- fields --------------------------------------------------------------- - + /** sub repository cache */ private final Map> subrepositoryCache = Maps.newHashMap(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index fac6b7b57b..193ab5bbc8 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -32,157 +32,201 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lfs.LfsPointer; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.GitUtil; -import sonia.scm.repository.PathNotFoundException; -import sonia.scm.repository.RepositoryException; +import sonia.scm.store.Blob; +import sonia.scm.store.BlobStore; +import sonia.scm.util.IOUtil; import sonia.scm.util.Util; +import sonia.scm.web.lfs.LfsBlobStoreFactory; -//~--- JDK imports ------------------------------------------------------------ - +import java.io.Closeable; +import java.io.FilterInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.util.Optional; -/** - * - * @author Sebastian Sdorra - */ -public class GitCatCommand extends AbstractGitCommand implements CatCommand -{ +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; - /** - * the logger for GitCatCommand - */ - private static final Logger logger = - LoggerFactory.getLogger(GitCatCommand.class); - //~--- constructors --------------------------------------------------------- +public class GitCatCommand extends AbstractGitCommand implements CatCommand { - /** - * Constructs ... - * - * - * - * @param context - * @param repository - */ - public GitCatCommand(GitContext context, - sonia.scm.repository.Repository repository) - { + private static final Logger logger = LoggerFactory.getLogger(GitCatCommand.class); + + private final LfsBlobStoreFactory lfsBlobStoreFactory; + + public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) { super(context, repository); + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param output - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void getCatResult(CatCommandRequest request, OutputStream output) - throws IOException, RepositoryException - { + public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException { logger.debug("try to read content for {}", request); - - org.eclipse.jgit.lib.Repository repo = open(); - - ObjectId revId = getCommitOrDefault(repo, request.getRevision()); - getContent(repo, revId, request.getPath(), output); + try (Loader closableObjectLoaderContainer = getLoader(request)) { + closableObjectLoaderContainer.copyTo(output); + } } - /** - * Method description - * - * - * - * @param repo - * @param revId - * @param path - * @param output - * - * - * @throws IOException - * @throws RepositoryException - */ - void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, - String path, OutputStream output) - throws IOException, RepositoryException - { - TreeWalk treeWalk = null; - RevWalk revWalk = null; + @Override + public InputStream getCatResultStream(CatCommandRequest request) throws IOException { + logger.debug("try to read content for {}", request); + return new InputStreamWrapper(getLoader(request)); + } - try - { - treeWalk = new TreeWalk(repo); - treeWalk.setRecursive(Util.nonNull(path).contains("/")); - - if (logger.isDebugEnabled()) - { - logger.debug("load content for {} at {}", path, revId.name()); - } - - revWalk = new RevWalk(repo); - - RevCommit entry = revWalk.parseCommit(revId); - RevTree revTree = entry.getTree(); - - if (revTree != null) - { - treeWalk.addTree(revTree); - } - else - { - logger.error("could not find tree for {}", revId.name()); - } - - treeWalk.setFilter(PathFilter.create(path)); - - if (treeWalk.next()) - { - - // Path exists - if (treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) - { - ObjectId blobId = treeWalk.getObjectId(0); - ObjectLoader loader = repo.open(blobId); - - loader.copyTo(output); - } - else - { - - // Not a blob, its something else (tree, gitlink) - throw new PathNotFoundException(path); - } - } - else - { - throw new PathNotFoundException(path); - } + void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException { + try (Loader closableObjectLoaderContainer = getLoader(repo, revId, path)) { + closableObjectLoaderContainer.copyTo(output); } - finally - { + } + + private Loader getLoader(CatCommandRequest request) throws IOException { + org.eclipse.jgit.lib.Repository repo = open(); + ObjectId revId = getCommitOrDefault(repo, request.getRevision()); + return getLoader(repo, revId, request.getPath()); + } + + private Loader getLoader(Repository repo, ObjectId revId, String path) throws IOException { + TreeWalk treeWalk = new TreeWalk(repo); + treeWalk.setRecursive(Util.nonNull(path).contains("/")); + + logger.debug("load content for {} at {}", path, revId.name()); + + RevWalk revWalk = new RevWalk(repo); + + RevCommit entry = null; + try { + entry = revWalk.parseCommit(revId); + } catch (MissingObjectException e) { + throw notFound(entity("Revision", revId.getName()).in(repository)); + } + RevTree revTree = entry.getTree(); + + if (revTree != null) { + treeWalk.addTree(revTree); + } else { + logger.error("could not find tree for {}", revId.name()); + } + + treeWalk.setFilter(PathFilter.create(path)); + + if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { + Optional lfsPointer = GitUtil.getLfsPointer(repo, path, entry, treeWalk); + if (lfsPointer.isPresent()) { + return loadFromLfsStore(treeWalk, revWalk, lfsPointer.get()); + } else { + return loadFromGit(repo, treeWalk, revWalk); + } + } else { + throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository)); + } + } + + private Loader loadFromGit(Repository repo, TreeWalk treeWalk, RevWalk revWalk) throws IOException { + ObjectId blobId = treeWalk.getObjectId(0); + ObjectLoader loader = repo.open(blobId); + + return new GitObjectLoaderWrapper(loader, treeWalk, revWalk); + } + + private Loader loadFromLfsStore(TreeWalk treeWalk, RevWalk revWalk, LfsPointer lfsPointer) throws IOException { + BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); + String oid = lfsPointer.getOid().getName(); + Blob blob = lfsBlobStore.get(oid); + if (blob == null) { + logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName()); + throw notFound(entity("LFS", oid).in(repository)); + } + GitUtil.release(revWalk); + GitUtil.release(treeWalk); + return new BlobLoader(blob); + } + + private interface Loader extends Closeable { + void copyTo(OutputStream output) throws IOException; + + InputStream openStream() throws IOException; + } + + private static class BlobLoader implements Loader { + private final InputStream inputStream; + + private BlobLoader(Blob blob) throws IOException { + this.inputStream = blob.getInputStream(); + } + + @Override + public void copyTo(OutputStream output) throws IOException { + IOUtil.copy(inputStream, output); + } + + @Override + public InputStream openStream() { + return inputStream; + } + + @Override + public void close() throws IOException { + this.inputStream.close(); + } + } + + private static class GitObjectLoaderWrapper implements Loader { + private final ObjectLoader objectLoader; + private final TreeWalk treeWalk; + private final RevWalk revWalk; + + private GitObjectLoaderWrapper(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) { + this.objectLoader = objectLoader; + this.treeWalk = treeWalk; + this.revWalk = revWalk; + } + + @Override + public void close() { GitUtil.release(revWalk); GitUtil.release(treeWalk); } + + public void copyTo(OutputStream output) throws IOException { + this.objectLoader.copyTo(output); + } + + public InputStream openStream() throws IOException { + return objectLoader.openStream(); + } + } + + private static class InputStreamWrapper extends FilterInputStream { + + private final Loader container; + + private InputStreamWrapper(Loader container) throws IOException { + super(container.openStream()); + this.container = container; + } + + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + container.close(); + } + } } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java index 2175846d5a..0b93f9cf2b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java @@ -37,7 +37,10 @@ package sonia.scm.repository.spi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.GitUtil; +import sonia.scm.repository.Repository; //~--- JDK imports ------------------------------------------------------------ @@ -65,10 +68,13 @@ public class GitContext implements Closeable * * * @param directory + * @param repository */ - public GitContext(File directory) + public GitContext(File directory, Repository repository, GitRepositoryConfigStoreProvider storeProvider) { this.directory = directory; + this.repository = repository; + this.storeProvider = storeProvider; } //~--- methods -------------------------------------------------------------- @@ -82,8 +88,8 @@ public class GitContext implements Closeable { logger.trace("close git repository {}", directory); - GitUtil.close(repository); - repository = null; + GitUtil.close(gitRepository); + gitRepository = null; } /** @@ -96,21 +102,44 @@ public class GitContext implements Closeable */ public org.eclipse.jgit.lib.Repository open() throws IOException { - if (repository == null) + if (gitRepository == null) { logger.trace("open git repository {}", directory); - repository = GitUtil.open(directory); + gitRepository = GitUtil.open(directory); } + return gitRepository; + } + + Repository getRepository() { return repository; } + File getDirectory() { + return directory; + } + + GitRepositoryConfig getConfig() { + GitRepositoryConfig config = storeProvider.get(repository).get(); + if (config == null) { + return new GitRepositoryConfig(); + } else { + return config; + } + } + + void setConfig(GitRepositoryConfig newConfig) { + storeProvider.get(repository).set(newConfig); + } + //~--- fields --------------------------------------------------------------- /** Field description */ private final File directory; + private final Repository repository; + private final GitRepositoryConfigStoreProvider storeProvider; /** Field description */ - private org.eclipse.jgit.lib.Repository repository; + private org.eclipse.jgit.lib.Repository gitRepository; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index 83977ef290..dce4d0622f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

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

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

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,147 +24,167 @@ * 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.spi; -//~--- non-JDK imports -------------------------------------------------------- - import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevTree; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.treewalk.EmptyTreeIterator; -import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.treewalk.filter.PathFilter; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.repository.GitUtil; +import org.eclipse.jgit.util.QuotedString; import sonia.scm.repository.Repository; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.api.DiffCommandBuilder; import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.OutputStream; -import java.util.List; +import static java.nio.charset.StandardCharsets.UTF_8; /** - * * @author Sebastian Sdorra */ -public class GitDiffCommand extends AbstractGitCommand implements DiffCommand -{ +public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { - /** - * the logger for GitDiffCommand - */ - private static final Logger logger = - LoggerFactory.getLogger(GitDiffCommand.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param context - * @param repository - */ - public GitDiffCommand(GitContext context, Repository repository) - { + GitDiffCommand(GitContext context, Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param output - */ @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) - { - RevWalk walk = null; - TreeWalk treeWalk = null; - DiffFormatter formatter = null; + public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) throws IOException { + @SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService + org.eclipse.jgit.lib.Repository repository = open(); - try - { - org.eclipse.jgit.lib.Repository gr = open(); + Differ.Diff diff = Differ.diff(repository, request); - walk = new RevWalk(gr); + return output -> { + try (DiffFormatter formatter = new DiffFormatter(new DequoteOutputStream(output))) { + formatter.setRepository(repository); - RevCommit commit = walk.parseCommit(gr.resolve(request.getRevision())); - - walk.markStart(commit); - commit = walk.next(); - treeWalk = new TreeWalk(gr); - treeWalk.reset(); - treeWalk.setRecursive(true); - - if (Util.isNotEmpty(request.getPath())) - { - treeWalk.setFilter(PathFilter.create(request.getPath())); - } - - if (commit.getParentCount() > 0) - { - RevTree tree = commit.getParent(0).getTree(); - - if (tree != null) - { - treeWalk.addTree(tree); - } - else - { - treeWalk.addTree(new EmptyTreeIterator()); + for (DiffEntry e : diff.getEntries()) { + if (!e.getOldId().equals(e.getNewId())) { + formatter.format(e); + } } + + formatter.flush(); } - else - { - treeWalk.addTree(new EmptyTreeIterator()); - } + }; + } - treeWalk.addTree(commit.getTree()); - formatter = new DiffFormatter(new BufferedOutputStream(output)); - formatter.setRepository(gr); + static class DequoteOutputStream extends OutputStream { - List entries = DiffEntry.scan(treeWalk); + private static final String[] DEQUOTE_STARTS = { + "--- ", + "+++ ", + "diff --git " + }; - for (DiffEntry e : entries) - { - if (!e.getOldId().equals(e.getNewId())) - { - formatter.format(e); - } - } + private final OutputStream target; - formatter.flush(); + private boolean afterNL = true; + private boolean writeToBuffer = false; + private int numberOfPotentialBeginning = -1; + private int potentialBeginningCharCount = 0; + private boolean inPotentialQuotedLine = false; + + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + DequoteOutputStream(OutputStream target) { + this.target = new BufferedOutputStream(target); } - catch (Exception ex) - { - // TODO throw exception - logger.error("could not create diff", ex); + @Override + public void write(int i) throws IOException { + if (i == (int) '\n') { + handleNewLine(i); + return; + } + + if (afterNL) { + afterNL = false; + if (foundPotentialBeginning(i)) { + return; + } + numberOfPotentialBeginning = -1; + inPotentialQuotedLine = false; + } + + if (inPotentialQuotedLine && i == '"') { + handleQuote(); + return; + } + + if (numberOfPotentialBeginning > -1 && checkForFurtherBeginning(i)) { + return; + } + + if (writeToBuffer) { + buffer.write(i); + } else { + target.write(i); + } } - finally - { - GitUtil.release(walk); - GitUtil.release(treeWalk); - GitUtil.release(formatter); + + private boolean checkForFurtherBeginning(int i) throws IOException { + if (i == DEQUOTE_STARTS[numberOfPotentialBeginning].charAt(potentialBeginningCharCount)) { + if (potentialBeginningCharCount + 1 < DEQUOTE_STARTS[numberOfPotentialBeginning].length()) { + ++potentialBeginningCharCount; + } else { + inPotentialQuotedLine = true; + } + target.write(i); + return true; + } else { + numberOfPotentialBeginning = -1; + } + return false; + } + + private boolean foundPotentialBeginning(int i) throws IOException { + for (int n = 0; n < DEQUOTE_STARTS.length; ++n) { + if (i == DEQUOTE_STARTS[n].charAt(0)) { + numberOfPotentialBeginning = n; + potentialBeginningCharCount = 1; + target.write(i); + return true; + } + } + return false; + } + + private void handleQuote() throws IOException { + if (writeToBuffer) { + buffer.write('"'); + dequoteBuffer(); + } else { + writeToBuffer = true; + buffer.reset(); + buffer.write('"'); + } + } + + private void handleNewLine(int i) throws IOException { + afterNL = true; + if (writeToBuffer) { + dequoteBuffer(); + } + target.write(i); + } + + private void dequoteBuffer() throws IOException { + byte[] bytes = buffer.toByteArray(); + String dequote = QuotedString.GIT_PATH.dequote(bytes, 0, bytes.length); + target.write(dequote.getBytes(UTF_8)); + writeToBuffer = false; + } + + @Override + public void flush() throws IOException { + target.flush(); } } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java new file mode 100644 index 0000000000..e7f26d6885 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java @@ -0,0 +1,107 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffFormatter; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.DiffFile; +import sonia.scm.repository.api.DiffResult; +import sonia.scm.repository.api.Hunk; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Iterator; +import java.util.stream.Collectors; + +public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand { + + GitDiffResultCommand(GitContext context, Repository repository) { + super(context, repository); + } + + public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException { + org.eclipse.jgit.lib.Repository repository = open(); + return new GitDiffResult(repository, Differ.diff(repository, diffCommandRequest)); + } + + private class GitDiffResult implements DiffResult { + + private final org.eclipse.jgit.lib.Repository repository; + private final Differ.Diff diff; + + private GitDiffResult(org.eclipse.jgit.lib.Repository repository, Differ.Diff diff) { + this.repository = repository; + this.diff = diff; + } + + @Override + public String getOldRevision() { + return GitUtil.getId(diff.getCommit().getParent(0).getId()); + } + + @Override + public String getNewRevision() { + return GitUtil.getId(diff.getCommit().getId()); + } + + @Override + public Iterator iterator() { + return diff.getEntries() + .stream() + .map(diffEntry -> new GitDiffFile(repository, diffEntry)) + .collect(Collectors.toList()) + .iterator(); + } + } + + private class GitDiffFile implements DiffFile { + + private final org.eclipse.jgit.lib.Repository repository; + private final DiffEntry diffEntry; + + private GitDiffFile(org.eclipse.jgit.lib.Repository repository, DiffEntry diffEntry) { + this.repository = repository; + this.diffEntry = diffEntry; + } + + @Override + public String getOldRevision() { + return GitUtil.getId(diffEntry.getOldId().toObjectId()); + } + + @Override + public String getNewRevision() { + return GitUtil.getId(diffEntry.getNewId().toObjectId()); + } + + @Override + public String getOldPath() { + return diffEntry.getOldPath(); + } + + @Override + public String getNewPath() { + return diffEntry.getNewPath(); + } + + @Override + public Iterator iterator() { + String content = format(repository, diffEntry); + GitHunkParser parser = new GitHunkParser(); + return parser.parse(content).iterator(); + } + + private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DiffFormatter formatter = new DiffFormatter(baos)) { + formatter.setRepository(repository); + formatter.format(entry); + return baos.toString(); + } catch (IOException ex) { + throw new InternalRepositoryException(GitDiffResultCommand.this.repository, "failed to format diff entry", ex); + } + } + + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java new file mode 100644 index 0000000000..64a20a33cb --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java @@ -0,0 +1,36 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeCommand; +import org.eclipse.jgit.api.MergeResult; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.MergeCommandResult; + +import java.io.IOException; + +class GitFastForwardIfPossible extends GitMergeStrategy { + + private GitMergeStrategy fallbackMerge; + + GitFastForwardIfPossible(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { + super(clone, request, context, repository); + fallbackMerge = new GitMergeCommit(clone, request, context, repository); + } + + @Override + MergeCommandResult run() throws IOException { + MergeResult fastForwardResult = mergeWithFastForwardOnlyMode(); + if (fastForwardResult.getMergeStatus().isSuccessful()) { + push(); + return MergeCommandResult.success(); + } else { + return fallbackMerge.run(); + } + } + + private MergeResult mergeWithFastForwardOnlyMode() throws IOException { + MergeCommand mergeCommand = getClone().merge(); + mergeCommand.setFastForward(MergeCommand.FastForwardMode.FF_ONLY); + return doMergeInClone(mergeCommand); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java new file mode 100644 index 0000000000..9a272d7b10 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java @@ -0,0 +1,48 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.Iterator; +import java.util.List; + +public class GitHunk implements Hunk { + + private final FileRange oldFileRange; + private final FileRange newFileRange; + private List lines; + + public GitHunk(FileRange oldFileRange, FileRange newFileRange) { + this.oldFileRange = oldFileRange; + this.newFileRange = newFileRange; + } + + @Override + public int getOldStart() { + return oldFileRange.getStart(); + } + + @Override + public int getOldLineCount() { + return oldFileRange.getLineCount(); + } + + @Override + public int getNewStart() { + return newFileRange.getStart(); + } + + @Override + public int getNewLineCount() { + return newFileRange.getLineCount(); + } + + @Override + public Iterator iterator() { + return lines.iterator(); + } + + void setLines(List lines) { + this.lines = lines; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java new file mode 100644 index 0000000000..a65b9a6b02 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java @@ -0,0 +1,178 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.Scanner; + +import static java.util.OptionalInt.of; + +final class GitHunkParser { + private static final int HEADER_PREFIX_LENGTH = "@@ -".length(); + private static final int HEADER_SUFFIX_LENGTH = " @@".length(); + + private GitHunk currentGitHunk = null; + private List collectedLines = null; + private int oldLineCounter = 0; + private int newLineCounter = 0; + + GitHunkParser() { + } + + public List parse(String content) { + List hunks = new ArrayList<>(); + + try (Scanner scanner = new Scanner(content)) { + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.startsWith("@@")) { + parseHeader(hunks, line); + } else if (currentGitHunk != null) { + parseDiffLine(line); + } + } + } + if (currentGitHunk != null) { + currentGitHunk.setLines(collectedLines); + } + + return hunks; + } + + private void parseHeader(List hunks, String line) { + if (currentGitHunk != null) { + currentGitHunk.setLines(collectedLines); + } + String hunkHeader = line.substring(HEADER_PREFIX_LENGTH, line.length() - HEADER_SUFFIX_LENGTH); + String[] split = hunkHeader.split("\\s"); + + FileRange oldFileRange = createFileRange(split[0]); + // TODO merge contains two two block which starts with "-" e.g. -1,3 -2,4 +3,6 + // check if it is relevant for our use case + FileRange newFileRange = createFileRange(split[1]); + + currentGitHunk = new GitHunk(oldFileRange, newFileRange); + hunks.add(currentGitHunk); + + collectedLines = new ArrayList<>(); + oldLineCounter = currentGitHunk.getOldStart(); + newLineCounter = currentGitHunk.getNewStart(); + } + + private void parseDiffLine(String line) { + String content = line.substring(1); + switch (line.charAt(0)) { + case ' ': + collectedLines.add(new UnchangedGitDiffLine(newLineCounter, oldLineCounter, content)); + ++newLineCounter; + ++oldLineCounter; + break; + case '+': + collectedLines.add(new AddedGitDiffLine(newLineCounter, content)); + ++newLineCounter; + break; + case '-': + collectedLines.add(new RemovedGitDiffLine(oldLineCounter, content)); + ++oldLineCounter; + break; + default: + if (!line.equals("\\ No newline at end of file")) { + throw new IllegalStateException("cannot handle diff line: " + line); + } + } + } + + private static class AddedGitDiffLine implements DiffLine { + private final int newLineNumber; + private final String content; + + private AddedGitDiffLine(int newLineNumber, String content) { + this.newLineNumber = newLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return OptionalInt.empty(); + } + + @Override + public OptionalInt getNewLineNumber() { + return of(newLineNumber); + } + + @Override + public String getContent() { + return content; + } + } + + private static class RemovedGitDiffLine implements DiffLine { + private final int oldLineNumber; + private final String content; + + private RemovedGitDiffLine(int oldLineNumber, String content) { + this.oldLineNumber = oldLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return of(oldLineNumber); + } + + @Override + public OptionalInt getNewLineNumber() { + return OptionalInt.empty(); + } + + @Override + public String getContent() { + return content; + } + } + + private static class UnchangedGitDiffLine implements DiffLine { + private final int newLineNumber; + private final int oldLineNumber; + private final String content; + + private UnchangedGitDiffLine(int newLineNumber, int oldLineNumber, String content) { + this.newLineNumber = newLineNumber; + this.oldLineNumber = oldLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return of(oldLineNumber); + } + + @Override + public OptionalInt getNewLineNumber() { + return of(newLineNumber); + } + + @Override + public String getContent() { + return content; + } + } + + private static FileRange createFileRange(String fileRangeString) { + int start; + int lineCount = 1; + int commaIndex = fileRangeString.indexOf(','); + if (commaIndex > 0) { + start = Integer.parseInt(fileRangeString.substring(0, commaIndex)); + lineCount = Integer.parseInt(fileRangeString.substring(commaIndex + 1)); + } else { + start = Integer.parseInt(fileRangeString); + } + + return new FileRange(start, lineCount); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java index 9bd982a909..a1c1a7e652 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java @@ -36,16 +36,14 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.LogCommand; import org.eclipse.jgit.lib.ObjectId; - import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -70,22 +68,8 @@ public class GitIncomingCommand extends AbstractGitIncomingOutgoingCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override - public ChangesetPagingResult getIncomingChangesets( - IncomingCommandRequest request) - throws IOException, RepositoryException - { + public ChangesetPagingResult getIncomingChangesets(IncomingCommandRequest request) throws IOException { return getIncomingOrOutgoingChangesets(request); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLfsFilterContextListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLfsFilterContextListener.java new file mode 100644 index 0000000000..c202874781 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLfsFilterContextListener.java @@ -0,0 +1,77 @@ +package sonia.scm.repository.spi; + +import com.google.common.io.ByteStreams; +import org.eclipse.jgit.attributes.FilterCommand; +import org.eclipse.jgit.attributes.FilterCommandRegistry; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Pattern; + +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +@Extension +public class GitLfsFilterContextListener implements ServletContextListener { + + public static final String GITCONFIG = "[filter \"lfs\"]\n" + + "clean = git-lfs clean -- %f\n" + + "smudge = git-lfs smudge -- %f\n" + + "process = git-lfs filter-process\n" + + "required = true\n"; + public static final Pattern COMMAND_NAME_PATTERN = Pattern.compile("git-lfs (smudge|clean) -- .*"); + + private static final Logger LOG = LoggerFactory.getLogger(GitLfsFilterContextListener.class); + + private final SCMContextProvider contextProvider; + + @Inject + public GitLfsFilterContextListener(SCMContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + Path gitconfig = contextProvider.getBaseDirectory().toPath().resolve("gitconfig"); + try { + Files.write(gitconfig, GITCONFIG.getBytes(Charset.defaultCharset()), TRUNCATE_EXISTING, CREATE); + FS.DETECTED.setGitSystemConfig(gitconfig.toFile()); + LOG.info("wrote git config file: {}", gitconfig); + } catch (IOException e) { + LOG.error("could not write git config in path {}; git lfs support may not work correctly", gitconfig, e); + } + FilterCommandRegistry.register(COMMAND_NAME_PATTERN, NoOpFilterCommand::new); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + FilterCommandRegistry.unregister(COMMAND_NAME_PATTERN); + } + + private static class NoOpFilterCommand extends FilterCommand { + NoOpFilterCommand(Repository db, InputStream in, OutputStream out) { + super(in, out); + } + + @Override + public int run() throws IOException { + ByteStreams.copy(in, out); + in.close(); + out.close(); + return -1; + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java index 2c6fac254b..8c44f33f7f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java @@ -37,33 +37,35 @@ package sonia.scm.repository.spi; import com.google.common.base.Strings; import com.google.common.collect.Lists; - +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import sonia.scm.NotFoundException; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitUtil; -import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.Collections; import java.util.Iterator; import java.util.List; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -76,6 +78,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand */ private static final Logger logger = LoggerFactory.getLogger(GitLogCommand.class); + public static final String REVISION = "Revision"; //~--- constructors --------------------------------------------------------- @@ -86,7 +89,6 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand * * @param context * @param repository - * @param repositoryDirectory */ GitLogCommand(GitContext context, sonia.scm.repository.Repository repository) { @@ -104,7 +106,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand * @return */ @Override - public Changeset getChangeset(String revision) + public Changeset getChangeset(String revision, LogCommandRequest request) { if (logger.isDebugEnabled()) { @@ -129,7 +131,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand if (commit != null) { converter = new GitChangesetConverter(gr, revWalk); - changeset = converter.createChangeset(commit); + + if (isBranchRequested(request)) { + String branch = request.getBranch(); + if (isMergedIntoBranch(gr, revWalk, commit, branch)) { + logger.trace("returning commit {} with branch {}", commit.getId(), branch); + changeset = converter.createChangeset(commit, branch); + } else { + logger.debug("returning null, because commit {} was not merged into branch {}", commit.getId(), branch); + } + } else { + changeset = converter.createChangeset(commit); + } } else if (logger.isWarnEnabled()) { @@ -141,6 +154,10 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand { logger.error("could not open repository", ex); } + catch (NullPointerException e) + { + throw notFound(entity(REVISION, revision).in(this.repository)); + } finally { IOUtil.close(converter); @@ -151,6 +168,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand return changeset; } + private boolean isMergedIntoBranch(Repository repository, RevWalk revWalk, RevCommit commit, String branchName) throws IOException { + return revWalk.isMergedInto(commit, findHeadCommitOfBranch(repository, revWalk, branchName)); + } + + private boolean isBranchRequested(LogCommandRequest request) { + return request != null && !Strings.isNullOrEmpty(request.getBranch()); + } + + private RevCommit findHeadCommitOfBranch(Repository repository, RevWalk revWalk, String branchName) throws IOException { + return revWalk.parseCommit(GitUtil.getCommit(repository, revWalk, repository.findRef(branchName))); + } + /** * Method description * @@ -160,15 +189,11 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand * @return * * @throws IOException - * @throws RepositoryException */ @Override @SuppressWarnings("unchecked") - public ChangesetPagingResult getChangesets(LogCommandRequest request) - throws IOException, RepositoryException - { - if (logger.isDebugEnabled()) - { + public ChangesetPagingResult getChangesets(LogCommandRequest request) { + if (logger.isDebugEnabled()) { logger.debug("fetch changesets for request: {}", request); } @@ -176,19 +201,13 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand GitChangesetConverter converter = null; RevWalk revWalk = null; - try - { - org.eclipse.jgit.lib.Repository gr = open(); - - if (!gr.getAllRefs().isEmpty()) - { + try (org.eclipse.jgit.lib.Repository repository = open()) { + if (!repository.getAllRefs().isEmpty()) { int counter = 0; int start = request.getPagingStart(); - if (start < 0) - { - if (logger.isErrorEnabled()) - { + if (start < 0) { + if (logger.isErrorEnabled()) { logger.error("start parameter is negative, reset to 0"); } @@ -199,43 +218,53 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand int limit = request.getPagingLimit(); ObjectId startId = null; - if (!Strings.isNullOrEmpty(request.getStartChangeset())) - { - startId = gr.resolve(request.getStartChangeset()); + if (!Strings.isNullOrEmpty(request.getStartChangeset())) { + startId = repository.resolve(request.getStartChangeset()); } ObjectId endId = null; - if (!Strings.isNullOrEmpty(request.getEndChangeset())) - { - endId = gr.resolve(request.getEndChangeset()); + if (!Strings.isNullOrEmpty(request.getEndChangeset())) { + endId = repository.resolve(request.getEndChangeset()); } - revWalk = new RevWalk(gr); + Ref branch = getBranchOrDefault(repository,request.getBranch()); - converter = new GitChangesetConverter(gr, revWalk); + ObjectId ancestorId = null; - if (!Strings.isNullOrEmpty(request.getPath())) - { + if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) { + ancestorId = repository.resolve(request.getAncestorChangeset()); + if (ancestorId == null) { + throw notFound(entity(REVISION, request.getAncestorChangeset()).in(this.repository)); + } + } + + revWalk = new RevWalk(repository); + + converter = new GitChangesetConverter(repository, revWalk); + + if (!Strings.isNullOrEmpty(request.getPath())) { revWalk.setTreeFilter( AndTreeFilter.create( PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF)); } - - ObjectId head = getBranchOrDefault(gr, request.getBranch()); - if (head != null) - { - if (startId != null) - { + if (branch != null) { + if (startId != null) { revWalk.markStart(revWalk.lookupCommit(startId)); - } - else - { - revWalk.markStart(revWalk.lookupCommit(head)); + } else { + revWalk.markStart(revWalk.lookupCommit(branch.getObjectId())); } - for (final RevCommit commit : revWalk) { + if (ancestorId != null) { + revWalk.markUninteresting(revWalk.lookupCommit(ancestorId)); + } + + Iterator iterator = revWalk.iterator(); + + while (iterator.hasNext()) { + RevCommit commit = iterator.next(); + if ((counter >= start) && ((limit < 0) || (counter < start + limit))) { changesetList.add(converter.createChangeset(commit)); @@ -243,25 +272,37 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand counter++; - if ((endId != null) && commit.getId().equals(endId)) { + if (commit.getId().equals(endId)) { break; } } + } else if (ancestorId != null) { + throw notFound(entity(REVISION, request.getBranch()).in(this.repository)); } - changesets = new ChangesetPagingResult(counter, changesetList); - } - else if (logger.isWarnEnabled()) - { + if (branch != null) { + changesets = new ChangesetPagingResult(counter, changesetList, GitUtil.getBranch(branch.getName())); + } else { + changesets = new ChangesetPagingResult(counter, changesetList); + } + } else if (logger.isWarnEnabled()) { logger.warn("the repository {} seems to be empty", - repository.getName()); + this.repository.getName()); changesets = new ChangesetPagingResult(0, Collections.EMPTY_LIST); } } + catch (MissingObjectException e) + { + throw notFound(entity(REVISION, e.getObjectId().getName()).in(repository)); + } + catch (NotFoundException e) + { + throw e; + } catch (Exception ex) { - throw new RepositoryException("could not create change log", ex); + throw new InternalRepositoryException(repository, "could not create change log", ex); } finally { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java new file mode 100644 index 0000000000..226dd0f285 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -0,0 +1,78 @@ +package sonia.scm.repository.spi; + +import com.google.common.collect.ImmutableSet; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.merge.ResolveMerger; +import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.api.MergeCommandResult; +import sonia.scm.repository.api.MergeDryRunCommandResult; +import sonia.scm.repository.api.MergeStrategy; +import sonia.scm.repository.api.MergeStrategyNotSupportedException; + +import java.io.IOException; +import java.util.Set; + +import static org.eclipse.jgit.merge.MergeStrategy.RECURSIVE; + +public class GitMergeCommand extends AbstractGitCommand implements MergeCommand { + + private final GitWorkdirFactory workdirFactory; + + private static final Set STRATEGIES = ImmutableSet.of( + MergeStrategy.MERGE_COMMIT, + MergeStrategy.FAST_FORWARD_IF_POSSIBLE, + MergeStrategy.SQUASH + ); + + GitMergeCommand(GitContext context, sonia.scm.repository.Repository repository, GitWorkdirFactory workdirFactory) { + super(context, repository); + this.workdirFactory = workdirFactory; + } + + @Override + public MergeCommandResult merge(MergeCommandRequest request) { + return mergeWithStrategy(request); + } + + private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) { + switch(request.getMergeStrategy()) { + case SQUASH: + return inClone(clone -> new GitMergeWithSquash(clone, request, context, repository), workdirFactory, request.getTargetBranch()); + + case FAST_FORWARD_IF_POSSIBLE: + return inClone(clone -> new GitFastForwardIfPossible(clone, request, context, repository), workdirFactory, request.getTargetBranch()); + + case MERGE_COMMIT: + return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workdirFactory, request.getTargetBranch()); + + default: + throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy()); + } + } + + @Override + public MergeDryRunCommandResult dryRun(MergeCommandRequest request) { + try { + Repository repository = context.open(); + ResolveMerger merger = (ResolveMerger) RECURSIVE.newMerger(repository, true); + return new MergeDryRunCommandResult( + merger.merge( + resolveRevisionOrThrowNotFound(repository, request.getBranchToMerge()), + resolveRevisionOrThrowNotFound(repository, request.getTargetBranch()))); + } catch (IOException e) { + throw new InternalRepositoryException(context.getRepository(), "could not clone repository for merge", e); + } + } + + @Override + public boolean isSupported(MergeStrategy strategy) { + return STRATEGIES.contains(strategy); + } + + @Override + public Set getSupportedMergeStrategies() { + return STRATEGIES; + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java new file mode 100644 index 0000000000..6aa68a0ea8 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java @@ -0,0 +1,31 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeCommand; +import org.eclipse.jgit.api.MergeResult; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.MergeCommandResult; + +import java.io.IOException; + +class GitMergeCommit extends GitMergeStrategy { + + GitMergeCommit(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { + super(clone, request, context, repository); + } + + @Override + MergeCommandResult run() throws IOException { + MergeCommand mergeCommand = getClone().merge(); + mergeCommand.setFastForward(MergeCommand.FastForwardMode.NO_FF); + MergeResult result = doMergeInClone(mergeCommand); + + if (result.getMergeStatus().isSuccessful()) { + doCommit(); + push(); + return MergeCommandResult.success(); + } else { + return analyseFailure(result); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java new file mode 100644 index 0000000000..1d53b99c99 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -0,0 +1,72 @@ +package sonia.scm.repository.spi; + +import com.google.common.base.Strings; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeCommand; +import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Person; +import sonia.scm.repository.api.MergeCommandResult; + +import java.io.IOException; +import java.text.MessageFormat; + +abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker { + + private static final Logger logger = LoggerFactory.getLogger(GitMergeStrategy.class); + + private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n", + "Merge of branch {0} into {1}", + "", + "Automatic merge by SCM-Manager."); + + private final String target; + private final String toMerge; + private final Person author; + private final String messageTemplate; + + GitMergeStrategy(Git clone, MergeCommandRequest request, GitContext context, sonia.scm.repository.Repository repository) { + super(clone, context, repository); + this.target = request.getTargetBranch(); + this.toMerge = request.getBranchToMerge(); + this.author = request.getAuthor(); + this.messageTemplate = request.getMessageTemplate(); + } + + MergeResult doMergeInClone(MergeCommand mergeCommand) throws IOException { + MergeResult result; + try { + ObjectId sourceRevision = resolveRevision(toMerge); + mergeCommand + .setCommit(false) // we want to set the author manually + .include(toMerge, sourceRevision); + + result = mergeCommand.call(); + } catch (GitAPIException e) { + throw new InternalRepositoryException(getContext().getRepository(), "could not merge branch " + toMerge + " into " + target, e); + } + return result; + } + + void doCommit() { + logger.debug("merged branch {} into {}", toMerge, target); + doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author); + } + + private String determineMessageTemplate() { + if (Strings.isNullOrEmpty(messageTemplate)) { + return MERGE_COMMIT_MESSAGE_TEMPLATE; + } else { + return messageTemplate; + } + } + + MergeCommandResult analyseFailure(MergeResult result) { + logger.info("could not merge branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet()); + return MergeCommandResult.failure(result.getConflicts().keySet()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java new file mode 100644 index 0000000000..b688956404 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java @@ -0,0 +1,31 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeResult; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.MergeCommandResult; +import org.eclipse.jgit.api.MergeCommand; + +import java.io.IOException; + +class GitMergeWithSquash extends GitMergeStrategy { + + GitMergeWithSquash(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { + super(clone, request, context, repository); + } + + @Override + MergeCommandResult run() throws IOException { + MergeCommand mergeCommand = getClone().merge(); + mergeCommand.setSquash(true); + MergeResult result = doMergeInClone(mergeCommand); + + if (result.getMergeStatus().isSuccessful()) { + doCommit(); + push(); + return MergeCommandResult.success(); + } else { + return analyseFailure(result); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java new file mode 100644 index 0000000000..5040069c12 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java @@ -0,0 +1,103 @@ +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; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + + +@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 { + 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 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(entity(repository), "could not open repository", 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(entity(repository), MessageFormat.format("The modification type: {0} is not supported.", type)); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java new file mode 100644 index 0000000000..19234c32e3 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -0,0 +1,135 @@ +package sonia.scm.repository.spi; + +import com.google.common.util.concurrent.Striped; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.attributes.FilterCommandRegistry; +import org.eclipse.jgit.revwalk.RevCommit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ConcurrentModificationException; +import sonia.scm.NoChangesMadeException; +import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.web.lfs.LfsBlobStoreFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.locks.Lock; + +public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand { + + private static final Logger LOG = LoggerFactory.getLogger(GitModifyCommand.class); + private static final Striped REGISTER_LOCKS = Striped.lock(5); + + private final GitWorkdirFactory workdirFactory; + private final LfsBlobStoreFactory lfsBlobStoreFactory; + + GitModifyCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory, LfsBlobStoreFactory lfsBlobStoreFactory) { + super(context, repository); + this.workdirFactory = workdirFactory; + this.lfsBlobStoreFactory = lfsBlobStoreFactory; + } + + @Override + public String execute(ModifyCommandRequest request) { + return inClone(clone -> new ModifyWorker(clone, request), workdirFactory, request.getBranch()); + } + + private class ModifyWorker extends GitCloneWorker implements ModifyWorkerHelper { + + private final File workDir; + private final ModifyCommandRequest request; + + ModifyWorker(Git clone, ModifyCommandRequest request) { + super(clone, context, repository); + this.workDir = clone.getRepository().getWorkTree(); + this.request = request; + } + + @Override + String run() throws IOException { + getClone().getRepository().getFullBranch(); + if (!StringUtils.isEmpty(request.getExpectedRevision()) + && !request.getExpectedRevision().equals(getCurrentRevision().getName())) { + throw new ConcurrentModificationException("branch", request.getBranch() == null ? "default" : request.getBranch()); + } + for (ModifyCommandRequest.PartialRequest r : request.getRequests()) { + r.execute(this); + } + failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())); + Optional revCommit = doCommit(request.getCommitMessage(), request.getAuthor()); + push(); + return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name(); + } + + @Override + public void addFileToScm(String name, Path file) { + addToGitWithLfsSupport(name, file); + } + + private void addToGitWithLfsSupport(String path, Path targetFile) { + REGISTER_LOCKS.get(targetFile).lock(); + try { + LfsBlobStoreCleanFilterFactory cleanFilterFactory = new LfsBlobStoreCleanFilterFactory(lfsBlobStoreFactory, repository, targetFile); + + String registerKey = "git-lfs clean -- '" + path + "'"; + LOG.debug("register lfs filter command factory for command '{}'", registerKey); + FilterCommandRegistry.register(registerKey, cleanFilterFactory::createFilter); + try { + addFileToGit(path); + } catch (GitAPIException e) { + throwInternalRepositoryException("could not add file to index", e); + } finally { + LOG.debug("unregister lfs filter command factory for command \"{}\"", registerKey); + FilterCommandRegistry.unregister(registerKey); + } + } finally { + REGISTER_LOCKS.get(targetFile).unlock(); + } + } + + private void addFileToGit(String toBeCreated) throws GitAPIException { + getClone().add().addFilepattern(removeStartingPathSeparators(toBeCreated)).call(); + } + + @Override + public void doScmDelete(String toBeDeleted) { + try { + getClone().rm().addFilepattern(removeStartingPathSeparators(toBeDeleted)).call(); + } catch (GitAPIException e) { + throwInternalRepositoryException("could not remove file from index", e); + } + } + + @Override + public File getWorkDir() { + return workDir; + } + + @Override + public Repository getRepository() { + return repository; + } + + @Override + public String getBranch() { + return request.getBranch(); + } + + private String removeStartingPathSeparators(String path) { + while (path.startsWith(File.separator)) { + path = path.substring(1); + } + return path; + } + + private String throwInternalRepositoryException(String message, Exception e) { + throw new InternalRepositoryException(context.getRepository(), message, e); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java index 7b40a60a08..6bdd8b793a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java @@ -36,16 +36,14 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.LogCommand; import org.eclipse.jgit.lib.ObjectId; - import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -79,12 +77,11 @@ public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand * @return * * @throws IOException - * @throws RepositoryException */ @Override public ChangesetPagingResult getOutgoingChangesets( OutgoingCommandRequest request) - throws IOException, RepositoryException + throws IOException { return getIncomingOrOutgoingChangesets(request); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java index 8ed1c4b3d3..8810c15c58 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java @@ -37,7 +37,6 @@ package sonia.scm.repository.spi; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; - import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; @@ -46,23 +45,20 @@ import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.TagOpt; import org.eclipse.jgit.transport.TrackingRefUpdate; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.PullResponse; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; - import java.net.URL; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -105,11 +101,10 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand * @return * * @throws IOException - * @throws RepositoryException */ @Override public PullResponse pull(PullCommandRequest request) - throws IOException, RepositoryException + throws IOException { PullResponse response; Repository sourceRepository = request.getRemoteRepository(); @@ -130,22 +125,8 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand return response; } - /** - * Method description - * - * - * @param git - * @param result - * @param fetch - * - * @return - * - * @throws RepositoryException - */ - private PullResponse convert(Git git, FetchResult fetch) - throws RepositoryException - { - long counter = 0L; + private PullResponse convert(Git git, FetchResult fetch) { + long counter = 0l; for (TrackingRefUpdate tru : fetch.getTrackingRefUpdates()) { @@ -212,26 +193,15 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand return counter; } - /** - * Method description - * - * - * @param sourceRepository - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ private PullResponse pullFromScmRepository(Repository sourceRepository) - throws IOException, RepositoryException + throws IOException { - File sourceDirectory = handler.getDirectory(sourceRepository); + File sourceDirectory = handler.getDirectory(sourceRepository.getId()); Preconditions.checkArgument(sourceDirectory.exists(), "source repository directory does not exists"); - File targetDirectory = handler.getDirectory(repository); + File targetDirectory = handler.getDirectory(repository.getId()); Preconditions.checkArgument(sourceDirectory.exists(), "target repository directory does not exists"); @@ -256,19 +226,8 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand return response; } - /** - * Method description - * - * - * @param url - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ private PullResponse pullFromUrl(URL url) - throws IOException, RepositoryException + throws IOException { logger.debug("pull changes from {} to {}", url, repository.getId()); @@ -289,7 +248,7 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand } catch (GitAPIException ex) { - throw new RepositoryException("error durring pull", ex); + throw new InternalRepositoryException(repository, "error during pull", ex); } return response; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java index c64465194a..3963366aa7 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java @@ -37,16 +37,14 @@ package sonia.scm.repository.spi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.PushResponse; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -87,11 +85,10 @@ public class GitPushCommand extends AbstractGitPushOrPullCommand * @return * * @throws IOException - * @throws RepositoryException */ @Override public PushResponse push(PushCommandRequest request) - throws IOException, RepositoryException + throws IOException { String remoteUrl = getRemoteUrl(request); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index 7ea26de83f..e1ae58ada5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -33,20 +33,22 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.collect.ImmutableSet; - +import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.Feature; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.IOException; - +import java.util.EnumSet; import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -60,32 +62,31 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider Command.BLAME, Command.BROWSE, Command.CAT, - Command.DIFF, + Command.DIFF, + Command.DIFF_RESULT, Command.LOG, Command.TAGS, + Command.BRANCH, Command.BRANCHES, Command.INCOMING, Command.OUTGOING, Command.PUSH, - Command.PULL + Command.PULL, + Command.MERGE, + Command.MODIFY ); + protected static final Set FEATURES = EnumSet.of(Feature.INCOMING_REVISION); //J+ //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param handler - * @param repository - */ - public GitRepositoryServiceProvider(GitRepositoryHandler handler, - Repository repository) - { + public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { this.handler = handler; this.repository = repository; - context = new GitContext(handler.getDirectory(repository)); + this.lfsBlobStoreFactory = lfsBlobStoreFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; + this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); } //~--- methods -------------------------------------------------------------- @@ -128,6 +129,18 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitBranchesCommand(context, repository); } + /** + * Method description + * + * + * @return + */ + @Override + public BranchCommand getBranchCommand() + { + return new GitBranchCommand(context, repository, hookContextFactory, eventBus); + } + /** * Method description * @@ -137,7 +150,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public BrowseCommand getBrowseCommand() { - return new GitBrowseCommand(context, repository); + return new GitBrowseCommand(context, repository, lfsBlobStoreFactory); } /** @@ -149,7 +162,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public CatCommand getCatCommand() { - return new GitCatCommand(context, repository); + return new GitCatCommand(context, repository, lfsBlobStoreFactory); } /** @@ -164,6 +177,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitDiffCommand(context, repository); } + @Override + public DiffResultCommand getDiffResultCommand() { + return new GitDiffResultCommand(context, repository); + } + /** * Method description * @@ -188,6 +206,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitLogCommand(context, repository); } + @Override + public ModificationsCommand getModificationsCommand() { + return new GitModificationsCommand(context,repository); + } + /** * Method description * @@ -248,14 +271,34 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitTagsCommand(context, repository); } - //~--- fields --------------------------------------------------------------- + @Override + public MergeCommand getMergeCommand() { + return new GitMergeCommand(context, repository, handler.getWorkdirFactory()); + } + + @Override + public ModifyCommand getModifyCommand() { + return new GitModifyCommand(context, repository, handler.getWorkdirFactory(), lfsBlobStoreFactory); + } + + @Override + public Set getSupportedFeatures() { + return FEATURES; + } +//~--- fields --------------------------------------------------------------- /** Field description */ - private GitContext context; + private final GitContext context; /** Field description */ - private GitRepositoryHandler handler; + private final GitRepositoryHandler handler; /** Field description */ - private Repository repository; + private final Repository repository; + + private final LfsBlobStoreFactory lfsBlobStoreFactory; + + private final HookContextFactory hookContextFactory; + + private final ScmEventBus eventBus; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java index 70609eee87..7fc5fb27c4 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java @@ -35,61 +35,44 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.Inject; - +import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.event.ScmEventBus; import sonia.scm.plugin.Extension; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.web.lfs.LfsBlobStoreFactory; /** * * @author Sebastian Sdorra */ @Extension -public class GitRepositoryServiceResolver implements RepositoryServiceResolver -{ +public class GitRepositoryServiceResolver implements RepositoryServiceResolver { - /** Field description */ - public static final String TYPE = "git"; + private final GitRepositoryHandler handler; + private final GitRepositoryConfigStoreProvider storeProvider; + private final LfsBlobStoreFactory lfsBlobStoreFactory; + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param handler - */ @Inject - public GitRepositoryServiceResolver(GitRepositoryHandler handler) - { + public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { this.handler = handler; + this.storeProvider = storeProvider; + this.lfsBlobStoreFactory = lfsBlobStoreFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * - * @return - */ @Override - public GitRepositoryServiceProvider reslove(Repository repository) - { + public GitRepositoryServiceProvider resolve(Repository repository) { GitRepositoryServiceProvider provider = null; - if (TYPE.equalsIgnoreCase(repository.getType())) - { - provider = new GitRepositoryServiceProvider(handler, repository); + if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { + provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus); } return provider; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private GitRepositoryHandler handler; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java index 14583ac7dc..807ec807e6 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java @@ -37,27 +37,23 @@ package sonia.scm.repository.spi; import com.google.common.base.Function; import com.google.common.collect.Lists; - import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.Tag; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -79,17 +75,8 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override - public List getTags() throws IOException, RepositoryException + public List getTags() throws IOException { List tags = null; @@ -108,7 +95,7 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand } catch (GitAPIException ex) { - throw new RepositoryException("could not read tags from repository", ex); + throw new InternalRepositoryException(repository, "could not read tags from repository", ex); } finally { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilter.java new file mode 100644 index 0000000000..bb54ea162a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilter.java @@ -0,0 +1,83 @@ +package sonia.scm.repository.spi; + +import com.google.common.io.ByteStreams; +import org.eclipse.jgit.attributes.FilterCommand; +import org.eclipse.jgit.lfs.LfsPointer; +import org.eclipse.jgit.lfs.lib.AnyLongObjectId; +import org.eclipse.jgit.lfs.lib.Constants; +import org.eclipse.jgit.lfs.lib.LongObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.store.Blob; +import sonia.scm.store.BlobStore; +import sonia.scm.util.IOUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestOutputStream; + +/** + * Adapted version of JGit's {@link org.eclipse.jgit.lfs.CleanFilter} to write the + * lfs file directly to the lfs blob store. + */ +class LfsBlobStoreCleanFilter extends FilterCommand { + + private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreCleanFilter.class); + + private final BlobStore lfsBlobStore; + private final Path targetFile; + + LfsBlobStoreCleanFilter(InputStream in, OutputStream out, BlobStore lfsBlobStore, Path targetFile) { + super(in, out); + this.lfsBlobStore = lfsBlobStore; + this.targetFile = targetFile; + } + + @Override + // Suppress warning for RuntimeException after check for wrong size, because mathematicians say this will never happen + @SuppressWarnings("squid:S00112") + public int run() throws IOException { + LOG.debug("running scm lfs filter for file {}", targetFile); + DigestOutputStream digestOutputStream = createDigestStream(); + try { + long size = ByteStreams.copy(in, digestOutputStream); + AnyLongObjectId loid = LongObjectId.fromRaw(digestOutputStream.getMessageDigest().digest()); + String hash = loid.getName(); + + Blob existingBlob = lfsBlobStore.get(hash); + if (existingBlob != null) { + LOG.debug("found existing lfs blob for oid {}", hash); + long blobSize = existingBlob.getSize(); + if (blobSize != size) { + throw new RuntimeException("lfs entry already exists for loid " + hash + " but has wrong size"); + } + } else { + LOG.debug("uploading new lfs blob for oid {}", hash); + Blob newBlob = lfsBlobStore.create(hash); + OutputStream outputStream = newBlob.getOutputStream(); + Files.copy(targetFile, outputStream); + newBlob.commit(); + } + + LfsPointer lfsPointer = new LfsPointer(loid, size); + lfsPointer.encode(out); + return -1; + } finally { + IOUtil.close(digestOutputStream); + IOUtil.close(in); + IOUtil.close(out); + } + } + + private DigestOutputStream createDigestStream() { + return new DigestOutputStream(new OutputStream() { + @Override + public void write(int b) { + // no further target here, we are just interested in the digest + } + }, Constants.newMessageDigest()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilterFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilterFactory.java new file mode 100644 index 0000000000..817d763d10 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilterFactory.java @@ -0,0 +1,27 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.lib.Repository; +import sonia.scm.web.lfs.LfsBlobStoreFactory; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Path; + +class LfsBlobStoreCleanFilterFactory { + + private final LfsBlobStoreFactory blobStoreFactory; + private final sonia.scm.repository.Repository repository; + private final Path targetFile; + + LfsBlobStoreCleanFilterFactory(LfsBlobStoreFactory blobStoreFactory, sonia.scm.repository.Repository repository, Path targetFile) { + this.blobStoreFactory = blobStoreFactory; + this.repository = repository; + this.targetFile = targetFile; + } + + @SuppressWarnings("squid:S1172") + // suppress unused parameter to keep the api compatible to jgit's FilterCommandFactory + LfsBlobStoreCleanFilter createFilter(Repository db, InputStream in, OutputStream out) { + return new LfsBlobStoreCleanFilter(in, out, blobStoreFactory.getLfsBlobStore(repository), targetFile); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java new file mode 100644 index 0000000000..f7b0c5567c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java @@ -0,0 +1,74 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ScmTransportProtocol; +import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.util.SimpleWorkdirFactory; +import sonia.scm.repository.util.WorkdirProvider; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory implements GitWorkdirFactory { + + @Inject + public SimpleGitWorkdirFactory(WorkdirProvider workdirProvider) { + super(workdirProvider); + } + + @Override + public ParentAndClone cloneRepository(GitContext context, File target, String initialBranch) { + try { + Repository clone = Git.cloneRepository() + .setURI(createScmTransportProtocolUri(context.getDirectory())) + .setDirectory(target) + .setBranch(initialBranch) + .call() + .getRepository(); + + Ref head = clone.exactRef(Constants.HEAD); + + if (head == null || !head.isSymbolic() || (initialBranch != null && !head.getTarget().getName().endsWith(initialBranch))) { + throw notFound(entity("Branch", initialBranch).in(context.getRepository())); + } + + return new ParentAndClone<>(null, clone); + } catch (GitAPIException | IOException e) { + throw new InternalRepositoryException(context.getRepository(), "could not clone working copy of repository", e); + } + } + + private String createScmTransportProtocolUri(File bareRepository) { + return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath(); + } + + @Override + protected void closeRepository(Repository repository) { + // we have to check for null here, because we do not create a repository for + // the parent in cloneRepository + if (repository != null) { + repository.close(); + } + } + + @Override + protected void closeWorkdirInternal(Repository workdir) throws Exception { + if (workdir != null) { + workdir.close(); + } + } + + @Override + protected sonia.scm.repository.Repository getScmRepository(GitContext context) { + return context.getRepository(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java new file mode 100644 index 0000000000..85119a3e9f --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java @@ -0,0 +1,10 @@ +package sonia.scm.repository.spi; + +import sonia.scm.ContextEntry; +import sonia.scm.repository.InternalRepositoryException; + +public class UnsupportedModificationTypeException extends InternalRepositoryException { + public UnsupportedModificationTypeException(ContextEntry.ContextBuilder entity, String message) { + super(entity, message); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java new file mode 100644 index 0000000000..afcde567e3 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java @@ -0,0 +1,70 @@ +package sonia.scm.repository.update; + +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import sonia.scm.migration.UpdateException; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.GitConfigHelper; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static sonia.scm.version.Version.parse; + +@Extension +public class GitV2UpdateStep implements UpdateStep { + + private final RepositoryLocationResolver locationResolver; + private final UpdateStepRepositoryMetadataAccess repositoryMetadataAccess; + + @Inject + public GitV2UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess repositoryMetadataAccess) { + this.locationResolver = locationResolver; + this.repositoryMetadataAccess = repositoryMetadataAccess; + } + + @Override + public void doUpdate() { + locationResolver.forClass(Path.class).forAllLocations( + (repositoryId, path) -> { + Repository repository = repositoryMetadataAccess.read(path); + if (isGitDirectory(repository)) { + try (org.eclipse.jgit.lib.Repository gitRepository = build(path.resolve("data").toFile())) { + new GitConfigHelper().createScmmConfig(repository, gitRepository); + } catch (IOException e) { + throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e); + } + } + } + ); + } + + private org.eclipse.jgit.lib.Repository build(File directory) throws IOException { + return new FileRepositoryBuilder() + .setGitDir(directory) + .readEnvironment() + .findGitDir() + .build(); + } + + private boolean isGitDirectory(Repository repository) { + return GitRepositoryHandler.TYPE_NAME.equals(repository.getType()); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.plugin.git"; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java deleted file mode 100644 index f06fcfcd39..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java +++ /dev/null @@ -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 webTokenGenerators) - { - super(configuration, webTokenGenerators); - } -} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java index 25d79422c4..e38f26a309 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java @@ -33,34 +33,24 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.annotations.VisibleForTesting; -import com.google.inject.Inject; import org.eclipse.jgit.http.server.GitSmartHttpTools; import sonia.scm.ClientMessages; -import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; import sonia.scm.repository.GitUtil; -import sonia.scm.repository.RepositoryProvider; -import sonia.scm.web.filter.ProviderPermissionFilter; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.web.filter.PermissionFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -//~--- JDK imports ------------------------------------------------------------ - /** * GitPermissionFilter decides if a git request requires write or read privileges. * * @author Sebastian Sdorra */ -@Priority(Filters.PRIORITY_AUTHORIZATION) -@WebElement(value = GitServletModule.PATTERN_GIT) -public class GitPermissionFilter extends ProviderPermissionFilter +public class GitPermissionFilter extends PermissionFilter { private static final String PARAMETER_SERVICE = "service"; @@ -79,11 +69,9 @@ public class GitPermissionFilter extends ProviderPermissionFilter * Constructs a new instance of the GitPermissionFilter. * * @param configuration scm main configuration - * @param repositoryProvider repository provider */ - @Inject - public GitPermissionFilter(ScmConfiguration configuration, RepositoryProvider repositoryProvider) { - super(configuration, repositoryProvider); + public GitPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) { + super(configuration, delegate); } @Override @@ -99,7 +87,7 @@ public class GitPermissionFilter extends ProviderPermissionFilter } @Override - protected boolean isWriteRequest(HttpServletRequest request) { + public boolean isWriteRequest(HttpServletRequest request) { return isReceivePackRequest(request) || isReceiveServiceRequest(request) || isLfsFileUpload(request); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilterFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilterFactory.java new file mode 100644 index 0000000000..c358da5fb1 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilterFactory.java @@ -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); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java index 3ecd6047e9..74a5039516 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java @@ -36,28 +36,24 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.transport.PostReceiveHook; import org.eclipse.jgit.transport.PreReceiveHook; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceivePack; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.RepositoryHookType; -import sonia.scm.repository.RepositoryUtil; import sonia.scm.repository.spi.GitHookContextProvider; import sonia.scm.repository.spi.HookEventFacade; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; import java.io.IOException; - import java.util.Collection; import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -131,15 +127,14 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook try { Repository repository = rpack.getRepository(); - String repositoryName = resolveRepositoryName(repository); + String repositoryId = resolveRepositoryId(repository); - logger.trace("resolved repository name to {}", repositoryName); + logger.trace("resolved repository to {}", repositoryId); GitHookContextProvider context = new GitHookContextProvider(rpack, receiveCommands); - hookEventFacade.handle(GitRepositoryHandler.TYPE_NAME, - repositoryName).fireHookEvent(type, context); + hookEventFacade.handle(repositoryId).fireHookEvent(type, context); } catch (Exception ex) @@ -191,20 +186,10 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook * * @throws IOException */ - private String resolveRepositoryName(Repository repository) throws IOException + private String resolveRepositoryId(Repository repository) { - File directory; - - if (repository.isBare()) - { - directory = repository.getDirectory(); - } - else - { - directory = repository.getWorkTree(); - } - - return RepositoryUtil.getRepositoryName(handler, directory); + StoredConfig gitConfig = repository.getConfig(); + return handler.getRepositoryId(gitConfig); } //~--- fields --------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java index 25bbe04cfc..b59fa7526b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java @@ -36,78 +36,40 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.Inject; - import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; - +import sonia.scm.protocolcommand.git.BaseReceivePackFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.spi.HookEventFacade; -//~--- JDK imports ------------------------------------------------------------ - import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ + /** + * GitReceivePackFactory creates {@link ReceivePack} objects and assigns the required + * Hook components. * * @author Sebastian Sdorra */ -public class GitReceivePackFactory - implements ReceivePackFactory +public class GitReceivePackFactory extends BaseReceivePackFactory { - /** - * Constructs ... - * - * - * - * @param hookEventFacade - * @param handler - */ + private ReceivePackFactory wrapped; + @Inject - public GitReceivePackFactory(HookEventFacade hookEventFacade, - GitRepositoryHandler handler) - { - hook = new GitReceiveHook(hookEventFacade, handler); + public GitReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { + super(handler, hookEventFacade); + this.wrapped = new DefaultReceivePackFactory(); } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param repository - * - * @return - * - * @throws ServiceNotAuthorizedException - * @throws ServiceNotEnabledException - */ @Override - public ReceivePack create(HttpServletRequest request, Repository repository) - throws ServiceNotEnabledException, ServiceNotAuthorizedException - { - ReceivePack rpack = defaultFactory.create(request, repository); - - rpack.setPreReceiveHook(hook); - rpack.setPostReceiveHook(hook); - // apply collecting listener, to be able to check which commits are new - CollectingPackParserListener.set(rpack); - - return rpack; + protected ReceivePack createBasicReceivePack(HttpServletRequest request, Repository repository) + throws ServiceNotEnabledException, ServiceNotAuthorizedException { + return wrapped.create(request, repository); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private DefaultReceivePackFactory defaultFactory = - new DefaultReceivePackFactory(); - - /** Field description */ - private GitReceiveHook hook; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java index 2ddf4b3de9..6db7d694d5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java @@ -35,9 +35,8 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.inject.Inject; - import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; @@ -46,19 +45,17 @@ import org.eclipse.jgit.transport.resolver.RepositoryResolver; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.eclipse.jgit.util.FS; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.RepositoryProvider; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -72,17 +69,11 @@ public class GitRepositoryResolver implements RepositoryResolver env = ImmutableMap.of( "repository", repository, - "branches", createBranchesModel(repository), - "commitViewLink", rup.getChangesetUrl(repository.getId(), 0, 20), - "sourceViewLink", rup.getBrowseUrl(repository.getId(), null, null) + "branches", createBranchesModel(repository) ); //J+ @@ -170,10 +151,9 @@ public class GitRepositoryViewer * @return * * @throws IOException - * @throws RepositoryException */ private BranchesModel createBranchesModel(Repository repository) - throws RepositoryException, IOException + throws IOException { BranchesModel model = null; RepositoryService service = null; @@ -297,14 +277,20 @@ public class GitRepositoryViewer { //J- ChangesetPagingResult cpr = service.getLogCommand() - .setDisableEscaping(true) .setBranch(name) .setPagingLimit(CHANGESET_PER_BRANCH) .getChangesets(); + + Iterable changesets = + Iterables.transform(cpr, new Function() + { - Iterable changesets = Streams.stream(cpr) - .map(ChangesetModel::new) - .collect(Collectors.toList()); + @Override + public ChangesetModel apply(Changeset changeset) + { + return new ChangesetModel(changeset); + } + }); //J+ model = new BranchModel(name, changesets); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitScmProtocolProviderWrapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitScmProtocolProviderWrapper.java new file mode 100644 index 0000000000..be97e7e539 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitScmProtocolProviderWrapper.java @@ -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 uriInfoStore, ScmConfiguration scmConfiguration) { + super(servletProvider, uriInfoStore, scmConfiguration); + } + + @Override + public String getType() { + return GitRepositoryHandler.TYPE_NAME; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java index 3d3442ce2a..0bbb993cc6 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java @@ -36,11 +36,14 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.servlet.ServletModule; - import org.eclipse.jgit.transport.ScmTransportProtocol; - +import org.mapstruct.factory.Mappers; +import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper; +import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper; +import sonia.scm.api.v2.resources.GitRepositoryConfigMapper; import sonia.scm.plugin.Extension; - +import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.spi.SimpleGitWorkdirFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory; /** @@ -51,18 +54,6 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory; public class GitServletModule extends ServletModule { - public static final String GIT_PATH = "/git"; - - /** Field description */ - public static final String PATTERN_GIT = GIT_PATH + "/*"; - - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ @Override protected void configureServlets() { @@ -73,7 +64,10 @@ public class GitServletModule extends ServletModule bind(LfsBlobStoreFactory.class); - // serlvelts and filters - serve(PATTERN_GIT).with(ScmGitServlet.class); + bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass()); + bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass()); + bind(GitRepositoryConfigMapper.class).to(Mappers.getMapper(GitRepositoryConfigMapper.class).getClass()); + + bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitVndMediaType.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitVndMediaType.java new file mode 100644 index 0000000000..9bfa9fe63e --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitVndMediaType.java @@ -0,0 +1,9 @@ +package sonia.scm.web; + +public class GitVndMediaType { + public static final String GIT_CONFIG = VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX; + public static final String GIT_REPOSITORY_CONFIG = VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX; + + private GitVndMediaType() { + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java index e8e62b52cc..2701764607 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java @@ -33,49 +33,40 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; - import org.eclipse.jgit.http.server.GitServlet; - -import org.slf4j.Logger; -import static org.slf4j.LoggerFactory.getLogger; - - import org.eclipse.jgit.lfs.lib.Constants; -import static org.eclipse.jgit.lfs.lib.Constants.CONTENT_TYPE_GIT_LFS_JSON; - +import org.slf4j.Logger; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryRequestListenerUtil; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.util.HttpUtil; import sonia.scm.web.lfs.servlet.LfsServletFactory; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; -import java.util.regex.Pattern; - import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import sonia.scm.repository.RepositoryException; +import java.io.IOException; +import java.util.regex.Pattern; + +import static org.eclipse.jgit.lfs.lib.Constants.CONTENT_TYPE_GIT_LFS_JSON; +import static org.slf4j.LoggerFactory.getLogger; /** * * @author Sebastian Sdorra */ @Singleton -public class ScmGitServlet extends GitServlet +public class ScmGitServlet extends GitServlet implements ScmProviderHttpServlet { - /** Field description */ + public static final String REPO_PATH = "/repo"; + public static final Pattern REGEX_GITHTTPBACKEND = Pattern.compile( - "(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$" + "(?x)^/repo/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$" ); /** Field description */ @@ -94,7 +85,6 @@ public class ScmGitServlet extends GitServlet * @param repositoryResolver * @param receivePackFactory * @param repositoryViewer - * @param repositoryProvider * @param repositoryRequestListenerUtil * @param lfsServletFactory */ @@ -102,11 +92,9 @@ public class ScmGitServlet extends GitServlet public ScmGitServlet(GitRepositoryResolver repositoryResolver, GitReceivePackFactory receivePackFactory, GitRepositoryViewer repositoryViewer, - RepositoryProvider repositoryProvider, RepositoryRequestListenerUtil repositoryRequestListenerUtil, LfsServletFactory lfsServletFactory) { - this.repositoryProvider = repositoryProvider; this.repositoryViewer = repositoryViewer; this.repositoryRequestListenerUtil = repositoryRequestListenerUtil; this.lfsServletFactory = lfsServletFactory; @@ -128,50 +116,16 @@ public class ScmGitServlet extends GitServlet * @throws ServletException */ @Override - protected void service(HttpServletRequest request, - HttpServletResponse response) + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { - Repository repository = repositoryProvider.get(); - if (repository != null) { - handleRequest(request, response, repository); - } else { - // logger - response.sendError(HttpServletResponse.SC_NOT_FOUND); - } - } - - /** - * Decides the type request being currently made and delegates it accordingly. - *

    - *
  • Batch API:
  • - *
      - *
    • used to provide the client with information on how handle the large files of a repository.
    • - *
    • response contains the information where to perform the actual upload and download of the large objects.
    • - *
    - *
  • Transfer API:
  • - *
      - *
    • receives and provides the actual large objects (resolves the pointer placed in the file of the working copy).
    • - *
    • invoked only after the Batch API has been questioned about what to do with the large files
    • - *
    - *
  • Regular Git Http API:
  • - *
      - *
    • regular git http wire protocol, use by normal git clients.
    • - *
    - *
  • Browser Overview:
  • - *
      - *
    • short repository overview for browser clients.
    • - *
    - *
  • - *
- */ - private void handleRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { - logger.trace("handle git repository at {}", repository.getName()); - if (isLfsBatchApiRequest(request, repository.getName())) { + String repoPath = repository.getNamespace() + "/" + repository.getName(); + logger.trace("handle git repository at {}", repoPath); + if (isLfsBatchApiRequest(request, repoPath)) { HttpServlet servlet = lfsServletFactory.createProtocolServletFor(repository, request); logger.trace("handle lfs batch api request"); handleGitLfsRequest(servlet, request, response, repository); - } else if (isLfsFileTransferRequest(request, repository.getName())) { + } else if (isLfsFileTransferRequest(request, repoPath)) { HttpServlet servlet = lfsServletFactory.createFileLfsServletFor(repository, request); logger.trace("handle lfs file transfer request"); handleGitLfsRequest(servlet, request, response, repository); @@ -215,10 +169,10 @@ public class ScmGitServlet extends GitServlet * @throws IOException * @throws ServletException */ - private void handleBrowserRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { + private void handleBrowserRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException { try { repositoryViewer.handleRequest(request, response, repository); - } catch (RepositoryException | IOException ex) { + } catch (IOException ex) { throw new ServletException("could not create repository view", ex); } } @@ -234,7 +188,7 @@ public class ScmGitServlet extends GitServlet */ private static boolean isLfsFileTransferRequest(HttpServletRequest request, String repository) { - String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/[a-z0-9]{64}$", request.getContextPath(), GitServletModule.GIT_PATH, repository); + String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/[a-z0-9]{64}$", request.getContextPath(), REPO_PATH, repository); boolean pathMatches = request.getRequestURI().matches(regex); boolean methodMatches = request.getMethod().equals("PUT") || request.getMethod().equals("GET"); @@ -253,7 +207,7 @@ public class ScmGitServlet extends GitServlet */ private static boolean isLfsBatchApiRequest(HttpServletRequest request, String repository) { - String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/batch$", request.getContextPath(), GitServletModule.GIT_PATH, repository); + String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/batch$", request.getContextPath(), REPO_PATH, repository); boolean pathMatches = request.getRequestURI().matches(regex); boolean methodMatches = "POST".equals(request.getMethod()); @@ -289,12 +243,8 @@ public class ScmGitServlet extends GitServlet return false; } - //~--- fields --------------------------------------------------------------- - /** Field description */ - private final RepositoryProvider repositoryProvider; - /** Field description */ private final RepositoryRequestListenerUtil repositoryRequestListenerUtil; @@ -304,5 +254,4 @@ public class ScmGitServlet extends GitServlet private final GitRepositoryViewer repositoryViewer; private final LfsServletFactory lfsServletFactory; - } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServletProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServletProvider.java new file mode 100644 index 0000000000..56a9e358be --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServletProvider.java @@ -0,0 +1,23 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletProvider; + +import javax.inject.Provider; + +public class ScmGitServletProvider extends ScmProviderHttpServletProvider { + + @Inject + private Provider servletProvider; + + public ScmGitServletProvider() { + super(GitRepositoryHandler.TYPE_NAME); + } + + @Override + protected ScmProviderHttpServlet getRootServlet() { + return servletProvider.get(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ExpiringAction.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ExpiringAction.java new file mode 100644 index 0000000000..223bb3151e --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ExpiringAction.java @@ -0,0 +1,28 @@ +package sonia.scm.web.lfs; + +import org.eclipse.jgit.lfs.server.Response; +import sonia.scm.security.AccessToken; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.TimeZone; + +class ExpiringAction extends Response.Action { + + @SuppressWarnings({"squid:S00116"}) + // This class is used for json serialization, only + public final String expires_at; + + ExpiringAction(String href, AccessToken accessToken) { + this.expires_at = createDateFormat().format(accessToken.getExpiration()); + this.href = href; + this.header = Collections.singletonMap("Authorization", "Bearer " + accessToken.compact()); + } + + private DateFormat createDateFormat() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + return dateFormat; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LFSAuthCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LFSAuthCommand.java new file mode 100644 index 0000000000..02f461bc5c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LFSAuthCommand.java @@ -0,0 +1,98 @@ +package sonia.scm.web.lfs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Charsets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.protocolcommand.CommandInterpreter; +import sonia.scm.protocolcommand.CommandInterpreterFactory; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.protocolcommand.RepositoryContextResolver; +import sonia.scm.protocolcommand.ScmCommandProtocol; +import sonia.scm.protocolcommand.git.GitRepositoryContextResolver; +import sonia.scm.repository.Repository; +import sonia.scm.security.AccessToken; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.Optional; + +import static java.lang.String.format; + +@Extension +public class LFSAuthCommand implements CommandInterpreterFactory { + + private static final Logger LOG = LoggerFactory.getLogger(LFSAuthCommand.class); + + private static final String LFS_INFO_URL_PATTERN = "%s/repo/%s/%s.git/info/lfs/"; + + private final LfsAccessTokenFactory tokenFactory; + private final GitRepositoryContextResolver gitRepositoryContextResolver; + private final ObjectMapper objectMapper; + private final ScmConfiguration configuration; + + @Inject + public LFSAuthCommand(LfsAccessTokenFactory tokenFactory, GitRepositoryContextResolver gitRepositoryContextResolver, ScmConfiguration configuration) { + this.tokenFactory = tokenFactory; + this.gitRepositoryContextResolver = gitRepositoryContextResolver; + + objectMapper = new ObjectMapper(); + this.configuration = configuration; + } + + @Override + public Optional canHandle(String command) { + if (command.startsWith("git-lfs-authenticate")) { + LOG.trace("create command for input: {}", command); + return Optional.of(new LfsAuthCommandInterpreter(command)); + } else { + return Optional.empty(); + } + } + + private class LfsAuthCommandInterpreter implements CommandInterpreter { + + private final String command; + + LfsAuthCommandInterpreter(String command) { + this.command = command; + } + + @Override + public String[] getParsedArgs() { + // we are interested only in the 'repo' argument, so we discard the rest + return new String[]{command.split("\\s+")[1]}; + } + + @Override + public ScmCommandProtocol getProtocolHandler() { + return (context, repositoryContext) -> { + ExpiringAction response = createResponseObject(repositoryContext); + // we buffer the response and write it with a single write, + // because otherwise the ssh connection is not closed + String buffer = serializeResponse(response); + context.getOutputStream().write(buffer.getBytes(Charsets.UTF_8)); + }; + } + + @Override + public RepositoryContextResolver getRepositoryContextResolver() { + return gitRepositoryContextResolver; + } + + private ExpiringAction createResponseObject(RepositoryContext repositoryContext) { + Repository repository = repositoryContext.getRepository(); + + String url = format(LFS_INFO_URL_PATTERN, configuration.getBaseUrl(), repository.getNamespace(), repository.getName()); + AccessToken accessToken = tokenFactory.createReadAccessToken(repository); + + return new ExpiringAction(url, accessToken); + } + + private String serializeResponse(ExpiringAction response) throws IOException { + return objectMapper.writeValueAsString(response); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsAccessTokenFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsAccessTokenFactory.java new file mode 100644 index 0000000000..c290b513e1 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsAccessTokenFactory.java @@ -0,0 +1,70 @@ +package sonia.scm.web.lfs; + +import com.github.sdorra.ssp.PermissionCheck; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.security.AccessToken; +import sonia.scm.security.AccessTokenBuilderFactory; +import sonia.scm.security.Scope; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class LfsAccessTokenFactory { + + private static final Logger LOG = LoggerFactory.getLogger(LfsAccessTokenFactory.class); + + private final AccessTokenBuilderFactory tokenBuilderFactory; + + @Inject + LfsAccessTokenFactory(AccessTokenBuilderFactory tokenBuilderFactory) { + this.tokenBuilderFactory = tokenBuilderFactory; + } + + AccessToken createReadAccessToken(Repository repository) { + PermissionCheck read = RepositoryPermissions.read(repository); + read.check(); + + PermissionCheck pull = RepositoryPermissions.pull(repository); + pull.check(); + + List permissions = new ArrayList<>(); + permissions.add(read.asShiroString()); + permissions.add(pull.asShiroString()); + + PermissionCheck push = RepositoryPermissions.push(repository); + if (push.isPermitted()) { + // we have to add push permissions, + // because this token is also used to obtain the write access token + permissions.add(push.asShiroString()); + } + + return createToken(Scope.valueOf(permissions)); + } + + AccessToken createWriteAccessToken(Repository repository) { + PermissionCheck read = RepositoryPermissions.read(repository); + read.check(); + + PermissionCheck pull = RepositoryPermissions.pull(repository); + pull.check(); + + PermissionCheck push = RepositoryPermissions.push(repository); + push.check(); + + return createToken(Scope.valueOf(read.asShiroString(), pull.asShiroString(), push.asShiroString())); + } + + private AccessToken createToken(Scope scope) { + LOG.trace("create access token with scope: {}", scope); + return tokenBuilderFactory + .create() + .expiresIn(5, TimeUnit.MINUTES) + .scope(scope) + .build(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java index eebd6b8f2b..84733b9ea3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java @@ -74,7 +74,11 @@ public class LfsBlobStoreFactory { * * @return blob store for the corresponding scm repository */ + @SuppressWarnings("unchecked") public BlobStore getLfsBlobStore(Repository repository) { - return blobStoreFactory.getBlobStore(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX); + return blobStoreFactory + .withName(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX) + .forRepository(repository) + .build(); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java deleted file mode 100644 index 3d69dcabe6..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java +++ /dev/null @@ -1,97 +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.lfs; - -import com.google.common.eventbus.Subscribe; -import com.google.inject.Inject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.EagerSingleton; -import sonia.scm.HandlerEventType; -import sonia.scm.plugin.Extension; -import sonia.scm.repository.GitRepositoryHandler; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryEvent; -import sonia.scm.store.Blob; -import sonia.scm.store.BlobStore; - -/** - * Listener which removes all lfs objects from a blob store, whenever its corresponding git repository gets deleted. - * - * @author Sebastian Sdorra - * @since 1.54 - */ -@Extension -@EagerSingleton -public class LfsStoreRemoveListener { - - private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreFactory.class); - - private final LfsBlobStoreFactory lfsBlobStoreFactory; - - @Inject - public LfsStoreRemoveListener(LfsBlobStoreFactory lfsBlobStoreFactory) { - this.lfsBlobStoreFactory = lfsBlobStoreFactory; - } - - /** - * Remove all object from the blob store, if the event is an delete event and the repository is a git repository. - * - * @param event repository event - */ - @Subscribe - public void handleRepositoryEvent(RepositoryEvent event) { - if ( isDeleteEvent(event) && isGitRepositoryEvent(event) ) { - removeLfsStore(event.getItem()); - } - } - - private boolean isDeleteEvent(RepositoryEvent event) { - return HandlerEventType.DELETE == event.getEventType(); - } - - private boolean isGitRepositoryEvent(RepositoryEvent event) { - return event.getItem() != null - && event.getItem().getType().equals(GitRepositoryHandler.TYPE_NAME); - } - - private void removeLfsStore(Repository repository) { - LOG.debug("remove all blobs from store, because corresponding git repository {} was removed", repository.getName()); - BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); - for ( Blob blob : blobStore.getAll() ) { - LOG.trace("remove blob {}, because repository {} was removed", blob.getId(), repository.getName()); - blobStore.remove(blob); - } - } - -} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java new file mode 100644 index 0000000000..2684222c32 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java @@ -0,0 +1,43 @@ +package sonia.scm.web.lfs; + +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.update.BlobDirectoryAccess; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.nio.file.Path; + +@Extension +public class LfsV1UpdateStep implements UpdateStep { + + private final BlobDirectoryAccess blobDirectoryAccess; + + @Inject + public LfsV1UpdateStep(BlobDirectoryAccess blobDirectoryAccess) { + this.blobDirectoryAccess = blobDirectoryAccess; + } + + @Override + public void doUpdate() throws Exception { + blobDirectoryAccess.forBlobDirectories( + f -> { + Path v1Directory = f.getFileName(); + String v1DirectoryName = v1Directory.toString(); + if (v1DirectoryName.endsWith("-git-lfs")) { + blobDirectoryAccess.moveToRepositoryBlobStore(f, v1DirectoryName, v1DirectoryName.substring(0, v1DirectoryName.length() - "-git-lfs".length())); + } + } + ); + } + + @Override + public Version getTargetVersion() { + return Version.parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.git.lfs"; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ScmBlobLfsRepository.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ScmBlobLfsRepository.java index 46a58f6f07..b70b948c9b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ScmBlobLfsRepository.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ScmBlobLfsRepository.java @@ -2,12 +2,13 @@ package sonia.scm.web.lfs; import org.eclipse.jgit.lfs.lib.AnyLongObjectId; import org.eclipse.jgit.lfs.server.LargeFileRepository; -import org.eclipse.jgit.lfs.server.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Repository; +import sonia.scm.security.AccessToken; import sonia.scm.store.Blob; import sonia.scm.store.BlobStore; -import java.io.IOException; - /** * This LargeFileRepository is used for jGit-Servlet implementation. Under the jgit LFS Servlet hood, the * SCM-Repository API is used to implement the Repository. @@ -17,49 +18,67 @@ import java.io.IOException; */ public class ScmBlobLfsRepository implements LargeFileRepository { + private static final Logger LOG = LoggerFactory.getLogger(ScmBlobLfsRepository.class); + private final BlobStore blobStore; + private final LfsAccessTokenFactory tokenFactory; /** * This URI is used to determine the actual URI for Upload / Download. Must be full URI (or rewritable by reverse * proxy). */ private final String baseUri; + private final Repository repository; + + /** + * A {@link ScmBlobLfsRepository} is created for either download or upload, not both. Therefore we can cache the + * access token and do not have to create them anew for each action. + */ + private AccessToken accessToken; /** * Creates a {@link ScmBlobLfsRepository} for the provided repository. * - * @param blobStore The SCM Blobstore used for this @{@link LargeFileRepository}. - * @param baseUri This URI is used to determine the actual URI for Upload / Download. Must be full URI (or - * rewritable by reverse proxy). + * @param repository The current scm repository this LFS repository is used for. + * @param blobStore The SCM Blobstore used for this @{@link LargeFileRepository}. + * @param tokenFactory The token builder for subsequent LFS requests. + * @param baseUri This URI is used to determine the actual URI for Upload / Download. Must be full URI (or */ - public ScmBlobLfsRepository(BlobStore blobStore, String baseUri) { - + public ScmBlobLfsRepository(Repository repository, BlobStore blobStore, LfsAccessTokenFactory tokenFactory, String baseUri) { + this.repository = repository; this.blobStore = blobStore; + this.tokenFactory = tokenFactory; this.baseUri = baseUri; } @Override - public Response.Action getDownloadAction(AnyLongObjectId id) { - - return getAction(id); + public ExpiringAction getDownloadAction(AnyLongObjectId id) { + if (accessToken == null) { + LOG.trace("create access token to download lfs object {} from repository {}", id, repository.getNamespaceAndName()); + accessToken = tokenFactory.createReadAccessToken(repository); + } + return getAction(id, accessToken); } @Override - public Response.Action getUploadAction(AnyLongObjectId id, long size) { - - return getAction(id); + public ExpiringAction getUploadAction(AnyLongObjectId id, long size) { + if (accessToken == null) { + LOG.trace("create access token to upload lfs object {} to repository {}", id, repository.getNamespaceAndName()); + accessToken = tokenFactory.createWriteAccessToken(repository); + } + return getAction(id, accessToken); } @Override - public Response.Action getVerifyAction(AnyLongObjectId id) { + public ExpiringAction getVerifyAction(AnyLongObjectId id) { //validation is optional. We do not support it. return null; } @Override - public long getSize(AnyLongObjectId id) throws IOException { + public long getSize(AnyLongObjectId id) { //this needs to be size of what is will be written into the response of the download. Clients are likely to // verify it. @@ -77,14 +96,11 @@ public class ScmBlobLfsRepository implements LargeFileRepository { /** * Constructs the Download / Upload actions to be supplied to the client. */ - private Response.Action getAction(AnyLongObjectId id) { + private ExpiringAction getAction(AnyLongObjectId id, AccessToken token) { //LFS protocol has to provide the information on where to put or get the actual content, i. e. //the actual URI for up- and download. - Response.Action a = new Response.Action(); - a.href = baseUri + id.getName(); - - return a; + return new ExpiringAction(baseUri + id.getName(), token); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java index 2ca10559a2..3b200ade36 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java @@ -9,13 +9,14 @@ import org.slf4j.LoggerFactory; import sonia.scm.repository.Repository; import sonia.scm.store.BlobStore; import sonia.scm.util.HttpUtil; +import sonia.scm.web.lfs.LfsAccessTokenFactory; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import sonia.scm.web.lfs.ScmBlobLfsRepository; import javax.inject.Inject; import javax.inject.Singleton; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; -import sonia.scm.web.lfs.LfsBlobStoreFactory; /** * This factory class is a helper class to provide the {@link LfsProtocolServlet} and the {@link FileLfsServlet} @@ -27,13 +28,15 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory; @Singleton public class LfsServletFactory { - private static final Logger logger = LoggerFactory.getLogger(LfsServletFactory.class); + private static final Logger LOG = LoggerFactory.getLogger(LfsServletFactory.class); private final LfsBlobStoreFactory lfsBlobStoreFactory; + private final LfsAccessTokenFactory tokenFactory; @Inject - public LfsServletFactory(LfsBlobStoreFactory lfsBlobStoreFactory) { + public LfsServletFactory(LfsBlobStoreFactory lfsBlobStoreFactory, LfsAccessTokenFactory tokenFactory) { this.lfsBlobStoreFactory = lfsBlobStoreFactory; + this.tokenFactory = tokenFactory; } /** @@ -44,10 +47,11 @@ public class LfsServletFactory { * @return The {@link LfsProtocolServlet} to provide the LFS Batch API for a SCM Repository. */ public LfsProtocolServlet createProtocolServletFor(Repository repository, HttpServletRequest request) { + LOG.trace("create lfs protocol servlet for repository {}", repository.getNamespaceAndName()); BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); String baseUri = buildBaseUri(repository, request); - LargeFileRepository largeFileRepository = new ScmBlobLfsRepository(blobStore, baseUri); + LargeFileRepository largeFileRepository = new ScmBlobLfsRepository(repository, blobStore, tokenFactory, baseUri); return new ScmLfsProtocolServlet(largeFileRepository); } @@ -59,6 +63,7 @@ public class LfsServletFactory { * @return The {@link FileLfsServlet} to provide the LFS Upload / Download API for a SCM Repository. */ public HttpServlet createFileLfsServletFor(Repository repository, HttpServletRequest request) { + LOG.trace("create lfs file servlet for repository {}", repository.getNamespaceAndName()); return new ScmFileTransferServlet(lfsBlobStoreFactory.getLfsBlobStore(repository)); } @@ -70,7 +75,7 @@ public class LfsServletFactory { */ @VisibleForTesting static String buildBaseUri(Repository repository, HttpServletRequest request) { - return String.format("%s/git/%s.git/info/lfs/objects/", HttpUtil.getCompleteUrl(request), repository.getName()); + return String.format("%s/repo/%s/%s.git/info/lfs/objects/", HttpUtil.getCompleteUrl(request), repository.getNamespace(), repository.getName()); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmLfsProtocolServlet.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmLfsProtocolServlet.java index 332cf12e09..5c63cad571 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmLfsProtocolServlet.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmLfsProtocolServlet.java @@ -20,7 +20,7 @@ public class ScmLfsProtocolServlet extends LfsProtocolServlet { @Override - protected LargeFileRepository getLargeFileRepository(LfsRequest request, String path) throws LfsException { + protected LargeFileRepository getLargeFileRepository(LfsRequest request, String path, String auth) throws LfsException { return repository; } } diff --git a/scm-plugins/scm-git-plugin/src/main/js/CloneInformation.tsx b/scm-plugins/scm-git-plugin/src/main/js/CloneInformation.tsx new file mode 100644 index 0000000000..ac365004e5 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/CloneInformation.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Repository } from "@scm-manager/ui-types"; + +type Props = WithTranslation & { + url: string; + repository: Repository; +}; + +class CloneInformation extends React.Component { + render() { + const { url, repository, t } = this.props; + + return ( +
+

{t("scm-git-plugin.information.clone")}

+
+          git clone {url}
+        
+

{t("scm-git-plugin.information.create")}

+
+          
+            git init {repository.name}
+            
+ cd {repository.name} +
+ echo "# {repository.name} + " > README.md +
+ git add README.md +
+ git commit -m "added readme" +
+ git remote add origin {url} +
+ git push -u origin master +
+
+
+

{t("scm-git-plugin.information.replace")}

+
+          
+            git remote add origin {url}
+            
+ git push -u origin master +
+
+
+
+ ); + } +} + +export default withTranslation("plugins")(CloneInformation); diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitAvatar.tsx b/scm-plugins/scm-git-plugin/src/main/js/GitAvatar.tsx new file mode 100644 index 0000000000..372f270a5c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/GitAvatar.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Image } from "@scm-manager/ui-components"; + +type Props = {}; + +class GitAvatar extends React.Component { + render() { + return Git Logo; + } +} + +export default GitAvatar; diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitBranchInformation.tsx b/scm-plugins/scm-git-plugin/src/main/js/GitBranchInformation.tsx new file mode 100644 index 0000000000..de26ebb390 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/GitBranchInformation.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Branch } from "@scm-manager/ui-types"; + +type Props = WithTranslation & { + branch: Branch; +}; + +class GitBranchInformation extends React.Component { + render() { + const { branch, t } = this.props; + + return ( +
+

{t("scm-git-plugin.information.fetch")}

+
+          git fetch
+        
+

{t("scm-git-plugin.information.checkout")}

+
+          git checkout {branch.name}
+        
+
+ ); + } +} + +export default withTranslation("plugins")(GitBranchInformation); diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.tsx b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.tsx new file mode 100644 index 0000000000..e23ac8ef77 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Links } from "@scm-manager/ui-types"; +import { InputField, Checkbox } from "@scm-manager/ui-components"; + +type Configuration = { + repositoryDirectory?: string; + gcExpression?: string; + nonFastForwardDisallowed: boolean; + _links: Links; +}; + +type Props = WithTranslation & { + initialConfiguration: Configuration; + readOnly: boolean; + + onConfigurationChange: (p1: Configuration, p2: boolean) => void; +}; + +type State = Configuration & {}; + +class GitConfigurationForm extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + ...props.initialConfiguration + }; + } + + onGcExpressionChange = (value: string) => { + this.setState( + { + gcExpression: value + }, + () => this.props.onConfigurationChange(this.state, true) + ); + }; + + onNonFastForwardDisallowed = (value: boolean) => { + this.setState( + { + nonFastForwardDisallowed: value + }, + () => this.props.onConfigurationChange(this.state, true) + ); + }; + + render() { + const { gcExpression, nonFastForwardDisallowed } = this.state; + const { readOnly, t } = this.props; + + return ( + <> + + + + ); + } +} + +export default withTranslation("plugins")(GitConfigurationForm); diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.tsx b/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.tsx new file mode 100644 index 0000000000..e17d3adc8a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Title, Configuration } from "@scm-manager/ui-components"; +import GitConfigurationForm from "./GitConfigurationForm"; + +type Props = WithTranslation & { + link: string; +}; + +class GitGlobalConfiguration extends React.Component { + render() { + const { link, t } = this.props; + + return ( +
+ + <Configuration link={link} render={(props: any) => <GitConfigurationForm {...props} />} /> + </div> + ); + } +} + +export default withTranslation("plugins")(GitGlobalConfiguration); diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitMergeInformation.tsx b/scm-plugins/scm-git-plugin/src/main/js/GitMergeInformation.tsx new file mode 100644 index 0000000000..be83596c13 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/GitMergeInformation.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Repository } from "@scm-manager/ui-types"; + +type Props = WithTranslation & { + repository: Repository; + target: string; + source: string; +}; + +class GitMergeInformation extends React.Component<Props> { + render() { + const { source, target, t } = this.props; + + return ( + <div> + <h4>{t("scm-git-plugin.information.merge.heading")}</h4> + {t("scm-git-plugin.information.merge.checkout")} + <pre> + <code>git checkout {target}</code> + </pre> + {t("scm-git-plugin.information.merge.update")} + <pre> + <code>git pull</code> + </pre> + {t("scm-git-plugin.information.merge.merge")} + <pre> + <code>git merge {source}</code> + </pre> + {t("scm-git-plugin.information.merge.resolve")} + <pre> + <code>git add <conflict file></code> + </pre> + {t("scm-git-plugin.information.merge.commit")} + <pre> + <code> + git commit -m "Merge {source} into {target}" + </code> + </pre> + {t("scm-git-plugin.information.merge.push")} + <pre> + <code>git push</code> + </pre> + </div> + ); + } +} + +export default withTranslation("plugins")(GitMergeInformation); diff --git a/scm-plugins/scm-git-plugin/src/main/js/ProtocolInformation.tsx b/scm-plugins/scm-git-plugin/src/main/js/ProtocolInformation.tsx new file mode 100644 index 0000000000..f871ab8f2f --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/ProtocolInformation.tsx @@ -0,0 +1,96 @@ +import React from "react"; +import styled from "styled-components"; +import { Repository, Link } from "@scm-manager/ui-types"; +import { ButtonAddons, Button } from "@scm-manager/ui-components"; +import CloneInformation from "./CloneInformation"; + +const Wrapper = styled.div` + position: relative; +`; + +const Switcher = styled(ButtonAddons)` + position: absolute; + top: 0; + right: 0; +`; + +type Props = { + repository: Repository; +}; + +type State = { + selected?: Link; +}; + +function selectHttpOrFirst(repository: Repository) { + const protocols = (repository._links["protocol"] as Link[]) || []; + + for (const protocol of protocols) { + if (protocol.name === "http") { + return protocol; + } + } + + if (protocols.length > 0) { + return protocols[0]; + } + return undefined; +} + +export default class ProtocolInformation extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + selected: selectHttpOrFirst(props.repository) + }; + } + + selectProtocol = (protocol: Link) => { + this.setState({ + selected: protocol + }); + }; + + renderProtocolButton = (protocol: Link) => { + const name = protocol.name || "unknown"; + + let color; + + const { selected } = this.state; + if (selected && protocol.name === selected.name) { + color = "link is-selected"; + } + + return ( + <Button color={color} action={() => this.selectProtocol(protocol)}> + {name.toUpperCase()} + </Button> + ); + }; + + render() { + const { repository } = this.props; + + const protocols = repository._links["protocol"] as Link[]; + if (!protocols || protocols.length === 0) { + return null; + } + + if (protocols.length === 1) { + return <CloneInformation url={protocols[0].href} repository={repository} />; + } + + const { selected } = this.state; + let cloneInformation = null; + if (selected) { + cloneInformation = <CloneInformation repository={repository} url={selected.href} />; + } + + return ( + <Wrapper> + <Switcher>{protocols.map(this.renderProtocolButton)}</Switcher> + {cloneInformation} + </Wrapper> + ); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.tsx b/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.tsx new file mode 100644 index 0000000000..3ef02306fe --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.tsx @@ -0,0 +1,197 @@ +import React, { FormEvent } from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Branch, Repository, Link } from "@scm-manager/ui-types"; +import { apiClient, BranchSelector, ErrorPage, Loading, Subtitle, SubmitButton } from "@scm-manager/ui-components"; + +type Props = WithTranslation & { + repository: Repository; +}; + +type State = { + loadingBranches: boolean; + loadingDefaultBranch: boolean; + submitPending: boolean; + error?: Error; + branches: Branch[]; + selectedBranchName?: string; + defaultBranchChanged: boolean; + disabled: boolean; +}; + +const GIT_CONFIG_CONTENT_TYPE = "application/vnd.scmm-gitConfig+json"; + +class RepositoryConfig extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + loadingBranches: true, + loadingDefaultBranch: true, + submitPending: false, + branches: [], + defaultBranchChanged: false, + disabled: true + }; + } + + componentDidMount() { + const { repository } = this.props; + this.setState({ + ...this.state, + loadingBranches: true + }); + const branchesLink = repository._links.branches as Link; + apiClient + .get(branchesLink.href) + .then(response => response.json()) + .then(payload => payload._embedded.branches) + .then(branches => + this.setState({ + ...this.state, + branches, + loadingBranches: false + }) + ) + .catch(error => + this.setState({ + ...this.state, + error + }) + ); + + const configurationLink = repository._links.configuration as Link; + this.setState({ + ...this.state, + loadingDefaultBranch: true + }); + apiClient + .get(configurationLink.href) + .then(response => response.json()) + .then(payload => + this.setState({ + ...this.state, + selectedBranchName: payload.defaultBranch, + disabled: !payload._links.update, + loadingDefaultBranch: false + }) + ) + .catch(error => + this.setState({ + ...this.state, + error + }) + ); + } + + branchSelected = (branch?: Branch) => { + if (!branch) { + this.setState({ + ...this.state, + selectedBranchName: undefined, + defaultBranchChanged: false + }); + return; + } + this.setState({ + ...this.state, + selectedBranchName: branch.name, + defaultBranchChanged: false + }); + }; + + submit = (event: FormEvent) => { + event.preventDefault(); + + const { repository } = this.props; + const newConfig = { + defaultBranch: this.state.selectedBranchName + }; + this.setState({ + ...this.state, + submitPending: true + }); + const configurationLink = repository._links.configuration as Link; + apiClient + .put(configurationLink.href, newConfig, GIT_CONFIG_CONTENT_TYPE) + .then(() => + this.setState({ + ...this.state, + submitPending: false, + defaultBranchChanged: true + }) + ) + .catch(error => + this.setState({ + ...this.state, + error + }) + ); + }; + + render() { + const { t } = this.props; + const { loadingBranches, loadingDefaultBranch, submitPending, error, disabled } = this.state; + + if (error) { + return ( + <ErrorPage + title={t("scm-git-plugin.repo-config.error.title")} + subtitle={t("scm-git-plugin.repo-config.error.subtitle")} + error={error} + /> + ); + } + + const submitButton = disabled ? null : ( + <SubmitButton + label={t("scm-git-plugin.repo-config.submit")} + loading={submitPending} + disabled={!this.state.selectedBranchName} + /> + ); + + if (!(loadingBranches || loadingDefaultBranch)) { + return ( + <> + <hr /> + <Subtitle subtitle={t("scm-git-plugin.repo-config.title")} /> + {this.renderBranchChangedNotification()} + <form onSubmit={this.submit}> + <BranchSelector + label={t("scm-git-plugin.repo-config.default-branch")} + branches={this.state.branches} + selected={this.branchSelected} + selectedBranch={this.state.selectedBranchName} + disabled={disabled} + /> + {submitButton} + </form> + </> + ); + } else { + return <Loading />; + } + } + + renderBranchChangedNotification = () => { + if (this.state.defaultBranchChanged) { + return ( + <div className="notification is-primary"> + <button + className="delete" + onClick={() => + this.setState({ + ...this.state, + defaultBranchChanged: false + }) + } + /> + {this.props.t("scm-git-plugin.repo-config.success")} + </div> + ); + } + return null; + }; +} + +export default withTranslation("plugins")(RepositoryConfig); diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.test.ts b/scm-plugins/scm-git-plugin/src/main/js/index.test.ts new file mode 100644 index 0000000000..b5c9820f99 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/index.test.ts @@ -0,0 +1,31 @@ +import "@scm-manager/ui-tests/i18n"; +import { gitPredicate } from "./index"; + +describe("test git predicate", () => { + it("should return false", () => { + expect(gitPredicate(undefined)).toBe(false); + expect(gitPredicate({})).toBe(false); + expect( + gitPredicate({ + repository: {} + }) + ).toBe(false); + expect( + gitPredicate({ + repository: { + type: "hg" + } + }) + ).toBe(false); + }); + + it("should return true", () => { + expect( + gitPredicate({ + repository: { + type: "git" + } + }) + ).toBe(true); + }); +}); diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.ts b/scm-plugins/scm-git-plugin/src/main/js/index.ts new file mode 100644 index 0000000000..0c68eda573 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/index.ts @@ -0,0 +1,27 @@ +import React from "react"; +import { binder } from "@scm-manager/ui-extensions"; +import ProtocolInformation from "./ProtocolInformation"; +import GitAvatar from "./GitAvatar"; + +import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components"; +import GitGlobalConfiguration from "./GitGlobalConfiguration"; +import GitBranchInformation from "./GitBranchInformation"; +import GitMergeInformation from "./GitMergeInformation"; +import RepositoryConfig from "./RepositoryConfig"; + +// repository + +// @visibleForTesting +export const gitPredicate = (props: any) => { + return !!(props && props.repository && props.repository.type === "git"); +}; + +binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate); +binder.bind("repos.branch-details.information", GitBranchInformation, gitPredicate); +binder.bind("repos.repository-merge.information", GitMergeInformation, gitPredicate); +binder.bind("repos.repository-avatar", GitAvatar, gitPredicate); + +binder.bind("repo-config.route", RepositoryConfig, gitPredicate); + +// global config +cfgBinder.bindGlobal("/git", "scm-git-plugin.config.link", "gitConfig", GitGlobalConfiguration); diff --git a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml new file mode 100644 index 0000000000..4823cb5f4f --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, Sebastian Sdorra + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of SCM-Manager; nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.dd7s + + http://bitbucket.org/sdorra/scm-manager + + +--> +<permissions> + + <permission> + <value>configuration:read,write:git</value> + </permission> + <permission> + <value>repository:git:*</value> + </permission> + +</permissions> diff --git a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/plugin.xml b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/plugin.xml index bc3689618d..0c4a7619c5 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/plugin.xml +++ b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/plugin.xml @@ -46,14 +46,10 @@ <scm-version>2</scm-version> <information> - <author>Sebastian Sdorra</author> - <category>Git</category> - <tags> - <tag>git</tag> - <tag>scm</tag> - <tag>vcs</tag> - <tag>dvcs</tag> - </tags> + <displayName>Git</displayName> + <author>Cloudogu GmbH</author> + <category>Source Code Management</category> + <avatarUrl>/images/git-logo.png</avatarUrl> </information> <conditions> @@ -61,7 +57,7 @@ </conditions> <resources> - <script>/sonia/scm/git.config.js</script> + <script>assets/scm-git-plugin.bundle.js</script> </resources> </plugin> diff --git a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/repository-permissions.xml b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/repository-permissions.xml new file mode 100644 index 0000000000..6c93929625 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/repository-permissions.xml @@ -0,0 +1,7 @@ +<repository-permissions> + <verbs> + <verb>git</verb> + </verbs> + <roles> + </roles> +</repository-permissions> diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json new file mode 100644 index 0000000000..e150ceb42b --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json @@ -0,0 +1,68 @@ +{ + "scm-git-plugin": { + "information": { + "clone": "Repository klonen", + "create": "Neues Repository erstellen", + "replace": "Ein bestehendes Repository aktualisieren", + "fetch": "Remote-Änderungen herunterladen", + "checkout": "Branch wechseln", + "merge": { + "heading": "Merge des Source Branch in den Target Branch", + "checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.", + "update": "2. Update Workspace", + "merge": "3. Merge Source Branch", + "resolve": "4. Merge Konflikte auflösen und korrigierte Dateien dem Index hinzufügen.", + "commit": "5. Commit", + "push": "6. Push des Merge" + } + }, + "config": { + "link": "Git", + "title": "Git Konfiguration", + "gcExpression": "GC Cron Ausdruck", + "gcExpressionHelpText": "Benutze Quartz Cron Ausdrücke (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK), um git GC regelmäßig auszuführen.", + "nonFastForwardDisallowed": "Deaktiviere \"Non Fast-Forward\"", + "nonFastForwardDisallowedHelpText": "Git Pushes ablehnen, die nicht \"fast-forward\" sind, wie \"--force\".", + "disabled": "Deaktiviert", + "disabledHelpText": "Aktiviere oder deaktiviere das Git Plugin", + "submit": "Speichern" + }, + "repo-config": { + "link": "Konfiguration", + "title": "Git Einstellungen", + "default-branch": "Standard Branch", + "submit": "Speichern", + "error": { + "title": "Fehler", + "subtitle": "Ein Fehler ist aufgetreten." + }, + "success": "Der standard Branch wurde geändert!" + } + }, + "permissions": { + "configuration": { + "read,write": { + "git": { + "displayName": "Git Konfiguration ändern", + "description": "Darf die git Konfiguration verändern." + } + } + }, + "repository": { + "git": { + "*": { + "displayName": "Repository-spezifische Git Konfiguration ändern", + "description": "Darf die git Konfiguration für alle Repositories verändern." + } + } + } + }, + "verbs": { + "repository": { + "git": { + "displayName": "Git konfigurieren", + "description": "Darf die git Konfiguration für dieses Repository verändern." + } + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json new file mode 100644 index 0000000000..22eb46b9f8 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json @@ -0,0 +1,68 @@ +{ + "scm-git-plugin": { + "information": { + "clone": "Clone the repository", + "create": "Create a new repository", + "replace": "Push an existing repository", + "fetch": "Get remote changes", + "checkout": "Switch branch", + "merge": { + "heading": "How to merge source branch into target branch", + "checkout": "1. Make sure your workspace is clean and checkout target branch", + "update": "2. Update workspace", + "merge": "3. Merge source branch", + "resolve": "4. Resolve merge conflicts and add corrected files to index", + "commit": "5. Commit", + "push": "6. Push your merge" + } + }, + "config": { + "link": "Git", + "title": "Git Configuration", + "gcExpression": "GC Cron Expression", + "gcExpressionHelpText": "Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.", + "nonFastForwardDisallowed": "Disallow Non Fast-Forward", + "nonFastForwardDisallowedHelpText": "Reject git pushes which are non fast-forward such as --force.", + "disabled": "Disabled", + "disabledHelpText": "Enable or disable the Git plugin", + "submit": "Submit" + }, + "repo-config": { + "link": "Configuration", + "title": "Git Settings", + "default-branch": "Default branch", + "submit": "Submit", + "error": { + "title": "Error", + "subtitle": "Something went wrong" + }, + "success": "Default branch changed!" + } + }, + "permissions" : { + "configuration": { + "read,write": { + "git": { + "displayName": "Modify git configuration", + "description": "May change the git configuration" + } + } + }, + "repository": { + "git": { + "*": { + "displayName": "Modify repository specific git configuration", + "description": "May change the git configuration for repositories" + } + } + } + }, + "verbs": { + "repository": { + "git": { + "displayName": "configure Git", + "description": "May change the git configuration for this repository" + } + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js b/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js deleted file mode 100644 index 9e038e4dee..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js +++ /dev/null @@ -1,232 +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 - * - */ - -Ext.ns("Sonia.git"); - -Sonia.git.ConfigPanel = Ext.extend(Sonia.config.SimpleConfigForm, { - - // labels - titleText: 'Git Settings', - repositoryDirectoryText: 'Repository directory', - gcExpressionText: 'Git GC Cron Expression', - disabledText: 'Disabled', - - // helpTexts - repositoryDirectoryHelpText: 'Location of the Git repositories.', - // TODO i18n - gcExpressionHelpText: '<p>Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.</p>\n\ - <table>\n\ - <tr><th><b>SECOND</b></th><td>Seconds within the minute (0–59)</td></tr>\n\ - <tr><th><b>MINUTE</b></th><td>Minutes within the hour (0–59)</td></tr>\n\ - <tr><th><b>HOUR</b></th><td>The hour of the day (0–23)</td></tr>\n\ - <tr><th><b>DAYOFMONTH</b></th><td>The day of the month (1–31)</td></tr>\n\ - <tr><th><b>MONTH</b></th><td>The month (1–12)</td></tr>\n\ - <tr><th><b>DAYOFWEEK</b></th><td>The day of the week (MON, TUE, WED, THU, FRI, SAT, SUN)</td></tr>\n\ - </table>\n\ - <p>E.g.: To run the task on every sunday at two o\'clock in the morning: 0 0 2 ? * SUN</p>\n\ - <p>For more informations please have a look at <a href="http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/crontrigger.html">Quartz CronTrigger</a></p>', - disabledHelpText: 'Enable or disable the Git plugin.\n\ - Note you have to reload the page, after changing this value.', - - initComponent: function(){ - - var config = { - title : this.titleText, - configUrl: restUrl + 'config/repositories/git.json', - items : [{ - xtype: 'textfield', - name: 'repositoryDirectory', - fieldLabel: this.repositoryDirectoryText, - helpText: this.repositoryDirectoryHelpText, - allowBlank : false - },{ - xtype: 'textfield', - name: 'gc-expression', - fieldLabel: this.gcExpressionText, - helpText: this.gcExpressionHelpText, - allowBlank : true - },{ - xtype: 'checkbox', - name: 'disabled', - fieldLabel: this.disabledText, - inputValue: 'true', - helpText: this.disabledHelpText - }] - }; - - Ext.apply(this, Ext.apply(this.initialConfig, config)); - Sonia.git.ConfigPanel.superclass.initComponent.apply(this, arguments); - } - -}); - -Ext.reg("gitConfigPanel", Sonia.git.ConfigPanel); - -// add default branch chooser to settings panel -Sonia.git.GitSettingsFormPanel = Ext.extend(Sonia.repository.SettingsFormPanel, { - - defaultBranchText: 'Default Branch', - defaultBranchHelpText: 'The default branch which is show first on source or commit view.', - - modifyDefaultConfig: function(config){ - if (this.item) { - var position = -1; - for ( var i=0; i<config.items.length; i++ ) { - var field = config.items[i]; - if (field.name === 'public') { - position = i; - break; - } - } - - var defaultBranchComboxBox = { - fieldLabel: this.defaultBranchText, - name: 'defaultBranch', - repositoryId: this.item.id, - value: this.getDefaultBranch(this.item), - useNameAsValue: true, - xtype: 'repositoryBranchComboBox', - helpText: this.defaultBranchHelpText - }; - - if (position >= 0) { - config.items.splice(position, 0, defaultBranchComboxBox); - } else { - config.items.push(defaultBranchComboxBox); - } - } - }, - - getDefaultBranch: function(item){ - if (item.properties) { - for ( var i=0; i<item.properties.length; i++ ) { - var prop = item.properties[i]; - if (prop.key === 'git.default-branch') { - return prop.value; - } - } - } - return undefined; - }, - - setDefaultBranch: function(item, defaultBranch){ - if (!item.properties) { - item.properties = [{ - key: 'git.default-branch', - value: defaultBranch - }]; - } else { - - var found = false; - for ( var i=0; i<item.properties.length; i++ ) { - var prop = item.properties[i]; - if (prop.key === 'git.default-branch') { - prop.value = defaultBranch; - found = true; - break; - } - } - - if (!found) { - item.properties.push({ - key: 'git.default-branch', - value: defaultBranch - }); - } - } - }, - - prepareUpdate: function(item) { - if (item.defaultBranch) { - var defaultBranch = item.defaultBranch; - delete item.defaultBranch; - this.setDefaultBranch(item, defaultBranch); - } - } - -}); - -Ext.reg("gitSettingsForm", Sonia.git.GitSettingsFormPanel); - - -// i18n - -if ( i18n && i18n.country === 'de' ){ - - Ext.override(Sonia.git.ConfigPanel, { - - // labels - titleText: 'Git Einstellungen', - repositoryDirectoryText: 'Repository-Verzeichnis', - disabledText: 'Deaktivieren', - - // helpTexts - repositoryDirectoryHelpText: 'Verzeichnis der Git-Repositories.', - disabledHelpText: 'Aktivieren oder deaktivieren des Git Plugins.\n\ - Die Seite muss neu geladen werden wenn dieser Wert geändert wird.' - - }); - - Ext.override(Sonia.git.GitSettingsFormPanel, { - - // labels - defaultBranchText: 'Standard Branch', - - // helpTexts - defaultBranchHelpText: 'Der Standard Branch wird für die Source und Commit Ansicht verwendet, \n\ - wenn kein anderer Branch eingestellt wurde.' - - }); - -} - - -// register information panel - -initCallbacks.push(function(main){ - main.registerInfoPanel('git', { - checkoutTemplate: 'git clone <a href="{0}" target="_blank">{0}</a>', - xtype: 'repositoryExtendedInfoPanel' - }); - main.registerSettingsForm('git', { - xtype: 'gitSettingsForm' - }); -}); - -// register panel - -registerConfigPanel({ - xtype : 'gitConfigPanel' -}); - -// register type icon - -Sonia.repository.typeIcons['git'] = 'resources/images/icons/16x16/git.png'; diff --git a/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.index.mustache b/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.index.mustache index 81d7a5bfa3..4571e327e3 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.index.mustache +++ b/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.index.mustache @@ -118,23 +118,6 @@ </tbody> </table> {{/branches}} - - <h2>Notes</h2> - - <div> - <p> - This page is only a quick view for git commits. - The full commit view is <a href="{{commitViewLink}}">here</a>. - </p> - <ul> - <li> - <a href="{{commitViewLink}}">Commits</a> - </li> - <li> - <a href="{{sourceViewLink}}">Source</a> - </li> - </ul> - </div> <h2>Git Informations</h2> <ul> @@ -144,4 +127,4 @@ </ul> </body> -</html> \ No newline at end of file +</html> diff --git a/scm-plugins/scm-git-plugin/src/main/webapp/images/git-logo.png b/scm-plugins/scm-git-plugin/src/main/webapp/images/git-logo.png new file mode 100644 index 0000000000..ed9393dc36 Binary files /dev/null and b/scm-plugins/scm-git-plugin/src/main/webapp/images/git-logo.png differ diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java new file mode 100644 index 0000000000..6a05875aa9 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java @@ -0,0 +1,33 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.GitConfig; + +import static org.junit.Assert.*; + +@RunWith(MockitoJUnitRunner.class) +public class GitConfigDtoToGitConfigMapperTest { + + @InjectMocks + private GitConfigDtoToGitConfigMapperImpl mapper; + + @Test + public void shouldMapFields() { + GitConfigDto dto = createDefaultDto(); + GitConfig config = mapper.map(dto); + assertEquals("express", config.getGcExpression()); + assertFalse(config.isDisabled()); + assertTrue(config.isNonFastForwardDisallowed()); + } + + private GitConfigDto createDefaultDto() { + GitConfigDto gitConfigDto = new GitConfigDto(); + gitConfigDto.setGcExpression("express"); + gitConfigDto.setDisabled(false); + gitConfigDto.setNonFastForwardDisallowed(true); + return gitConfigDto; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java new file mode 100644 index 0000000000..d6519a0013 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java @@ -0,0 +1,65 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class GitConfigInIndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectNode root = objectMapper.createObjectNode(); + private final GitConfigInIndexResource gitConfigInIndexResource; + + public GitConfigInIndexResourceTest() { + root.put("_links", objectMapper.createObjectNode()); + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + gitConfigInIndexResource = new GitConfigInIndexResource(Providers.of(pathInfoStore), objectMapper); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void admin() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + gitConfigInIndexResource.enrich(context); + + assertEquals("/v2/config/git", root.get("_links").get("gitConfig").get("href").asText()); + } + + @Test + @SubjectAware(username = "readOnly", password = "secret") + public void user() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + gitConfigInIndexResource.enrich(context); + + assertTrue(root.get("_links").iterator().hasNext()); + } + + @Test + public void anonymous() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + gitConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java new file mode 100644 index 0000000000..c25b5c5b60 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java @@ -0,0 +1,256 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.GitConfig; +import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.web.GitVndMediaType; + +import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; + +import static com.google.inject.util.Providers.of; +import static junit.framework.TestCase.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class GitConfigResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + + @InjectMocks + private GitConfigDtoToGitConfigMapperImpl dtoToConfigMapper; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @Mock + private RepositoryManager repositoryManager; + + @InjectMocks + private GitConfigToGitConfigDtoMapperImpl configToDtoMapper; + @InjectMocks + private GitRepositoryConfigMapperImpl repositoryConfigMapper; + + @Mock + private GitRepositoryHandler repositoryHandler; + + @Mock(answer = Answers.CALLS_REAL_METHODS) + private ConfigurationStoreFactory configurationStoreFactory; + @Spy + private ConfigurationStore<Object> configurationStore; + @Captor + private ArgumentCaptor<Object> configurationStoreCaptor; + + @Before + public void prepareEnvironment() { + GitConfig gitConfig = createConfiguration(); + when(repositoryHandler.getConfig()).thenReturn(gitConfig); + GitRepositoryConfigResource gitRepositoryConfigResource = new GitRepositoryConfigResource(repositoryConfigMapper, repositoryManager, new GitRepositoryConfigStoreProvider(configurationStoreFactory)); + GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, of(gitRepositoryConfigResource)); + dispatcher.getRegistry().addSingletonResource(gitConfigResource); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + } + + @Before + public void initConfigStore() { + when(configurationStoreFactory.getStore(any())).thenReturn(configurationStore); + doNothing().when(configurationStore).set(configurationStoreCaptor.capture()); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetGitConfig() throws URISyntaxException, UnsupportedEncodingException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String responseString = response.getContentAsString(); + + assertTrue(responseString.contains("\"disabled\":false")); + assertTrue(responseString.contains("\"gcExpression\":\"valid Git GC Cron Expression\"")); + assertTrue(responseString.contains("\"self\":{\"href\":\"/v2/config/git")); + assertTrue(responseString.contains("\"update\":{\"href\":\"/v2/config/git")); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException, UnsupportedEncodingException { + when(repositoryHandler.getConfig()).thenReturn(null); + + MockHttpResponse response = get(); + String responseString = response.getContentAsString(); + + assertTrue(responseString.contains("\"disabled\":false")); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetGitConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertFalse(response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config/git")); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:read:git]"); + + get(); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldUpdateConfig() throws URISyntaxException { + MockHttpResponse response = put(); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:write:git]"); + + put(); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldReadDefaultRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException { + when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); + + MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertThat(response.getContentAsString()) + .contains("\"defaultBranch\":null") + .contains("self") + .contains("update"); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotHaveUpdateLinkForReadOnlyUser() throws URISyntaxException, UnsupportedEncodingException { + when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); + + MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertThat(response.getContentAsString()) + .contains("\"defaultBranch\":null") + .contains("self") + .doesNotContain("update"); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldReadStoredRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException { + when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); + GitRepositoryConfig gitRepositoryConfig = new GitRepositoryConfig(); + gitRepositoryConfig.setDefaultBranch("test"); + when(configurationStore.get()).thenReturn(gitRepositoryConfig); + + MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertThat(response.getContentAsString()) + .contains("\"defaultBranch\":\"test\""); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldStoreChangedRepositoryConfig() throws URISyntaxException { + when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); + + MockHttpRequest request = MockHttpRequest + .put("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X") + .contentType(GitVndMediaType.GIT_REPOSITORY_CONFIG) + .content("{\"defaultBranch\": \"new\"}".getBytes()); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + assertThat(configurationStoreCaptor.getValue()) + .isInstanceOfSatisfying(GitRepositoryConfig.class, x -> { }) + .extracting("defaultBranch") + .containsExactly("new"); + } + + private MockHttpResponse get() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private MockHttpResponse put() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.put("/" + GitConfigResource.GIT_CONFIG_PATH_V2) + .contentType(GitVndMediaType.GIT_CONFIG) + .content("{\"disabled\":true}".getBytes()); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private GitConfig createConfiguration() { + GitConfig config = new GitConfig(); + config.setGcExpression("valid Git GC Cron Expression"); + config.setDisabled(false); + return config; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java new file mode 100644 index 0000000000..62fa8d33b4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java @@ -0,0 +1,84 @@ +package sonia.scm.api.v2.resources; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.GitConfig; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GitConfigToGitConfigDtoMapperTest { + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @InjectMocks + private GitConfigToGitConfigDtoMapperImpl mapper; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private URI expectedBaseUri; + + @Before + public void init() { + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(GitConfigResource.GIT_CONFIG_PATH_V2); + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @After + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapFields() { + GitConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:git")).thenReturn(true); + GitConfigDto dto = mapper.map(config); + + assertEquals("express", dto.getGcExpression()); + assertFalse(dto.isDisabled()); + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); + } + + @Test + public void shouldMapFieldsWithoutUpdate() { + GitConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:git")).thenReturn(false); + GitConfigDto dto = mapper.map(config); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertFalse(dto.getLinks().hasLink("update")); + } + + private GitConfig createConfiguration() { + GitConfig config = new GitConfig(); + config.setDisabled(false); + config.setGcExpression("express"); + return config; + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java new file mode 100644 index 0000000000..a1e349dd57 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java @@ -0,0 +1,149 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Resources; +import com.google.inject.Provider; +import com.google.inject.util.Providers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.net.URI; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GitRepositoryConfigEnricherTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private GitRepositoryConfigEnricher linkEnricher; + private JsonNode rootNode; + @Mock + private RepositoryManager manager; + + @BeforeEach + void globalSetUp() { + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + Provider<ScmPathInfoStore> pathInfoStoreProvider = Providers.of(pathInfoStore); + + linkEnricher = new GitRepositoryConfigEnricher(pathInfoStoreProvider, objectMapper, manager); + } + + @Nested + class ForSingleRepository { + @BeforeEach + void setUp() throws IOException { + URL resource = Resources.getResource("sonia/scm/repository/repository-001.json"); + rootNode = objectMapper.readTree(resource); + + when(manager.get(new NamespaceAndName("scmadmin", "web-resources"))).thenReturn(new Repository("id", "git", "scmadmin", "web-resources")); + } + + @Test + void shouldEnrichGitRepositories() { + JsonEnricherContext context = new JsonEnricherContext( + URI.create("/"), + MediaType.valueOf(VndMediaType.REPOSITORY), + rootNode + ); + + linkEnricher.enrich(context); + + String configLink = context.getResponseEntity() + .get("_links") + .get("configuration") + .get("href") + .asText(); + + assertThat(configLink).isEqualTo("/v2/config/git/scmadmin/web-resources"); + } + + @Test + void shouldNotEnrichOtherRepositories() { + when(manager.get(new NamespaceAndName("scmadmin", "web-resources"))).thenReturn(new Repository("id", "hg", "scmadmin", "web-resources")); + + JsonEnricherContext context = new JsonEnricherContext( + URI.create("/"), + MediaType.valueOf(VndMediaType.REPOSITORY), + rootNode + ); + + linkEnricher.enrich(context); + + JsonNode configLink = context.getResponseEntity() + .get("_links") + .get("configuration"); + + assertThat(configLink).isNull(); + } + } + + @Nested + class ForRepositoryCollection { + @BeforeEach + void setUp() throws IOException { + URL resource = Resources.getResource("sonia/scm/repository/repository-collection-001.json"); + rootNode = objectMapper.readTree(resource); + + when(manager.get(new NamespaceAndName("scmadmin", "web-resources"))).thenReturn(new Repository("id", "git", "scmadmin", "web-resources")); + } + + @Test + void shouldEnrichAllRepositories() { + JsonEnricherContext context = new JsonEnricherContext( + URI.create("/"), + MediaType.valueOf(VndMediaType.REPOSITORY_COLLECTION), + rootNode + ); + + linkEnricher.enrich(context); + + context.getResponseEntity() + .get("_embedded") + .withArray("repositories") + .elements() + .forEachRemaining(node -> { + String configLink = node + .get("_links") + .get("configuration") + .get("href") + .asText(); + + assertThat(configLink).isEqualTo("/v2/config/git/scmadmin/web-resources"); + }); + } + } + + @Test + void shouldNotModifyObjectsWithUnsupportedMediaType() throws IOException { + URL resource = Resources.getResource("sonia/scm/repository/repository-001.json"); + rootNode = objectMapper.readTree(resource); + JsonEnricherContext context = new JsonEnricherContext( + URI.create("/"), + MediaType.valueOf(VndMediaType.USER), + rootNode + ); + + linkEnricher.enrich(context); + + boolean hasNewPullRequestLink = context.getResponseEntity() + .get("_links") + .has("configuration"); + + assertThat(hasNewPullRequestLink).isFalse(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java new file mode 100644 index 0000000000..fa1a97ec3b --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java @@ -0,0 +1,122 @@ +/** + * 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.protocolcommand.git; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.GitConfig; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.web.CollectingPackParserListener; +import sonia.scm.web.GitReceiveHook; + +import java.io.File; +import java.io.IOException; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@RunWith(MockitoJUnitRunner.class) +public class BaseReceivePackFactoryTest { + + @Mock + private GitRepositoryHandler handler; + + private GitConfig config; + + @Mock + private ReceivePackFactory<Object> wrappedReceivePackFactory; + + private BaseReceivePackFactory<Object> factory; + + private Object request = new Object(); + + private Repository repository; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void setUpObjectUnderTest() throws Exception { + this.repository = createRepositoryForTesting(); + + config = new GitConfig(); + when(handler.getConfig()).thenReturn(config); + + ReceivePack receivePack = new ReceivePack(repository); + when(wrappedReceivePackFactory.create(request, repository)).thenReturn(receivePack); + + factory = new BaseReceivePackFactory<Object>(handler, null) { + @Override + protected ReceivePack createBasicReceivePack(Object request, Repository repository) throws ServiceNotEnabledException, ServiceNotAuthorizedException { + return wrappedReceivePackFactory.create(request, repository); + } + }; + } + + private Repository createRepositoryForTesting() throws GitAPIException, IOException { + File directory = temporaryFolder.newFolder(); + return Git.init().setDirectory(directory).call().getRepository(); + } + + @Test + public void testCreate() throws Exception { + ReceivePack receivePack = factory.create(request, repository); + assertThat(receivePack.getPackParserListener(), instanceOf(CollectingPackParserListener.class)); + assertThat(receivePack.getPreReceiveHook(), instanceOf(GitReceiveHook.class)); + assertThat(receivePack.getPostReceiveHook(), instanceOf(GitReceiveHook.class)); + assertTrue(receivePack.isAllowNonFastForwards()); + verify(wrappedReceivePackFactory).create(request, repository); + } + + @Test + public void testCreateWithDisabledNonFastForward() throws Exception { + config.setNonFastForwardDisallowed(true); + ReceivePack receivePack = factory.create(request, repository); + assertFalse(receivePack.isAllowNonFastForwards()); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java new file mode 100644 index 0000000000..7e7566f6f9 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java @@ -0,0 +1,47 @@ +package sonia.scm.protocolcommand.git; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryManager; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GitRepositoryContextResolverTest { + + private static final Repository REPOSITORY = new Repository("id", "git", "space", "X"); + + @Mock + RepositoryManager repositoryManager; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + RepositoryLocationResolver locationResolver; + + @InjectMocks + GitRepositoryContextResolver resolver; + + @Test + void shouldResolveCorrectRepository() throws IOException { + when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(REPOSITORY); + Path repositoryPath = File.createTempFile("test", "scm").toPath(); + when(locationResolver.forClass(any()).getLocation("id")).thenReturn(repositoryPath); + + RepositoryContext context = resolver.resolve(new String[] {"git", "repo/space/X/something/else"}); + + assertThat(context.getRepository()).isSameAs(REPOSITORY); + assertThat(context.getDirectory()).isEqualTo(repositoryPath.resolve("data")); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java new file mode 100644 index 0000000000..7c22559f44 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java @@ -0,0 +1,31 @@ +package sonia.scm.repository; + +import org.junit.Test; +import sonia.scm.repository.util.CloseableWrapper; + +import java.util.function.Consumer; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class CloseableWrapperTest { + + @Test + public void shouldExecuteGivenMethodAtClose() { + Consumer<AutoCloseable> wrapped = new Consumer<AutoCloseable>() { + // no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy + @Override + public void accept(AutoCloseable s) { + } + }; + + Consumer<AutoCloseable> closer = spy(wrapped); + + AutoCloseable autoCloseable = () -> {}; + try (CloseableWrapper<AutoCloseable> wrapper = new CloseableWrapper<>(autoCloseable, closer)) { + // nothing to do here + } + + verify(closer).accept(autoCloseable); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitGcTaskTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitGcTaskTest.java index 57fdd59c2f..d008cb0618 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitGcTaskTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitGcTaskTest.java @@ -32,19 +32,23 @@ package sonia.scm.repository; import com.google.common.collect.Lists; +import org.eclipse.jgit.api.GarbageCollectCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + import java.io.File; import java.io.IOException; import java.util.List; import java.util.Properties; -import org.eclipse.jgit.api.GarbageCollectCommand; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.junit.Test; -import static org.mockito.Mockito.*; -import org.junit.Before; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Unit tests for {@link GitGcTask}. @@ -103,8 +107,7 @@ public class GitGcTaskTest // prepare repositories for task Repository unhealthy = mock(Repository.class); when(unhealthy.getType()).thenReturn("git"); - when(unhealthy.isHealthy()).thenReturn(Boolean.FALSE); - + Repository invalid = mock(Repository.class); when(unhealthy.getType()).thenReturn("git"); when(unhealthy.isValid()).thenReturn(Boolean.FALSE); @@ -125,4 +128,4 @@ public class GitGcTaskTest verify(gcc).call(); } -} \ No newline at end of file +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java new file mode 100644 index 0000000000..3362c8a22b --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2014, 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; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GitHeadModifierTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock + private GitRepositoryHandler repositoryHandler; + + @InjectMocks + private GitHeadModifier modifier; + + @Test + public void testEnsure() throws IOException, GitAPIException { + Repository repository = RepositoryTestData.createHeartOfGold("git"); + File headFile = create(repository, "master"); + + boolean result = modifier.ensure(repository, "develop"); + + assertEquals("ref: refs/heads/develop", Files.readFirstLine(headFile, Charsets.UTF_8)); + assertTrue(result); + } + + @Test + public void testEnsureWithSameBranch() throws IOException, GitAPIException { + Repository repository = RepositoryTestData.createHeartOfGold("git"); + create(repository, "develop"); + + boolean result = modifier.ensure(repository, "develop"); + + assertFalse(result); + } + + private File create(Repository repository, String head) throws IOException, GitAPIException { + File directory = temporaryFolder.newFolder(); + + Git.init() + .setBare(true) + .setDirectory(directory) + .call(); + + File headFile = new File(directory, "HEAD"); + Files.write(String.format("ref: refs/heads/%s\n", head), headFile, Charsets.UTF_8); + + when(repositoryHandler.getDirectory(repository.getId())).thenReturn(directory); + + return headFile; + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java index 5d9b338875..2f5a8c4984 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + * <p> * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * <p> * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * <p> * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,50 +24,50 @@ * 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. - * + * <p> * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.io.DefaultFileSystem; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.schedule.Scheduler; +import sonia.scm.store.ConfigurationStoreFactory; -import static org.junit.Assert.*; +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ -import java.io.File; -import sonia.scm.store.ConfigurationStoreFactory; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; -import sonia.scm.schedule.Scheduler; - /** - * * @author Sebastian Sdorra */ -@RunWith(MockitoJUnitRunner.class) -public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase -{ +@RunWith(MockitoJUnitRunner.Silent.class) +public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Mock private Scheduler scheduler; - - /** - * Method description - * - * - * @param directory - */ + + @Mock + private ConfigurationStoreFactory factory; + + @Mock + private GitWorkdirFactory gitWorkdirFactory; + + @Override - protected void checkDirectory(File directory) - { + protected void checkDirectory(File directory) { File head = new File(directory, "HEAD"); assertTrue(head.exists()); @@ -84,30 +84,39 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase assertTrue(refs.isDirectory()); } - /** - * Method description - * - * - * @param factory - * @param directory - * - * @return - */ + @Before + public void initFactory() { + when(factory.withType(any())).thenCallRealMethod(); + } + @Override protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, - File directory) - { + RepositoryLocationResolver locationResolver, + File directory) { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - new DefaultFileSystem(), scheduler); - + scheduler, locationResolver, gitWorkdirFactory, null); repositoryHandler.init(contextProvider); GitConfig config = new GitConfig(); - config.setRepositoryDirectory(directory); // TODO fix event bus exception repositoryHandler.setConfig(config); return repositoryHandler; } + + @Test + public void getDirectory() { + GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, + scheduler, locationResolver, gitWorkdirFactory, null); + GitConfig config = new GitConfig(); + config.setDisabled(false); + config.setGcExpression("gc exp"); + + repositoryHandler.setConfig(config); + + initRepository(); + File path = repositoryHandler.getDirectory(repository.getId()); + assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath()); + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryModifyListenerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryModifyListenerTest.java deleted file mode 100644 index a542674484..0000000000 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryModifyListenerTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright (c) 2014, 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; - -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Before; -import sonia.scm.HandlerEventType; - -/** - * Unit tests for {@link GitRepositoryModifyListener}. - * - * @author Sebastian Sdorra - */ -public class GitRepositoryModifyListenerTest { - - private GitRepositoryModifyTestListener repositoryModifyListener; - - /** - * Set up test object. - */ - @Before - public void setUpObjectUnderTest(){ - repositoryModifyListener = new GitRepositoryModifyTestListener(); - } - - /** - * Tests happy path. - */ - @Test - public void testHandleEvent() { - Repository old = RepositoryTestData.createHeartOfGold("git"); - old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); - Repository current = RepositoryTestData.createHeartOfGold("git"); - current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop"); - - RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); - repositoryModifyListener.handleEvent(event); - - assertNotNull(repositoryModifyListener.repository); - assertSame(current, repositoryModifyListener.repository); - } - - /** - * Tests with new default branch. - */ - @Test - public void testWithNewDefaultBranch() { - Repository old = RepositoryTestData.createHeartOfGold("git"); - Repository current = RepositoryTestData.createHeartOfGold("git"); - current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop"); - - RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); - repositoryModifyListener.handleEvent(event); - - assertNotNull(repositoryModifyListener.repository); - assertSame(current, repositoryModifyListener.repository); - } - - /** - * Tests with non git repositories. - */ - @Test - public void testNonGitRepository(){ - Repository old = RepositoryTestData.createHeartOfGold("hg"); - old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); - Repository current = RepositoryTestData.createHeartOfGold("hg"); - current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop"); - - RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); - repositoryModifyListener.handleEvent(event); - - assertNull(repositoryModifyListener.repository); - } - - /** - * Tests without default branch. - */ - @Test - public void testWithoutDefaultBranch(){ - Repository old = RepositoryTestData.createHeartOfGold("git"); - Repository current = RepositoryTestData.createHeartOfGold("git"); - - RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); - repositoryModifyListener.handleEvent(event); - - assertNull(repositoryModifyListener.repository); - } - - /** - * Tests with non modify event. - */ - @Test - public void testNonModifyEvent(){ - Repository old = RepositoryTestData.createHeartOfGold("git"); - old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); - Repository current = RepositoryTestData.createHeartOfGold("git"); - current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop"); - - RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.CREATE, current, old); - repositoryModifyListener.handleEvent(event); - - assertNull(repositoryModifyListener.repository); - } - - /** - * Tests with non git repositories. - */ - @Test - public void testNoModification(){ - Repository old = RepositoryTestData.createHeartOfGold("git"); - old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); - Repository current = RepositoryTestData.createHeartOfGold("git"); - current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); - - RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); - repositoryModifyListener.handleEvent(event); - - assertNull(repositoryModifyListener.repository); - } - - private static class GitRepositoryModifyTestListener extends GitRepositoryModifyListener { - - private Repository repository; - - @Override - protected void sendClearRepositoryCacheEvent(Repository repository) { - this.repository = repository; - } - - } - - -} \ No newline at end of file diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryPathMatcherTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryPathMatcherTest.java index 7adc4a6913..598d3b6400 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryPathMatcherTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryPathMatcherTest.java @@ -31,7 +31,9 @@ package sonia.scm.repository; import org.junit.Test; -import static org.junit.Assert.*; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * Unit tests for {@link GitRepositoryPathMatcher}. @@ -45,18 +47,18 @@ public class GitRepositoryPathMatcherTest { @Test public void testIsPathMatching() { - assertFalse(pathMatcher.isPathMatching(repository("my-repo"), "my-repoo")); - assertFalse(pathMatcher.isPathMatching(repository("my"), "my-repo")); - assertFalse(pathMatcher.isPathMatching(repository("my"), "my-repo/with/path")); + assertFalse(pathMatcher.isPathMatching(repository("space", "my-repo"), "my-repoo")); + assertFalse(pathMatcher.isPathMatching(repository("space", "my"), "my-repo")); + assertFalse(pathMatcher.isPathMatching(repository("space", "my"), "my-repo/with/path")); - assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo")); - assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo.git")); - assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo/with/path")); - assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo.git/with/path")); + assertTrue(pathMatcher.isPathMatching(repository("space", "my-repo"), "my-repo")); + assertTrue(pathMatcher.isPathMatching(repository("space", "my-repo"), "my-repo.git")); + assertTrue(pathMatcher.isPathMatching(repository("space", "my-repo"), "my-repo/with/path")); + assertTrue(pathMatcher.isPathMatching(repository("space", "my-repo"), "my-repo.git/with/path")); } - private Repository repository(String name) { - return new Repository(name, GitRepositoryHandler.TYPE_NAME, name); + private Repository repository(String namespace, String name) { + return new Repository(name, GitRepositoryHandler.TYPE_NAME, namespace, name); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookBranchProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookBranchProviderTest.java index 9e5e0eef36..7699f43686 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookBranchProviderTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookBranchProviderTest.java @@ -43,7 +43,7 @@ import static org.hamcrest.Matchers.*; import org.junit.Before; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * Unit tests for {@link GitHookBranchProvider}. @@ -112,4 +112,4 @@ public class GitHookBranchProviderTest { return new GitHookBranchProvider(commands); } -} \ No newline at end of file +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java index 87e277b633..46f28692f5 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java @@ -32,20 +32,23 @@ package sonia.scm.repository.api; import com.google.common.collect.Lists; -import java.util.List; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.transport.ReceiveCommand; -import org.junit.Test; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; -import static org.hamcrest.Matchers.*; import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; -import org.mockito.stubbing.OngoingStubbing; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Tag; +import java.util.List; + +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + /** * Unit tests for {@link GitHookTagProvider}. * @@ -54,6 +57,11 @@ import sonia.scm.repository.Tag; @RunWith(MockitoJUnitRunner.class) public class GitHookTagProviderTest { + private static final String ZERO = ObjectId.zeroId().getName(); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Mock private ReceiveCommand command; @@ -73,7 +81,7 @@ public class GitHookTagProviderTest { @Test public void testGetCreatedTags() { String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; - GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/tags/1.0.0", revision); + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/tags/1.0.0", revision, ZERO); assertTag("1.0.0", revision, provider.getCreatedTags()); assertThat(provider.getDeletedTags(), empty()); @@ -85,7 +93,7 @@ public class GitHookTagProviderTest { @Test public void testGetDeletedTags() { String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; - GitHookTagProvider provider = createProvider(ReceiveCommand.Type.DELETE, "refs/tags/1.0.0", revision); + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.DELETE, "refs/tags/1.0.0", ZERO, revision); assertThat(provider.getCreatedTags(), empty()); assertTag("1.0.0", revision, provider.getDeletedTags()); @@ -97,12 +105,25 @@ public class GitHookTagProviderTest { @Test public void testWithBranch(){ String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; - GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/heads/1.0.0", revision); + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/heads/1.0.0", revision, revision); assertThat(provider.getCreatedTags(), empty()); assertThat(provider.getDeletedTags(), empty()); } - + + /** + * Tests {@link GitHookTagProvider} with update command. + */ + @Test + public void testUpdateTags() { + String newId = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; + String oldId = "e0f2be968b147ff7043684a7715d2fe852553db4"; + + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.UPDATE, "refs/tags/1.0.0", newId, oldId); + assertTag("1.0.0", newId, provider.getCreatedTags()); + assertTag("1.0.0", oldId, provider.getDeletedTags()); + } + private void assertTag(String name, String revision, List<Tag> tags){ assertNotNull(tags); assertFalse(tags.isEmpty()); @@ -112,19 +133,12 @@ public class GitHookTagProviderTest { assertEquals(revision, tag.getRevision()); } - private GitHookTagProvider createProvider(ReceiveCommand.Type type, String ref, String id){ - OngoingStubbing<ObjectId> ongoing; - if (type == ReceiveCommand.Type.CREATE){ - ongoing = when(command.getNewId()); - } else { - ongoing = when(command.getOldId()); - } - ongoing.thenReturn(ObjectId.fromString(id)); - + private GitHookTagProvider createProvider(ReceiveCommand.Type type, String ref, String newId, String oldId){ + when(command.getNewId()).thenReturn(ObjectId.fromString(newId)); + when(command.getOldId()).thenReturn(ObjectId.fromString(oldId)); when(command.getType()).thenReturn(type); when(command.getRefName()).thenReturn(ref); - return new GitHookTagProvider(commands); } -} \ No newline at end of file +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitBranchCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitBranchCommand.java index 5a721a7aa5..19197d009d 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitBranchCommand.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitBranchCommand.java @@ -72,7 +72,7 @@ public class GitBranchCommand implements BranchCommand try { Ref ref = git.branchCreate().setName(name).call(); - return new Branch(name, GitUtil.getId(ref.getObjectId())); + return Branch.normalBranch(name, GitUtil.getId(ref.getObjectId())); } catch (GitAPIException ex) { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitPushCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitPushCommand.java index 3b9d29abdf..8d65b54b06 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitPushCommand.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitPushCommand.java @@ -37,12 +37,12 @@ package sonia.scm.repository.client.spi; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.transport.CredentialsProvider; - import sonia.scm.repository.client.api.RepositoryClientException; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +import java.util.function.Supplier; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -73,11 +73,20 @@ public class GitPushCommand implements PushCommand * @throws IOException */ @Override - public void push() throws IOException + public void push() throws IOException { + push(() -> git.push().setPushAll()); + } + + @Override + public void pushTags() throws IOException { + push(() -> git.push().setPushTags()); + } + + private void push(Supplier<org.eclipse.jgit.api.PushCommand> commandSupplier) throws RepositoryClientException { try { - org.eclipse.jgit.api.PushCommand cmd = git.push().setPushAll(); + org.eclipse.jgit.api.PushCommand cmd = commandSupplier.get(); if (credentialsProvider != null) { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java index f545540a38..41b63440b6 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java @@ -35,20 +35,17 @@ package sonia.scm.repository.client.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.ImmutableSet; -import java.io.File; - import org.eclipse.jgit.api.Git; import org.eclipse.jgit.transport.CredentialsProvider; - import sonia.scm.repository.GitUtil; import sonia.scm.repository.client.api.ClientCommand; -//~--- JDK imports ------------------------------------------------------------ - +import java.io.File; import java.io.IOException; - import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -59,7 +56,7 @@ public class GitRepositoryClientProvider extends RepositoryClientProvider /** Field description */ private static final Set<ClientCommand> SUPPORTED_COMMANDS = ImmutableSet.of(ClientCommand.ADD, ClientCommand.REMOVE, - ClientCommand.COMMIT, ClientCommand.TAG, ClientCommand.BANCH, + ClientCommand.COMMIT, ClientCommand.TAG, ClientCommand.BRANCH, ClientCommand.PUSH); //~--- constructors --------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java index ee03dc7458..e5e1f36155 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java @@ -35,22 +35,20 @@ package sonia.scm.repository.client.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Strings; - import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; - import sonia.scm.repository.GitUtil; import sonia.scm.repository.Tag; import sonia.scm.repository.client.api.RepositoryClientException; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -119,7 +117,11 @@ public class GitTagCommand implements TagCommand ref = git.tag().setObjectId(revObject).call(); } - tag = new Tag(request.getName(), ref.getPeeledObjectId().toString()); + if (ref.isPeeled()) { + tag = new Tag(request.getName(), ref.getPeeledObjectId().toString()); + } else { + tag = new Tag(request.getName(), ref.getObjectId().toString()); + } } catch (GitAPIException ex) diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java index 496b71e656..8c4b682b18 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java @@ -34,7 +34,18 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import org.eclipse.jgit.transport.ScmTransportProtocol; +import org.eclipse.jgit.transport.Transport; import org.junit.After; +import org.junit.Before; +import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.repository.PreProcessorUtil; +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.store.InMemoryConfigurationStoreFactory; + +import static com.google.inject.util.Providers.of; +import static org.mockito.Mockito.mock; /** * @@ -50,7 +61,10 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase @After public void close() { - context.close(); + if (context != null) { + context.setConfig(new GitRepositoryConfig()); + context.close(); + } } /** @@ -63,7 +77,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase { if (context == null) { - context = new GitContext(repositoryDirectory); + context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create())); } return context; @@ -99,4 +113,5 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase /** Field description */ private GitContext context; + private ScmTransportProtocol scmTransportProtocol; } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java index 3b6c6797bb..1e366c919b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java @@ -37,6 +37,7 @@ package sonia.scm.repository.spi; import com.google.common.base.Charsets; import com.google.common.io.Files; +import com.google.inject.Provider; import org.eclipse.jgit.api.CommitCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -84,16 +85,16 @@ public class AbstractRemoteCommandTestBase outgoingDirectory = tempFolder.newFile("outgoing"); outgoingDirectory.delete(); - incomgingRepository = new Repository("1", "git", "incoming"); - outgoingRepository = new Repository("2", "git", "outgoing"); + incomingRepository = new Repository("1", "git", "space", "incoming"); + outgoingRepository = new Repository("2", "git", "space", "outgoing"); incoming = Git.init().setDirectory(incomingDirectory).setBare(false).call(); outgoing = Git.init().setDirectory(outgoingDirectory).setBare(false).call(); handler = mock(GitRepositoryHandler.class); - when(handler.getDirectory(incomgingRepository)).thenReturn( + when(handler.getDirectory(incomingRepository.getId())).thenReturn( incomingDirectory); - when(handler.getDirectory(outgoingRepository)).thenReturn( + when(handler.getDirectory(outgoingRepository.getId())).thenReturn( outgoingDirectory); } @@ -118,7 +119,23 @@ public class AbstractRemoteCommandTestBase { // store reference to handle weak references - proto = new ScmTransportProtocol(() -> null, () -> null); + proto = new ScmTransportProtocol(new Provider<HookEventFacade>() + { + + @Override + public HookEventFacade get() + { + return null; + } + }, new Provider<GitRepositoryHandler>() + { + + @Override + public GitRepositoryHandler get() + { + return null; + } + }); Transport.register(proto); } @@ -194,7 +211,7 @@ public class AbstractRemoteCommandTestBase protected GitRepositoryHandler handler; /** Field description */ - protected Repository incomgingRepository; + protected Repository incomingRepository; /** Field description */ protected Git incoming; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java new file mode 100644 index 0000000000..49800fc9e8 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java @@ -0,0 +1,38 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.transport.ScmTransportProtocol; +import org.eclipse.jgit.transport.Transport; +import org.junit.rules.ExternalResource; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.PreProcessorUtil; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.api.HookContextFactory; + +import static com.google.inject.util.Providers.of; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BindTransportProtocolRule extends ExternalResource { + + private ScmTransportProtocol scmTransportProtocol; + + @Override + protected void before() throws Throwable { + HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); + RepositoryManager repositoryManager = mock(RepositoryManager.class); + HookEventFacade hookEventFacade = new HookEventFacade(of(repositoryManager), hookContextFactory); + GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); + scmTransportProtocol = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler)); + + Transport.register(scmTransportProtocol); + + when(gitRepositoryHandler.getRepositoryId(any())).thenReturn("1"); + when(repositoryManager.get("1")).thenReturn(new sonia.scm.repository.Repository()); + } + + @Override + protected void after() { + Transport.unregister(scmTransportProtocol); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java index d049447d7f..c8d260d503 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java @@ -35,17 +35,16 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; - import sonia.scm.repository.BlameLine; import sonia.scm.repository.BlameResult; -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.GitRepositoryConfig; import java.io.IOException; -import sonia.scm.repository.GitConstants; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +//~--- JDK imports ------------------------------------------------------------ /** * Unit tests for {@link GitBlameCommand}. @@ -59,10 +58,10 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase * Tests blame command with default branch. * * @throws IOException - * @throws RepositoryException + * @ */ @Test - public void testDefaultBranch() throws IOException, RepositoryException { + public void testDefaultBranch() throws IOException { // without default branch, the repository head should be used BlameCommandRequest request = new BlameCommandRequest(); request.setPath("a.txt"); @@ -74,7 +73,7 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase assertEquals("fcd0ef1831e4002ac43ea539f4094334c79ea9ec", result.getLine(1).getRevision()); // set default branch and test again - repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); + createContext().setConfig(new GitRepositoryConfig("test-branch")); result = createCommand().getBlameResult(request); assertNotNull(result); assertEquals(1, result.getTotal()); @@ -86,10 +85,9 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase * * * @throws IOException - * @throws RepositoryException */ @Test - public void testGetBlameResult() throws IOException, RepositoryException + public void testGetBlameResult() throws IOException { BlameCommandRequest request = new BlameCommandRequest(); @@ -120,11 +118,10 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase * * * @throws IOException - * @throws RepositoryException */ @Test public void testGetBlameResultWithRevision() - throws IOException, RepositoryException + throws IOException { BlameCommandRequest request = new BlameCommandRequest(); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java new file mode 100644 index 0000000000..8bfb0d16ad --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java @@ -0,0 +1,136 @@ +package sonia.scm.repository.spi; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.Branch; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.PreReceiveRepositoryHookEvent; +import sonia.scm.repository.api.BranchRequest; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; + +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +@RunWith(MockitoJUnitRunner.class) +public class GitBranchCommandTest extends AbstractGitCommandTestBase { + + @Mock + private HookContextFactory hookContextFactory; + @Mock + private ScmEventBus eventBus; + + @Test + public void shouldCreateBranchWithDefinedSourceBranch() throws IOException { + GitContext context = createContext(); + + Branch source = findBranch(context, "mergeable"); + + BranchRequest branchRequest = new BranchRequest(); + branchRequest.setParentBranch(source.getName()); + branchRequest.setNewBranch("new_branch"); + + createCommand().branch(branchRequest); + + Branch newBranch = findBranch(context, "new_branch"); + assertThat(newBranch.getRevision()).isEqualTo(source.getRevision()); + } + + private Branch findBranch(GitContext context, String name) throws IOException { + List<Branch> branches = readBranches(context); + return branches.stream().filter(b -> name.equals(b.getName())).findFirst().get(); + } + + @Test + public void shouldCreateBranch() throws IOException { + GitContext context = createContext(); + + assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isEmpty(); + + BranchRequest branchRequest = new BranchRequest(); + branchRequest.setNewBranch("new_branch"); + + createCommand().branch(branchRequest); + + assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); + } + + @Test + public void shouldDeleteBranch() throws IOException { + GitContext context = createContext(); + String branchToBeDeleted = "squash"; + createCommand().deleteOrClose(branchToBeDeleted); + assertThat(readBranches(context)).filteredOn(b -> b.getName().equals(branchToBeDeleted)).isEmpty(); + } + + @Test + public void shouldThrowExceptionWhenDeletingDefaultBranch() { + String branchToBeDeleted = "master"; + assertThrows(CannotDeleteDefaultBranchException.class, () -> createCommand().deleteOrClose(branchToBeDeleted)); + } + + private GitBranchCommand createCommand() { + return new GitBranchCommand(createContext(), repository, hookContextFactory, eventBus); + } + + private List<Branch> readBranches(GitContext context) throws IOException { + return new GitBranchesCommand(context, repository).getBranches(); + } + + @Test + public void shouldPostCreateEvents() { + ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class); + doNothing().when(eventBus).post(captor.capture()); + when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext); + + BranchRequest branchRequest = new BranchRequest(); + branchRequest.setParentBranch("mergeable"); + branchRequest.setNewBranch("new_branch"); + + createCommand().branch(branchRequest); + + List<Object> events = captor.getAllValues(); + assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class); + assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class); + + PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0); + assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).containsExactly("new_branch"); + assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).isEmpty(); + } + + @Test + public void shouldPostDeleteEvents() { + ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class); + doNothing().when(eventBus).post(captor.capture()); + when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext); + + createCommand().deleteOrClose("squash"); + + List<Object> events = captor.getAllValues(); + assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class); + assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class); + + PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0); + assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).containsExactly("squash"); + assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).isEmpty(); + } + + private HookContext createMockedContext(InvocationOnMock invocation) { + HookContext mock = mock(HookContext.class); + when(mock.getBranchProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getBranchProvider()); + return mock; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java new file mode 100644 index 0000000000..1c737deab8 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java @@ -0,0 +1,117 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ListBranchCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.Branch; +import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.repository.Repository; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Optional.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GitBranchesCommandTest { + + @Mock + GitContext context; + @Mock + Git git; + @Mock + ListBranchCommand listBranchCommand; + @Mock + GitRepositoryConfig gitRepositoryConfig; + + GitBranchesCommand branchesCommand; + private Ref master; + + @BeforeEach + void initContext() { + when(context.getConfig()).thenReturn(gitRepositoryConfig); + } + + @BeforeEach + void initCommand() { + master = createRef("master", "0000"); + branchesCommand = new GitBranchesCommand(context, new Repository("1", "git", "space", "X")) { + @Override + Git createGit() { + return git; + } + + @Override + Optional<Ref> getRepositoryHeadRef(Git git) { + return of(master); + } + }; + when(git.branchList()).thenReturn(listBranchCommand); + } + + @Test + void shouldCreateEmptyListWithoutBranches() throws IOException, GitAPIException { + when(listBranchCommand.call()).thenReturn(emptyList()); + + List<Branch> branches = branchesCommand.getBranches(); + + assertThat(branches).isEmpty(); + } + + @Test + void shouldMapNormalBranch() throws IOException, GitAPIException { + Ref branch = createRef("branch", "1337"); + when(listBranchCommand.call()).thenReturn(asList(branch)); + + List<Branch> branches = branchesCommand.getBranches(); + + assertThat(branches).containsExactly(Branch.normalBranch("branch", "1337")); + } + + @Test + void shouldMarkMasterBranchWithMasterFromConfig() throws IOException, GitAPIException { + Ref branch = createRef("branch", "1337"); + when(listBranchCommand.call()).thenReturn(asList(branch)); + when(gitRepositoryConfig.getDefaultBranch()).thenReturn("branch"); + + List<Branch> branches = branchesCommand.getBranches(); + + assertThat(branches).containsExactlyInAnyOrder(Branch.defaultBranch("branch", "1337")); + } + + @Test + void shouldMarkMasterBranchWithMasterFromHead() throws IOException, GitAPIException { + Ref branch = createRef("branch", "1337"); + when(listBranchCommand.call()).thenReturn(asList(branch, master)); + + List<Branch> branches = branchesCommand.getBranches(); + + assertThat(branches).containsExactlyInAnyOrder( + Branch.normalBranch("branch", "1337"), + Branch.defaultBranch("master", "0000") + ); + } + + private Ref createRef(String branchName, String revision) { + Ref ref = mock(Ref.class); + lenient().when(ref.getName()).thenReturn("refs/heads/" + branchName); + ObjectId objectId = mock(ObjectId.class); + lenient().when(objectId.name()).thenReturn(revision); + lenient().when(ref.getObjectId()).thenReturn(objectId); + return ref; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 727034b9ca..4b854f6209 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -6,13 +6,13 @@ * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE @@ -26,170 +26,113 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import org.junit.Test; - import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.GitRepositoryConfig; import java.io.IOException; +import java.util.Collection; -import java.util.List; -import sonia.scm.repository.GitConstants; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** * Unit tests for {@link GitBrowseCommand}. - * + * * @author Sebastian Sdorra */ -public class GitBrowseCommandTest extends AbstractGitCommandTestBase -{ - - /** - * Test browse command with default branch. - * - * @throws IOException - * @throws RepositoryException - */ +public class GitBrowseCommandTest extends AbstractGitCommandTestBase { + @Test - public void testDefaultBranch() throws IOException, RepositoryException { - // without default branch, the repository head should be used - BrowserResult result = createCommand().getBrowserResult(new BrowseCommandRequest()); - assertNotNull(result); - - List<FileObject> foList = result.getFiles(); - assertNotNull(foList); - assertFalse(foList.isEmpty()); - assertEquals(4, foList.size()); - - assertEquals("a.txt", foList.get(0).getName()); - assertEquals("b.txt", foList.get(1).getName()); - assertEquals("c", foList.get(2).getName()); - assertEquals("f.txt", foList.get(3).getName()); - - // set default branch and fetch again - repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); - result = createCommand().getBrowserResult(new BrowseCommandRequest()); - assertNotNull(result); - - foList = result.getFiles(); - assertNotNull(foList); - assertFalse(foList.isEmpty()); - assertEquals(2, foList.size()); - - assertEquals("a.txt", foList.get(0).getName()); - assertEquals("c", foList.get(1).getName()); + public void testDefaultBranch() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setPath("a.txt"); + BrowserResult result = createCommand().getBrowserResult(request); + FileObject fileObject = result.getFile(); + assertEquals("a.txt", fileObject.getName()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testBrowse() throws IOException, RepositoryException - { - BrowserResult result = - createCommand().getBrowserResult(new BrowseCommandRequest()); - - assertNotNull(result); - - List<FileObject> foList = result.getFiles(); + public void testDefaultDefaultBranch() throws IOException { + // without default branch, the repository head should be used + FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile(); + assertNotNull(root); + Collection<FileObject> foList = root.getChildren(); assertNotNull(foList); assertFalse(foList.isEmpty()); - assertEquals(4, foList.size()); - FileObject a = null; - FileObject c = null; + assertThat(foList) + .extracting("name") + .containsExactly("a.txt", "b.txt", "c", "f.txt"); + } - for (FileObject f : foList) - { - if ("a.txt".equals(f.getName())) - { - a = f; - } - else if ("c".equals(f.getName())) - { - c = f; - } - } + @Test + public void testExplicitDefaultBranch() throws IOException { + createContext().setConfig(new GitRepositoryConfig("test-branch")); + + FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile(); + assertNotNull(root); + + Collection<FileObject> foList = root.getChildren(); + assertThat(foList) + .extracting("name") + .containsExactly("a.txt", "c"); + } + + @Test + public void testBrowse() throws IOException { + FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile(); + assertNotNull(root); + + Collection<FileObject> foList = root.getChildren(); + + FileObject a = findFile(foList, "a.txt"); + FileObject c = findFile(foList, "c"); - assertNotNull(a); assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); assertEquals("a.txt", a.getPath()); assertEquals("added new line for blame", a.getDescription()); assertTrue(a.getLength() > 0); checkDate(a.getLastModified()); - assertNotNull(c); + assertTrue(c.isDirectory()); assertEquals("c", c.getName()); assertEquals("c", c.getPath()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testBrowseSubDirectory() throws IOException, RepositoryException - { + public void testBrowseSubDirectory() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setPath("c"); - BrowserResult result = createCommand().getBrowserResult(request); + FileObject root = createCommand().getBrowserResult(request).getFile(); - assertNotNull(result); + Collection<FileObject> foList = root.getChildren(); - List<FileObject> foList = result.getFiles(); + assertThat(foList).hasSize(2); - assertNotNull(foList); - assertFalse(foList.isEmpty()); - assertEquals(2, foList.size()); + FileObject d = findFile(foList, "d.txt"); + FileObject e = findFile(foList, "e.txt"); - FileObject d = null; - FileObject e = null; - - for (FileObject f : foList) - { - if ("d.txt".equals(f.getName())) - { - d = f; - } - else if ("e.txt".equals(f.getName())) - { - e = f; - } - } - - assertNotNull(d); assertFalse(d.isDirectory()); assertEquals("d.txt", d.getName()); assertEquals("c/d.txt", d.getPath()); assertEquals("added file d and e in folder c", d.getDescription()); assertTrue(d.getLength() > 0); checkDate(d.getLastModified()); - assertNotNull(e); + assertFalse(e.isDirectory()); assertEquals("e.txt", e.getName()); assertEquals("c/e.txt", e.getPath()); @@ -198,39 +141,36 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase checkDate(e.getLastModified()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testRecusive() throws IOException, RepositoryException - { + public void testRecusive() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setRecursive(true); - BrowserResult result = createCommand().getBrowserResult(request); + FileObject root = createCommand().getBrowserResult(request).getFile(); - assertNotNull(result); + Collection<FileObject> foList = root.getChildren(); - List<FileObject> foList = result.getFiles(); + assertThat(foList) + .extracting("name") + .containsExactly("a.txt", "b.txt", "c", "f.txt"); - assertNotNull(foList); - assertFalse(foList.isEmpty()); - assertEquals(5, foList.size()); + FileObject c = findFile(foList, "c"); + + Collection<FileObject> cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); } - /** - * Method description - * - * - * @return - */ - private GitBrowseCommand createCommand() - { - return new GitBrowseCommand(createContext(), repository); + private FileObject findFile(Collection<FileObject> foList, String name) { + return foList.stream() + .filter(f -> name.equals(f.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("file " + name + " not found")); + } + + private GitBrowseCommand createCommand() { + return new GitBrowseCommand(createContext(), repository, null); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java index ede6a53429..eea8bc0017 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java @@ -32,19 +32,25 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import sonia.scm.NotFoundException; +import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.store.Blob; +import sonia.scm.store.BlobStore; +import sonia.scm.web.lfs.LfsBlobStoreFactory; -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import sonia.scm.repository.GitConstants; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for {@link GitCatCommand}. @@ -53,17 +59,13 @@ import sonia.scm.repository.GitConstants; * * @author Sebastian Sdorra */ -public class GitCatCommandTest extends AbstractGitCommandTestBase -{ - - /** - * Tests cat command with default branch. - * - * @throws IOException - * @throws RepositoryException - */ +public class GitCatCommandTest extends AbstractGitCommandTestBase { + + @Rule + public final ExpectedException expectedException = ExpectedException.none(); + @Test - public void testDefaultBranch() throws IOException, RepositoryException { + public void testDefaultBranch() throws IOException { // without default branch, the repository head should be used CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -71,20 +73,12 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase assertEquals("a\nline for blame", execute(request)); // set default branch for repository and check again - repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); + createContext().setConfig(new GitRepositoryConfig("test-branch")); assertEquals("a and b", execute(request)); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testCat() throws IOException, RepositoryException - { + public void testCat() throws IOException { CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -92,42 +86,103 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase assertEquals("a and b", execute(request)); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testSimpleCat() throws IOException, RepositoryException - { + public void testSimpleCat() throws IOException { CatCommandRequest request = new CatCommandRequest(); request.setPath("b.txt"); assertEquals("b", execute(request)); } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private String execute(CatCommandRequest request) - throws IOException, RepositoryException - { + @Test + public void testUnknownFile() throws IOException { + CatCommandRequest request = new CatCommandRequest(); + + request.setPath("unknown"); + + expectedException.expect(new BaseMatcher<Object>() { + @Override + public void describeTo(Description description) { + description.appendText("expected NotFoundException for path"); + } + + @Override + public boolean matches(Object item) { + return "Path".equals(((NotFoundException)item).getContext().get(0).getType()); + } + }); + + execute(request); + } + + @Test + public void testUnknownRevision() throws IOException { + CatCommandRequest request = new CatCommandRequest(); + + request.setRevision("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + request.setPath("a.txt"); + + expectedException.expect(new BaseMatcher<Object>() { + @Override + public void describeTo(Description description) { + description.appendText("expected NotFoundException for revision"); + } + + @Override + public boolean matches(Object item) { + return "Revision".equals(((NotFoundException)item).getContext().get(0).getType()); + } + }); + + execute(request); + } + + @Test + public void testSimpleStream() throws IOException { + CatCommandRequest request = new CatCommandRequest(); + request.setPath("b.txt"); + + InputStream catResultStream = new GitCatCommand(createContext(), repository, null).getCatResultStream(request); + + assertEquals('b', catResultStream.read()); + assertEquals('\n', catResultStream.read()); + assertEquals(-1, catResultStream.read()); + + catResultStream.close(); + } + + @Test + public void testLfsStream() throws IOException { + LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + BlobStore blobStore = mock(BlobStore.class); + Blob blob = mock(Blob.class); + when(lfsBlobStoreFactory.getLfsBlobStore(repository)).thenReturn(blobStore); + when(blobStore.get("d2252bd9fde1bb2ae7531b432c48262c3cbe4df4376008986980de40a7c9cf8b")) + .thenReturn(blob); + when(blob.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[]{'i', 's'})); + + CatCommandRequest request = new CatCommandRequest(); + request.setRevision("lfs-test"); + request.setPath("lfs-image.png"); + + InputStream catResultStream = new GitCatCommand(createContext(), repository, lfsBlobStoreFactory) + .getCatResultStream(request); + + assertEquals('i', catResultStream.read()); + assertEquals('s', catResultStream.read()); + + assertEquals(-1, catResultStream.read()); + + catResultStream.close(); + } + + private String execute(CatCommandRequest request) throws IOException { String content = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { - new GitCatCommand(createContext(), repository).getCatResult(request, + new GitCatCommand(createContext(), repository, null).getCatResult(request, baos); } finally diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java new file mode 100644 index 0000000000..52932e83ae --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -0,0 +1,94 @@ +package sonia.scm.repository.spi; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class GitDiffCommandTest extends AbstractGitCommandTestBase { + + public static final String DIFF_FILE_A = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..1dc60c7 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1 +1 @@\n" + + "-a\n" + + "+a and b\n"; + public static final String DIFF_FILE_B = "diff --git a/b.txt b/b.txt\n" + + "deleted file mode 100644\n" + + "index 6178079..0000000\n" + + "--- a/b.txt\n" + + "+++ /dev/null\n" + + "@@ -1 +0,0 @@\n" + + "-b\n"; + public static final String DIFF_FILE_A_MULTIPLE_REVISIONS = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1 +1,2 @@\n" + + " a\n" + + "+line for blame\n"; + public static final String DIFF_FILE_F_MULTIPLE_REVISIONS = "diff --git a/f.txt b/f.txt\n" + + "new file mode 100644\n" + + "index 0000000..6a69f92\n" + + "--- /dev/null\n" + + "+++ b/f.txt\n" + + "@@ -0,0 +1 @@\n" + + "+f\n"; + + @Test + public void diffForOneRevisionShouldCreateDiff() throws IOException { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); + assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); + } + + @Test + public void diffForOneBranchShouldCreateDiff() throws IOException { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("test-branch"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); + assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); + } + + @Test + public void diffForPathShouldCreateLimitedDiff() throws IOException { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("test-branch"); + diffCommandRequest.setPath("a.txt"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); + assertEquals(DIFF_FILE_A, output.toString()); + } + + @Test + public void diffBetweenTwoBranchesShouldCreateDiff() throws IOException { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("master"); + diffCommandRequest.setAncestorChangeset("test-branch"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); + assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS + DIFF_FILE_F_MULTIPLE_REVISIONS, output.toString()); + } + + @Test + public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() throws IOException { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("master"); + diffCommandRequest.setAncestorChangeset("test-branch"); + diffCommandRequest.setPath("a.txt"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); + assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java new file mode 100644 index 0000000000..6067356a09 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java @@ -0,0 +1,35 @@ +package sonia.scm.repository.spi; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class GitDiffCommand_DequoteOutputStreamTest { + + @Test + void shouldDequoteText() throws IOException { + String s = "diff --git \"a/file \\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 a\" \"b/file \\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 b\"\n" + + "new file mode 100644\n" + + "index 0000000..8cb0607\n" + + "--- /dev/null\n" + + "+++ \"b/\\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 \\303\\245g\\303\\260f\\303\\237\"\n" + + "@@ -0,0 +1 @@\n" + + "+String s = \"quotes shall be kept\";"; + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + GitDiffCommand.DequoteOutputStream stream = new GitDiffCommand.DequoteOutputStream(buffer); + byte[] bytes = s.getBytes(); + stream.write(bytes, 0, bytes.length); + stream.flush(); + + Assertions.assertThat(buffer.toString()).isEqualTo("diff --git a/file úüþëéåëåé a b/file úüþëéåëåé b\n" + + "new file mode 100644\n" + + "index 0000000..8cb0607\n" + + "--- /dev/null\n" + + "+++ b/úüþëéåëåé ågðfß\n" + + "@@ -0,0 +1 @@\n" + + "+String s = \"quotes shall be kept\";"); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java new file mode 100644 index 0000000000..f359ae987c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java @@ -0,0 +1,89 @@ +package sonia.scm.repository.spi; + +import org.junit.Test; +import sonia.scm.repository.api.DiffFile; +import sonia.scm.repository.api.DiffResult; +import sonia.scm.repository.api.Hunk; + +import java.io.IOException; +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GitDiffResultCommandTest extends AbstractGitCommandTestBase { + + @Test + public void shouldReturnOldAndNewRevision() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + + assertThat(diffResult.getNewRevision()).isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + assertThat(diffResult.getOldRevision()).isEqualTo("592d797cd36432e591416e8b2b98154f4f163411"); + } + + @Test + public void shouldReturnFilePaths() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator<DiffFile> iterator = diffResult.iterator(); + DiffFile a = iterator.next(); + assertThat(a.getNewPath()).isEqualTo("a.txt"); + assertThat(a.getOldPath()).isEqualTo("a.txt"); + + DiffFile b = iterator.next(); + assertThat(b.getOldPath()).isEqualTo("b.txt"); + assertThat(b.getNewPath()).isEqualTo("/dev/null"); + } + + @Test + public void shouldReturnFileRevisions() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator<DiffFile> iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + assertThat(a.getOldRevision()).isEqualTo("78981922613b2afb6025042ff6bd878ac1994e85"); + assertThat(a.getNewRevision()).isEqualTo("1dc60c7504f4326bc83b9b628c384ec8d7e57096"); + + DiffFile b = iterator.next(); + assertThat(b.getOldRevision()).isEqualTo("61780798228d17af2d34fce4cfbdf35556832472"); + assertThat(b.getNewRevision()).isEqualTo("0000000000000000000000000000000000000000"); + } + + @Test + public void shouldReturnFileHunks() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator<DiffFile> iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + Iterator<Hunk> hunks = a.iterator(); + + Hunk hunk = hunks.next(); + assertThat(hunk.getOldStart()).isEqualTo(1); + assertThat(hunk.getOldLineCount()).isEqualTo(1); + + assertThat(hunk.getNewStart()).isEqualTo(1); + assertThat(hunk.getNewLineCount()).isEqualTo(1); + } + + @Test + public void shouldReturnFileHunksWithFullFileRange() throws IOException { + DiffResult diffResult = createDiffResult("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); + Iterator<DiffFile> iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + Iterator<Hunk> hunks = a.iterator(); + + Hunk hunk = hunks.next(); + assertThat(hunk.getOldStart()).isEqualTo(1); + assertThat(hunk.getOldLineCount()).isEqualTo(1); + + assertThat(hunk.getNewStart()).isEqualTo(1); + assertThat(hunk.getNewLineCount()).isEqualTo(2); + } + + private DiffResult createDiffResult(String s) throws IOException { + GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision(s); + + return gitDiffResultCommand.getDiffResult(diffCommandRequest); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java new file mode 100644 index 0000000000..e3f09ce5ef --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java @@ -0,0 +1,170 @@ +package sonia.scm.repository.spi; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.Iterator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class GitHunkParserTest { + + private static final String DIFF_001 = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1 +1,2 @@\n" + + " a\n" + + "+added line\n"; + + private static final String DIFF_002 = "diff --git a/file b/file\n" + + "index 5e89957..e8823e1 100644\n" + + "--- a/file\n" + + "+++ b/file\n" + + "@@ -2,6 +2,9 @@\n" + + " 2\n" + + " 3\n" + + " 4\n" + + "+5\n" + + "+6\n" + + "+7\n" + + " 8\n" + + " 9\n" + + " 10\n" + + "@@ -15,14 +18,13 @@\n" + + " 18\n" + + " 19\n" + + " 20\n" + + "+21\n" + + "+22\n" + + " 23\n" + + " 24\n" + + " 25\n" + + " 26\n" + + " 27\n" + + "-a\n" + + "-b\n" + + "-c\n" + + " 28\n" + + " 29\n" + + " 30"; + + private static final String DIFF_003 = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1,2 +1 @@\n" + + " a\n" + + "-removed line\n"; + + private static final String ILLEGAL_DIFF = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1,2 +1 @@\n" + + " a\n" + + "~illegal line\n"; + + private static final String NO_NEWLINE_DIFF = "diff --git a/.editorconfig b/.editorconfig\n" + + "index ea2a3ba..2f02f32 100644\n" + + "--- a/.editorconfig\n" + + "+++ b/.editorconfig\n" + + "@@ -10,3 +10,4 @@\n" + + " indent_style = space\n" + + " indent_size = 2\n" + + " charset = utf-8\n" + + "+added line\n" + + "\\ No newline at end of file\n"; + + @Test + void shouldParseHunks() { + List<Hunk> hunks = new GitHunkParser().parse(DIFF_001); + assertThat(hunks).hasSize(1); + assertHunk(hunks.get(0), 1, 1, 1, 2); + } + + @Test + void shouldParseMultipleHunks() { + List<Hunk> hunks = new GitHunkParser().parse(DIFF_002); + + assertThat(hunks).hasSize(2); + assertHunk(hunks.get(0), 2, 6, 2, 9); + assertHunk(hunks.get(1), 15, 14, 18, 13); + } + + @Test + void shouldParseAddedHunkLines() { + List<Hunk> hunks = new GitHunkParser().parse(DIFF_001); + + Hunk hunk = hunks.get(0); + + Iterator<DiffLine> lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(1); + assertThat(line1.getNewLineNumber()).hasValue(1); + assertThat(line1.getContent()).isEqualTo("a"); + + DiffLine line2 = lines.next(); + assertThat(line2.getOldLineNumber()).isEmpty(); + assertThat(line2.getNewLineNumber()).hasValue(2); + assertThat(line2.getContent()).isEqualTo("added line"); + } + + @Test + void shouldParseRemovedHunkLines() { + List<Hunk> hunks = new GitHunkParser().parse(DIFF_003); + + Hunk hunk = hunks.get(0); + + Iterator<DiffLine> lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(1); + assertThat(line1.getNewLineNumber()).hasValue(1); + assertThat(line1.getContent()).isEqualTo("a"); + + DiffLine line2 = lines.next(); + assertThat(line2.getOldLineNumber()).hasValue(2); + assertThat(line2.getNewLineNumber()).isEmpty(); + assertThat(line2.getContent()).isEqualTo("removed line"); + } + + @Test + void shouldFailForIllegalLine() { + assertThrows(IllegalStateException.class, () -> new GitHunkParser().parse(ILLEGAL_DIFF)); + } + + @Test + void shouldIgnoreNoNewlineLine() { + List<Hunk> hunks = new GitHunkParser().parse(NO_NEWLINE_DIFF); + + Hunk hunk = hunks.get(0); + + Iterator<DiffLine> lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(10); + assertThat(line1.getNewLineNumber()).hasValue(10); + assertThat(line1.getContent()).isEqualTo("indent_style = space"); + + lines.next(); + lines.next(); + DiffLine lastLine = lines.next(); + assertThat(lastLine.getOldLineNumber()).isEmpty(); + assertThat(lastLine.getNewLineNumber()).hasValue(13); + assertThat(lastLine.getContent()).isEqualTo("added line"); + } + + private void assertHunk(Hunk hunk, int oldStart, int oldLineCount, int newStart, int newLineCount) { + assertThat(hunk.getOldStart()).isEqualTo(oldStart); + assertThat(hunk.getOldLineCount()).isEqualTo(oldLineCount); + + assertThat(hunk.getNewStart()).isEqualTo(newStart); + assertThat(hunk.getNewLineCount()).isEqualTo(newLineCount); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java index 7cfe33f858..fc5c9c9fc2 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java @@ -36,18 +36,19 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.revwalk.RevCommit; - +import org.junit.Ignore; import org.junit.Test; - +import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.Repository; +import sonia.scm.store.InMemoryConfigurationStoreFactory; import java.io.IOException; -import org.junit.Ignore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -63,11 +64,10 @@ public class GitIncomingCommandTest * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test public void testGetIncomingChangesets() - throws IOException, GitAPIException, RepositoryException + throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); @@ -97,17 +97,16 @@ public class GitIncomingCommandTest * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test public void testGetIncomingChangesetsWithAllreadyPullChangesets() - throws IOException, GitAPIException, RepositoryException + throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); commit(outgoing, "added a"); - GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory), incomgingRepository); + GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, null, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), incomingRepository); PullCommandRequest req = new PullCommandRequest(); req.setRemoteRepository(outgoingRepository); pull.pull(req); @@ -134,11 +133,10 @@ public class GitIncomingCommandTest * * * @throws IOException - * @throws RepositoryException */ @Test public void testGetIncomingChangesetsWithEmptyRepository() - throws IOException, RepositoryException + throws IOException { GitIncomingCommand cmd = createCommand(); IncomingCommandRequest request = new IncomingCommandRequest(); @@ -158,12 +156,11 @@ public class GitIncomingCommandTest * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test @Ignore public void testGetIncomingChangesetsWithUnrelatedRepository() - throws IOException, RepositoryException, GitAPIException + throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); @@ -193,7 +190,7 @@ public class GitIncomingCommandTest */ private GitIncomingCommand createCommand() { - return new GitIncomingCommand(handler, new GitContext(incomingDirectory), - incomgingRepository); + return new GitIncomingCommand(handler, new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), + this.incomingRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandAncestorTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandAncestorTest.java new file mode 100644 index 0000000000..a2bfa1bd36 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandAncestorTest.java @@ -0,0 +1,125 @@ + +/** + * 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.spi; + +import org.junit.Test; +import sonia.scm.NotFoundException; +import sonia.scm.repository.ChangesetPagingResult; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Unit tests for {@link GitLogCommand} with an ancestor commit. This test uses the following git repository: + * + * <pre> + * * 86e9ca0 (HEAD -> b) b5 + * * d69edb3 Merge branch 'master' into b + * |\ + * | * 946a8db (master) f + * | * b19b9cc e + * * | 3d6109c b4 + * * | 6330653 b3 + * * | a49a28e Merge branch 'master' into b + * |\ \ + * | |/ + * | * 0235584 d + * | * 20251c5 c + * * | 5023b85 b2 + * * | 201ecc1 b1 + * |/ + * * 36b19e4 b + * * c2190a9 a + * </pre> + * @author Sebastian Sdorra + */ +public class GitLogCommandAncestorTest extends AbstractGitCommandTestBase +{ + @Override + protected String getZippedRepositoryResource() + { + return "sonia/scm/repository/spi/scm-git-ancestor-test.zip"; + } + + @Test + public void testGetAncestor() + { + LogCommandRequest request = new LogCommandRequest(); + + request.setBranch("b"); + request.setAncestorChangeset("master"); + + ChangesetPagingResult result = createCommand().getChangesets(request); + + assertNotNull(result); + assertEquals(7, result.getTotal()); + assertEquals(7, result.getChangesets().size()); + + assertEquals("86e9ca012202b36865373a63c12ef4f4353506cd", result.getChangesets().get(0).getId()); + assertEquals("d69edb314d07ab20ad626e3101597702d3510b5d", result.getChangesets().get(1).getId()); + assertEquals("3d6109c4c830e91eaf12ac6a331a5fccd670fe3c", result.getChangesets().get(2).getId()); + assertEquals("63306538d06924d6b254f86541c638021c001141", result.getChangesets().get(3).getId()); + assertEquals("a49a28e0beb0ab55f985598d05b8628c2231c9b6", result.getChangesets().get(4).getId()); + assertEquals("5023b850c2077db857593a3c0269329c254a370d", result.getChangesets().get(5).getId()); + assertEquals("201ecc1131e6b99fb0a0fe9dcbc8c044383e1a07", result.getChangesets().get(6).getId()); + } + + @Test(expected = NotFoundException.class) + public void testAncestorWithDeletedSourceBranch() + { + LogCommandRequest request = new LogCommandRequest(); + + request.setBranch("no_such_branch"); + request.setAncestorChangeset("master"); + + createCommand().getChangesets(request); + } + + @Test(expected = NotFoundException.class) + public void testAncestorWithDeletedAncestorBranch() + { + LogCommandRequest request = new LogCommandRequest(); + + request.setBranch("b"); + request.setAncestorChangeset("no_such_branch"); + + createCommand().getChangesets(request); + } + + private GitLogCommand createCommand() + { + return new GitLogCommand(createContext(), repository); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java index c5af70e3a9..71dac42f72 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java @@ -1,3 +1,4 @@ + /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. @@ -33,42 +34,45 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import com.google.common.io.Files; +import org.assertj.core.api.Assertions; import org.junit.Test; - +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.Modifications; -import sonia.scm.repository.RepositoryException; - -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import java.io.File; import java.io.IOException; -import org.eclipse.jgit.api.errors.GitAPIException; -import sonia.scm.repository.GitConstants; + +import static java.nio.charset.Charset.defaultCharset; +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; /** * Unit tests for {@link GitLogCommand}. - * + * * @author Sebastian Sdorra */ +@RunWith(MockitoJUnitRunner.class) public class GitLogCommandTest extends AbstractGitCommandTestBase { + @Mock + LogCommandRequest request; /** * Tests log command with the usage of a default branch. - * - * @throws IOException - * @throws GitAPIException - * @throws RepositoryException */ @Test - public void testGetDefaultBranch() throws IOException, GitAPIException, RepositoryException { + public void testGetDefaultBranch() { // without default branch, the repository head should be used ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); @@ -78,28 +82,25 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", result.getChangesets().get(1).getId()); assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(2).getId()); assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(3).getId()); - + assertEquals("master", result.getBranchName()); + assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty())); + // set default branch and fetch again - repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); - + createContext().setConfig(new GitRepositoryConfig("test-branch")); + result = createCommand().getChangesets(new LogCommandRequest()); assertNotNull(result); + assertEquals("test-branch", result.getBranchName()); assertEquals(3, result.getTotal()); assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", result.getChangesets().get(0).getId()); assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(1).getId()); assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(2).getId()); + assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty())); } - - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ + @Test - public void testGetAll() throws IOException, RepositoryException + public void testGetAll() { ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); @@ -109,15 +110,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals(4, result.getChangesets().size()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllByPath() throws IOException, RepositoryException + public void testGetAllByPath() { LogCommandRequest request = new LogCommandRequest(); @@ -132,15 +126,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(1).getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllWithLimit() throws IOException, RepositoryException + public void testGetAllWithLimit() { LogCommandRequest request = new LogCommandRequest(); @@ -163,15 +150,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", c2.getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllWithPaging() throws IOException, RepositoryException + public void testGetAllWithPaging() { LogCommandRequest request = new LogCommandRequest(); @@ -195,43 +175,51 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("592d797cd36432e591416e8b2b98154f4f163411", c2.getId()); } - /** - * Method description - * - */ @Test public void testGetCommit() { GitLogCommand command = createCommand(); - Changeset c = command.getChangeset("435df2f061add3589cb3"); + Changeset c = command.getChangeset("435df2f061add3589cb3", null); assertNotNull(c); - assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c.getId()); + String revision = "435df2f061add3589cb326cc64be9b9c3897ceca"; + assertEquals(revision, c.getId()); assertEquals("added a and b files", c.getDescription()); checkDate(c.getDate()); assertEquals("Douglas Adams", c.getAuthor().getName()); assertEquals("douglas.adams@hitchhiker.com", c.getAuthor().getMail()); assertEquals("added a and b files", c.getDescription()); - Modifications mods = c.getModifications(); + GitModificationsCommand gitModificationsCommand = new GitModificationsCommand(createContext(), repository); + Modifications modifications = gitModificationsCommand.getModifications(revision); - assertNotNull(mods); - assertTrue("modified list should be empty", mods.getModified().isEmpty()); - assertTrue("removed list should be empty", mods.getRemoved().isEmpty()); - assertFalse("added list should not be empty", mods.getAdded().isEmpty()); - assertEquals(2, mods.getAdded().size()); - assertThat(mods.getAdded(), contains("a.txt", "b.txt")); + assertNotNull(modifications); + assertTrue("modified list should be empty", modifications.getModified().isEmpty()); + assertTrue("removed list should be empty", modifications.getRemoved().isEmpty()); + assertFalse("added list should not be empty", modifications.getAdded().isEmpty()); + assertEquals(2, modifications.getAdded().size()); + assertThat(modifications.getAdded(), contains("a.txt", "b.txt")); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetRange() throws IOException, RepositoryException + public void commitShouldContainBranchIfLogCommandRequestHasBranch() + { + when(request.getBranch()).thenReturn("master"); + GitLogCommand command = createCommand(); + Changeset c = command.getChangeset("435df2f061add3589cb3", request); + + Assertions.assertThat(c.getBranches()).containsOnly("master"); + } + + @Test + public void shouldNotReturnCommitFromDifferentBranch() { + when(request.getBranch()).thenReturn("master"); + Changeset changeset = createCommand().getChangeset("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", request); + Assertions.assertThat(changeset).isNull(); + } + + @Test + public void testGetRange() { LogCommandRequest request = new LogCommandRequest(); @@ -253,12 +241,52 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId()); } - /** - * Method description - * - * - * @return - */ + @Test + public void testGetAncestor() + { + LogCommandRequest request = new LogCommandRequest(); + + request.setBranch("test-branch"); + request.setAncestorChangeset("master"); + + ChangesetPagingResult result = createCommand().getChangesets(request); + + assertNotNull(result); + assertEquals(1, result.getTotal()); + assertEquals(1, result.getChangesets().size()); + + Changeset c = result.getChangesets().get(0); + + assertNotNull(c); + assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", c.getId()); + } + + @Test + public void shouldFindDefaultBranchFromHEAD() throws Exception { + setRepositoryHeadReference("ref: refs/heads/test-branch"); + + ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest()); + + assertEquals("test-branch", changesets.getBranchName()); + } + + @Test + public void shouldFindMasterBranchWhenHEADisNoRef() throws Exception { + setRepositoryHeadReference("592d797cd36432e591416e8b2b98154f4f163411"); + + ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest()); + + assertEquals("master", changesets.getBranchName()); + } + + private void setRepositoryHeadReference(String s) throws IOException { + Files.write(s, repositoryHeadReferenceFile(), defaultCharset()); + } + + private File repositoryHeadReferenceFile() { + return new File(repositoryDirectory, "HEAD"); + } + private GitLogCommand createCommand() { return new GitLogCommand(createContext(), repository); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java new file mode 100644 index 0000000000..fcd721c3a2 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -0,0 +1,367 @@ +package sonia.scm.repository.spi; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.NotFoundException; +import sonia.scm.repository.Person; +import sonia.scm.repository.api.MergeCommandResult; +import sonia.scm.repository.api.MergeStrategy; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.user.User; + +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") +public class GitMergeCommandTest extends AbstractGitCommandTestBase { + + private static final String REALM = "AdminRealm"; + + @Rule + public ShiroRule shiro = new ShiroRule(); + @Rule + public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + + @Test + public void shouldDetectMergeableBranches() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setBranchToMerge("mergeable"); + request.setTargetBranch("master"); + + boolean mergeable = command.dryRun(request).isMergeable(); + + assertThat(mergeable).isTrue(); + } + + @Test + public void shouldDetectNotMergeableBranches() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setBranchToMerge("test-branch"); + request.setTargetBranch("master"); + + boolean mergeable = command.dryRun(request).isMergeable(); + + assertThat(mergeable).isFalse(); + } + + @Test + public void shouldMergeMergeableBranches() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det"); + assertThat(message).contains("master", "mergeable"); + // We expect the merge result of file b.txt here by looking up the sha hash of its content. + // If the file is missing (aka not merged correctly) this will throw a MissingObjectException: + byte[] contentOfFileB = repository.open(repository.resolve("9513e9c76e73f3e562fd8e4c909d0607113c77c6")).getBytes(); + assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n"); + } + + @Test + public void shouldAllowEmptyMergeCommit() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("empty_merge"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + assertThat(mergeCommit.getParentCount()).isEqualTo(2); + assertThat(mergeCommit.getParent(0).name()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); + assertThat(mergeCommit.getParent(1).name()).isEqualTo("d81ad6c63d7e2162308d69637b339dedd1d9201c"); + } + + @Test + public void shouldNotMergeTwice() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + ObjectId firstMergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId(); + + MergeCommandResult secondMergeCommandResult = command.merge(request); + + assertThat(secondMergeCommandResult.isSuccess()).isTrue(); + + ObjectId secondMergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId(); + + assertThat(secondMergeCommit).isEqualTo(firstMergeCommit); + } + + @Test + public void shouldUseConfiguredCommitMessageTemplate() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setMessageTemplate("simple"); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + String message = mergeCommit.getFullMessage(); + assertThat(message).isEqualTo("simple"); + } + + @Test + public void shouldNotMergeConflictingBranches() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setBranchToMerge("test-branch"); + request.setTargetBranch("master"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isFalse(); + assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt"); + } + + @Test + public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException { + SimplePrincipalCollection principals = new SimplePrincipalCollection(); + principals.add("admin", REALM); + principals.add( new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM); + shiro.setSubject( + new Subject.Builder() + .principals(principals) + .authenticated(true) + .buildSubject()); + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable<RevCommit> mergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + PersonIdent mergeAuthor = mergeCommit.iterator().next().getAuthorIdent(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det"); + } + + @Test + public void shouldMergeIntoNotDefaultBranch() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setTargetBranch("mergeable"); + request.setBranchToMerge("master"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + + MergeCommandResult mergeCommandResult = command.merge(request); + + Repository repository = createContext().open(); + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("mergeable")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det"); + assertThat(message).contains("master", "mergeable"); + // We expect the merge result of file b.txt here by looking up the sha hash of its content. + // If the file is missing (aka not merged correctly) this will throw a MissingObjectException: + byte[] contentOfFileB = repository.open(repository.resolve("9513e9c76e73f3e562fd8e4c909d0607113c77c6")).getBytes(); + assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n"); + } + + @Test + public void shouldSquashCommitsIfSquashIsEnabled() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setBranchToMerge("squash"); + request.setTargetBranch("master"); + request.setMessageTemplate("this is a squash"); + request.setMergeStrategy(MergeStrategy.SQUASH); + + MergeCommandResult mergeCommandResult = command.merge(request); + + Repository repository = createContext().open(); + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(message).isEqualTo("this is a squash"); + } + + @Test + public void shouldSquashThreeCommitsIntoOne() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setBranchToMerge("squash"); + request.setTargetBranch("master"); + request.setMessageTemplate("squash three commits"); + request.setMergeStrategy(MergeStrategy.SQUASH); + Repository gitRepository = createContext().open(); + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Iterable<RevCommit> commits = new Git(gitRepository).log().add(gitRepository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(message).isEqualTo("squash three commits"); + + GitModificationsCommand modificationsCommand = new GitModificationsCommand(createContext(), repository); + List<String> changes = modificationsCommand.getModifications("master").getAdded(); + assertThat(changes.size()).isEqualTo(3); + } + + + @Test + public void shouldMergeWithFastForward() throws IOException, GitAPIException { + Repository repository = createContext().open(); + + ObjectId featureBranchHead = new Git(repository).log().add(repository.resolve("squash")).setMaxCount(1).call().iterator().next().getId(); + + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setBranchToMerge("squash"); + request.setTargetBranch("master"); + request.setMergeStrategy(MergeStrategy.FAST_FORWARD_IF_POSSIBLE); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + assertThat(mergeCommit.getParentCount()).isEqualTo(1); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + assertThat(mergeAuthor.getName()).isEqualTo("Philip J Fry"); + assertThat(mergeCommit.getId()).isEqualTo(featureBranchHead); + } + + @Test + public void shouldDoMergeCommitIfFastForwardIsNotPossible() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.FAST_FORWARD_IF_POSSIBLE); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + assertThat(mergeCommit.getParentCount()).isEqualTo(2); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det"); + assertThat(message).contains("master", "mergeable"); + } + + @Test(expected = NotFoundException.class) + public void shouldHandleNotExistingSourceBranchInMerge() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("mergeable"); + request.setBranchToMerge("not_existing"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + + command.merge(request); + } + + @Test(expected = NotFoundException.class) + public void shouldHandleNotExistingTargetBranchInMerge() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setTargetBranch("not_existing"); + request.setBranchToMerge("master"); + + command.merge(request); + } + + @Test(expected = NotFoundException.class) + public void shouldHandleNotExistingSourceBranchInDryRun() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("mergeable"); + request.setBranchToMerge("not_existing"); + + command.dryRun(request); + } + + @Test(expected = NotFoundException.class) + public void shouldHandleNotExistingTargetBranchInDryRun() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("not_existing"); + request.setBranchToMerge("master"); + + command.dryRun(request); + } + + private GitMergeCommand createCommand() { + return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java new file mode 100644 index 0000000000..dbb510fb7e --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java @@ -0,0 +1,126 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.repository.Modifications; + +import java.io.File; +import java.io.IOException; +import java.util.function.Consumer; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase { + + private GitModificationsCommand incomingModificationsCommand; + private GitModificationsCommand outgoingModificationsCommand; + + @Before + public void init() { + incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory, null, null), incomingRepository); + outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory, null, null), outgoingRepository); + } + + @Test + public void shouldReadAddedFiles() throws Exception { + write(outgoing, outgoingDirectory, "a.txt", "bal bla"); + RevCommit addedFileCommit = commit(outgoing, "add file"); + String revision = addedFileCommit.getName(); + Consumer<Modifications> assertModifications = assertAddedFiles("a.txt"); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + pushOutgoingAndPullIncoming(); + assertModifications.accept(incomingModificationsCommand.getModifications(revision)); + } + + @Test + public void shouldReadModifiedFiles() throws Exception { + write(outgoing, outgoingDirectory, "a.txt", "bal bla"); + commit(outgoing, "add file"); + write(outgoing, outgoingDirectory, "a.txt", "modified content"); + RevCommit modifiedFileCommit = commit(outgoing, "modify file"); + String revision = modifiedFileCommit.getName(); + Consumer<Modifications> assertModifications = assertModifiedFiles("a.txt"); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + pushOutgoingAndPullIncoming(); + assertModifications.accept(incomingModificationsCommand.getModifications(revision)); + } + + @Test + public void shouldReadRemovedFiles() throws Exception { + String fileName = "a.txt"; + write(outgoing, outgoingDirectory, fileName, "bal bla"); + commit(outgoing, "add file"); + File file = new File(outgoingDirectory, fileName); + file.delete(); + outgoing.rm().addFilepattern(fileName).call(); + RevCommit removedFileCommit = commit(outgoing, "remove file"); + String revision = removedFileCommit.getName(); + Consumer<Modifications> assertModifications = assertRemovedFiles(fileName); + pushOutgoingAndPullIncoming(); + assertModifications.accept(incomingModificationsCommand.getModifications(revision)); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + } + + void pushOutgoingAndPullIncoming() throws IOException { + GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, null, null), + outgoingRepository); + PushCommandRequest request = new PushCommandRequest(); + request.setRemoteRepository(incomingRepository); + cmd.push(request); + GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, null, null), + incomingRepository); + PullCommandRequest pullRequest = new PullCommandRequest(); + pullRequest.setRemoteRepository(incomingRepository); + pullCommand.pull(pullRequest); + } + + Consumer<Modifications> assertRemovedFiles(String fileName) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(0); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(0); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(1) + .containsOnly(fileName); + }; + } + + + Consumer<Modifications> assertModifiedFiles(String file) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(0); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(1) + .containsOnly(file); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(0); + }; + } + + Consumer<Modifications> assertAddedFiles(String file) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(1) + .containsOnly(file); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(0); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(0); + }; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java new file mode 100644 index 0000000000..37d0202d41 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -0,0 +1,309 @@ +package sonia.scm.repository.spi; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.AlreadyExistsException; +import sonia.scm.BadRequestException; +import sonia.scm.ConcurrentModificationException; +import sonia.scm.NotFoundException; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.web.lfs.LfsBlobStoreFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") +public class GitModifyCommandTest extends AbstractGitCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + @Rule + public ShiroRule shiro = new ShiroRule(); + + private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + + @Test + public void shouldCreateCommit() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + String newRef = command.execute(request); + + try (Git git = new Git(createContext().open())) { + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getFullMessage()).isEqualTo("test commit"); + assertThat(lastCommit.getAuthorIdent().getName()).isEqualTo("Dirk Gently"); + assertThat(newRef).isEqualTo(lastCommit.toObjectId().name()); + } + } + + @Test + public void shouldCreateCommitOnSelectedBranch() throws IOException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.setBranch("test-branch"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + String newRef = command.execute(request); + + ObjectId commitId = ObjectId.fromString(newRef); + try (RevWalk revWalk = new RevWalk(createContext().open())) { + RevCommit commit = revWalk.parseCommit(commitId); + assertThat(commit.getParent(0).name()).isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + } + } + + @Test + public void shouldCreateNewFile() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("new_file")).isTrue(); + + assertInTree(assertions); + } + + @Test + public void shouldCreateNewFileWhenPathStartsWithSlash() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("/new_file", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("new_file")).isTrue(); + + assertInTree(assertions); + } + + @Test(expected = AlreadyExistsException.class) + public void shouldFailIfOverwritingExistingFileWithoutOverwriteFlag() throws IOException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + + @Test(expected = AlreadyExistsException.class) + public void shouldFailIfPathAlreadyExistsAsAFile() throws IOException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt/newFile", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + + @Test + public void shouldOverwriteExistingFileIfOverwriteFlagSet() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", newFile, true)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("a.txt")).isTrue(); + + assertInTree(assertions); + } + + @Test + public void shouldModifyExistingFile() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.ModifyFileRequest("a.txt", newFile)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("a.txt")).isTrue(); + + assertInTree(assertions); + } + + @Test(expected = NotFoundException.class) + public void shouldFailIfFileToModifyDoesNotExist() throws IOException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.ModifyFileRequest("no.such.file", newFile)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + + @Test(expected = BadRequestException.class) + public void shouldFailIfNoChangesMade() throws IOException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "b\n".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("b.txt", newFile, true)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + + @Test(expected = ConcurrentModificationException.class) + public void shouldFailBranchDoesNotHaveExpectedRevision() throws IOException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "irrelevant\n".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("irrelevant", newFile, true)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setExpectedRevision("abc"); + + command.execute(request); + } + + @Test + public void shouldDeleteExistingFile() throws IOException, GitAPIException { + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt")); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("a.txt")).isFalse(); + + assertInTree(assertions); + } + + @Test(expected = NotFoundException.class) + public void shouldThrowNotFoundExceptionWhenFileToDeleteDoesNotExist() { + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("no/such/file")); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + + @Test(expected = NotFoundException.class) + public void shouldThrowNotFoundExceptionWhenBranchDoesNotExist() { + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setBranch("does-not-exist"); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt")); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + + @Test(expected = NotFoundException.class) + public void shouldFailWithNotFoundExceptionIfBranchIsNoBranch() throws IOException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "irrelevant\n".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.setBranch("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("irrelevant", newFile, true)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + + private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException { + try (Git git = new Git(createContext().open())) { + RevCommit lastCommit = getLastCommit(git); + try (RevWalk walk = new RevWalk(git.getRepository())) { + RevCommit commit = walk.parseCommit(lastCommit); + ObjectId treeId = commit.getTree().getId(); + try (ObjectReader reader = git.getRepository().newObjectReader()) { + assertions.checkAssertions(new CanonicalTreeParser(null, reader, treeId)); + } + } + } + } + + private RevCommit getLastCommit(Git git) throws GitAPIException { + return git.log().setMaxCount(1).call().iterator().next(); + } + + private GitModifyCommand createCommand() { + return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()), lfsBlobStoreFactory); + } + + @FunctionalInterface + private interface TreeAssertions { + void checkAssertions(CanonicalTreeParser treeParser) throws CorruptObjectException; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java new file mode 100644 index 0000000000..b1a5c7bbcc --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java @@ -0,0 +1,116 @@ +package sonia.scm.repository.spi; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.store.Blob; +import sonia.scm.store.BlobStore; +import sonia.scm.web.lfs.LfsBlobStoreFactory; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") +public class GitModifyCommand_LFSTest extends AbstractGitCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + @Rule + public ShiroRule shiro = new ShiroRule(); + + private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + + @Before + public void registerFilter() { + new GitLfsFilterContextListener(contextProvider).contextInitialized(null); + } + + @After + public void unregisterFilter() { + new GitLfsFilterContextListener(contextProvider).contextDestroyed(null); + } + + @Test + public void shouldCreateCommit() throws IOException, GitAPIException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + String newRef = createCommit("new_lfs.png", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", outputStream); + + try (Git git = new Git(createContext().open())) { + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getFullMessage()).isEqualTo("test commit"); + assertThat(lastCommit.getAuthorIdent().getName()).isEqualTo("Dirk Gently"); + assertThat(newRef).isEqualTo(lastCommit.toObjectId().name()); + } + + assertThat(outputStream.toString()).isEqualTo("new content"); + } + + @Test + public void shouldCreateSecondCommits() throws IOException, GitAPIException { + createCommit("new_lfs.png", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", new ByteArrayOutputStream()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + String newRef = createCommit("more_lfs.png", "more content", "2c2316737c9313956dfc0083da3a2a62ce259f66484f3e26440f0d1b02dd4128", outputStream); + + try (Git git = new Git(createContext().open())) { + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getFullMessage()).isEqualTo("test commit"); + assertThat(lastCommit.getAuthorIdent().getName()).isEqualTo("Dirk Gently"); + assertThat(newRef).isEqualTo(lastCommit.toObjectId().name()); + } + + assertThat(outputStream.toString()).isEqualTo("more content"); + } + + private String createCommit(String fileName, String content, String hashOfContent, ByteArrayOutputStream outputStream) throws IOException { + BlobStore blobStore = mock(BlobStore.class); + Blob blob = mock(Blob.class); + when(lfsBlobStoreFactory.getLfsBlobStore(any())).thenReturn(blobStore); + when(blobStore.create(hashOfContent)).thenReturn(blob); + when(blobStore.get(hashOfContent)).thenReturn(null, blob); + when(blob.getOutputStream()).thenReturn(outputStream); + when(blob.getSize()).thenReturn((long) content.length()); + + File newFile = Files.write(temporaryFolder.newFile().toPath(), content.getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest(fileName, newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + return command.execute(request); + } + + private RevCommit getLastCommit(Git git) throws GitAPIException { + return git.log().setMaxCount(1).call().iterator().next(); + } + + private GitModifyCommand createCommand() { + return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()), lfsBlobStoreFactory); + } + + @Override + protected String getZippedRepositoryResource() { + return "sonia/scm/repository/spi/scm-git-spi-lfs-test.zip"; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java new file mode 100644 index 0000000000..443eedb14a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java @@ -0,0 +1,87 @@ +package sonia.scm.repository.spi; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.web.lfs.LfsBlobStoreFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") +public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + @Rule + public ShiroRule shiro = new ShiroRule(); + + private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + + @Test + public void shouldCreateNewFileInEmptyRepository() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("new_file")).isTrue(); + + assertInTree(assertions); + } + + @Override + protected String getZippedRepositoryResource() { + return "sonia/scm/repository/spi/scm-git-empty-repo.zip"; + } + + private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException { + try (Git git = new Git(createContext().open())) { + RevCommit lastCommit = getLastCommit(git); + try (RevWalk walk = new RevWalk(git.getRepository())) { + RevCommit commit = walk.parseCommit(lastCommit); + ObjectId treeId = commit.getTree().getId(); + try (ObjectReader reader = git.getRepository().newObjectReader()) { + assertions.checkAssertions(new CanonicalTreeParser(null, reader, treeId)); + } + } + } + } + + private RevCommit getLastCommit(Git git) throws GitAPIException { + return git.log().setMaxCount(1).call().iterator().next(); + } + + private GitModifyCommand createCommand() { + return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()), lfsBlobStoreFactory); + } + + @FunctionalInterface + private interface TreeAssertions { + void checkAssertions(CanonicalTreeParser treeParser) throws CorruptObjectException; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java index 28be6f7df0..158973e710 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java @@ -37,19 +37,19 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.revwalk.RevCommit; - import org.junit.Test; - +import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.store.InMemoryConfigurationStoreFactory; + +import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; //~--- JDK imports ------------------------------------------------------------ -import java.io.IOException; - /** * Unit tests for {@link OutgoingCommand}. * @@ -64,11 +64,10 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test public void testGetOutgoingChangesets() - throws IOException, GitAPIException, RepositoryException + throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); @@ -81,7 +80,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase GitOutgoingCommand cmd = createCommand(); OutgoingCommandRequest request = new OutgoingCommandRequest(); - request.setRemoteRepository(incomgingRepository); + request.setRemoteRepository(incomingRepository); ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request); @@ -98,22 +97,21 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test - public void testGetOutgoingChangesetsWithAllreadyPushedChanges() - throws IOException, GitAPIException, RepositoryException + public void testGetOutgoingChangesetsWithAlreadyPushedChanges() + throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); commit(outgoing, "added a"); GitPushCommand push = new GitPushCommand(handler, - new GitContext(outgoingDirectory), + new GitContext(outgoingDirectory, null, null), outgoingRepository); PushCommandRequest req = new PushCommandRequest(); - req.setRemoteRepository(incomgingRepository); + req.setRemoteRepository(incomingRepository); push.push(req); write(outgoing, outgoingDirectory, "b.txt", "content of b.txt"); @@ -123,7 +121,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase GitOutgoingCommand cmd = createCommand(); OutgoingCommandRequest request = new OutgoingCommandRequest(); - request.setRemoteRepository(incomgingRepository); + request.setRemoteRepository(incomingRepository); ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request); @@ -138,16 +136,15 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase * * * @throws IOException - * @throws RepositoryException */ @Test public void testGetOutgoingChangesetsWithEmptyRepository() - throws IOException, RepositoryException + throws IOException { GitOutgoingCommand cmd = createCommand(); OutgoingCommandRequest request = new OutgoingCommandRequest(); - request.setRemoteRepository(incomgingRepository); + request.setRemoteRepository(incomingRepository); ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request); @@ -164,7 +161,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase */ private GitOutgoingCommand createCommand() { - return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory), + return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, outgoingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), outgoingRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java index 0eb23572fc..6aa831ec60 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java @@ -37,20 +37,17 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.revwalk.RevCommit; - import org.junit.Test; - -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.PushResponse; -import static org.junit.Assert.*; +import java.io.IOException; +import java.util.Iterator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; //~--- JDK imports ------------------------------------------------------------ -import java.io.IOException; - -import java.util.Iterator; - /** * * @author Sebastian Sdorra @@ -64,11 +61,10 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test public void testPush() - throws IOException, GitAPIException, RepositoryException + throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); @@ -81,12 +77,12 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase GitPushCommand cmd = createCommand(); PushCommandRequest request = new PushCommandRequest(); - request.setRemoteRepository(incomgingRepository); + request.setRemoteRepository(incomingRepository); PushResponse response = cmd.push(request); assertNotNull(response); - assertEquals(2L, response.getChangesetCount()); + assertEquals(2l, response.getChangesetCount()); Iterator<RevCommit> commits = incoming.log().call().iterator(); @@ -102,7 +98,7 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase */ private GitPushCommand createCommand() { - return new GitPushCommand(handler, new GitContext(outgoingDirectory), + return new GitPushCommand(handler, new GitContext(outgoingDirectory, null, null), outgoingRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java new file mode 100644 index 0000000000..d61a48508d --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java @@ -0,0 +1,97 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ScmTransportProtocol; +import org.eclipse.jgit.transport.Transport; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.PreProcessorUtil; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; + +import static com.google.inject.util.Providers.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + // keep this so that it will not be garbage collected (Transport keeps this in a week reference) + private ScmTransportProtocol proto; + private WorkdirProvider workdirProvider; + + @Before + public void bindScmProtocol() throws IOException { + HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); + HookEventFacade hookEventFacade = new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory); + GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); + proto = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler)); + Transport.register(proto); + workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); + } + + @Test + public void emptyPoolShouldCreateNewWorkdir() { + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + File masterRepo = createRepositoryDirectory(); + + try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) { + + assertThat(workingCopy.getDirectory()) + .exists() + .isNotEqualTo(masterRepo) + .isDirectory(); + assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt")) + .exists() + .isFile() + .hasContent("a\nline for blame"); + } + } + + @Test + public void shouldCheckoutInitialBranch() { + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + + try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), "test-branch")) { + assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt")) + .exists() + .isFile() + .hasContent("a and b"); + } + } + + @Test + public void cloneFromPoolShouldNotBeReused() { + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + + File firstDirectory; + try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) { + firstDirectory = workingCopy.getDirectory(); + } + try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) { + File secondDirectory = workingCopy.getDirectory(); + assertThat(secondDirectory).isNotEqualTo(firstDirectory); + } + } + + @Test + public void cloneFromPoolShouldBeDeletedOnClose() { + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + + File directory; + try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) { + directory = workingCopy.getWorkingRepository().getWorkTree(); + } + assertThat(directory).doesNotExist(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java index d667290197..831402261b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java @@ -3,9 +3,9 @@ package sonia.scm.web; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.config.ScmConfiguration; -import sonia.scm.repository.RepositoryProvider; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.util.HttpUtil; import javax.servlet.ServletOutputStream; @@ -17,7 +17,9 @@ import java.io.IOException; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Unit tests for {@link GitPermissionFilter}. @@ -27,12 +29,7 @@ import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class GitPermissionFilterTest { - @Mock - private RepositoryProvider repositoryProvider; - - private final GitPermissionFilter permissionFilter = new GitPermissionFilter( - new ScmConfiguration(), repositoryProvider - ); + private final GitPermissionFilter permissionFilter = new GitPermissionFilter(new ScmConfiguration(), mock(ScmProviderHttpServlet.class)); @Mock private HttpServletResponse response; @@ -69,7 +66,6 @@ public class GitPermissionFilterTest { when(mock.getMethod()).thenReturn(method); when(mock.getRequestURI()).thenReturn(requestURI); - when(mock.getContextPath()).thenReturn("/scm"); return mock; } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitRepositoryResolverTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitRepositoryResolverTest.java deleted file mode 100644 index d41e0acafc..0000000000 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitRepositoryResolverTest.java +++ /dev/null @@ -1,109 +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; - -import java.io.File; -import java.io.IOException; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Before; -import org.junit.Rule; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import static org.mockito.Mockito.*; -import org.mockito.runners.MockitoJUnitRunner; -import sonia.scm.repository.GitConfig; -import sonia.scm.repository.GitRepositoryHandler; - -/** - * Unit tests for {@link GitRepositoryResolver}. - * - * @author Sebastian Sdorra - */ -@RunWith(MockitoJUnitRunner.class) -public class GitRepositoryResolverTest { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private File parentDirectory; - - @Mock - private GitRepositoryHandler handler; - - @InjectMocks - private GitRepositoryResolver resolver; - - @Before - public void setUp() throws IOException { - parentDirectory = temporaryFolder.newFolder(); - - GitConfig config = new GitConfig(); - config.setRepositoryDirectory(parentDirectory); - - when(handler.getConfig()).thenReturn(config); - } - - @Test - public void testFindRepositoryWithoutDotGit() { - createRepositories("a", "ab"); - - File directory = resolver.findRepository(parentDirectory, "a"); - assertNotNull(directory); - assertEquals("a", directory.getName()); - - directory = resolver.findRepository(parentDirectory, "ab"); - assertNotNull(directory); - assertEquals("ab", directory.getName()); - } - - @Test - public void testFindRepositoryWithDotGit() { - createRepositories("a", "ab"); - - File directory = resolver.findRepository(parentDirectory, "a.git"); - assertNotNull(directory); - assertEquals("a", directory.getName()); - - directory = resolver.findRepository(parentDirectory, "ab.git"); - assertNotNull(directory); - assertEquals("ab", directory.getName()); - } - - private void createRepositories(String... names) { - for (String name : names) { - assertTrue(new File(parentDirectory, name).mkdirs()); - } - } - -} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LFSAuthCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LFSAuthCommandTest.java new file mode 100644 index 0000000000..1c3d55bc91 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LFSAuthCommandTest.java @@ -0,0 +1,92 @@ +package sonia.scm.web.lfs; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.protocolcommand.CommandContext; +import sonia.scm.protocolcommand.CommandInterpreter; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.protocolcommand.git.GitRepositoryContextResolver; +import sonia.scm.repository.Repository; +import sonia.scm.security.AccessToken; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Date; +import java.util.Optional; + +import static java.time.Instant.parse; +import static java.util.Date.from; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class LFSAuthCommandTest { + + static final Repository REPOSITORY = new Repository("1", "git", "space", "X"); + static final Date EXPIRATION = from(parse("2007-05-03T10:15:30.00Z")); + + @Mock + LfsAccessTokenFactory tokenFactory; + @Mock + GitRepositoryContextResolver gitRepositoryContextResolver; + @Mock + ScmConfiguration configuration; + + @InjectMocks + LFSAuthCommand lfsAuthCommand; + + @BeforeEach + void initAuthorizationToken() { + AccessToken accessToken = mock(AccessToken.class); + lenient().when(this.tokenFactory.createReadAccessToken(REPOSITORY)).thenReturn(accessToken); + lenient().when(this.tokenFactory.createWriteAccessToken(REPOSITORY)).thenReturn(accessToken); + lenient().when(accessToken.getExpiration()).thenReturn(EXPIRATION); + lenient().when(accessToken.compact()).thenReturn("ACCESS_TOKEN"); + } + + @BeforeEach + void initConfig() { + lenient().when(configuration.getBaseUrl()).thenReturn("http://example.com"); + } + + @Test + void shouldHandleGitLfsAuthenticate() { + Optional<CommandInterpreter> commandInterpreter = lfsAuthCommand.canHandle("git-lfs-authenticate repo/space/X upload"); + assertThat(commandInterpreter).isPresent(); + } + + @Test + void shouldNotHandleOtherCommands() { + Optional<CommandInterpreter> commandInterpreter = lfsAuthCommand.canHandle("git-lfs-something repo/space/X upload"); + assertThat(commandInterpreter).isEmpty(); + } + + @Test + void shouldExtractRepositoryArgument() { + CommandInterpreter commandInterpreter = lfsAuthCommand.canHandle("git-lfs-authenticate repo/space/X\t upload").get(); + assertThat(commandInterpreter.getParsedArgs()).containsOnly("repo/space/X"); + } + + @Test + void shouldCreateJsonResponse() throws IOException { + CommandInterpreter commandInterpreter = lfsAuthCommand.canHandle("git-lfs-authenticate repo/space/X\t upload").get(); + CommandContext commandContext = createCommandContext(); + commandInterpreter.getProtocolHandler().handle(commandContext, createRepositoryContext()); + assertThat(commandContext.getOutputStream().toString()) + .isEqualTo("{\"href\":\"http://example.com/repo/space/X.git/info/lfs/\",\"header\":{\"Authorization\":\"Bearer ACCESS_TOKEN\"},\"expires_at\":\"2007-05-03T10:15:30Z\"}"); + } + + private CommandContext createCommandContext() { + return new CommandContext(null, null, null, new ByteArrayOutputStream(), null); + } + + private RepositoryContext createRepositoryContext() { + return new RepositoryContext(REPOSITORY, null); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java index 2eb8968405..9af1125696 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java @@ -35,14 +35,18 @@ package sonia.scm.web.lfs; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; -import static org.mockito.Matchers.matches; import org.mockito.Mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Repository; import sonia.scm.store.BlobStoreFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + /** * Unit tests for {@link LfsBlobStoreFactory}. * @@ -58,15 +62,21 @@ public class LfsBlobStoreFactoryTest { private LfsBlobStoreFactory lfsBlobStoreFactory; @Test - public void getBlobStore() throws Exception { - lfsBlobStoreFactory.getLfsBlobStore(new Repository("the-id", "GIT", "the-name")); + public void getBlobStore() { + when(blobStoreFactory.withName(any())).thenCallRealMethod(); + Repository repository = new Repository("the-id", "GIT", "space", "the-name"); + lfsBlobStoreFactory.getLfsBlobStore(repository); // just make sure the right parameter is passed, as properly validating the return value is nearly impossible with // the return value (and should not be part of this test) - verify(blobStoreFactory).getBlobStore(matches("the-id-git-lfs")); + verify(blobStoreFactory).getStore(argThat(blobStoreParameters -> { + assertThat(blobStoreParameters.getName()).isEqualTo("the-id-git-lfs"); + assertThat(blobStoreParameters.getRepositoryId()).isEqualTo("the-id"); + return true; + })); // make sure there have been no further usages of the factory - verifyNoMoreInteractions(blobStoreFactory); + verify(blobStoreFactory, times(1)).getStore(any()); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java deleted file mode 100644 index 5a1bfdbf9a..0000000000 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java +++ /dev/null @@ -1,122 +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.lfs; - -import com.google.common.collect.Lists; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import static org.mockito.Mockito.*; -import org.mockito.runners.MockitoJUnitRunner; -import sonia.scm.HandlerEventType; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryEvent; -import sonia.scm.repository.RepositoryTestData; -import sonia.scm.store.Blob; -import sonia.scm.store.BlobStore; - -/** - * Unit tests for {@link LfsStoreRemoveListener}. - * - * @author Sebastian Sdorra - */ -@RunWith(MockitoJUnitRunner.class) -public class LfsStoreRemoveListenerTest { - - @Mock - private LfsBlobStoreFactory lfsBlobStoreFactory; - - @Mock - private BlobStore blobStore; - - @InjectMocks - private LfsStoreRemoveListener lfsStoreRemoveListener; - - @Test - public void testHandleRepositoryEventWithNonDeleteEvents() { - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_CREATE)); - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.CREATE)); - - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_MODIFY)); - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.MODIFY)); - - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_DELETE)); - - verifyZeroInteractions(lfsBlobStoreFactory); - } - - @Test - public void testHandleRepositoryEventWithNonGitRepositories() { - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "svn")); - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "hg")); - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "dummy")); - - verifyZeroInteractions(lfsBlobStoreFactory); - } - - @Test - public void testHandleRepositoryEvent() { - Repository heartOfGold = RepositoryTestData.createHeartOfGold("git"); - - when(lfsBlobStoreFactory.getLfsBlobStore(heartOfGold)).thenReturn(blobStore); - Blob blobA = mockBlob("a"); - Blob blobB = mockBlob("b"); - List<Blob> blobs = Lists.newArrayList(blobA, blobB); - when(blobStore.getAll()).thenReturn(blobs); - - - lfsStoreRemoveListener.handleRepositoryEvent(new RepositoryEvent(HandlerEventType.DELETE, heartOfGold)); - verify(blobStore).getAll(); - verify(blobStore).remove(blobA); - verify(blobStore).remove(blobB); - - verifyNoMoreInteractions(blobStore); - } - - private Blob mockBlob(String id) { - Blob blob = mock(Blob.class); - when(blob.getId()).thenReturn(id); - return blob; - } - - private RepositoryEvent event(HandlerEventType eventType) { - return event(eventType, "git"); - } - - private RepositoryEvent event(HandlerEventType eventType, String repositoryType) { - return new RepositoryEvent(eventType, RepositoryTestData.create42Puzzle(repositoryType)); - } - -} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/ScmBlobLfsRepositoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/ScmBlobLfsRepositoryTest.java new file mode 100644 index 0000000000..eefa4314c2 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/ScmBlobLfsRepositoryTest.java @@ -0,0 +1,98 @@ +package sonia.scm.web.lfs; + +import org.eclipse.jgit.lfs.lib.LongObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.Repository; +import sonia.scm.security.AccessToken; +import sonia.scm.store.BlobStore; + +import java.util.Date; + +import static java.time.Instant.parse; +import static java.util.Date.from; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.jgit.lfs.lib.LongObjectId.fromString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ScmBlobLfsRepositoryTest { + + static final Repository REPOSITORY = new Repository("1", "git", "space", "X"); + static final Date EXPIRATION = from(parse("2007-05-03T10:15:30.00Z")); + static final LongObjectId OBJECT_ID = fromString("976ed944c37cc5d1606af316937edb9d286ecf6c606af316937edb9d286ecf6c"); + + @Mock + BlobStore blobStore; + @Mock + LfsAccessTokenFactory tokenFactory; + + ScmBlobLfsRepository lfsRepository; + + @BeforeEach + void initializeLfsRepository() { + lfsRepository = new ScmBlobLfsRepository(REPOSITORY, blobStore, tokenFactory, "http://scm.org/"); + } + + @BeforeEach + void initAuthorizationToken() { + AccessToken readToken = createToken("READ_TOKEN"); + lenient().when(this.tokenFactory.createReadAccessToken(REPOSITORY)) + .thenReturn(readToken); + AccessToken writeToken = createToken("WRITE_TOKEN"); + lenient().when(this.tokenFactory.createWriteAccessToken(REPOSITORY)) + .thenReturn(writeToken); + } + + AccessToken createToken(String mockedValue) { + AccessToken accessToken = mock(AccessToken.class); + lenient().when(accessToken.getExpiration()).thenReturn(EXPIRATION); + lenient().when(accessToken.compact()).thenReturn(mockedValue); + return accessToken; + } + + @Test + void shouldTakeExpirationFromToken() { + ExpiringAction downloadAction = lfsRepository.getDownloadAction(OBJECT_ID); + assertThat(downloadAction.expires_at).isEqualTo("2007-05-03T10:15:30Z"); + } + + @Test + void shouldContainReadTokenForDownlo() { + ExpiringAction downloadAction = lfsRepository.getDownloadAction(OBJECT_ID); + assertThat(downloadAction.header.get("Authorization")).isEqualTo("Bearer READ_TOKEN"); + } + + @Test + void shouldContainWriteTokenForUpload() { + ExpiringAction downloadAction = lfsRepository.getUploadAction(OBJECT_ID, 42L); + assertThat(downloadAction.header.get("Authorization")).isEqualTo("Bearer WRITE_TOKEN"); + } + + @Test + void shouldContainUrl() { + ExpiringAction downloadAction = lfsRepository.getDownloadAction(OBJECT_ID); + assertThat(downloadAction.href).isEqualTo("http://scm.org/976ed944c37cc5d1606af316937edb9d286ecf6c606af316937edb9d286ecf6c"); + } + + @Test + void shouldCreateTokenForDownloadActionOnlyOnce() { + lfsRepository.getDownloadAction(OBJECT_ID); + lfsRepository.getDownloadAction(OBJECT_ID); + verify(tokenFactory, times(1)).createReadAccessToken(REPOSITORY); + } + + @Test + void shouldCreateTokenForUploadActionOnlyOnce() { + lfsRepository.getUploadAction(OBJECT_ID, 42L); + lfsRepository.getUploadAction(OBJECT_ID, 42L); + verify(tokenFactory, times(1)).createWriteAccessToken(REPOSITORY); + } +} + diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java index c670032089..f386dc2125 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java @@ -7,47 +7,32 @@ import javax.servlet.http.HttpServletRequest; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -/** - * Created by omilke on 18.05.2017. - */ public class LfsServletFactoryTest { + private static final String NAMESPACE = "space"; + private static final String NAME = "git-lfs-demo"; + private static final Repository REPOSITORY = new Repository("", "GIT", NAMESPACE, NAME); + @Test - public void buildBaseUri() throws Exception { - - String repositoryName = "git-lfs-demo"; - - String result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryName), RequestWithUri(repositoryName, true)); - assertThat(result, is(equalTo("http://localhost:8081/scm/git/git-lfs-demo.git/info/lfs/objects/"))); - - - //result will be with dot-gix suffix, ide - result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryName), RequestWithUri(repositoryName, false)); - assertThat(result, is(equalTo("http://localhost:8081/scm/git/git-lfs-demo.git/info/lfs/objects/"))); + public void shouldBuildBaseUri() { + String result = LfsServletFactory.buildBaseUri(REPOSITORY, requestWithUri("git-lfs-demo")); + assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/"))); } - private HttpServletRequest RequestWithUri(String repositoryName, boolean withDotGitSuffix) { + private HttpServletRequest requestWithUri(String repositoryName) { HttpServletRequest mockedRequest = mock(HttpServletRequest.class); - final String suffix; - if (withDotGitSuffix) { - suffix = ".git"; - } else { - suffix = ""; - } - //build from valid live request data when(mockedRequest.getRequestURL()).thenReturn( - new StringBuffer(String.format("http://localhost:8081/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix))); - when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix)); + new StringBuffer(String.format("http://localhost:8081/scm/repo/%s/info/lfs/objects/batch", repositoryName))); + when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/repo/%s/info/lfs/objects/batch", repositoryName)); when(mockedRequest.getContextPath()).thenReturn("/scm"); return mockedRequest; } - - } diff --git a/scm-plugins/scm-git-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/scm-plugins/scm-git-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini new file mode 100644 index 0000000000..083c685dbe --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -0,0 +1,13 @@ +[users] +readOnly = secret, reader, repoRead +writeOnly = secret, writer, repoWrite +readWrite = secret, readerWriter +admin = secret, admin + +[roles] +reader = configuration:read:git +writer = configuration:write:git +readerWriter = configuration:*:git,repository:*:id +admin = * +repoRead = repository:read:* +repoWrite = repository:modify:*,repository:git:* diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/repository-001.json b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/repository-001.json new file mode 100644 index 0000000000..43ea136942 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/repository-001.json @@ -0,0 +1,42 @@ +{ + "creationDate": "2018-11-09T09:48:32.732Z", + "description": "Handling static webresources made easy", + "healthCheckFailures": [], + "lastModified": "2018-11-09T09:49:20.973Z", + "namespace": "scmadmin", + "name": "web-resources", + "archived": false, + "type": "git", + "_links": { + "self": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "delete": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "update": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "permissions": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/" + }, + "protocol": [ + { + "href": "http://localhost:8081/scm/repo/scmadmin/web-resources", + "name": "http" + } + ], + "tags": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/" + }, + "branches": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/" + }, + "changesets": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/" + }, + "sources": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/" + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/repository-collection-001.json b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/repository-collection-001.json new file mode 100644 index 0000000000..f4eeb24bbc --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/repository-collection-001.json @@ -0,0 +1,106 @@ +{ + "page": 0, + "pageTotal": 1, + "_links": { + "self": { + "href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10" + }, + "first": { + "href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10" + }, + "last": { + "href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10" + }, + "create": { + "href": "http://localhost:8081/scm/api/v2/repositories/" + } + }, + "_embedded": { + "repositories": [ + { + "creationDate": "2018-11-09T09:48:32.732Z", + "description": "Handling static webresources made easy", + "healthCheckFailures": [], + "lastModified": "2018-11-09T09:49:20.973Z", + "namespace": "scmadmin", + "name": "web-resources", + "archived": false, + "type": "git", + "_links": { + "self": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "delete": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "update": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "permissions": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/" + }, + "protocol": [ + { + "href": "http://localhost:8081/scm/repo/scmadmin/web-resources", + "name": "http" + } + ], + "tags": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/" + }, + "branches": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/" + }, + "changesets": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/" + }, + "sources": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/" + } + } + }, + { + "creationDate": "2018-11-09T09:48:32.732Z", + "description": "Handling static webresources made easy", + "healthCheckFailures": [], + "lastModified": "2018-11-09T09:49:20.973Z", + "namespace": "scmadmin", + "name": "web-resources", + "archived": false, + "type": "git", + "_links": { + "self": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "delete": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "update": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources" + }, + "permissions": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/" + }, + "protocol": [ + { + "href": "http://localhost:8081/scm/repo/scmadmin/web-resources", + "name": "http" + } + ], + "tags": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/" + }, + "branches": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/" + }, + "changesets": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/" + }, + "sources": { + "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/" + } + } + } + ] + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-ancestor-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-ancestor-test.zip new file mode 100644 index 0000000000..d740de7674 Binary files /dev/null and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-ancestor-test.zip differ diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-empty-repo.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-empty-repo.zip new file mode 100644 index 0000000000..fdd0af4483 Binary files /dev/null and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-empty-repo.zip differ diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip new file mode 100644 index 0000000000..b97f519684 Binary files /dev/null and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip differ diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip index 3fbab0be38..8e43da1d82 100644 Binary files a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip differ diff --git a/scm-plugins/scm-git-plugin/tsconfig.json b/scm-plugins/scm-git-plugin/tsconfig.json new file mode 100644 index 0000000000..7cfe32efe6 --- /dev/null +++ b/scm-plugins/scm-git-plugin/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@scm-manager/tsconfig", + "include": [ + "./src/main/js" + ] +} diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json new file mode 100644 index 0000000000..8baaa729cb --- /dev/null +++ b/scm-plugins/scm-hg-plugin/package.json @@ -0,0 +1,24 @@ +{ + "name": "@scm-manager/scm-hg-plugin", + "private": true, + "version": "2.0.0-SNAPSHOT", + "license": "BSD-3-Clause", + "main": "./src/main/js/index.ts", + "scripts": { + "build": "ui-scripts plugin", + "watch": "ui-scripts plugin-watch", + "typecheck": "tsc" + }, + "babel": { + "presets": [ + "@scm-manager/babel-preset" + ] + }, + "jest": { + "preset": "@scm-manager/jest-preset" + }, + "prettier": "@scm-manager/prettier-config", + "dependencies": { + "@scm-manager/ui-plugins": "^2.0.0-SNAPSHOT" + } +} diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index 252d504552..e57652bf0f 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - + <modelVersion>4.0.0</modelVersion> <parent> @@ -10,18 +10,16 @@ </parent> <artifactId>scm-hg-plugin</artifactId> - <version>2.0.0-SNAPSHOT</version> - <name>scm-hg-plugin</name> <packaging>smp</packaging> <url>https://bitbucket.org/sdorra/scm-manager</url> <description>Plugin for the version control system Mercurial</description> <dependencies> - + <dependency> <groupId>com.aragost.javahg</groupId> <artifactId>javahg</artifactId> - <version>0.8-scm1</version> + <version>0.13-java7</version> <exclusions> <exclusion> <groupId>com.google.guava</groupId> @@ -30,12 +28,10 @@ </exclusions> </dependency> </dependencies> - - <!-- create test jar --> - + <build> <plugins> - + <plugin> <groupId>com.mycila.maven-license-plugin</groupId> <artifactId>maven-license-plugin</artifactId> @@ -54,7 +50,18 @@ <strictCheck>true</strictCheck> </configuration> </plugin> - + + <plugin> + <groupId>sonia.scm.maven</groupId> + <artifactId>smp-maven-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <corePlugin>true</corePlugin> + </configuration> + </plugin> + + <!-- create test jar --> + <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> @@ -67,31 +74,18 @@ </execution> </executions> </plugin> - + </plugins> </build> - + <repositories> - + <repository> <id>maven.scm-manager.org</id> <name>scm-manager release repository</name> <url>http://maven.scm-manager.org/nexus/content/groups/public</url> </repository> - - <repository> - <releases> - <enabled>false</enabled> - </releases> - <snapshots> - <enabled>true</enabled> - </snapshots> - <id>sonatype-ossrh</id> - <name>Sonatype Open Source Software Repository Hosting</name> - <layout>default</layout> - <url>https://oss.sonatype.org/content/groups/public/</url> - </repository> - + </repositories> - + </project> diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java deleted file mode 100644 index 3ef39c163f..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java +++ /dev/null @@ -1,348 +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.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import sonia.scm.SCMContext; -import sonia.scm.installer.HgInstallerFactory; -import sonia.scm.installer.HgPackage; -import sonia.scm.installer.HgPackageReader; -import sonia.scm.installer.HgPackages; -import sonia.scm.net.ahc.AdvancedHttpClient; -import sonia.scm.repository.HgConfig; -import sonia.scm.repository.HgRepositoryHandler; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.List; - -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * - * @author Sebastian Sdorra - */ -@Singleton -@Path("config/repositories/hg") -public class HgConfigResource -{ - - /** - * Constructs ... - * - * - * - * - * @param client - * @param handler - * @param pkgReader - */ - @Inject - public HgConfigResource(AdvancedHttpClient client, - HgRepositoryHandler handler, HgPackageReader pkgReader) - { - this.client = client; - this.handler = handler; - this.pkgReader = pkgReader; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param uriInfo - * - * @return - */ - @POST - @Path("auto-configuration") - @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public HgConfig autoConfiguration(@Context UriInfo uriInfo) - { - return autoConfiguration(uriInfo, null); - } - - /** - * Method description - * - * - * @param uriInfo - * @param config - * - * @return - */ - @POST - @Path("auto-configuration") - @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public HgConfig autoConfiguration(@Context UriInfo uriInfo, HgConfig config) - { - if (config == null) - { - config = new HgConfig(); - } - - handler.doAutoConfiguration(config); - - return handler.getConfig(); - } - - /** - * Method description - * - * - * - * @param id - * @return - */ - @POST - @Path("packages/{pkgId}") - public Response installPackage(@PathParam("pkgId") String id) - { - Response response = null; - HgPackage pkg = pkgReader.getPackage(id); - - if (pkg != null) - { - if (HgInstallerFactory.createInstaller().installPackage(client, handler, - SCMContext.getContext().getBaseDirectory(), pkg)) - { - response = Response.noContent().build(); - } - else - { - response = - Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } - } - else - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - - return response; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @GET - @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public HgConfig getConfig() - { - HgConfig config = handler.getConfig(); - - if (config == null) - { - config = new HgConfig(); - } - - return config; - } - - /** - * Method description - * - * - * @return - */ - @GET - @Path("installations/hg") - @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public InstallationsResponse getHgInstallations() - { - List<String> installations = - HgInstallerFactory.createInstaller().getHgInstallations(); - - return new InstallationsResponse(installations); - } - - /** - * Method description - * - * - * @return - */ - @GET - @Path("packages") - @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public HgPackages getPackages() - { - return pkgReader.getPackages(); - } - - /** - * Method description - * - * - * @return - */ - @GET - @Path("installations/python") - @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public InstallationsResponse getPythonInstallations() - { - List<String> installations = - HgInstallerFactory.createInstaller().getPythonInstallations(); - - return new InstallationsResponse(installations); - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param uriInfo - * @param config - * - * @return - * - * @throws IOException - */ - @POST - @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public Response setConfig(@Context UriInfo uriInfo, HgConfig config) - throws IOException - { - handler.setConfig(config); - handler.storeConfig(); - - return Response.created(uriInfo.getRequestUri()).build(); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 11/04/25 - * @author Enter your name here... - */ - @XmlAccessorType(XmlAccessType.FIELD) - @XmlRootElement(name = "installations") - public static class InstallationsResponse - { - - /** - * Constructs ... - * - */ - public InstallationsResponse() {} - - /** - * Constructs ... - * - * - * @param paths - */ - public InstallationsResponse(List<String> paths) - { - this.paths = paths; - } - - //~--- get methods -------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public List<String> getPaths() - { - return paths; - } - - //~--- set methods -------------------------------------------------------- - - /** - * Method description - * - * - * @param paths - */ - public void setPaths(List<String> paths) - { - this.paths = paths; - } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "path") - private List<String> paths; - } - - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private AdvancedHttpClient client; - - /** Field description */ - private HgRepositoryHandler handler; - - /** Field description */ - private HgPackageReader pkgReader; -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java new file mode 100644 index 0000000000..b265f2929d --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java @@ -0,0 +1,76 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.Inject; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +public class HgConfigAutoConfigurationResource { + + private final HgRepositoryHandler repositoryHandler; + private final HgConfigDtoToHgConfigMapper dtoToConfigMapper; + + @Inject + public HgConfigAutoConfigurationResource(HgConfigDtoToHgConfigMapper dtoToConfigMapper, + HgRepositoryHandler repositoryHandler) { + this.dtoToConfigMapper = dtoToConfigMapper; + this.repositoryHandler = repositoryHandler; + } + + /** + * Sets the default hg config and installs the hg binary. + */ + @PUT + @Path("") + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response autoConfiguration() { + return autoConfiguration(null); + } + + /** + * Modifies the hg config and installs the hg binary. + * + * @param configDto new configuration object + */ + @PUT + @Path("") + @Consumes(HgVndMediaType.CONFIG) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response autoConfiguration(HgConfigDto configDto) { + + HgConfig config; + + if (configDto != null) { + config = dtoToConfigMapper.map(configDto); + } else { + config = new HgConfig(); + } + + ConfigurationPermissions.write(config).check(); + + repositoryHandler.doAutoConfiguration(config); + + return Response.noContent().build(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java new file mode 100644 index 0000000000..641b8650fc --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java @@ -0,0 +1,30 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@Getter +@Setter +public class HgConfigDto extends HalRepresentation { + + private boolean disabled; + + private String encoding; + private String hgBinary; + private String pythonBinary; + private String pythonPath; + private boolean useOptimizedBytecode; + private boolean showRevisionInId; + private boolean enableHttpPostArgs; + private boolean disableHookSSLValidation; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapper.java new file mode 100644 index 0000000000..af3879013e --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapper.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import sonia.scm.repository.HgConfig; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class HgConfigDtoToHgConfigMapper { + public abstract HgConfig map(HgConfigDto dto); +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java new file mode 100644 index 0000000000..73c6e2e52f --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java @@ -0,0 +1,41 @@ +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.repository.HgConfig; +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 HgConfigInIndexResource extends JsonEnricherBase { + + private final Provider<ScmPathInfoStore> scmPathInfoStore; + + @Inject + public HgConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.read(HgConfig.PERMISSION).isPermitted()) { + String hgConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), HgConfigResource.class) + .method("get") + .parameters() + .href(); + + JsonNode hgConfigRefNode = createObject(singletonMap("href", value(hgConfigUrl))); + + addPropertyNode(context.getResponseEntity().get("_links"), "hgConfig", hgConfigRefNode); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsDto.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsDto.java new file mode 100644 index 0000000000..b60f7f5460 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsDto.java @@ -0,0 +1,21 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class HgConfigInstallationsDto extends HalRepresentation { + + private List<String> paths; + + public HgConfigInstallationsDto(Links links, List<String> paths) { + super(links); + this.paths = paths; + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java new file mode 100644 index 0000000000..8842d07569 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java @@ -0,0 +1,69 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.installer.HgInstallerFactory; +import sonia.scm.repository.HgConfig; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +public class HgConfigInstallationsResource { + + public static final String PATH_HG = "hg"; + public static final String PATH_PYTHON = "python"; + private final HgConfigInstallationsToDtoMapper hgConfigInstallationsToDtoMapper; + + @Inject + public HgConfigInstallationsResource(HgConfigInstallationsToDtoMapper hgConfigInstallationsToDtoMapper) { + this.hgConfigInstallationsToDtoMapper = hgConfigInstallationsToDtoMapper; + } + + /** + * Returns the hg installations. + */ + @GET + @Path(PATH_HG) + @Produces(HgVndMediaType.INSTALLATIONS) + @TypeHint(HalRepresentation.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public HalRepresentation getHgInstallations() { + + ConfigurationPermissions.read(HgConfig.PERMISSION).check(); + + return hgConfigInstallationsToDtoMapper.map( + HgInstallerFactory.createInstaller().getHgInstallations(), PATH_HG); + } + + /** + * Returns the python installations. + */ + @GET + @Path(PATH_PYTHON) + @Produces(HgVndMediaType.INSTALLATIONS) + @TypeHint(HalRepresentation.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public HalRepresentation getPythonInstallations() { + + ConfigurationPermissions.read(HgConfig.PERMISSION).check(); + + return hgConfigInstallationsToDtoMapper.map( + HgInstallerFactory.createInstaller().getPythonInstallations(), PATH_PYTHON); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java new file mode 100644 index 0000000000..da980f75c0 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + + +import javax.inject.Inject; +import java.util.List; + +import static de.otto.edison.hal.Links.linkingTo; + +public class HgConfigInstallationsToDtoMapper { + + private ScmPathInfoStore scmPathInfoStore; + + @Inject + public HgConfigInstallationsToDtoMapper(ScmPathInfoStore scmPathInfoStore) { + this.scmPathInfoStore = scmPathInfoStore; + } + + public HgConfigInstallationsDto map(List<String> installations, String path) { + return new HgConfigInstallationsDto(linkingTo().self(createSelfLink(path)).build(), installations); + } + + private String createSelfLink(String path) { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class); + return linkBuilder.method("getInstallationsResource").parameters().href() + '/' + path; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java new file mode 100644 index 0000000000..88a7de7ea0 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java @@ -0,0 +1,96 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.SCMContext; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.installer.HgInstallerFactory; +import sonia.scm.installer.HgPackage; +import sonia.scm.installer.HgPackageReader; +import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +public class HgConfigPackageResource { + + private final HgPackageReader pkgReader; + private final AdvancedHttpClient client; + private final HgRepositoryHandler handler; + private final HgConfigPackagesToDtoMapper configPackageCollectionToDtoMapper; + + @Inject + public HgConfigPackageResource(HgPackageReader pkgReader, AdvancedHttpClient client, HgRepositoryHandler handler, + HgConfigPackagesToDtoMapper hgConfigPackagesToDtoMapper) { + this.pkgReader = pkgReader; + this.client = client; + this.handler = handler; + this.configPackageCollectionToDtoMapper = hgConfigPackagesToDtoMapper; + } + + /** + * Returns all mercurial packages. + */ + @GET + @Path("") + @Produces(HgVndMediaType.PACKAGES) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(HalRepresentation.class) + public HalRepresentation getPackages() { + + ConfigurationPermissions.read(HgConfig.PERMISSION).check(); + + return configPackageCollectionToDtoMapper.map(pkgReader.getPackages()); + } + + /** + * Installs a mercurial package + * + * @param pkgId Identifier of the package to install + */ + @PUT + @Path("{pkgId}") + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), + @ResponseCode(code = 404, condition = "no package found for id"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response installPackage(@PathParam("pkgId") String pkgId) { + Response response; + + ConfigurationPermissions.write(HgConfig.PERMISSION).check(); + + HgPackage pkg = pkgReader.getPackage(pkgId); + + if (pkg != null) { + if (HgInstallerFactory.createInstaller() + .installPackage(client, handler, SCMContext.getContext().getBaseDirectory(), pkg)) { + response = Response.noContent().build(); + } else { + response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } else { + response = Response.status(Response.Status.NOT_FOUND).build(); + } + + return response; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesDto.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesDto.java new file mode 100644 index 0000000000..959df12b61 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesDto.java @@ -0,0 +1,38 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@NoArgsConstructor +@Getter +@Setter +public class HgConfigPackagesDto extends HalRepresentation { + + private List<HgConfigPackageDto> packages; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } + + @NoArgsConstructor + @Getter + @Setter + public static class HgConfigPackageDto { + + private String arch; + private HgConfigDto hgConfigTemplate; + private String hgVersion; + private String id; + private String platform; + private String pythonVersion; + private long size; + private String url; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java new file mode 100644 index 0000000000..3ee87cef84 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java @@ -0,0 +1,59 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import lombok.Getter; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import sonia.scm.installer.HgPackage; +import sonia.scm.installer.HgPackages; + +import javax.inject.Inject; +import java.util.List; + +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class HgConfigPackagesToDtoMapper { + + @Inject + private ScmPathInfoStore scmPathInfoStore; + + public HgConfigPackagesDto map(HgPackages hgpackages) { + return map(new HgPackagesNonIterable(hgpackages)); + } + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + /* Favor warning "Unmapped target property: "attributes", to packages[].hgConfigTemplate" + Over error "Unknown property "packages[].hgConfigTemplate.attributes" + @Mapping(target = "packages[].hgConfigTemplate.attributes", ignore = true) // Also not for nested DTOs + */ + protected abstract HgConfigPackagesDto map(HgPackagesNonIterable hgPackagesNonIterable); + + @AfterMapping + void appendLinks(@MappingTarget HgConfigPackagesDto target) { + Links.Builder linksBuilder = linkingTo().self(createSelfLink()); + target.add(linksBuilder.build()); + } + + private String createSelfLink() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class); + return linkBuilder.method("getPackagesResource").parameters().href(); + } + + /** + * Unfortunately, HgPackages is iterable, HgConfigPackagesDto does not need to be iterable and MapStruct refuses to + * map an iterable to a non-iterable. So use this little non-iterable "proxy". + */ + @Getter + static class HgPackagesNonIterable { + private List<HgPackage> packages; + + HgPackagesNonIterable(HgPackages hgPackages) { + this.packages = hgPackages.getPackages(); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java new file mode 100644 index 0000000000..e6a8f01238 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java @@ -0,0 +1,116 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +/** + * RESTful Web Service Resource to manage the configuration of the hg plugin. + */ +@Path(HgConfigResource.HG_CONFIG_PATH_V2) +public class HgConfigResource { + + static final String HG_CONFIG_PATH_V2 = "v2/config/hg"; + + private final HgConfigDtoToHgConfigMapper dtoToConfigMapper; + private final HgConfigToHgConfigDtoMapper configToDtoMapper; + private final HgRepositoryHandler repositoryHandler; + private final Provider<HgConfigPackageResource> packagesResource; + private final Provider<HgConfigAutoConfigurationResource> autoconfigResource; + private final Provider<HgConfigInstallationsResource> installationsResource; + + @Inject + public HgConfigResource(HgConfigDtoToHgConfigMapper dtoToConfigMapper, HgConfigToHgConfigDtoMapper configToDtoMapper, + HgRepositoryHandler repositoryHandler, Provider<HgConfigPackageResource> packagesResource, + Provider<HgConfigAutoConfigurationResource> autoconfigResource, + Provider<HgConfigInstallationsResource> installationsResource) { + this.dtoToConfigMapper = dtoToConfigMapper; + this.configToDtoMapper = configToDtoMapper; + this.repositoryHandler = repositoryHandler; + this.packagesResource = packagesResource; + this.autoconfigResource = autoconfigResource; + this.installationsResource = installationsResource; + } + + /** + * Returns the hg config. + */ + @GET + @Path("") + @Produces(HgVndMediaType.CONFIG) + @TypeHint(HgConfigDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get() { + + ConfigurationPermissions.read(HgConfig.PERMISSION).check(); + + HgConfig config = repositoryHandler.getConfig(); + + if (config == null) { + config = new HgConfig(); + repositoryHandler.setConfig(config); + } + + return Response.ok(configToDtoMapper.map(config)).build(); + } + + /** + * Modifies the hg config. + * + * @param configDto new configuration object + */ + @PUT + @Path("") + @Consumes(HgVndMediaType.CONFIG) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response update(HgConfigDto configDto) { + + HgConfig config = dtoToConfigMapper.map(configDto); + + ConfigurationPermissions.write(config).check(); + + repositoryHandler.setConfig(config); + repositoryHandler.storeConfig(); + + return Response.noContent().build(); + } + + @Path("packages") + public HgConfigPackageResource getPackagesResource() { + return packagesResource.get(); + } + + @Path("auto-configuration") + public HgConfigAutoConfigurationResource getAutoConfigurationResource() { + return autoconfigResource.get(); + } + + @Path("installations") + public HgConfigInstallationsResource getInstallationsResource() { + return installationsResource.get(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java new file mode 100644 index 0000000000..b2a67e2aa4 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java @@ -0,0 +1,41 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.HgConfig; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class HgConfigToHgConfigDtoMapper extends BaseMapper<HgConfig, HgConfigDto> { + + @Inject + private ScmPathInfoStore scmPathInfoStore; + + @AfterMapping + void appendLinks(HgConfig config, @MappingTarget HgConfigDto target) { + Links.Builder linksBuilder = linkingTo().self(self()); + if (ConfigurationPermissions.write(config).isPermitted()) { + linksBuilder.single(link("update", update())); + } + target.add(linksBuilder.build()); + } + + private String self() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class); + return linkBuilder.method("get").parameters().href(); + } + + private String update() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class); + return linkBuilder.method("update").parameters().href(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java index bd5dfd7095..27fdc7a296 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java @@ -35,14 +35,12 @@ package sonia.scm.installer; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; -import sonia.scm.util.IOUtil; //~--- JDK imports ------------------------------------------------------------ import java.io.File; -import java.io.IOException; + import sonia.scm.net.ahc.AdvancedHttpClient; /** @@ -52,32 +50,6 @@ import sonia.scm.net.ahc.AdvancedHttpClient; public abstract class AbstractHgInstaller implements HgInstaller { - /** Field description */ - public static final String DIRECTORY_REPOSITORY = "repositories"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param baseDirectory - * @param config - * - * @throws IOException - */ - @Override - public void install(File baseDirectory, HgConfig config) throws IOException - { - File repoDirectory = new File( - baseDirectory, - DIRECTORY_REPOSITORY.concat(File.separator).concat( - HgRepositoryHandler.TYPE_NAME)); - - IOUtil.mkdirs(repoDirectory); - config.setRepositoryDirectory(repoDirectory); - } /** * Method description diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java index 7406505b5a..763977cfa3 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java @@ -162,6 +162,7 @@ public class HgPackageInstaller implements Runnable catch (IOException ex) { logger.error("could not downlaod file ".concat(pkg.getUrl()), ex); + file = null; } finally { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/UnixHgInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/UnixHgInstaller.java index ed81b27bdb..74ca7dea34 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/UnixHgInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/UnixHgInstaller.java @@ -74,8 +74,6 @@ public class UnixHgInstaller extends AbstractHgInstaller @Override public void install(File baseDirectory, HgConfig config) throws IOException { - super.install(baseDirectory, config); - // search mercurial (hg) if (Util.isEmpty(config.getHgBinary())) { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/WindowsHgInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/WindowsHgInstaller.java index 528f6b941a..0b0ecddfa6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/WindowsHgInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/WindowsHgInstaller.java @@ -37,18 +37,21 @@ package sonia.scm.installer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import sonia.scm.repository.HgConfig; import sonia.scm.util.IOUtil; import sonia.scm.util.RegistryUtil; import sonia.scm.util.Util; +//~--- JDK imports ------------------------------------------------------------ + import java.io.File; +import java.io.FilenameFilter; import java.io.IOException; + import java.util.ArrayList; import java.util.List; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra @@ -113,8 +116,6 @@ public class WindowsHgInstaller extends AbstractHgInstaller @Override public void install(File baseDirectory, HgConfig config) throws IOException { - super.install(baseDirectory, config); - if (Util.isEmpty(config.getPythonBinary())) { String pythonBinary = getPythonBinary(); @@ -219,7 +220,14 @@ public class WindowsHgInstaller extends AbstractHgInstaller private boolean checkForOptimizedByteCode(String part) { File libDir = new File(part); - String[] pyoFiles = libDir.list((file, name) -> name.toLowerCase().endsWith(".pyo")); + String[] pyoFiles = libDir.list(new FilenameFilter() + { + @Override + public boolean accept(File file, String name) + { + return name.toLowerCase().endsWith(".pyo"); + } + }); return Util.isNotEmpty(pyoFiles); } @@ -282,7 +290,7 @@ public class WindowsHgInstaller extends AbstractHgInstaller */ private List<String> getInstallations(String[] registryKeys) { - List<String> installations = new ArrayList<>(); + List<String> installations = new ArrayList<String>(); for (String registryKey : registryKeys) { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java index 1d54d0ad87..832a7203f8 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java @@ -46,7 +46,12 @@ import javax.xml.bind.JAXBException; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; //~--- JDK imports ------------------------------------------------------------ @@ -119,7 +124,7 @@ public class AbstractHgHandler protected AbstractHgHandler(HgRepositoryHandler handler, HgContext context, Repository repository) { - this(handler, context, repository, handler.getDirectory(repository)); + this(handler, context, repository, handler.getDirectory(repository.getId())); } /** @@ -174,7 +179,7 @@ public class AbstractHgHandler */ protected Process createHgProcess(String... args) throws IOException { - return createHgProcess(new HashMap<>(), args); + return createHgProcess(new HashMap<String, String>(), args); } /** @@ -224,19 +229,24 @@ public class AbstractHgHandler { if (errorStream != null) { - new Thread(() -> { - try + new Thread(new Runnable() + { + @Override + public void run() { - String content = IOUtil.getContent(errorStream); - - if (Util.isNotEmpty(content)) + try { - logger.error(content.trim()); + String content = IOUtil.getContent(errorStream); + + if (Util.isNotEmpty(content)) + { + logger.error(content.trim()); + } + } + catch (IOException ex) + { + logger.error("error during logging", ex); } - } - catch (IOException ex) - { - logger.error("error during logging", ex); } }).start(); } @@ -244,45 +254,15 @@ public class AbstractHgHandler //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param resultType - * @param script - * @param <T> - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - protected <T> T getResultFromScript(Class<T> resultType, - HgPythonScript script) - throws IOException, RepositoryException - { + protected <T> T getResultFromScript(Class<T> resultType, HgPythonScript script) throws IOException { return getResultFromScript(resultType, script, - new HashMap<>()); + new HashMap<String, String>()); } - /** - * Method description - * - * - * @param resultType - * @param script - * @param extraEnv - * @param <T> - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @SuppressWarnings("unchecked") protected <T> T getResultFromScript(Class<T> resultType, HgPythonScript script, Map<String, String> extraEnv) - throws IOException, RepositoryException + throws IOException { Process p = createScriptProcess(script, extraEnv); @@ -292,7 +272,7 @@ public class AbstractHgHandler } catch (JAXBException ex) { logger.error("could not parse result", ex); - throw new RepositoryException("could not parse result", ex); + throw new InternalRepositoryException(repository, "could not parse result", ex); } } @@ -315,7 +295,7 @@ public class AbstractHgHandler throws IOException { HgConfig config = handler.getConfig(); - List<String> cmdList = new ArrayList<>(); + List<String> cmdList = new ArrayList<String>(); cmdList.add(cmd); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java index b9bd650925..bb3d2b1cfb 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java @@ -33,22 +33,23 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; + /** * * @author Sebastian Sdorra */ @XmlRootElement(name = "config") -public class HgConfig extends SimpleRepositoryConfig +public class HgConfig extends RepositoryConfig { + public static final String PERMISSION = "hg"; + /** * Constructs ... * @@ -57,6 +58,14 @@ public class HgConfig extends SimpleRepositoryConfig //~--- get methods ---------------------------------------------------------- + + @Override + @XmlTransient // Only for permission checks, don't serialize to XML + public String getId() { + // Don't change this without migrating SCM permission configuration! + return PERMISSION; + } + /** * Method description * @@ -123,6 +132,14 @@ public class HgConfig extends SimpleRepositoryConfig return useOptimizedBytecode; } + public boolean isDisableHookSSLValidation() { + return disableHookSSLValidation; + } + + public boolean isEnableHttpPostArgs() { + return enableHttpPostArgs; + } + /** * Method description * @@ -193,6 +210,10 @@ public class HgConfig extends SimpleRepositoryConfig this.showRevisionInId = showRevisionInId; } + public void setEnableHttpPostArgs(boolean enableHttpPostArgs) { + this.enableHttpPostArgs = enableHttpPostArgs; + } + /** * Method description * @@ -204,6 +225,10 @@ public class HgConfig extends SimpleRepositoryConfig this.useOptimizedBytecode = useOptimizedBytecode; } + public void setDisableHookSSLValidation(boolean disableHookSSLValidation) { + this.disableHookSSLValidation = disableHookSSLValidation; + } + //~--- fields --------------------------------------------------------------- /** Field description */ @@ -223,4 +248,12 @@ public class HgConfig extends SimpleRepositoryConfig /** Field description */ private boolean showRevisionInId = false; + + private boolean enableHttpPostArgs = false; + + /** + * disable validation of ssl certificates for mercurial hook + * @see <a href="https://goo.gl/zH5eY8">Issue 959</a> + */ + private boolean disableHookSSLValidation = false; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContext.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContext.java index cbf7804444..21e27af328 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContext.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContext.java @@ -35,13 +35,10 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import com.google.inject.servlet.RequestScoped; - /** * * @author Sebastian Sdorra */ -@RequestScoped public class HgContext { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextProvider.java index c86588fb27..33477e04b8 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextProvider.java @@ -35,13 +35,20 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import com.google.inject.Inject; -import com.google.inject.Provider; +import com.google.common.annotations.VisibleForTesting; +import com.google.inject.OutOfScopeException; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; + /** + * Injection provider for {@link HgContext}. + * This provider returns an instance {@link HgContext} from request scope, if no {@link HgContext} could be found in + * request scope (mostly because the scope is not available) a new {@link HgContext} gets returned. * * @author Sebastian Sdorra */ @@ -49,36 +56,50 @@ public class HgContextProvider implements Provider<HgContext> { /** - * the logger for HgContextProvider + * the LOG for HgContextProvider */ - private static final Logger logger = + private static final Logger LOG = LoggerFactory.getLogger(HgContextProvider.class); //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - */ - @Override - public HgContext get() - { - HgContext ctx = context; + private Provider<HgContextRequestStore> requestStoreProvider; - if (ctx == null) - { - ctx = new HgContext(); - logger.trace("context is null, we are probably out of request scope"); - } - - return ctx; + @Inject + public HgContextProvider(Provider<HgContextRequestStore> requestStoreProvider) { + this.requestStoreProvider = requestStoreProvider; } - //~--- fields --------------------------------------------------------------- + @VisibleForTesting + public HgContextProvider() { + } - /** Field description */ - @Inject(optional = true) - private HgContext context; + @Override + public HgContext get() { + HgContext context = fetchContextFromRequest(); + if (context != null) { + LOG.trace("return HgContext from request store"); + return context; + } + LOG.trace("could not find context in request scope, returning new instance"); + return new HgContext(); + } + + private HgContext fetchContextFromRequest() { + try { + if (requestStoreProvider != null) { + return requestStoreProvider.get().get(); + } else { + LOG.trace("no request store provider defined, could not return context from request"); + return null; + } + } catch (ProvisionException ex) { + if (ex.getCause() instanceof OutOfScopeException) { + LOG.trace("we are currently out of request scope, failed to retrieve context"); + return null; + } else { + throw ex; + } + } + } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextRequestStore.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextRequestStore.java new file mode 100644 index 0000000000..ff08c2fcd2 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextRequestStore.java @@ -0,0 +1,24 @@ +package sonia.scm.repository; + +import com.google.inject.servlet.RequestScoped; + +/** + * Holds an instance of {@link HgContext} in the request scope. + * + * <p>The problem seems to be that guice had multiple options for injecting HgContext. {@link HgContextProvider} + * bound via Module and {@link HgContext} bound void {@link RequestScoped} annotation. It looks like that Guice 4 + * injects randomly the one or the other, in SCMv1 (Guice 3) everything works as expected.</p> + * + * <p>To fix the problem we have created this class annotated with {@link RequestScoped}, which holds an instance + * of {@link HgContext}. This way only the {@link HgContextProvider} is used for injection.</p> + */ +@RequestScoped +public class HgContextRequestStore { + + private final HgContext context = new HgContext(); + + public HgContext get() { + return context; + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java index d80c0c5afb..1d227fb54e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java @@ -35,22 +35,15 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Strings; - -import org.apache.shiro.codec.Base64; - +import com.google.inject.ProvisionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import sonia.scm.security.CipherUtil; -import sonia.scm.util.HttpUtil; import sonia.scm.web.HgUtil; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.util.Map; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -59,6 +52,8 @@ import javax.servlet.http.HttpServletRequest; public final class HgEnvironment { + private static final Logger LOG = LoggerFactory.getLogger(HgEnvironment.class); + /** Field description */ public static final String ENV_PYTHON_PATH = "PYTHONPATH"; @@ -68,14 +63,7 @@ public final class HgEnvironment /** Field description */ private static final String ENV_URL = "SCM_URL"; - /** Field description */ - private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS"; - - /** - * the logger for HgEnvironment - */ - private static final Logger logger = - LoggerFactory.getLogger(HgEnvironment.class); + private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN"; //~--- constructors --------------------------------------------------------- @@ -87,6 +75,20 @@ public final class HgEnvironment //~--- methods -------------------------------------------------------------- + /** + * Method description + * + * + * @param environment + * @param handler + * @param hookManager + */ + public static void prepareEnvironment(Map<String, String> environment, + HgRepositoryHandler handler, HgHookManager hookManager) + { + prepareEnvironment(environment, handler, hookManager, null); + } + /** * Method description * @@ -105,65 +107,20 @@ public final class HgEnvironment if (request != null) { hookUrl = hookManager.createUrl(request); - environment.put(SCM_CREDENTIALS, getCredentials(request)); } else { hookUrl = hookManager.createUrl(); } + try { + String credentials = hookManager.getCredentials(); + environment.put(SCM_BEARER_TOKEN, credentials); + } catch (ProvisionException e) { + LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e); + } environment.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(handler.getConfig())); environment.put(ENV_URL, hookUrl); environment.put(ENV_CHALLENGE, hookManager.getChallenge()); } - - /** - * Method description - * - * - * @param environment - * @param handler - * @param hookManager - */ - public static void prepareEnvironment(Map<String, String> environment, - HgRepositoryHandler handler, HgHookManager hookManager) - { - prepareEnvironment(environment, handler, hookManager, null); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - private static String getCredentials(HttpServletRequest request) - { - String credentials = null; - String header = request.getHeader(HttpUtil.HEADER_AUTHORIZATION); - - if (!Strings.isNullOrEmpty(header)) - { - String[] parts = header.split("\\s+"); - - if (parts.length > 0) - { - CipherUtil cu = CipherUtil.getInstance(); - - credentials = cu.encode(Base64.decodeToString(parts[1])); - } - else - { - logger.warn("invalid basic authentication header"); - } - } - else - { - logger.warn("could not find authentication header on request"); - } - - return Strings.nullToEmpty(credentials); - } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java index b0bc088c98..6815bdad96 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java @@ -37,12 +37,19 @@ package sonia.scm.repository; import com.github.legman.Subscribe; import com.google.common.base.MoreObjects; -import com.google.inject.*; +import com.google.inject.Inject; +import com.google.inject.OutOfScopeException; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfigurationChangedEvent; import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.security.AccessToken; +import sonia.scm.security.AccessTokenBuilderFactory; +import sonia.scm.security.CipherUtil; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; @@ -74,19 +81,20 @@ public class HgHookManager /** * Constructs ... * - * - * @param configuration + * @param configuration * @param httpServletRequestProvider * @param httpClient + * @param accessTokenBuilderFactory */ @Inject public HgHookManager(ScmConfiguration configuration, - Provider<HttpServletRequest> httpServletRequestProvider, - AdvancedHttpClient httpClient) + Provider<HttpServletRequest> httpServletRequestProvider, + AdvancedHttpClient httpClient, AccessTokenBuilderFactory accessTokenBuilderFactory) { this.configuration = configuration; this.httpServletRequestProvider = httpServletRequestProvider; this.httpClient = httpClient; + this.accessTokenBuilderFactory = accessTokenBuilderFactory; } //~--- methods -------------------------------------------------------------- @@ -188,6 +196,13 @@ public class HgHookManager return this.challenge.equals(challenge); } + public String getCredentials() + { + AccessToken accessToken = accessTokenBuilderFactory.create().build(); + + return CipherUtil.getInstance().encode(accessToken.compact()); + } + //~--- methods -------------------------------------------------------------- /** @@ -321,7 +336,11 @@ public class HgHookManager { request = httpServletRequestProvider.get(); } - catch (ProvisionException | OutOfScopeException ex) + catch (ProvisionException ex) + { + logger.debug("http servlet request is not available"); + } + catch (OutOfScopeException ex) { logger.debug("http servlet request is not available"); } @@ -383,4 +402,6 @@ public class HgHookManager /** Field description */ private Provider<HttpServletRequest> httpServletRequestProvider; + + private final AccessTokenBuilderFactory accessTokenBuilderFactory; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java index 30b5063c11..b1d431c742 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java @@ -37,18 +37,16 @@ package sonia.scm.repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.io.INIConfiguration; import sonia.scm.io.INIConfigurationReader; -import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.util.ValidationUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -80,22 +78,10 @@ public class HgImportHandler extends AbstactImportHandler //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param repositoryDirectory - * @param repositoryName - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override protected Repository createRepository(File repositoryDirectory, String repositoryName) - throws IOException, RepositoryException + throws IOException { Repository repository = super.createRepository(repositoryDirectory, repositoryName); @@ -107,12 +93,7 @@ public class HgImportHandler extends AbstactImportHandler INIConfiguration c = reader.read(hgrc); INISection web = c.getSection("web"); - if (web == null) - { - handler.appendWebSection(c); - } - else - { + if (web != null) { repository.setDescription(web.getParameter("description")); String contact = web.getParameter("contact"); @@ -125,16 +106,7 @@ public class HgImportHandler extends AbstactImportHandler { logger.warn("contact {} is not a valid mail address", contact); } - - handler.setWebParameter(web); } - - // issue-97 - handler.registerMissingHook(c, repositoryName); - - INIConfigurationWriter writer = new INIConfigurationWriter(); - - writer.write(c, hgrc); } else { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index 40987fa4da..0a8dfe065a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -38,41 +38,34 @@ package sonia.scm.repository; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.ConfigurationException; import sonia.scm.SCMContextProvider; -import sonia.scm.Type; import sonia.scm.installer.HgInstaller; import sonia.scm.installer.HgInstallerFactory; -import sonia.scm.io.DirectoryFileFilter; import sonia.scm.io.ExtendedCommand; -import sonia.scm.io.FileSystem; import sonia.scm.io.INIConfiguration; -import sonia.scm.io.INIConfigurationReader; import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.spi.HgRepositoryServiceProvider; +import sonia.scm.repository.spi.HgWorkdirFactory; +import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.IOUtil; import sonia.scm.util.SystemUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; - import java.text.MessageFormat; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import sonia.scm.store.ConfigurationStoreFactory; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -98,7 +91,7 @@ public class HgRepositoryHandler public static final String TYPE_NAME = "hg"; /** Field description */ - public static final Type TYPE = new RepositoryType(TYPE_NAME, + public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, HgRepositoryServiceProvider.COMMANDS, HgRepositoryServiceProvider.FEATURES); @@ -110,23 +103,20 @@ public class HgRepositoryHandler /** Field description */ public static final String PATH_HGRC = ".hg".concat(File.separator).concat("hgrc"); + private static final String CONFIG_SECTION_SCMM = "scmm"; + private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param storeFactory - * @param fileSystem - * @param hgContextProvider - */ @Inject - public HgRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, - Provider<HgContext> hgContextProvider) + public HgRepositoryHandler(ConfigurationStoreFactory storeFactory, + Provider<HgContext> hgContextProvider, + RepositoryLocationResolver repositoryLocationResolver, + PluginLoader pluginLoader, HgWorkdirFactory workdirFactory) { - super(storeFactory, fileSystem); + super(storeFactory, repositoryLocationResolver, pluginLoader); this.hgContextProvider = hgContextProvider; + this.workdirFactory = workdirFactory; try { @@ -184,7 +174,6 @@ public class HgRepositoryHandler public void init(SCMContextProvider context) { super.init(context); - registerMissingHooks(); writePythonScripts(context); // fix wrong hg.bat from package installation @@ -259,7 +248,7 @@ public class HgRepositoryHandler * @return */ @Override - public Type getType() + public RepositoryType getType() { return TYPE; } @@ -304,100 +293,6 @@ public class HgRepositoryHandler return version; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param hgrc - */ - void appendHookSection(INIConfiguration hgrc) - { - INISection hooksSection = new INISection("hooks"); - - setHookParameter(hooksSection); - hgrc.addSection(hooksSection); - } - - /** - * Method description - * - * - * @param hgrc - */ - void appendWebSection(INIConfiguration hgrc) - { - INISection webSection = new INISection("web"); - - setWebParameter(webSection); - hgrc.addSection(webSection); - } - - /** - * Method description - * - * - * @param c - * @param repositoryName - * - * @return - */ - boolean registerMissingHook(INIConfiguration c, String repositoryName) - { - INISection hooks = c.getSection("hooks"); - - if (hooks == null) - { - hooks = new INISection("hooks"); - c.addSection(hooks); - } - - boolean write = false; - - if (appendHook(repositoryName, hooks, "changegroup.scm")) - { - write = true; - } - - if (appendHook(repositoryName, hooks, "pretxnchangegroup.scm")) - { - write = true; - } - - return write; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param hooksSection - */ - void setHookParameter(INISection hooksSection) - { - hooksSection.setParameter("changegroup.scm", "python:scmhooks.callback"); - hooksSection.setParameter("pretxnchangegroup.scm", - "python:scmhooks.callback"); - } - - /** - * Method description - * - * - * @param webSection - */ - void setWebParameter(INISection webSection) - { - webSection.setParameter("push_ssl", "false"); - webSection.setParameter("allow_read", "*"); - webSection.setParameter("allow_push", "*"); - } - - //~--- methods -------------------------------------------------------------- - /** * Method description * @@ -431,19 +326,19 @@ public class HgRepositoryHandler * @param directory * * @throws IOException - * @throws RepositoryException */ @Override protected void postCreate(Repository repository, File directory) - throws IOException, RepositoryException + throws IOException { File hgrcFile = new File(directory, PATH_HGRC); INIConfiguration hgrc = new INIConfiguration(); - appendWebSection(hgrc); - - // register hooks - appendHookSection(hgrc); + INISection iniSection = new INISection(CONFIG_SECTION_SCMM); + iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId()); + INIConfiguration iniConfiguration = new INIConfiguration(); + iniConfiguration.addSection(iniSection); + hgrc.addSection(iniSection); INIConfigurationWriter writer = new INIConfigurationWriter(); @@ -466,157 +361,6 @@ public class HgRepositoryHandler //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param repositoryName - * @param hooks - * @param hookName - * - * @return - */ - private boolean appendHook(String repositoryName, INISection hooks, - String hookName) - { - boolean write = false; - String hook = hooks.getParameter(hookName); - - if (Util.isEmpty(hook)) - { - if (logger.isInfoEnabled()) - { - logger.info("register missing {} hook for respository {}", hookName, - repositoryName); - } - - hooks.setParameter(hookName, "python:scmhooks.callback"); - write = true; - } - - return write; - } - - /** - * Method description - * - * - * @param file - */ - private void createNewFile(File file) - { - try - { - if (!file.createNewFile() && logger.isErrorEnabled()) - { - logger.error("could not create file {}", file); - } - } - catch (IOException ex) - { - logger.error("could not create file {}".concat(file.getPath()), ex); - } - } - - /** - * Method description - * - * - * @param repositoryDir - * - * @return - */ - private boolean registerMissingHook(File repositoryDir) - { - boolean result = false; - File hgrc = new File(repositoryDir, PATH_HGRC); - - if (hgrc.exists()) - { - try - { - INIConfigurationReader reader = new INIConfigurationReader(); - INIConfiguration c = reader.read(hgrc); - String repositoryName = repositoryDir.getName(); - - if (registerMissingHook(c, repositoryName)) - { - if (logger.isDebugEnabled()) - { - logger.debug("rewrite hgrc for repository {}", repositoryName); - } - - INIConfigurationWriter writer = new INIConfigurationWriter(); - - writer.write(c, hgrc); - } - - result = true; - } - catch (IOException ex) - { - logger.error("could not register missing hook", ex); - } - } - - return result; - } - - /** - * Method description - * - */ - private void registerMissingHooks() - { - HgConfig c = getConfig(); - - if (c != null) - { - File repositoryDirectroy = c.getRepositoryDirectory(); - - if (repositoryDirectroy.exists()) - { - File lockFile = new File(repositoryDirectroy, PATH_HOOK); - - if (!lockFile.exists()) - { - File[] dirs = - repositoryDirectroy.listFiles(DirectoryFileFilter.instance); - boolean success = true; - - if (Util.isNotEmpty(dirs)) - { - for (File dir : dirs) - { - if (!registerMissingHook(dir)) - { - success = false; - } - } - } - - if (success) - { - createNewFile(lockFile); - } - } - else if (logger.isDebugEnabled()) - { - logger.debug("hooks allready registered"); - } - } - else if (logger.isDebugEnabled()) - { - logger.debug( - "repository directory does not exists, could not register missing hooks"); - } - } - else if (logger.isDebugEnabled()) - { - logger.debug("config is not available, could not register missing hooks"); - } - } - /** * Method description * @@ -656,6 +400,10 @@ public class HgRepositoryHandler } } + public HgWorkdirFactory getWorkdirFactory() { + return workdirFactory; + } + //~--- fields --------------------------------------------------------------- /** Field description */ @@ -663,4 +411,6 @@ public class HgRepositoryHandler /** Field description */ private JAXBContext jaxbContext; + + private final HgWorkdirFactory workdirFactory; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersion.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersion.java index ea4a96ba6e..097046e7f7 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersion.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersion.java @@ -37,12 +37,12 @@ package sonia.scm.repository; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -//~--- JDK imports ------------------------------------------------------------ - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -102,9 +102,9 @@ public class HgVersion { //J- return MoreObjects.toStringHelper(this) - .add("mercurial", mercurial) - .add("python", python) - .toString(); + .add("mercurial", mercurial) + .add("python", python) + .toString(); //J+ } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersionHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersionHandler.java index e52eee2c8a..26c9095ead 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersionHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersionHandler.java @@ -44,15 +44,6 @@ import java.io.IOException; public class HgVersionHandler extends AbstractHgHandler { - /** - * Constructs ... - * - * - * @param handler - * @param jaxbContext - * @param context - * @param directory - */ public HgVersionHandler(HgRepositoryHandler handler, HgContext context, File directory) { @@ -61,17 +52,7 @@ public class HgVersionHandler extends AbstractHgHandler //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - public HgVersion getVersion() throws IOException, RepositoryException - { + public HgVersion getVersion() throws IOException { return getResultFromScript(HgVersion.class, HgPythonScript.VERSION); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/AbstractHgPushOrPullCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/AbstractHgPushOrPullCommand.java index 43fb759433..4782d03756 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/AbstractHgPushOrPullCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/AbstractHgPushOrPullCommand.java @@ -78,7 +78,7 @@ public class AbstractHgPushOrPullCommand extends AbstractCommand if (repo != null) { url = - handler.getDirectory(request.getRemoteRepository()).getAbsolutePath(); + handler.getDirectory(request.getRemoteRepository().getId()).getAbsolutePath(); } else if (request.getRemoteUrl() != null) { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBlameCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBlameCommand.java index be5dbeda04..96a57ece65 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBlameCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBlameCommand.java @@ -38,26 +38,21 @@ package sonia.scm.repository.spi; import com.aragost.javahg.Changeset; import com.aragost.javahg.commands.AnnotateCommand; import com.aragost.javahg.commands.AnnotateLine; - import com.google.common.base.Strings; import com.google.common.collect.Lists; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.BlameLine; import sonia.scm.repository.BlameResult; import sonia.scm.repository.Person; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.web.HgUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -87,20 +82,9 @@ public class HgBlameCommand extends AbstractCommand implements BlameCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override public BlameResult getBlameResult(BlameCommandRequest request) - throws IOException, RepositoryException + throws IOException { if (logger.isDebugEnabled()) { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java new file mode 100644 index 0000000000..37e0937aca --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2014, 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.spi; + +import com.aragost.javahg.Changeset; +import com.aragost.javahg.commands.CommitCommand; +import com.aragost.javahg.commands.PullCommand; +import org.apache.shiro.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ContextEntry; +import sonia.scm.repository.Branch; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.BranchRequest; +import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.user.User; + +/** + * Mercurial implementation of the {@link BranchCommand}. + * Note that this creates an empty commit to "persist" the new branch. + */ +public class HgBranchCommand extends AbstractCommand implements BranchCommand { + + private static final Logger LOG = LoggerFactory.getLogger(HgBranchCommand.class); + + private final HgWorkdirFactory workdirFactory; + + HgBranchCommand(HgCommandContext context, Repository repository, HgWorkdirFactory workdirFactory) { + super(context, repository); + this.workdirFactory = workdirFactory; + } + + @Override + public Branch branch(BranchRequest request) { + try (WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(getContext(), request.getParentBranch())) { + com.aragost.javahg.Repository repository = workingCopy.getWorkingRepository(); + + Changeset emptyChangeset = createNewBranchWithEmptyCommit(request, repository); + + LOG.debug("Created new branch '{}' in repository {} with changeset {}", + request.getNewBranch(), getRepository().getNamespaceAndName(), emptyChangeset.getNode()); + + pullChangesIntoCentralRepository(workingCopy, request.getNewBranch()); + + return Branch.normalBranch(request.getNewBranch(), emptyChangeset.getNode()); + } + } + + @Override + public void deleteOrClose(String branchName) { + try (WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(getContext(), branchName)) { + User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class); + + LOG.debug("Closing branch '{}' in repository {}", branchName, getRepository().getNamespaceAndName()); + + com.aragost.javahg.commands.CommitCommand + .on(workingCopy.getWorkingRepository()) + .user(getFormattedUser(currentUser)) + .message(String.format("Close branch: %s", branchName)) + .closeBranch() + .execute(); + pullChangesIntoCentralRepository(workingCopy, branchName); + } catch (Exception ex) { + throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity(getContext().getScmRepository()), String.format("Could not close branch: %s", branchName)); + } + } + + private String getFormattedUser(User currentUser) { + return String.format("%s <%s>", currentUser.getDisplayName(), currentUser.getMail()); + } + + private Changeset createNewBranchWithEmptyCommit(BranchRequest request, com.aragost.javahg.Repository repository) { + com.aragost.javahg.commands.BranchCommand.on(repository).set(request.getNewBranch()); + User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class); + return CommitCommand + .on(repository) + .user(getFormattedUser(currentUser)) + .message("Create new branch " + request.getNewBranch()) + .execute(); + } + + private void pullChangesIntoCentralRepository(WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy, String branch) { + try { + PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); + workdirFactory.configure(pullCommand); + pullCommand.execute(workingCopy.getDirectory().getAbsolutePath()); + } catch (Exception e) { + // TODO handle failed update + throw new IntegrateChangesFromWorkdirException(getRepository(), + String.format("Could not pull changes '%s' into central repository", branch), + e); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java index a2630d3730..55b937be4b 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java @@ -40,9 +40,7 @@ import com.google.common.base.Function; import com.google.common.collect.Lists; import sonia.scm.repository.Branch; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; -import java.io.IOException; import java.util.List; //~--- JDK imports ------------------------------------------------------------ @@ -55,6 +53,8 @@ public class HgBranchesCommand extends AbstractCommand implements BranchesCommand { + private static final String DEFAULT_BRANCH_NAME = "default"; + /** * Constructs ... * @@ -69,33 +69,35 @@ public class HgBranchesCommand extends AbstractCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override - public List<Branch> getBranches() throws RepositoryException, IOException - { + public List<Branch> getBranches() { List<com.aragost.javahg.commands.Branch> hgBranches = com.aragost.javahg.commands.BranchesCommand.on(open()).execute(); - final Function<com.aragost.javahg.commands.Branch, Branch> branchFunction = hgBranch -> { - String node = null; - Changeset changeset = hgBranch.getBranchTip(); + List<Branch> branches = Lists.transform(hgBranches, + new Function<com.aragost.javahg.commands.Branch, + Branch>() + { - if (changeset != null) { - node = changeset.getNode(); + @Override + public Branch apply(com.aragost.javahg.commands.Branch hgBranch) + { + String node = null; + Changeset changeset = hgBranch.getBranchTip(); + + if (changeset != null) + { + node = changeset.getNode(); + } + + if (DEFAULT_BRANCH_NAME.equals(hgBranch.getName())) { + return Branch.defaultBranch(hgBranch.getName(), node); + } else { + return Branch.normalBranch(hgBranch.getName(), node); + } } + }); - return new Branch(hgBranch.getName(), node); - }; - - return Lists.transform(hgBranches, - branchFunction); + return branches; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java index 0303275adf..b3e8e89a5f 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java @@ -35,18 +35,21 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import com.aragost.javahg.Changeset; +import com.aragost.javahg.commands.LogCommand; +import com.google.common.base.MoreObjects; import com.google.common.base.Strings; - import sonia.scm.repository.BrowserResult; +import sonia.scm.repository.FileObject; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.javahg.HgFileviewCommand; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** + * Utilizes the mercurial fileview extension in order to support mercurial repository browsing. * * @author Sebastian Sdorra */ @@ -67,26 +70,15 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override - public BrowserResult getBrowserResult(BrowseCommandRequest request) - throws IOException, RepositoryException - { + public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { HgFileviewCommand cmd = HgFileviewCommand.on(open()); - if (!Strings.isNullOrEmpty(request.getRevision())) - { - cmd.rev(request.getRevision()); + String revision = MoreObjects.firstNonNull(request.getRevision(), "tip"); + Changeset c = LogCommand.on(getContext().open()).rev(revision).limit(1).single(); + + if (c != null) { + cmd.rev(c.getNode()); } if (!Strings.isNullOrEmpty(request.getPath())) @@ -109,10 +101,7 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand cmd.disableSubRepositoryDetection(); } - BrowserResult result = new BrowserResult(); - - result.setFiles(cmd.execute()); - - return result; + FileObject file = cmd.execute(); + return new BrowserResult(c == null? "tip": c.getNode(), revision, file); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java index 76438c1f1e..6309720f75 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java @@ -33,71 +33,50 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import com.aragost.javahg.commands.ExecutionException; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; - +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ContextEntry; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.web.HgUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -/** - * - * @author Sebastian Sdorra - */ -public class HgCatCommand extends AbstractCommand implements CatCommand -{ +public class HgCatCommand extends AbstractCommand implements CatCommand { - /** - * Constructs ... - * - * - * @param context - * @param repository - */ - HgCatCommand(HgCommandContext context, Repository repository) - { + private static final Logger log = LoggerFactory.getLogger(HgCatCommand.class); + + HgCatCommand(HgCommandContext context, Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param output - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void getCatResult(CatCommandRequest request, OutputStream output) - throws IOException, RepositoryException - { + public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException { + InputStream input = getCatResultStream(request); + try { + ByteStreams.copy(input, output); + } finally { + Closeables.close(input, true); + } + } + + @Override + public InputStream getCatResultStream(CatCommandRequest request) throws IOException { com.aragost.javahg.commands.CatCommand cmd = com.aragost.javahg.commands.CatCommand.on(open()); cmd.rev(HgUtil.getRevision(request.getRevision())); - InputStream input = null; - - try - { - input = cmd.execute(request.getPath()); - ByteStreams.copy(input, output); - } - finally - { - Closeables.close(input, true); + try { + return cmd.execute(request.getPath()); + } catch (ExecutionException e) { + log.error("could not execute cat command", e); + throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity(getRepository()), "could not execute cat command", e); } } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java index fee73513cd..c744abc186 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java @@ -49,6 +49,8 @@ import sonia.scm.web.HgUtil; import java.io.Closeable; import java.io.File; import java.io.IOException; +import java.util.Map; +import java.util.function.BiConsumer; /** * @@ -66,15 +68,14 @@ public class HgCommandContext implements Closeable * Constructs ... * * - * * @param hookManager * @param handler * @param repository * @param directory */ public HgCommandContext(HgHookManager hookManager, - HgRepositoryHandler handler, sonia.scm.repository.Repository repository, - File directory) + HgRepositoryHandler handler, sonia.scm.repository.Repository repository, + File directory) { this(hookManager, handler, repository, directory, handler.getHgContext().isPending()); @@ -84,26 +85,26 @@ public class HgCommandContext implements Closeable * Constructs ... * * - * * @param hookManager - * @param hanlder + * @param handler * @param repository * @param directory * @param pending */ public HgCommandContext(HgHookManager hookManager, - HgRepositoryHandler hanlder, sonia.scm.repository.Repository repository, - File directory, boolean pending) + HgRepositoryHandler handler, sonia.scm.repository.Repository repository, + File directory, boolean pending) { this.hookManager = hookManager; - this.hanlder = hanlder; + this.handler = handler; this.directory = directory; + this.scmRepository = repository; this.encoding = repository.getProperty(PROPERTY_ENCODING); this.pending = pending; if (Strings.isNullOrEmpty(encoding)) { - encoding = hanlder.getConfig().getEncoding(); + encoding = handler.getConfig().getEncoding(); } } @@ -134,13 +135,18 @@ public class HgCommandContext implements Closeable { if (repository == null) { - repository = HgUtil.open(hanlder, hookManager, directory, encoding, - pending); + repository = HgUtil.open(handler, hookManager, directory, encoding, pending); } return repository; } + public Repository openWithSpecialEnvironment(BiConsumer<sonia.scm.repository.Repository, Map<String, String>> prepareEnvironment) + { + return HgUtil.open(handler, directory, encoding, + pending, environment -> prepareEnvironment.accept(scmRepository, environment)); + } + //~--- get methods ---------------------------------------------------------- /** @@ -151,7 +157,11 @@ public class HgCommandContext implements Closeable */ public HgConfig getConfig() { - return hanlder.getConfig(); + return handler.getConfig(); + } + + public sonia.scm.repository.Repository getScmRepository() { + return scmRepository; } //~--- fields --------------------------------------------------------------- @@ -163,7 +173,7 @@ public class HgCommandContext implements Closeable private String encoding; /** Field description */ - private HgRepositoryHandler hanlder; + private HgRepositoryHandler handler; /** Field description */ private HgHookManager hookManager; @@ -173,4 +183,6 @@ public class HgCommandContext implements Closeable /** Field description */ private Repository repository; + + private final sonia.scm.repository.Repository scmRepository; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java index dcdcdeec2f..9e43e26014 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java @@ -38,18 +38,15 @@ package sonia.scm.repository.spi; import com.google.common.base.Strings; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; - import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.spi.javahg.HgDiffInternalCommand; import sonia.scm.web.HgUtil; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -72,52 +69,37 @@ public class HgDiffCommand extends AbstractCommand implements DiffCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * @param output - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) - throws IOException, RepositoryException + public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) { - com.aragost.javahg.Repository hgRepo = open(); + return output -> { + com.aragost.javahg.Repository hgRepo = open(); - HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo); - DiffFormat format = request.getFormat(); + HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo); + DiffFormat format = request.getFormat(); - if (format == DiffFormat.GIT) - { - cmd.git(); - } - - cmd.change(HgUtil.getRevision(request.getRevision())); - - InputStream inputStream = null; - - try - { - - if (!Strings.isNullOrEmpty(request.getPath())) + if (format == DiffFormat.GIT) { - inputStream = cmd.stream(hgRepo.file(request.getPath())); - } - else - { - inputStream = cmd.stream(); + cmd.git(); } - ByteStreams.copy(inputStream, output); + cmd.change(HgUtil.getRevision(request.getRevision())); - } - finally - { - Closeables.close(inputStream, true); - } + InputStream inputStream = null; + + try { + + if (!Strings.isNullOrEmpty(request.getPath())) { + inputStream = cmd.stream(hgRepo.file(request.getPath())); + } else { + inputStream = cmd.stream(); + } + + ByteStreams.copy(inputStream, output); + + } finally { + Closeables.close(inputStream, true); + } + }; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookChangesetProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookChangesetProvider.java index 17c0816686..e4e3dc238e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookChangesetProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookChangesetProvider.java @@ -34,20 +34,18 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.aragost.javahg.Repository; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.spi.javahg.HgLogChangesetCommand; import sonia.scm.web.HgUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -63,22 +61,12 @@ public class HgHookChangesetProvider implements HookChangesetProvider //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param handler - * @param repositoryName - * @param hookManager - * @param startRev - * @param type - */ public HgHookChangesetProvider(HgRepositoryHandler handler, - String repositoryName, HgHookManager hookManager, String startRev, + File repositoryDirectory, HgHookManager hookManager, String startRev, RepositoryHookType type) { this.handler = handler; - this.repositoryName = repositoryName; + this.repositoryDirectory = repositoryDirectory; this.hookManager = hookManager; this.startRev = startRev; this.type = type; @@ -135,9 +123,6 @@ public class HgHookChangesetProvider implements HookChangesetProvider */ private Repository open() { - File directory = handler.getConfig().getRepositoryDirectory(); - File repositoryDirectory = new File(directory, repositoryName); - // use HG_PENDING only for pre receive hooks boolean pending = type == RepositoryHookType.PRE_RECEIVE; @@ -155,7 +140,7 @@ public class HgHookChangesetProvider implements HookChangesetProvider private HgHookManager hookManager; /** Field description */ - private String repositoryName; + private File repositoryDirectory; /** Field description */ private HookChangesetResponse response; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java index dede59796c..414cfe27b8 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java @@ -38,16 +38,17 @@ import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.api.HgHookBranchProvider; import sonia.scm.repository.api.HgHookMessageProvider; +import sonia.scm.repository.api.HgHookTagProvider; import sonia.scm.repository.api.HookBranchProvider; import sonia.scm.repository.api.HookFeature; import sonia.scm.repository.api.HookMessageProvider; +import sonia.scm.repository.api.HookTagProvider; -//~--- JDK imports ------------------------------------------------------------ - +import java.io.File; import java.util.EnumSet; import java.util.Set; -import sonia.scm.repository.api.HgHookTagProvider; -import sonia.scm.repository.api.HookTagProvider; + +//~--- JDK imports ------------------------------------------------------------ /** * Mercurial implementation of {@link HookContextProvider}. @@ -67,17 +68,16 @@ public class HgHookContextProvider extends HookContextProvider * Constructs a new instance. * * @param handler mercurial repository handler - * @param repositoryName name of changed repository + * @param repositoryDirectory the directory of the changed repository * @param hookManager mercurial hook manager * @param startRev start revision * @param type type of hook */ public HgHookContextProvider(HgRepositoryHandler handler, - String repositoryName, HgHookManager hookManager, String startRev, - RepositoryHookType type) + File repositoryDirectory, HgHookManager hookManager, String startRev, + RepositoryHookType type) { - this.hookChangesetProvider = new HgHookChangesetProvider(handler, - repositoryName, hookManager, startRev, type); + this.hookChangesetProvider = new HgHookChangesetProvider(handler, repositoryDirectory, hookManager, startRev, type); } //~--- get methods ---------------------------------------------------------- diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java index 73284d886a..52a4ddf261 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java @@ -35,21 +35,19 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.aragost.javahg.commands.ExecutionException; - import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.javahg.HgIncomingChangesetCommand; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; - import java.util.Collections; import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -80,23 +78,10 @@ public class HgIncomingCommand extends AbstractCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws RepositoryException - */ @Override @SuppressWarnings("unchecked") - public ChangesetPagingResult getIncomingChangesets( - IncomingCommandRequest request) - throws RepositoryException - { - File remoteRepository = handler.getDirectory(request.getRemoteRepository()); + public ChangesetPagingResult getIncomingChangesets(IncomingCommandRequest request) { + File remoteRepository = handler.getDirectory(request.getRemoteRepository().getId()); com.aragost.javahg.Repository repository = open(); @@ -118,7 +103,7 @@ public class HgIncomingCommand extends AbstractCommand } else { - throw new RepositoryException("could not execute incoming command", ex); + throw new InternalRepositoryException(getRepository(), "could not execute incoming command", ex); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java index 6dbba81a16..8a9a1c8e84 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java @@ -36,20 +36,16 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Strings; - import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.javahg.HgLogChangesetCommand; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import java.util.ArrayList; import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -71,42 +67,16 @@ public class HgLogCommand extends AbstractCommand implements LogCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param id - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override - public Changeset getChangeset(String id) - throws IOException, RepositoryException - { + public Changeset getChangeset(String id, LogCommandRequest request) { com.aragost.javahg.Repository repository = open(); HgLogChangesetCommand cmd = on(repository); return cmd.rev(id).single(); } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override - public ChangesetPagingResult getChangesets(LogCommandRequest request) - throws IOException, RepositoryException - { + public ChangesetPagingResult getChangesets(LogCommandRequest request) { ChangesetPagingResult result = null; com.aragost.javahg.Repository repository = open(); @@ -162,13 +132,17 @@ public class HgLogCommand extends AbstractCommand implements LogCommand List<Changeset> changesets = on(repository).rev(start + ":" + end).execute(); - result = new ChangesetPagingResult(total, changesets); + if (request.getBranch() == null) { + result = new ChangesetPagingResult(total, changesets); + } else { + result = new ChangesetPagingResult(total, changesets, request.getBranch()); + } } else { // empty repository - result = new ChangesetPagingResult(0, new ArrayList<>()); + result = new ChangesetPagingResult(0, new ArrayList<Changeset>()); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModificationsCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModificationsCommand.java new file mode 100644 index 0000000000..f9a67f8656 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModificationsCommand.java @@ -0,0 +1,29 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.Modifications; +import sonia.scm.repository.Repository; +import sonia.scm.repository.spi.javahg.HgLogChangesetCommand; + +public class HgModificationsCommand extends AbstractCommand implements ModificationsCommand { + + HgModificationsCommand(HgCommandContext context, Repository repository) { + super(context, repository); + } + + + @Override + public Modifications getModifications(String revision) { + com.aragost.javahg.Repository repository = open(); + HgLogChangesetCommand hgLogChangesetCommand = HgLogChangesetCommand.on(repository, getContext().getConfig()); + Modifications modifications = hgLogChangesetCommand.rev(revision).extractModifications(); + modifications.setRevision(revision); + return modifications; + } + + @Override + public Modifications getModifications(ModificationsCommandRequest request) { + return getModifications(request.getRevision()); + } + + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java new file mode 100644 index 0000000000..419b7de161 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java @@ -0,0 +1,103 @@ +package sonia.scm.repository.spi; + +import com.aragost.javahg.Changeset; +import com.aragost.javahg.Repository; +import com.aragost.javahg.commands.CommitCommand; +import com.aragost.javahg.commands.ExecutionException; +import com.aragost.javahg.commands.PullCommand; +import com.aragost.javahg.commands.RemoveCommand; +import com.aragost.javahg.commands.StatusCommand; +import sonia.scm.NoChangesMadeException; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +public class HgModifyCommand implements ModifyCommand { + + private HgCommandContext context; + private final HgWorkdirFactory workdirFactory; + + public HgModifyCommand(HgCommandContext context, HgWorkdirFactory workdirFactory) { + this.context = context; + this.workdirFactory = workdirFactory; + } + + @Override + public String execute(ModifyCommandRequest request) { + + try (WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(context, request.getBranch())) { + Repository workingRepository = workingCopy.getWorkingRepository(); + request.getRequests().forEach( + partialRequest -> { + try { + partialRequest.execute(new ModifyWorkerHelper() { + + @Override + public void addFileToScm(String name, Path file) { + try { + addFileToHg(file.toFile()); + } catch (ExecutionException e) { + throwInternalRepositoryException("could not add new file to index", e); + } + } + + @Override + public void doScmDelete(String toBeDeleted) { + RemoveCommand.on(workingRepository).execute(toBeDeleted); + } + + @Override + public sonia.scm.repository.Repository getRepository() { + return context.getScmRepository(); + } + + @Override + public String getBranch() { + return request.getBranch(); + } + + public File getWorkDir() { + return workingRepository.getDirectory(); + } + + private void addFileToHg(File file) { + workingRepository.workingCopy().add(file.getAbsolutePath()); + } + }); + } catch (IOException e) { + throwInternalRepositoryException("could not execute command on repository", e); + } + } + ); + if (StatusCommand.on(workingRepository).lines().isEmpty()) { + throw new NoChangesMadeException(context.getScmRepository()); + } + CommitCommand.on(workingRepository).user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail())).message(request.getCommitMessage()).execute(); + List<Changeset> execute = pullModifyChangesToCentralRepository(request, workingCopy); + return execute.get(0).getNode(); + } catch (ExecutionException e) { + throwInternalRepositoryException("could not execute command on repository", e); + return null; + } + } + + private List<Changeset> pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy) { + try { + com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); + workdirFactory.configure(pullCommand); + return pullCommand.execute(workingCopy.getDirectory().getAbsolutePath()); + } catch (Exception e) { + throw new IntegrateChangesFromWorkdirException(context.getScmRepository(), + String.format("Could not pull modify changes from working copy to central repository for branch %s", request.getBranch()), + e); + } + } + + private String throwInternalRepositoryException(String message, Exception e) { + throw new InternalRepositoryException(context.getScmRepository(), message, e); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java index 1322424198..4bbf30e936 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java @@ -35,21 +35,19 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.aragost.javahg.commands.ExecutionException; - import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.javahg.HgOutgoingChangesetCommand; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; - import java.util.Collections; import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -80,23 +78,12 @@ public class HgOutgoingCommand extends AbstractCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws RepositoryException - */ @Override @SuppressWarnings("unchecked") public ChangesetPagingResult getOutgoingChangesets( OutgoingCommandRequest request) - throws RepositoryException { - File remoteRepository = handler.getDirectory(request.getRemoteRepository()); + File remoteRepository = handler.getDirectory(request.getRemoteRepository().getId()); com.aragost.javahg.Repository repository = open(); @@ -116,7 +103,7 @@ public class HgOutgoingCommand extends AbstractCommand } else { - throw new RepositoryException("could not execute outgoing command", ex); + throw new InternalRepositoryException(getRepository(), "could not execute outgoing command", ex); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java index 5243cbcc92..1d130a0f79 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java @@ -37,22 +37,19 @@ package sonia.scm.repository.spi; import com.aragost.javahg.Changeset; import com.aragost.javahg.commands.ExecutionException; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.PullResponse; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.Collections; import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -83,21 +80,10 @@ public class HgPullCommand extends AbstractHgPushOrPullCommand //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override @SuppressWarnings("unchecked") public PullResponse pull(PullCommandRequest request) - throws RepositoryException, IOException + throws IOException { String url = getRemoteUrl(request); @@ -111,7 +97,7 @@ public class HgPullCommand extends AbstractHgPushOrPullCommand } catch (ExecutionException ex) { - throw new RepositoryException("could not execute push command", ex); + throw new InternalRepositoryException(getRepository(), "could not execute push command", ex); } return new PullResponse(result.size()); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java index 10b4af002e..b1c12037db 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java @@ -37,22 +37,19 @@ package sonia.scm.repository.spi; import com.aragost.javahg.Changeset; import com.aragost.javahg.commands.ExecutionException; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.PushResponse; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.Collections; import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -83,21 +80,10 @@ public class HgPushCommand extends AbstractHgPushOrPullCommand //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override @SuppressWarnings("unchecked") public PushResponse push(PushCommandRequest request) - throws RepositoryException, IOException + throws IOException { String url = getRemoteUrl(request); @@ -111,7 +97,7 @@ public class HgPushCommand extends AbstractHgPushOrPullCommand } catch (ExecutionException ex) { - throw new RepositoryException("could not execute push command", ex); + throw new InternalRepositoryException(getRepository(), "could not execute push command", ex); } return new PushResponse(result.size()); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index 4295bf45e4..6286269d02 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -33,21 +33,16 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.io.Closeables; - import sonia.scm.repository.Feature; import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.api.CommandNotSupportedException; import java.io.File; import java.io.IOException; - import java.util.EnumSet; import java.util.Set; @@ -66,12 +61,14 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider Command.CAT, Command.DIFF, Command.LOG, - Command.TAGS, + Command.TAGS, + Command.BRANCH, Command.BRANCHES, Command.INCOMING, Command.OUTGOING, Command.PUSH, - Command.PULL + Command.PULL, + Command.MODIFY ); //J+ @@ -81,22 +78,12 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * - * - * @param hookManager - * @param handler - * @param repository - */ HgRepositoryServiceProvider(HgRepositoryHandler handler, - HgHookManager hookManager, Repository repository) + HgHookManager hookManager, Repository repository) { this.repository = repository; this.handler = handler; - this.repositoryDirectory = handler.getDirectory(repository); + this.repositoryDirectory = handler.getDirectory(repository.getId()); this.context = new HgCommandContext(hookManager, handler, repository, repositoryDirectory); } @@ -141,6 +128,11 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider return new HgBranchesCommand(context, repository); } + @Override + public BranchCommand getBranchCommand() { + return new HgBranchCommand(context, repository, handler.getWorkdirFactory()); + } + /** * Method description * @@ -201,6 +193,17 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider return new HgLogCommand(context, repository); } + /** + * 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 + */ + @Override + public ModificationsCommand getModificationsCommand() { + return new HgModificationsCommand(context,repository); + } + /** * Method description * @@ -237,6 +240,11 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider return new HgPushCommand(handler, context, repository); } + @Override + public ModifyCommand getModifyCommand() { + return new HgModifyCommand(context, handler.getWorkdirFactory()); + } + /** * Method description * diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java index 9b87991ae6..7519cb564d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java @@ -33,10 +33,7 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.Inject; - import sonia.scm.plugin.Extension; import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgRepositoryHandler; @@ -50,56 +47,25 @@ import sonia.scm.repository.Repository; public class HgRepositoryServiceResolver implements RepositoryServiceResolver { - /** Field description */ - private static final String TYPE = "hg"; + private final HgRepositoryHandler handler; + private final HgHookManager hookManager; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param hookManager - * @param handler - */ @Inject public HgRepositoryServiceResolver(HgRepositoryHandler handler, - HgHookManager hookManager) + HgHookManager hookManager) { this.handler = handler; this.hookManager = hookManager; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * - * @return - */ @Override - public HgRepositoryServiceProvider reslove(Repository repository) - { + public HgRepositoryServiceProvider resolve(Repository repository) { HgRepositoryServiceProvider provider = null; - if (TYPE.equalsIgnoreCase(repository.getType())) - { - provider = new HgRepositoryServiceProvider(handler, hookManager, - repository); + if (HgRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { + provider = new HgRepositoryServiceProvider(handler, hookManager, repository); } return provider; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private HgRepositoryHandler handler; - - /** Field description */ - private HgHookManager hookManager; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java new file mode 100644 index 0000000000..2920abc422 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java @@ -0,0 +1,9 @@ +package sonia.scm.repository.spi; + +import com.aragost.javahg.Repository; +import com.aragost.javahg.commands.PullCommand; +import sonia.scm.repository.util.WorkdirFactory; + +public interface HgWorkdirFactory extends WorkdirFactory<Repository, Repository, HgCommandContext> { + void configure(PullCommand pullCommand); +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java new file mode 100644 index 0000000000..412e9b7bf6 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java @@ -0,0 +1,64 @@ +package sonia.scm.repository.spi; + +import com.aragost.javahg.BaseRepository; +import com.aragost.javahg.Repository; +import com.aragost.javahg.commands.CloneCommand; +import com.aragost.javahg.commands.PullCommand; +import com.aragost.javahg.commands.flags.CloneCommandFlags; +import sonia.scm.repository.util.SimpleWorkdirFactory; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.web.HgRepositoryEnvironmentBuilder; + +import javax.inject.Inject; +import javax.inject.Provider; +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.function.BiConsumer; + +public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory<Repository, Repository, HgCommandContext> implements HgWorkdirFactory { + + private final Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder; + + @Inject + public SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder, WorkdirProvider workdirProvider) { + super(workdirProvider); + this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder; + } + @Override + public ParentAndClone<Repository, Repository> cloneRepository(HgCommandContext context, File target, String initialBranch) throws IOException { + BiConsumer<sonia.scm.repository.Repository, Map<String, String>> repositoryMapBiConsumer = + (repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment); + Repository centralRepository = context.openWithSpecialEnvironment(repositoryMapBiConsumer); + CloneCommand cloneCommand = CloneCommandFlags.on(centralRepository); + if (initialBranch != null) { + cloneCommand.updaterev(initialBranch); + } + cloneCommand.execute(target.getAbsolutePath()); + + BaseRepository clone = Repository.open(target); + + return new ParentAndClone<>(centralRepository, clone); + } + + @Override + protected void closeRepository(Repository repository) { + repository.close(); + } + + @Override + protected void closeWorkdirInternal(Repository workdir) throws Exception { + workdir.close(); + } + + @Override + protected sonia.scm.repository.Repository getScmRepository(HgCommandContext context) { + return context.getScmRepository(); + } + + @Override + public void configure(PullCommand pullCommand) { + pullCommand.cmdAppend("--config", "hooks.changegroup.scm=python:scmhooks.postHook"); + pullCommand.cmdAppend("--config", "hooks.pretxnchangegroup.scm=python:scmhooks.preHook"); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java index 827f86ded1..89164a8d80 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java @@ -41,21 +41,18 @@ import com.aragost.javahg.internals.AbstractCommand; import com.aragost.javahg.internals.HgInputStream; import com.aragost.javahg.internals.RuntimeIOException; import com.aragost.javahg.internals.Utils; - import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; - import sonia.scm.repository.Changeset; import sonia.scm.repository.HgConfig; import sonia.scm.repository.Modifications; import sonia.scm.repository.Person; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -219,10 +216,7 @@ public abstract class AbstractChangesetCommand extends AbstractCommand String branch = in.textUpTo('\n'); - if (!BRANCH_DEFAULT.equals(branch)) - { - changeset.getBranches().add(branch); - } + changeset.getBranches().add(branch); String p1 = readId(in, changeset, PROPERTY_PARENT1_REVISION); @@ -251,33 +245,14 @@ public abstract class AbstractChangesetCommand extends AbstractCommand changeset.getProperties().put(PROPERTY_CLOSE, "true"); } - Modifications modifications = changeset.getModifications(); - String line = in.textUpTo('\n'); - - while (line.length() > 0) - { - - if (line.startsWith("a ")) - { - modifications.getAdded().add(line.substring(2)); - } - else if (line.startsWith("m ")) - { - modifications.getModified().add(line.substring(2)); - } - else if (line.startsWith("d ")) - { - modifications.getRemoved().add(line.substring(2)); - } - else if (line.startsWith("t ")) + while (line.length() > 0) { + if (line.startsWith("t ")) { changeset.getTags().add(line.substring(2)); } - line = in.textUpTo('\n'); } - String message = in.textUpTo('\0'); changeset.setDescription(message); @@ -285,6 +260,36 @@ public abstract class AbstractChangesetCommand extends AbstractCommand return changeset; } + protected Modifications readModificationsFromStream(HgInputStream in) { + try { + boolean found = in.find(CHANGESET_PATTERN); + if (found) { + while (!in.match(CHANGESET_PATTERN)) { + return extractModifications(in); + } + } + } catch (IOException e) { + throw new RuntimeIOException(e); + } + return null; + } + + private Modifications extractModifications(HgInputStream in) throws IOException { + Modifications modifications = new Modifications(); + String line = in.textUpTo('\n'); + while (line.length() > 0) { + if (line.startsWith("a ")) { + modifications.getAdded().add(line.substring(2)); + } else if (line.startsWith("m ")) { + modifications.getModified().add(line.substring(2)); + } else if (line.startsWith("d ")) { + modifications.getRemoved().add(line.substring(2)); + } + line = in.textUpTo('\n'); + } + return modifications; + } + /** * Method description * diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 74695217d2..0897a191a1 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -41,7 +41,6 @@ import com.aragost.javahg.internals.AbstractCommand; import com.aragost.javahg.internals.HgInputStream; import com.google.common.base.Strings; -import com.google.common.collect.Lists; import sonia.scm.repository.FileObject; import sonia.scm.repository.SubRepository; @@ -50,35 +49,30 @@ import sonia.scm.repository.SubRepository; import java.io.IOException; -import java.util.List; +import java.util.Deque; +import java.util.LinkedList; /** + * Mercurial command to list files of a repository. * * @author Sebastian Sdorra */ public class HgFileviewCommand extends AbstractCommand { - /** - * Constructs ... - * - * - * @param repository - */ - public HgFileviewCommand(Repository repository) + private boolean disableLastCommit = false; + + private HgFileviewCommand(Repository repository) { super(repository); } - //~--- methods -------------------------------------------------------------- - /** - * Method description + * Create command for the given repository. * + * @param repository repository * - * @param repository - * - * @return + * @return fileview command */ public static HgFileviewCommand on(Repository repository) { @@ -86,13 +80,11 @@ public class HgFileviewCommand extends AbstractCommand } /** - * Method description + * Disable last commit fetching for file objects. * - * - * @return + * @return {@code this} */ - public HgFileviewCommand disableLastCommit() - { + public HgFileviewCommand disableLastCommit() { disableLastCommit = true; cmdAppend("-d"); @@ -100,132 +92,128 @@ public class HgFileviewCommand extends AbstractCommand } /** - * Method description + * Disables sub repository detection * - * - * @return + * @return {@code this} */ - public HgFileviewCommand disableSubRepositoryDetection() - { + public HgFileviewCommand disableSubRepositoryDetection() { cmdAppend("-s"); return this; } /** - * Method description + * Start file object fetching at the given path. * * - * @return + * @param path path to start fetching * - * @throws IOException + * @return {@code this} */ - public List<FileObject> execute() throws IOException - { - cmdAppend("-t"); - - List<FileObject> files = Lists.newArrayList(); - - HgInputStream stream = launchStream(); - - while (stream.peek() != -1) - { - FileObject file = null; - char type = (char) stream.read(); - - if (type == 'd') - { - file = readDirectory(stream); - } - else if (type == 'f') - { - file = readFile(stream); - } - else if (type == 's') - { - file = readSubRepository(stream); - } - - if (file != null) - { - files.add(file); - } - } - - return files; - } - - /** - * Method description - * - * - * @param path - * - * @return - */ - public HgFileviewCommand path(String path) - { + public HgFileviewCommand path(String path) { cmdAppend("-p", path); return this; } /** - * Method description + * Fetch file objects recursive. * * - * @return + * @return {@code this} */ - public HgFileviewCommand recursive() - { + public HgFileviewCommand recursive() { cmdAppend("-c"); return this; } /** - * Method description + * Use given revision for file view. * + * @param revision revision id, hash, tag or branch * - * @param revision - * - * @return + * @return {@code this} */ - public HgFileviewCommand rev(String revision) - { + public HgFileviewCommand rev(String revision) { cmdAppend("-r", revision); return this; } - //~--- get methods ---------------------------------------------------------- - /** - * Method description + * Executes the mercurial command and parses the output. * - * - * @return - */ - @Override - public String getCommandName() - { - return HgFileviewExtension.NAME; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param stream - * - * @return + * @return file object * * @throws IOException */ - private FileObject readDirectory(HgInputStream stream) throws IOException + public FileObject execute() throws IOException { + cmdAppend("-t"); + + Deque<FileObject> stack = new LinkedList<>(); + + HgInputStream stream = launchStream(); + + FileObject last = null; + while (stream.peek() != -1) { + FileObject file = read(stream); + + while (!stack.isEmpty()) { + FileObject current = stack.peek(); + if (isParent(current, file)) { + current.addChild(file); + break; + } else { + stack.pop(); + } + } + + if (file.isDirectory()) { + stack.push(file); + } + last = file; + } + + if (stack.isEmpty()) { + // if the stack is empty, the requested path is probably a file + return last; + } else { + // if the stack is not empty, the requested path is a directory + return stack.getLast(); + } + } + + private FileObject read(HgInputStream stream) throws IOException { + char type = (char) stream.read(); + + FileObject file; + switch (type) { + case 'd': + file = readDirectory(stream); + break; + case 'f': + file = readFile(stream); + break; + case 's': + file = readSubRepository(stream); + break; + default: + throw new IOException("unknown file object type: " + type); + } + return file; + } + + private boolean isParent(FileObject parent, FileObject child) { + String parentPath = parent.getPath(); + if (parentPath.equals("")) { + return true; + } + return child.getParentPath().equals(parentPath); + } + + private FileObject readDirectory(HgInputStream stream) throws IOException { FileObject directory = new FileObject(); String path = removeTrailingSlash(stream.textUpTo('\0')); @@ -236,18 +224,7 @@ public class HgFileviewCommand extends AbstractCommand return directory; } - /** - * Method description - * - * - * @param stream - * - * @return - * - * @throws IOException - */ - private FileObject readFile(HgInputStream stream) throws IOException - { + private FileObject readFile(HgInputStream stream) throws IOException { FileObject file = new FileObject(); String path = removeTrailingSlash(stream.textUpTo('\n')); @@ -259,8 +236,7 @@ public class HgFileviewCommand extends AbstractCommand DateTime timestamp = stream.dateTimeUpTo(' '); String description = stream.textUpTo('\0'); - if (!disableLastCommit) - { + if (!disableLastCommit) { file.setLastModified(timestamp.getDate().getTime()); file.setDescription(description); } @@ -268,18 +244,7 @@ public class HgFileviewCommand extends AbstractCommand return file; } - /** - * Method description - * - * - * @param stream - * - * @return - * - * @throws IOException - */ - private FileObject readSubRepository(HgInputStream stream) throws IOException - { + private FileObject readSubRepository(HgInputStream stream) throws IOException { FileObject directory = new FileObject(); String path = removeTrailingSlash(stream.textUpTo('\n')); @@ -292,8 +257,7 @@ public class HgFileviewCommand extends AbstractCommand SubRepository subRepository = new SubRepository(url); - if (!Strings.isNullOrEmpty(revision)) - { + if (!Strings.isNullOrEmpty(revision)) { subRepository.setRevision(revision); } @@ -302,48 +266,33 @@ public class HgFileviewCommand extends AbstractCommand return directory; } - /** - * Method description - * - * - * @param path - * - * @return - */ - private String removeTrailingSlash(String path) - { - if (path.endsWith("/")) - { + private String removeTrailingSlash(String path) { + if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } return path; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param path - * - * @return - */ - private String getNameFromPath(String path) - { + private String getNameFromPath(String path) { int index = path.lastIndexOf('/'); - if (index > 0) - { + if (index > 0) { path = path.substring(index + 1); } return path; } - //~--- fields --------------------------------------------------------------- + /** + * Returns the name of the mercurial command. + * + * @return command name + */ + @Override + public String getCommandName() + { + return HgFileviewExtension.NAME; + } - /** Field description */ - private boolean disableLastCommit = false; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgLogChangesetCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgLogChangesetCommand.java index 4c62fb4f93..12a77ac717 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgLogChangesetCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgLogChangesetCommand.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + * <p> * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * <p> * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * <p> * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,186 +24,104 @@ * 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. - * + * <p> * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository.spi.javahg; -//~--- non-JDK imports -------------------------------------------------------- - import com.aragost.javahg.Repository; import com.aragost.javahg.internals.HgInputStream; import com.aragost.javahg.internals.Utils; - +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.repository.Changeset; import sonia.scm.repository.HgConfig; +import sonia.scm.repository.Modifications; -//~--- JDK imports ------------------------------------------------------------ - +import java.io.IOException; import java.util.List; /** - * * @author Sebastian Sdorra */ -public class HgLogChangesetCommand extends AbstractChangesetCommand -{ +public class HgLogChangesetCommand extends AbstractChangesetCommand { - /** - * Constructs ... - * - * - * @param repository - * @param config - */ - private HgLogChangesetCommand(Repository repository, HgConfig config) - { + private static final Logger LOG = LoggerFactory.getLogger(HgLogChangesetCommand.class); + + private HgLogChangesetCommand(Repository repository, HgConfig config) { super(repository, config); } - //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param repository - * @param config - * - * @return - */ - public static HgLogChangesetCommand on(Repository repository, HgConfig config) - { + public static HgLogChangesetCommand on(Repository repository, HgConfig config) { return new HgLogChangesetCommand(repository, config); } - /** - * Method description - * - * - * @param branch - * - * @return - */ - public HgLogChangesetCommand branch(String branch) - { + + public HgLogChangesetCommand branch(String branch) { cmdAppend("-b", branch); return this; } - /** - * Method description - * - * - * @param files - * - * @return - */ - public List<Changeset> execute(String... files) - { - cmdAppend("--style", CHANGESET_EAGER_STYLE_PATH); - HgInputStream stream = launchStream(files); - - return readListFromStream(stream); + public List<Changeset> execute(String... files) { + return readListFromStream(getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH)); } - /** - * Method description - * - * - * @param limit - * - * @return - */ - public HgLogChangesetCommand limit(int limit) - { + public Modifications extractModifications(String... files) { + HgInputStream hgInputStream = getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH); + try { + return readModificationsFromStream(hgInputStream); + } finally { + try { + hgInputStream.close(); + } catch (IOException e) { + LOG.error("Could not close HgInputStream", e); + } + } + } + + HgInputStream getHgInputStream(String[] files, String changesetStylePath) { + cmdAppend("--style", changesetStylePath); + return launchStream(files); + } + + public HgLogChangesetCommand limit(int limit) { cmdAppend("-l", limit); return this; } - /** - * Method description - * - * - * @param files - * - * @return - */ - public List<Integer> loadRevisions(String... files) - { - cmdAppend("--style", CHANGESET_LAZY_STYLE_PATH); - HgInputStream stream = launchStream(files); - - return loadRevisionsFromStream(stream); + public List<Integer> loadRevisions(String... files) { + return loadRevisionsFromStream(getHgInputStream(files, CHANGESET_LAZY_STYLE_PATH)); } - /** - * Method description - * - * - * @param rev - * - * @return - */ - public HgLogChangesetCommand rev(String... rev) - { + public HgLogChangesetCommand rev(String... rev) { cmdAppend("-r", rev); return this; } - /** - * Method description - * - * - * @param files - * - * @return - */ - public Changeset single(String... files) - { + public Changeset single(String... files) { return Utils.single(execute(files)); } - /** - * Method description - * - * - * @param files - * - * @return - */ - public int singleRevision(String... files) - { + public int singleRevision(String... files) { Integer rev = Utils.single(loadRevisions(files)); - if (rev == null) - { + if (rev == null) { rev = -1; } return rev; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ @Override - public String getCommandName() - { + public String getCommandName() { return "log"; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgBasicAuthenticationFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgBasicAuthenticationFilter.java deleted file mode 100644 index abc295fd68..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgBasicAuthenticationFilter.java +++ /dev/null @@ -1,102 +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; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * - * @author Sebastian Sdorra - */ -@Priority(Filters.PRIORITY_AUTHENTICATION) -@WebElement(value = HgServletModule.MAPPING_HG) -public class HgBasicAuthenticationFilter extends AuthenticationFilter -{ - - /** - * Constructs ... - * - * - * @param configuration - * @param webTokenGenerators - */ - @Inject - public HgBasicAuthenticationFilter(ScmConfiguration configuration, - Set<WebTokenGenerator> webTokenGenerators) - { - super(configuration, webTokenGenerators); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - */ - @Override - protected void sendFailedAuthenticationError(HttpServletRequest request, - HttpServletResponse response) - throws IOException - { - if (HgUtil.isHgClient(request) - && (configuration.isLoginAttemptLimitEnabled())) - { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED); - } - else - { - super.sendFailedAuthenticationError(request, response); - } - } -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java index 32478f6203..2961f4ecb1 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java @@ -33,56 +33,48 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Stopwatch; -import com.google.common.base.Strings; import com.google.inject.Inject; import com.google.inject.Singleton; -import com.sun.jersey.core.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; -import sonia.scm.repository.*; -import sonia.scm.security.CipherUtil; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgPythonScript; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryRequestListenerUtil; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.util.AssertUtil; -import sonia.scm.util.HttpUtil; import sonia.scm.web.cgi.CGIExecutor; import sonia.scm.web.cgi.CGIExecutorFactory; import sonia.scm.web.cgi.EnvList; +//~--- JDK imports ------------------------------------------------------------ + +import java.io.File; +import java.io.IOException; + +import java.util.Enumeration; + import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import java.io.File; -import java.io.IOException; -import java.util.Enumeration; - -//~--- JDK imports ------------------------------------------------------------ /** * * @author Sebastian Sdorra */ @Singleton -public class HgCGIServlet extends HttpServlet +public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet { - /** Field description */ - public static final String ENV_REPOSITORY_NAME = "REPO_NAME"; - - /** Field description */ - public static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH"; - /** Field description */ public static final String ENV_SESSION_PREFIX = "SCM_"; - /** Field description */ - private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS"; - /** Field description */ private static final long serialVersionUID = -3492811300905099810L; @@ -92,78 +84,29 @@ public class HgCGIServlet extends HttpServlet //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * - * - * - * @param cgiExecutorFactory - * @param configuration - * @param repositoryProvider - * @param handler - * @param hookManager - * @param requestListenerUtil - */ @Inject public HgCGIServlet(CGIExecutorFactory cgiExecutorFactory, - ScmConfiguration configuration, RepositoryProvider repositoryProvider, - HgRepositoryHandler handler, HgHookManager hookManager, - RepositoryRequestListenerUtil requestListenerUtil) + ScmConfiguration configuration, + HgRepositoryHandler handler, + RepositoryRequestListenerUtil requestListenerUtil, + HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder) { this.cgiExecutorFactory = cgiExecutorFactory; this.configuration = configuration; - this.repositoryProvider = repositoryProvider; this.handler = handler; - this.hookManager = hookManager; this.requestListenerUtil = requestListenerUtil; + this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder; this.exceptionHandler = new HgCGIExceptionHandler(); this.command = HgPythonScript.HGWEB.getFile(SCMContext.getContext()); } //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @throws ServletException - */ @Override - public void init() throws ServletException + public void service(HttpServletRequest request, + HttpServletResponse response, Repository repository) { - - super.init(); - } - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void service(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException - { - Repository repository = repositoryProvider.get(); - - if (repository == null) - { - if (logger.isDebugEnabled()) - { - logger.debug("no hg repository found at {}", request.getRequestURI()); - } - - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - } - else if (!handler.isConfigured()) + if (!handler.isConfigured()) { exceptionHandler.sendFormattedError(request, response, HgCGIExceptionHandler.ERROR_NOT_CONFIGURED); @@ -174,43 +117,14 @@ public class HgCGIServlet extends HttpServlet { handleRequest(request, response, repository); } - catch (ServletException | IOException ex) + catch (ServletException ex) { exceptionHandler.handleException(request, response, ex); } - } - } - - /** - * Method description - * - * - * @param env - * @param request - */ - private void addCredentials(EnvList env, HttpServletRequest request) - { - String authorization = request.getHeader(HttpUtil.HEADER_AUTHORIZATION); - - if (!Strings.isNullOrEmpty(authorization)) - { - if (authorization.startsWith(HttpUtil.AUTHORIZATION_SCHEME_BASIC)) + catch (IOException ex) { - String encodedUserInfo = - authorization.substring( - HttpUtil.AUTHORIZATION_SCHEME_BASIC.length()).trim(); - String userInfo = Base64.base64Decode(encodedUserInfo); - - env.set(SCM_CREDENTIALS, CipherUtil.getInstance().encode(userInfo)); + exceptionHandler.handleException(request, response, ex); } - else - { - logger.warn("unknow authentication scheme used"); - } - } - else - { - logger.trace("no authorization header found"); } } @@ -279,8 +193,6 @@ public class HgCGIServlet extends HttpServlet HttpServletResponse response, Repository repository) throws IOException, ServletException { - String name = repository.getName(); - File directory = handler.getDirectory(repository); CGIExecutor executor = cgiExecutorFactory.createExecutor(configuration, getServletContext(), request, response); @@ -289,29 +201,7 @@ public class HgCGIServlet extends HttpServlet executor.setExceptionHandler(exceptionHandler); executor.setStatusCodeHandler(exceptionHandler); executor.setContentLengthWorkaround(true); - executor.getEnvironment().set(ENV_REPOSITORY_NAME, name); - executor.getEnvironment().set(ENV_REPOSITORY_PATH, - directory.getAbsolutePath()); - - // add hook environment - //J- - HgEnvironment.prepareEnvironment( - executor.getEnvironment().asMutableMap(), - handler, - hookManager, - request - ); - //J+ - - addCredentials(executor.getEnvironment(), request); - - // unused ??? - HttpSession session = request.getSession(false); - - if (session != null) - { - passSessionAttributes(executor.getEnvironment(), session); - } + hgRepositoryEnvironmentBuilder.buildFor(repository, request, executor.getEnvironment().asMutableMap()); String interpreter = getInterpreter(); @@ -364,12 +254,8 @@ public class HgCGIServlet extends HttpServlet /** Field description */ private final HgRepositoryHandler handler; - /** Field description */ - private final HgHookManager hookManager; - - /** Field description */ - private final RepositoryProvider repositoryProvider; - /** Field description */ private final RepositoryRequestListenerUtil requestListenerUtil; + + private final HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServletProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServletProvider.java new file mode 100644 index 0000000000..db7a6be7b3 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServletProvider.java @@ -0,0 +1,23 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletProvider; + +import javax.inject.Provider; + +public class HgCGIServletProvider extends ScmProviderHttpServletProvider { + + @Inject + private Provider<HgCGIServlet> servletProvider; + + public HgCGIServletProvider() { + super(HgRepositoryHandler.TYPE_NAME); + } + + @Override + protected ScmProviderHttpServlet getRootServlet() { + return servletProvider.get(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java index abbf09380a..a2ab84c768 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java @@ -35,46 +35,43 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.io.Closeables; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; - import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.subject.Subject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import sonia.scm.NotFoundException; import sonia.scm.repository.HgContext; import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.RepositoryHookType; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RepositoryUtil; import sonia.scm.repository.api.HgHookMessage; import sonia.scm.repository.api.HgHookMessage.Severity; import sonia.scm.repository.spi.HgHookContextProvider; import sonia.scm.repository.spi.HookEventFacade; +import sonia.scm.security.BearerToken; import sonia.scm.security.CipherUtil; -import sonia.scm.security.Tokens; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; -import java.io.PrintWriter; - -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -91,13 +88,13 @@ public class HgHookCallbackServlet extends HttpServlet public static final String HGHOOK_PRE_RECEIVE = "pretxnchangegroup"; /** Field description */ - public static final String PARAM_REPOSITORYPATH = "repositoryPath"; + public static final String PARAM_REPOSITORYID = "repositoryId"; /** Field description */ private static final String PARAM_CHALLENGE = "challenge"; /** Field description */ - private static final String PARAM_CREDENTIALS = "credentials"; + private static final String PARAM_TOKEN = "token"; /** Field description */ private static final String PARAM_NODE = "node"; @@ -118,20 +115,10 @@ public class HgHookCallbackServlet extends HttpServlet //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * - * @param hookEventFacade - * @param handler - * @param hookManager - * @param contextProvider - */ @Inject public HgHookCallbackServlet(HookEventFacade hookEventFacade, - HgRepositoryHandler handler, HgHookManager hookManager, - Provider<HgContext> contextProvider) + HgRepositoryHandler handler, HgHookManager hookManager, + Provider<HgContext> contextProvider) { this.hookEventFacade = hookEventFacade; this.handler = handler; @@ -153,7 +140,6 @@ public class HgHookCallbackServlet extends HttpServlet */ @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { String ping = request.getParameter(PARAM_PING); @@ -167,27 +153,24 @@ public class HgHookCallbackServlet extends HttpServlet } } - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ @Override - protected void doPost(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + try { + handlePostRequest(request, response); + } catch (IOException ex) { + logger.warn("error in hook callback execution, sending internal server error", ex); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + private void handlePostRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { String strippedURI = HttpUtil.getStrippedURI(request); Matcher m = REGEX_URL.matcher(strippedURI); if (m.matches()) { - String repositoryId = getRepositoryName(request); + String repositoryId = getRepositoryId(request); String type = m.group(1); String challenge = request.getParameter(PARAM_CHALLENGE); @@ -197,14 +180,14 @@ public class HgHookCallbackServlet extends HttpServlet if (Util.isNotEmpty(node)) { - String credentials = request.getParameter(PARAM_CREDENTIALS); + String token = request.getParameter(PARAM_TOKEN); - if (Util.isNotEmpty(credentials)) + if (Util.isNotEmpty(token)) { - authenticate(request, credentials); + authenticate(token); } - hookCallback(response, repositoryId, type, challenge, node); + hookCallback(response, type, repositoryId, challenge, node); } else if (logger.isDebugEnabled()) { @@ -227,41 +210,20 @@ public class HgHookCallbackServlet extends HttpServlet } } - /** - * Method description - * - * - * @param request - * @param credentials - */ - private void authenticate(HttpServletRequest request, String credentials) + private void authenticate(String token) { try { - credentials = CipherUtil.getInstance().decode(credentials); + token = CipherUtil.getInstance().decode(token); - if (Util.isNotEmpty(credentials)) + if (Util.isNotEmpty(token)) { - int index = credentials.indexOf(':'); + Subject subject = SecurityUtils.getSubject(); - if (index > 0 && index < credentials.length()) - { - Subject subject = SecurityUtils.getSubject(); + AuthenticationToken accessToken = createToken(token); - //J- - subject.login( - Tokens.createAuthenticationToken( - request, - credentials.substring(0, index), - credentials.substring(index + 1) - ) - ); - //J+ - } - else - { - logger.error("could not find delimiter"); - } + //J- + subject.login(accessToken); } } catch (Exception ex) @@ -270,19 +232,12 @@ public class HgHookCallbackServlet extends HttpServlet } } - /** - * Method description - * - * - * @param response - * @param repositoryName - * @param node - * @param type - * - * @throws IOException - */ - private void fireHook(HttpServletResponse response, String repositoryName, - String node, RepositoryHookType type) + private AuthenticationToken createToken(String tokenString) + { + return BearerToken.valueOf(tokenString); + } + + private void fireHook(HttpServletResponse response, String repositoryId, String node, RepositoryHookType type) throws IOException { HgHookContextProvider context = null; @@ -294,25 +249,19 @@ public class HgHookCallbackServlet extends HttpServlet contextProvider.get().setPending(true); } - context = new HgHookContextProvider(handler, repositoryName, hookManager, + File repositoryDirectory = handler.getDirectory(repositoryId); + context = new HgHookContextProvider(handler, repositoryDirectory, hookManager, node, type); - hookEventFacade.handle(HgRepositoryHandler.TYPE_NAME, - repositoryName).fireHookEvent(type, context); + hookEventFacade.handle(repositoryId).fireHookEvent(type, context); printMessages(response, context); } - catch (RepositoryNotFoundException ex) + catch (NotFoundException ex) { - if (logger.isErrorEnabled()) - { - logger.error("could not find repository {}", repositoryName); + logger.error(ex.getMessage()); - if (logger.isTraceEnabled()) - { - logger.trace("repository not found", ex); - } - } + logger.trace("repository not found", ex); response.sendError(HttpServletResponse.SC_NOT_FOUND); } @@ -322,22 +271,7 @@ public class HgHookCallbackServlet extends HttpServlet } } - /** - * Method description - * - * - * @param response - * @param repositoryName - * @param typeName - * @param challenge - * @param node - * - * @throws IOException - */ - private void hookCallback(HttpServletResponse response, - String repositoryName, String typeName, String challenge, String node) - throws IOException - { + private void hookCallback(HttpServletResponse response, String typeName, String repositoryId, String challenge, String node) throws IOException { if (hookManager.isAcceptAble(challenge)) { RepositoryHookType type = null; @@ -353,7 +287,7 @@ public class HgHookCallbackServlet extends HttpServlet if (type != null) { - fireHook(response, repositoryName, node, type); + fireHook(response, repositoryId, node, type); } else { @@ -403,12 +337,12 @@ public class HgHookCallbackServlet extends HttpServlet * Method description * * - * @param resonse + * @param response * @param context * * @throws IOException */ - private void printMessages(HttpServletResponse resonse, + private void printMessages(HttpServletResponse response, HgHookContextProvider context) throws IOException { @@ -420,7 +354,7 @@ public class HgHookCallbackServlet extends HttpServlet try { - writer = resonse.getWriter(); + writer = response.getWriter(); printMessages(writer, msgs); } @@ -498,41 +432,11 @@ public class HgHookCallbackServlet extends HttpServlet //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - */ - private String getRepositoryName(HttpServletRequest request) + private String getRepositoryId(HttpServletRequest request) { - String name = null; - String path = request.getParameter(PARAM_REPOSITORYPATH); - - if (Util.isNotEmpty(path)) - { - - /** - * use canonical path to fix symbolic links - * https://bitbucket.org/sdorra/scm-manager/issue/82/symbolic-link-in-hg-repository-path - */ - try - { - name = RepositoryUtil.getRepositoryName(handler, path); - } - catch (IOException ex) - { - logger.error("could not find name of repository", ex); - } - } - else if (logger.isWarnEnabled()) - { - logger.warn("no repository path parameter found"); - } - - return name; + String id = request.getParameter(PARAM_REPOSITORYID); + Preconditions.checkArgument(!Strings.isNullOrEmpty(id), "repository id not found in request"); + return id; } //~--- fields --------------------------------------------------------------- diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index de2835dd8f..18b716b665 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -35,52 +35,73 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; -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.repository.RepositoryProvider; -import sonia.scm.web.filter.ProviderPermissionFilter; +import sonia.scm.repository.Repository; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.web.filter.PermissionFilter; -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Set; +import sonia.scm.repository.HgRepositoryHandler; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; +import java.util.Set; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; /** * Permission filter for mercurial repositories. * * @author Sebastian Sdorra */ -@Priority(Filters.PRIORITY_AUTHORIZATION) -@WebElement(value = HgServletModule.MAPPING_HG) -public class HgPermissionFilter extends ProviderPermissionFilter +public class HgPermissionFilter extends PermissionFilter { private static final Set<String> READ_METHODS = ImmutableSet.of("GET", "HEAD", "OPTIONS", "TRACE"); - /** - * Constructs a new instance. - * - * @param configuration scm configuration - * @param repositoryProvider repository provider - */ - @Inject - public HgPermissionFilter(ScmConfiguration configuration, - RepositoryProvider repositoryProvider) - { - super(configuration, repositoryProvider); - } + private final HgRepositoryHandler repositoryHandler; - //~--- get methods ---------------------------------------------------------- + public HgPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate, HgRepositoryHandler repositoryHandler) + { + super(configuration, delegate); + this.repositoryHandler = repositoryHandler; + } @Override - protected boolean isWriteRequest(HttpServletRequest request) + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws IOException, ServletException { + super.service(wrapRequestIfRequired(request), response, repository); + } + + @VisibleForTesting + HttpServletRequest wrapRequestIfRequired(HttpServletRequest request) { + if (isHttpPostArgsEnabled()) { + return new HgServletRequest(request); + } + return request; + } + + @Override + public boolean isWriteRequest(HttpServletRequest request) { - return !READ_METHODS.contains(request.getMethod()); + if (isHttpPostArgsEnabled()) { + return isHttpPostArgsWriteRequest(request); + } + return isDefaultWriteRequest(request); + } + + private boolean isHttpPostArgsEnabled() { + return repositoryHandler.getConfig().isEnableHttpPostArgs(); + } + + private boolean isHttpPostArgsWriteRequest(HttpServletRequest request) { + return WireProtocol.isWriteRequest(request); + } + + private boolean isDefaultWriteRequest(HttpServletRequest request) { + if (READ_METHODS.contains(request.getMethod())) { + return WireProtocol.isWriteRequest(request); + } + return true; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java new file mode 100644 index 0000000000..479c3bd986 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java @@ -0,0 +1,32 @@ +package sonia.scm.web; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +import javax.inject.Inject; + +@Extension +public class HgPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final ScmConfiguration configuration; + private final HgRepositoryHandler repositoryHandler; + + @Inject + public HgPermissionFilterFactory(ScmConfiguration configuration, HgRepositoryHandler repositoryHandler) { + this.configuration = configuration; + this.repositoryHandler = repositoryHandler; + } + + @Override + public boolean handlesScmType(String type) { + return HgRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new HgPermissionFilter(configuration, delegate,repositoryHandler); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgRepositoryEnvironmentBuilder.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgRepositoryEnvironmentBuilder.java new file mode 100644 index 0000000000..2cb40cbc1b --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgRepositoryEnvironmentBuilder.java @@ -0,0 +1,56 @@ +package sonia.scm.web; + +import sonia.scm.repository.HgEnvironment; +import sonia.scm.repository.HgHookManager; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.util.Map; + +public class HgRepositoryEnvironmentBuilder { + + private static final String ENV_REPOSITORY_NAME = "REPO_NAME"; + private static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH"; + private static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID"; + private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY"; + private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS"; + + private final HgRepositoryHandler handler; + private final HgHookManager hookManager; + + @Inject + public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager) { + this.handler = handler; + this.hookManager = hookManager; + } + + public void buildFor(Repository repository, HttpServletRequest request, Map<String, String> environment) { + File directory = handler.getDirectory(repository.getId()); + + environment.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName()); + environment.put(ENV_REPOSITORY_ID, repository.getId()); + environment.put(ENV_REPOSITORY_PATH, + directory.getAbsolutePath()); + + // add hook environment + if (handler.getConfig().isDisableHookSSLValidation()) { + // disable ssl validation + // Issue 959: https://goo.gl/zH5eY8 + environment.put(ENV_PYTHON_HTTPS_VERIFY, "0"); + } + + // enable experimental httppostargs protocol of mercurial + // Issue 970: https://goo.gl/poascp + environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs())); + + HgEnvironment.prepareEnvironment( + environment, + handler, + hookManager, + request + ); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgScmProtocolProviderWrapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgScmProtocolProviderWrapper.java new file mode 100644 index 0000000000..37360a5845 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgScmProtocolProviderWrapper.java @@ -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.HgRepositoryHandler; +import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; + +@Singleton +@Extension +public class HgScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper { + @Inject + public HgScmProtocolProviderWrapper(HgCGIServletProvider servletProvider, Provider<ScmPathInfoStore> uriInfoStore, ScmConfiguration scmConfiguration) { + super(servletProvider, uriInfoStore, scmConfiguration); + } + + @Override + public String getType() { + return HgRepositoryHandler.TYPE_NAME; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java new file mode 100644 index 0000000000..b0b2f8ef0d --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java @@ -0,0 +1,55 @@ +package sonia.scm.web; + +import com.google.common.base.Preconditions; + +import javax.servlet.ServletInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * HgServletInputStream is a wrapper around the original {@link ServletInputStream} and provides some extra + * functionality to support the mercurial client. + */ +public class HgServletInputStream extends ServletInputStream { + + private final ServletInputStream original; + private ByteArrayInputStream captured; + + HgServletInputStream(ServletInputStream original) { + this.original = original; + } + + /** + * Reads the given amount of bytes from the stream and captures them, if the {@link #read()} methods is called the + * captured bytes are returned before the rest of the stream. + * + * @param size amount of bytes to read + * + * @return byte array + * + * @throws IOException if the method is called twice + */ + public byte[] readAndCapture(int size) throws IOException { + Preconditions.checkState(captured == null, "readAndCapture can only be called once per request"); + + // TODO should we enforce a limit? to prevent OOM? + byte[] bytes = new byte[size]; + original.read(bytes); + captured = new ByteArrayInputStream(bytes); + + return bytes; + } + + @Override + public int read() throws IOException { + if (captured != null && captured.available() > 0) { + return captured.read(); + } + return original.read(); + } + + @Override + public void close() throws IOException { + original.close(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java index 71d6f9f419..7b66688036 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java @@ -36,12 +36,18 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.servlet.ServletModule; - +import org.mapstruct.factory.Mappers; +import sonia.scm.api.v2.resources.HgConfigDtoToHgConfigMapper; +import sonia.scm.api.v2.resources.HgConfigInstallationsToDtoMapper; +import sonia.scm.api.v2.resources.HgConfigPackagesToDtoMapper; +import sonia.scm.api.v2.resources.HgConfigToHgConfigDtoMapper; import sonia.scm.installer.HgPackageReader; import sonia.scm.plugin.Extension; import sonia.scm.repository.HgContext; import sonia.scm.repository.HgContextProvider; import sonia.scm.repository.HgHookManager; +import sonia.scm.repository.spi.HgWorkdirFactory; +import sonia.scm.repository.spi.SimpleHgWorkdirFactory; /** * @@ -70,10 +76,14 @@ public class HgServletModule extends ServletModule bind(HgHookManager.class); bind(HgPackageReader.class); + bind(HgConfigDtoToHgConfigMapper.class).to(Mappers.getMapper(HgConfigDtoToHgConfigMapper.class).getClass()); + bind(HgConfigToHgConfigDtoMapper.class).to(Mappers.getMapper(HgConfigToHgConfigDtoMapper.class).getClass()); + bind(HgConfigPackagesToDtoMapper.class).to(Mappers.getMapper(HgConfigPackagesToDtoMapper.class).getClass()); + bind(HgConfigInstallationsToDtoMapper.class); + // bind servlets serve(MAPPING_HOOK).with(HgHookCallbackServlet.class); - // register hg cgi servlet - serve(MAPPING_HG).with(HgCGIServlet.class); + bind(HgWorkdirFactory.class).to(SimpleHgWorkdirFactory.class); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java new file mode 100644 index 0000000000..80251c140a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java @@ -0,0 +1,31 @@ +package sonia.scm.web; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.IOException; + +/** + * {@link HttpServletRequestWrapper} which adds some functionality in order to support the mercurial client. + */ +public final class HgServletRequest extends HttpServletRequestWrapper { + + private HgServletInputStream hgServletInputStream; + + /** + * Constructs a request object wrapping the given request. + * + * @param request + * @throws IllegalArgumentException if the request is null + */ + public HgServletRequest(HttpServletRequest request) { + super(request); + } + + @Override + public HgServletInputStream getInputStream() throws IOException { + if (hgServletInputStream == null) { + hgServletInputStream = new HgServletInputStream(super.getInputStream()); + } + return hgServletInputStream; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java index ec35762de7..427b8179ad 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java @@ -58,6 +58,8 @@ import sonia.scm.util.Util; import java.io.File; import java.nio.charset.Charset; +import java.util.Map; +import java.util.function.Consumer; import javax.servlet.http.HttpServletRequest; @@ -103,6 +105,19 @@ public final class HgUtil */ public static Repository open(HgRepositoryHandler handler, HgHookManager hookManager, File directory, String encoding, boolean pending) + { + return open( + handler, + directory, + encoding, + pending, + environment -> HgEnvironment.prepareEnvironment(environment, handler, hookManager) + ); + } + + public static Repository open(HgRepositoryHandler handler, + File directory, String encoding, boolean pending, + Consumer<Map<String, String>> prepareEnvironment) { String enc = encoding; @@ -113,8 +128,7 @@ public final class HgUtil RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT; - HgEnvironment.prepareEnvironment(repoConfiguration.getEnvironment(), - handler, hookManager); + prepareEnvironment.accept(repoConfiguration.getEnvironment()); repoConfiguration.addExtension(HgFileviewExtension.class); repoConfiguration.setEnablePendingChangesets(pending); @@ -134,6 +148,8 @@ public final class HgUtil repoConfiguration.setHgBin(handler.getConfig().getHgBinary()); + logger.debug("open hg repository {}: encoding: {}, pending: {}", directory, enc, pending); + return Repository.open(repoConfiguration, directory); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgVndMediaType.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgVndMediaType.java new file mode 100644 index 0000000000..033c5e8361 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgVndMediaType.java @@ -0,0 +1,13 @@ +package sonia.scm.web; + + +public class HgVndMediaType { + private static final String PREFIX = VndMediaType.PREFIX + "hgConfig"; + + public static final String CONFIG = PREFIX + VndMediaType.SUFFIX; + public static final String PACKAGES = PREFIX + "-packages" + VndMediaType.SUFFIX; + public static final String INSTALLATIONS = PREFIX + "-installation" + VndMediaType.SUFFIX; + + private HgVndMediaType() { + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java new file mode 100644 index 0000000000..fb84692805 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java @@ -0,0 +1,236 @@ +/** + * Copyright (c) 2018, 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; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.collect.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.util.HttpUtil; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.*; + +/** + * WireProtocol provides methods for handling the mercurial wire protocol. + * + * @see <a href="https://goo.gl/WaVJzw">Mercurial Wire Protocol</a> + */ +public final class WireProtocol { + + private static final Logger LOG = LoggerFactory.getLogger(WireProtocol.class); + + private static final Set<String> READ_COMMANDS = ImmutableSet.of( + "batch", "between", "branchmap", "branches", "capabilities", "changegroup", "changegroupsubset", "clonebundles", + "getbundle", "heads", "hello", "listkeys", "lookup", "known", "stream_out", + // could not find lheads in the wireprotocol description but mercurial 4.5.2 uses it for clone + "lheads" + ); + + private static final Set<String> WRITE_COMMANDS = ImmutableSet.of( + "pushkey", "unbundle" + ); + + private WireProtocol() { + } + + /** + * Returns {@code true} if the request is a write request. The method will always return {@code true}, expect for the + * following cases: + * + * - no command was specified with the request (is required for the hgweb ui) + * - the command in the query string was found in the list of read request + * - if query string contains the batch command, then all commands specified in X-HgArg headers must be + * in the list of read requests + * - in case of enabled HttpPostArgs protocol and query string container the batch command, the header X-HgArgs-Post + * is read and the commands which are specified in the body from 0 to the value of X-HgArgs-Post must be in the list + * of read requests + * + * @param request http request + * + * @return {@code true} for write requests. + */ + public static boolean isWriteRequest(HttpServletRequest request) { + List<String> commands = commandsOf(request); + boolean write = isWriteRequest(commands); + LOG.trace("mercurial request {} is write: {}", commands, write); + return write; + } + + @VisibleForTesting + static boolean isWriteRequest(List<String> commands) { + return !READ_COMMANDS.containsAll(commands); + } + + @VisibleForTesting + static List<String> commandsOf(HttpServletRequest request) { + List<String> listOfCmds = Lists.newArrayList(); + + String cmd = getCommandFromQueryString(request); + if (cmd != null) { + listOfCmds.add(cmd); + if (isBatchCommand(cmd)) { + parseHgArgHeaders(request, listOfCmds); + handleHttpPostArgs(request, listOfCmds); + } + } + return Collections.unmodifiableList(listOfCmds); + } + + private static void handleHttpPostArgs(HttpServletRequest request, List<String> listOfCmds) { + int hgArgsPostSize = request.getIntHeader("X-HgArgs-Post"); + if (hgArgsPostSize > 0) { + + if (request instanceof HgServletRequest) { + HgServletRequest hgRequest = (HgServletRequest) request; + + parseHttpPostArgs(listOfCmds, hgArgsPostSize, hgRequest); + } else { + throw new IllegalArgumentException("could not process the httppostargs protocol without HgServletRequest"); + } + + } + } + + private static void parseHttpPostArgs(List<String> listOfCmds, int hgArgsPostSize, HgServletRequest hgRequest) { + try { + byte[] bytes = hgRequest.getInputStream().readAndCapture(hgArgsPostSize); + // we use iso-8859-1 for encoding, because the post args are normally http headers which are using iso-8859-1 + // see https://tools.ietf.org/html/rfc7230#section-3.2.4 + String hgArgs = new String(bytes, Charsets.ISO_8859_1); + String decoded = decodeValue(hgArgs); + parseHgCommandHeader(listOfCmds, decoded); + } catch (IOException ex) { + throw Throwables.propagate(ex); + } + } + + private static void parseHgArgHeaders(HttpServletRequest request, List<String> listOfCmds) { + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String header = (String) headerNames.nextElement(); + parseHgArgHeader(request, listOfCmds, header); + } + } + + private static void parseHgArgHeader(HttpServletRequest request, List<String> listOfCmds, String header) { + if (isHgArgHeader(header)) { + String value = getHeaderDecoded(request, header); + parseHgArgValue(listOfCmds, value); + } + } + + private static void parseHgArgValue(List<String> listOfCmds, String value) { + if (isHgArgCommandHeader(value)) { + parseHgCommandHeader(listOfCmds, value); + } + } + + private static void parseHgCommandHeader(List<String> listOfCmds, String value) { + String[] cmds = value.substring(5).split(";"); + for (String cmd : cmds ) { + String normalizedCmd = normalize(cmd); + int index = normalizedCmd.indexOf(' '); + if (index > 0) { + listOfCmds.add(normalizedCmd.substring(0, index)); + } else { + listOfCmds.add(normalizedCmd); + } + } + } + + private static String normalize(String cmd) { + return cmd.trim().toLowerCase(Locale.ENGLISH); + } + + private static boolean isHgArgCommandHeader(String value) { + return value.startsWith("cmds="); + } + + private static String getHeaderDecoded(HttpServletRequest request, String header) { + return decodeValue(request.getHeader(header)); + } + + private static String decodeValue(String value) { + return HttpUtil.decode(Strings.nullToEmpty(value)); + } + + private static boolean isHgArgHeader(String header) { + return header.toLowerCase(Locale.ENGLISH).startsWith("x-hgarg-"); + } + + private static boolean isBatchCommand(String cmd) { + return "batch".equalsIgnoreCase(cmd); + } + + private static String getCommandFromQueryString(HttpServletRequest request) { + // we can't use getParameter, because this would inspect the body for form parameters as well + Multimap<String, String> queryParameterMap = createQueryParameterMap(request); + + Collection<String> cmd = queryParameterMap.get("cmd"); + Preconditions.checkArgument(cmd.size() <= 1, "found more than one cmd query parameter"); + Iterator<String> iterator = cmd.iterator(); + + String command = null; + if (iterator.hasNext()) { + command = iterator.next(); + } + return command; + } + + private static Multimap<String,String> createQueryParameterMap(HttpServletRequest request) { + Multimap<String,String> parameterMap = HashMultimap.create(); + + String queryString = request.getQueryString(); + if (!Strings.isNullOrEmpty(queryString)) { + + String[] parameters = queryString.split("&"); + for (String parameter : parameters) { + int index = parameter.indexOf('='); + if (index > 0) { + parameterMap.put(parameter.substring(0, index), parameter.substring(index + 1)); + } else { + parameterMap.put(parameter, "true"); + } + } + + } + + return parameterMap; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.tsx b/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.tsx new file mode 100644 index 0000000000..7c84656e85 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Image } from "@scm-manager/ui-components"; + +type Props = {}; + +class HgAvatar extends React.Component<Props> { + render() { + return <Image src="/images/hg-logo.png" alt="Mercurial Logo" />; + } +} + +export default HgAvatar; diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgBranchInformation.tsx b/scm-plugins/scm-hg-plugin/src/main/js/HgBranchInformation.tsx new file mode 100644 index 0000000000..ee302f6014 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgBranchInformation.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Branch } from "@scm-manager/ui-types"; + +type Props = WithTranslation & { + branch: Branch; +}; + +class HgBranchInformation extends React.Component<Props> { + render() { + const { branch, t } = this.props; + + return ( + <div> + <h4>{t("scm-hg-plugin.information.fetch")}</h4> + <pre> + <code>hg pull</code> + </pre> + <h4>{t("scm-hg-plugin.information.checkout")}</h4> + <pre> + <code>hg update {branch.name}</code> + </pre> + </div> + ); + } +} + +export default withTranslation("plugins")(HgBranchInformation); diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.tsx b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.tsx new file mode 100644 index 0000000000..bb6330bd1d --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.tsx @@ -0,0 +1,127 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Links } from "@scm-manager/ui-types"; +import { InputField, Checkbox } from "@scm-manager/ui-components"; + +type Configuration = { + hgBinary: string; + pythonBinary: string; + pythonPath?: string; + encoding: string; + useOptimizedBytecode: boolean; + showRevisionInId: boolean; + disableHookSSLValidation: boolean; + enableHttpPostArgs: boolean; + _links: Links; +}; + +type Props = WithTranslation & { + initialConfiguration: Configuration; + readOnly: boolean; + + onConfigurationChange: (p1: Configuration, p2: boolean) => void; +}; + +type State = Configuration & { + validationErrors: string[]; +}; + +class HgConfigurationForm extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + ...props.initialConfiguration, + validationErrors: [] + }; + } + + updateValidationStatus = () => { + const requiredFields = ["hgBinary", "pythonBinary", "encoding"]; + + const validationErrors = []; + for (const field of requiredFields) { + // @ts-ignore + if (!this.state[field]) { + validationErrors.push(field); + } + } + + this.setState({ + validationErrors + }); + + return validationErrors.length === 0; + }; + + hasValidationError = (name: string) => { + return this.state.validationErrors.indexOf(name) >= 0; + }; + + handleChange = (value: string | boolean, name?: string) => { + if (!name) { + throw new Error("name not set"); + } + this.setState( + // @ts-ignore + { + [name]: value + }, + () => this.props.onConfigurationChange(this.state, this.updateValidationStatus()) + ); + }; + + inputField = (name: string) => { + const { readOnly, t } = this.props; + return ( + <div className="column is-half"> + <InputField + name={name} + label={t("scm-hg-plugin.config." + name)} + helpText={t("scm-hg-plugin.config." + name + "HelpText")} + // @ts-ignore + value={this.state[name]} + onChange={this.handleChange} + validationError={this.hasValidationError(name)} + errorMessage={t("scm-hg-plugin.config.required")} + disabled={readOnly} + /> + </div> + ); + }; + + checkbox = (name: string) => { + const { readOnly, t } = this.props; + return ( + <Checkbox + name={name} + label={t("scm-hg-plugin.config." + name)} + helpText={t("scm-hg-plugin.config." + name + "HelpText")} + // @ts-ignore + checked={this.state[name]} + onChange={this.handleChange} + disabled={readOnly} + /> + ); + }; + + render() { + return ( + <div className="columns is-multiline"> + {this.inputField("hgBinary")} + {this.inputField("pythonBinary")} + {this.inputField("pythonPath")} + {this.inputField("encoding")} + <div className="column is-half"> + {this.checkbox("useOptimizedBytecode")} + {this.checkbox("showRevisionInId")} + </div> + <div className="column is-half"> + {this.checkbox("disableHookSSLValidation")} + {this.checkbox("enableHttpPostArgs")} + </div> + </div> + ); + } +} + +export default withTranslation("plugins")(HgConfigurationForm); diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgGlobalConfiguration.tsx b/scm-plugins/scm-hg-plugin/src/main/js/HgGlobalConfiguration.tsx new file mode 100644 index 0000000000..3cd0fb63ab --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgGlobalConfiguration.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Title, Configuration } from "@scm-manager/ui-components"; +import HgConfigurationForm from "./HgConfigurationForm"; + +type Props = WithTranslation & { + link: string; +}; + +class HgGlobalConfiguration extends React.Component<Props> { + render() { + const { link, t } = this.props; + return ( + <div> + <Title title={t("scm-hg-plugin.config.title")} /> + <Configuration link={link} render={(props: any) => <HgConfigurationForm {...props} />} /> + </div> + ); + } +} + +export default withTranslation("plugins")(HgGlobalConfiguration); diff --git a/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.tsx b/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.tsx new file mode 100644 index 0000000000..b9d9fe1b2f --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Repository } from "@scm-manager/ui-types"; +import { repositories } from "@scm-manager/ui-components"; + +type Props = WithTranslation & { + repository: Repository; +}; + +class ProtocolInformation extends React.Component<Props> { + render() { + const { repository, t } = this.props; + const href = repositories.getProtocolLinkByType(repository, "http"); + if (!href) { + return null; + } + return ( + <div> + <h4>{t("scm-hg-plugin.information.clone")}</h4> + <pre> + <code>hg clone {href}</code> + </pre> + <h4>{t("scm-hg-plugin.information.create")}</h4> + <pre> + <code> + hg init {repository.name} + <br /> + cd {repository.name} + <br /> + echo "[paths]" > .hg/hgrc + <br /> + echo "default = {href} + " > .hg/hgrc + <br /> + echo "# {repository.name} + " > README.md + <br /> + hg add README.md + <br /> + hg commit -m "added readme" + <br /> + <br /> + hg push + <br /> + </code> + </pre> + <h4>{t("scm-hg-plugin.information.replace")}</h4> + <pre> + <code> + # add the repository url as default to your .hg/hgrc e.g: + <br /> + default = {href} + <br /> + # push to remote repository + <br /> + hg push + </code> + </pre> + </div> + ); + } +} + +export default withTranslation("plugins")(ProtocolInformation); diff --git a/scm-plugins/scm-hg-plugin/src/main/js/index.ts b/scm-plugins/scm-hg-plugin/src/main/js/index.ts new file mode 100644 index 0000000000..97f24969e4 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/js/index.ts @@ -0,0 +1,18 @@ +import { binder } from "@scm-manager/ui-extensions"; +import ProtocolInformation from "./ProtocolInformation"; +import HgAvatar from "./HgAvatar"; +import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components"; +import HgGlobalConfiguration from "./HgGlobalConfiguration"; +import HgBranchInformation from "./HgBranchInformation"; + +const hgPredicate = (props: any) => { + return props.repository && props.repository.type === "hg"; +}; + +binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate); +binder.bind("repos.branch-details.information", HgBranchInformation, hgPredicate); +binder.bind("repos.repository-avatar", HgAvatar, hgPredicate); + +// bind global configuration + +cfgBinder.bindGlobal("/hg", "scm-hg-plugin.config.link", "hgConfig", HgGlobalConfiguration); diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml new file mode 100644 index 0000000000..951bca4d76 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, Sebastian Sdorra + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of SCM-Manager; nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.dd7s + + http://bitbucket.org/sdorra/scm-manager + + +--> +<permissions> + + <permission> + <value>configuration:read,write:hg</value> + </permission> + <permission> + <value>repository:hg:*</value> + </permission> + +</permissions> diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/plugin.xml b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/plugin.xml index 22dd3dbdb5..ff2a45eac3 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/plugin.xml +++ b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/plugin.xml @@ -29,7 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. http://bitbucket.org/sdorra/scm-manager - +jo --> @@ -42,19 +42,14 @@ --> <plugin> - + <scm-version>2</scm-version> <information> - <author>Sebastian Sdorra</author> - <category>Mercurial</category> - <tags> - <tag>mercurial</tag> - <tag>hg</tag> - <tag>scm</tag> - <tag>vcs</tag> - <tag>dvcs</tag> - </tags> + <displayName>Mercurial</displayName> + <author>Cloudogu GmbH</author> + <category>Source Code Management</category> + <avatarUrl>/images/hg-logo.png</avatarUrl> </information> <conditions> @@ -62,8 +57,7 @@ </conditions> <resources> - <script>/sonia/scm/hg.config.js</script> - <script>/sonia/scm/hg.config-wizard.js</script> + <script>assets/scm-hg-plugin.bundle.js</script> </resources> </plugin> diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/repository-permissions.xml b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/repository-permissions.xml new file mode 100644 index 0000000000..3b83051504 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/repository-permissions.xml @@ -0,0 +1,7 @@ +<repository-permissions> + <verbs> + <verb>hg</verb> + </verbs> + <roles> + </roles> +</repository-permissions> diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json new file mode 100644 index 0000000000..d32847a3af --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json @@ -0,0 +1,60 @@ +{ + "scm-hg-plugin": { + "information": { + "clone" : "Repository klonen", + "create" : "Neues Repository erstellen", + "replace" : "Ein bestehendes Repository aktualisieren", + "fetch": "Remote-Änderungen herunterladen", + "checkout": "Branch wechseln" + }, + "config": { + "link": "Mercurial", + "title": "Mercurial Konfiguration", + "hgBinary": "HG Binary", + "hgBinaryHelpText": "Pfad des Mercurial Binary.", + "pythonBinary": "Python Binary", + "pythonBinaryHelpText": "Pfad des Python binary.", + "pythonPath": "Python Module Such Pfad", + "pythonPathHelpText": "Python Module Such Pfad (PYTHONPATH).", + "encoding": "Encoding", + "encodingHelpText": "Repository Encoding.", + "useOptimizedBytecode": "Optimized Bytecode (.pyo)", + "useOptimizedBytecodeHelpText": "Verwende den Python '-O' Switch.", + "showRevisionInId": "Revision anzeigen", + "showRevisionInIdHelpText": "Die Revision als Teil der Node ID anzeigen.", + "enableHttpPostArgs": "HttpPostArgs Protocol aktivieren", + "enableHttpPostArgsHelpText": "Aktiviert das experimentelle HttpPostArgs Protokoll von Mercurial. Das HttpPostArgs Protokoll verwendet den Post Request Body anstatt des HTTP Headers um Meta Informationen zu versenden. Dieses Vorgehen reduziert die Header Größe der Mercurial Requests. HttpPostArgs wird seit Mercurial 3.8 unterstützt.", + "disableHookSSLValidation": "SSL Validierung für Hooks deaktivieren", + "disableHookSSLValidationHelpText": "Deaktiviert die Validierung von SSL Zertifikaten für den Mercurial Hook, der die Repositoryänderungen wieder zurück an den SCM-Manager leitet. Diese Option sollte nur benutzt werden, wenn der SCM-Manager ein selbstsigniertes Zertifikat verwendet.", + "disabled": "Deaktiviert", + "disabledHelpText": "Aktiviert oder deaktiviert das Mercurial Plugin.", + "required": "Dieser Konfigurationswert wird benötigt" + } + }, + "permissions" : { + "configuration": { + "read,write": { + "hg": { + "displayName": "Mercurial Konfiguration ändern", + "description": "Darf die Mercurial Konfiguration verändern" + } + } + }, + "repository": { + "hg": { + "*": { + "displayName": "Repository-spezifische Mercurial Konfiguration ändern", + "description": "Darf die Mercurial Konfiguration für alle Repositories verändern." + } + } + } + }, + "verbs": { + "repository": { + "hg": { + "displayName": "Mercurial konfigurieren", + "description": "Darf die Mercurial Konfiguration für dieses Repository verändern." + } + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json new file mode 100644 index 0000000000..3792bd4a47 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json @@ -0,0 +1,60 @@ +{ + "scm-hg-plugin": { + "information": { + "clone" : "Clone the repository", + "create" : "Create a new repository", + "replace" : "Push an existing repository", + "fetch": "Get remote changes", + "checkout": "Switch branch" + }, + "config": { + "link": "Mercurial", + "title": "Mercurial Configuration", + "hgBinary": "HG Binary", + "hgBinaryHelpText": "Location of Mercurial binary.", + "pythonBinary": "Python Binary", + "pythonBinaryHelpText": "Location of Python binary.", + "pythonPath": "Python Module Search Path", + "pythonPathHelpText": "Python Module Search Path (PYTHONPATH).", + "encoding": "Encoding", + "encodingHelpText": "Repository Encoding.", + "useOptimizedBytecode": "Optimized Bytecode (.pyo)", + "useOptimizedBytecodeHelpText": "Use the Python '-O' switch.", + "showRevisionInId": "Show Revision", + "showRevisionInIdHelpText": "Show revision as part of the node id.", + "enableHttpPostArgs": "Enable HttpPostArgs Protocol", + "enableHttpPostArgsHelpText": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.", + "disableHookSSLValidation": "Disable SSL Validation on Hooks", + "disableHookSSLValidationHelpText": "Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. This option should only be used, if SCM-Manager uses a self signed certificate.", + "disabled": "Disabled", + "disabledHelpText": "Enable or disable the Mercurial plugin.", + "required": "This configuration value is required" + } + }, + "permissions" : { + "configuration": { + "read,write": { + "hg": { + "displayName": "Modify Mercurial configuration", + "description": "May change the Mercurial configuration" + } + } + }, + "repository": { + "hg": { + "*": { + "displayName": "Modify repository specific Mercurial configuration", + "description": "May change the Mercurial configuration for repositories" + } + } + } + }, + "verbs": { + "repository": { + "hg": { + "displayName": "configure Mercurial", + "description": "May change the Mercurial configuration for this repository" + } + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config-wizard.js b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config-wizard.js deleted file mode 100644 index 5c867b8830..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config-wizard.js +++ /dev/null @@ -1,449 +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 - * - */ - -Ext.ns("Sonia.hg"); - -Sonia.hg.ConfigWizard = Ext.extend(Ext.Window,{ - - hgConfig: null, - title: 'Mercurial Configuration Wizard', - - initComponent: function(){ - - this.addEvents('finish'); - - var config = { - title: this.title, - layout: 'fit', - width: 420, - height: 140, - closable: true, - resizable: true, - plain: true, - border: false, - modal: true, - bodyCssClass: 'x-panel-mc', - items: [{ - id: 'hgConfigWizardPanel', - xtype: 'hgConfigWizardPanel', - hgConfig: this.hgConfig, - listeners: { - finish: { - fn: this.onFinish, - scope: this - } - } - }] - }; - - Ext.apply(this, Ext.apply(this.initialConfig, config)); - Sonia.hg.ConfigWizard.superclass.initComponent.apply(this, arguments); - }, - - onFinish: function(config){ - this.fireEvent('finish', config); - this.close(); - } - -}); - -Sonia.hg.InstallationJsonReader = function(){ - this.RecordType = Ext.data.Record.create([{ - name: "path", - mapping: "path", - type: "string" - }]); -}; - -Ext.extend(Sonia.hg.InstallationJsonReader, Ext.data.JsonReader, { - - readRecords: function(o){ - this.jsonData = o; - - if (debug){ - console.debug('read installation data from json'); - console.debug(o); - } - - var records = []; - var paths = o.path; - for ( var i=0; i<paths.length; i++ ){ - records.push(new this.RecordType({ - 'path': paths[i] - })); - } - return { - success: true, - records: records, - totalRecords: records.length - }; - } - -}); - -Sonia.hg.ConfigWizardPanel = Ext.extend(Ext.Panel,{ - - hgConfig: null, - packageTemplate: '<tpl for="."><div class="x-combo-list-item">\ - {id} (hg: {hg-version}, py: {python-version}, size: {size:fileSize})\ - </div></tpl>', - - // text - backText: 'Back', - nextText: 'Next', - finishText: 'Finish', - configureLocalText: 'Configure local installation', - configureRemoteText: 'Download and install', - loadingText: 'Loading ...', - hgInstallationText: 'Mercurial Installation', - pythonInstallationText: 'Python Installation', - hgPackageText: 'Mercurial Package', - errorTitleText: 'Error', - packageInstallationFailedText: 'Package installation failed', - installPackageText: 'install mercurial package {0}', - - initComponent: function(){ - this.addEvents('finish'); - - var packageStore = new Ext.data.JsonStore({ - storeId: 'pkgStore', - proxy: new Ext.data.HttpProxy({ - url: restUrl + 'config/repositories/hg/packages.json', - disableCaching: false - }), - fields: [ 'id', 'hg-version', 'python-version', 'size' ], - root: 'package', - listeners: { - load: { - fn: this.checkIfPackageAvailable, - scope: this - } - } - }); - - var hgInstallationStore = new Ext.data.Store({ - proxy: new Ext.data.HttpProxy({ - url: restUrl + 'config/repositories/hg/installations/hg.json' - }), - fields: [ 'path' ], - reader: new Sonia.hg.InstallationJsonReader(), - autoLoad: true, - autoDestroy: true - }); - - var pythonInstallationStore = new Ext.data.Store({ - proxy: new Ext.data.HttpProxy({ - url: restUrl + 'config/repositories/hg/installations/python.json' - }), - fields: [ 'path' ], - reader: new Sonia.hg.InstallationJsonReader(), - autoLoad: true, - autoDestroy: true - }); - - var config = { - layout: 'card', - activeItem: 0, - bodyStyle: 'padding: 5px', - defaults: { - bodyCssClass: 'x-panel-mc', - border: false, - labelWidth: 120, - width: 250 - }, - bbar: ['->',{ - id: 'move-prev', - text: this.backText, - handler: this.navHandler.createDelegate(this, [-1]), - disabled: true, - scope: this - },{ - id: 'move-next', - text: this.nextText, - handler: this.navHandler.createDelegate(this, [1]), - scope: this - },{ - id: 'finish', - text: this.finishText, - handler: this.applyChanges, - scope: this, - disabled: true - }], - items: [{ - id: 'cod', - items: [{ - id: 'configureOrDownload', - xtype: 'radiogroup', - name: 'configureOrDownload', - columns: 1, - items: [{ - boxLabel: this.configureLocalText, - name: 'cod', - inputValue: 'localInstall', - checked: true - },{ - id: 'remoteInstallRadio', - boxLabel: this.configureRemoteText, - name: 'cod', - inputValue: 'remoteInstall', - disabled: true - }] - }], - listeners: { - render: { - fn: function(panel){ - panel.body.mask(this.loadingText); - var store = Ext.StoreMgr.lookup('pkgStore'); - store.load.defer(100, store); - }, - scope: this - } - } - },{ - id: 'localInstall', - layout: 'form', - defaults: { - width: 250 - }, - items: [{ - id: 'mercurial', - fieldLabel: this.hgInstallationText, - name: 'mercurial', - xtype: 'combo', - readOnly: false, - triggerAction: 'all', - lazyRender: true, - mode: 'local', - editable: true, - store: hgInstallationStore, - valueField: 'path', - displayField: 'path', - allowBlank: false, - value: this.hgConfig.hgBinary - },{ - id: 'python', - fieldLabel: this.pythonInstallationText, - name: 'python', - xtype: 'combo', - readOnly: false, - triggerAction: 'all', - lazyRender: true, - mode: 'local', - editable: true, - store: pythonInstallationStore, - valueField: 'path', - displayField: 'path', - allowBlank: false, - value: this.hgConfig.pythonBinary - }] - },{ - id: 'remoteInstall', - layout: 'form', - defaults: { - width: 250 - }, - items: [{ - id: 'package', - fieldLabel: this.hgPackageText, - name: 'package', - xtype: 'combo', - readOnly: false, - triggerAction: 'all', - lazyRender: true, - mode: 'local', - editable: false, - store: packageStore, - valueField: 'id', - displayField: 'id', - allowBlank: false, - tpl: this.packageTemplate, - listeners: { - select: function(){ - Ext.getCmp('finish').setDisabled(false); - } - } - }] - }] - }; - - Ext.apply(this, Ext.apply(this.initialConfig, config)); - Sonia.hg.ConfigWizardPanel.superclass.initComponent.apply(this, arguments); - }, - - checkIfPackageAvailable: function(store){ - Ext.getCmp('cod').body.unmask(); - var c = store.getTotalCount(); - if ( debug ){ - console.debug( "found " + c + " package(s)" ); - } - if ( c > 0 ){ - Ext.getCmp('remoteInstallRadio').setDisabled(false); - } - }, - - navHandler: function(direction){ - var layout = this.getLayout(); - var id = layout.activeItem.id; - - var next = -1; - - if ( id === 'cod' && direction === 1 ){ - var v = Ext.getCmp('configureOrDownload').getValue().getRawValue(); - var df = false; - if ( v === 'localInstall' ){ - next = 1; - } else if ( v === 'remoteInstall' ){ - next = 2; - df = true; - } - Ext.getCmp('move-prev').setDisabled(false); - Ext.getCmp('move-next').setDisabled(true); - Ext.getCmp('finish').setDisabled(df); - } - else if (direction === -1 && (id === 'localInstall' || id === 'remoteInstall')) { - next = 0; - Ext.getCmp('move-prev').setDisabled(true); - Ext.getCmp('move-next').setDisabled(false); - Ext.getCmp('finish').setDisabled(true); - } - - if ( next >= 0 ){ - layout.setActiveItem(next); - } - }, - - applyChanges: function(){ - var v = Ext.getCmp('configureOrDownload').getValue().getRawValue(); - if ( v === 'localInstall' ){ - this.applyLocalConfiguration(); - } else if ( v === 'remoteInstall' ){ - this.applyRemoteConfiguration(); - } - }, - - applyRemoteConfiguration: function(){ - if ( debug ){ - console.debug( "apply remote configuration" ); - } - - var pkg = Ext.getCmp('package').getValue(); - if ( debug ){ - console.debug( 'install mercurial package ' + pkg ); - } - - var lbox = Ext.MessageBox.show({ - title: this.loadingText, - msg: String.format(this.installPackageText, pkg), - width: 300, - wait: true, - animate: true, - progress: true, - closable: false - }); - - Ext.Ajax.request({ - url: restUrl + 'config/repositories/hg/packages/' + pkg + '.json', - method: 'POST', - scope: this, - timeout: 900000, // 15min - success: function(){ - if ( debug ){ - console.debug('package successfully installed'); - } - lbox.hide(); - this.fireEvent('finish'); - }, - failure: function(){ - if ( debug ){ - console.debug('package installation failed'); - } - lbox.hide(); - Ext.MessageBox.show({ - title: this.errorTitleText, - msg: this.packageInstallationFailedText, - buttons: Ext.MessageBox.OK, - icon:Ext.MessageBox.ERROR - }); - } - }); - - - }, - - applyLocalConfiguration: function(){ - if ( debug ){ - console.debug( "apply remote configuration" ); - } - var mercurial = Ext.getCmp('mercurial').getValue(); - var python = Ext.getCmp('python').getValue(); - if (debug){ - console.debug( 'configure mercurial=' + mercurial + " and python=" + python ); - } - delete this.hgConfig.pythonPath; - delete this.hgConfig.useOptimizedBytecode; - this.hgConfig.hgBinary = mercurial; - this.hgConfig.pythonBinary = python; - - if ( debug ){ - console.debug( this.hgConfig ); - } - - this.fireEvent('finish', this.hgConfig); - } - -}); - -// register xtype -Ext.reg('hgConfigWizardPanel', Sonia.hg.ConfigWizardPanel); - - -// i18n - -if ( i18n && i18n.country === 'de' ){ - - Ext.override(Sonia.hg.ConfigWizardPanel, { - - backText: 'Zurück', - nextText: 'Weiter', - finishText: 'Fertigstellen', - configureLocalText: 'Eine lokale Installation Konfigurieren', - configureRemoteText: 'Herunterladen und installieren', - loadingText: 'Lade ...', - hgInstallationText: 'Mercurial Installation', - pythonInstallationText: 'Python Installation', - hgPackageText: 'Mercurial Package', - errorTitleText: 'Fehler', - packageInstallationFailedText: 'Package Installation fehlgeschlagen', - installPackageText: 'Installiere Mercurial-Package {0}' - - }); - -} \ No newline at end of file diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js deleted file mode 100644 index f8aa46e0a8..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg.config.js +++ /dev/null @@ -1,287 +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 - * - */ - - -Ext.ns("Sonia.hg"); - -Sonia.hg.ConfigPanel = Ext.extend(Sonia.config.ConfigForm, { - - // labels - titleText: 'Mercurial Settings', - hgBinaryText: 'HG Binary', - pythonBinaryText: 'Python Binary', - pythonPathText: 'Python Module Search Path', - repositoryDirectoryText: 'Repository directory', - useOptimizedBytecodeText: 'Optimized Bytecode (.pyo)', - configWizardText: 'Start Configuration Wizard', - configWizardLabelText: 'Start Configuration Wizard', - encodingText: 'Encoding', - disabledText: 'Disabled', - showRevisionInIdText: 'Show Revision', - - // helpText - hgBinaryHelpText: 'Location of Mercurial binary.', - pythonBinaryHelpText: 'Location of Python binary.', - pythonPathHelpText: 'Python Module Search Path (PYTHONPATH).', - repositoryDirectoryHelpText: 'Location of the Mercurial repositories.', - useOptimizedBytecodeHelpText: 'Use the Python "-O" switch.', - encodingHelpText: 'Repository Encoding.', - disabledHelpText: 'Enable or disable the Mercurial plugin. \n\ - Note you have to reload the page, after changing this value.', - showRevisionInIdHelpText: 'Show revision as part of the node id. Note: \n\ - You have to restart the ApplicationServer to affect cached changesets.', - - initComponent: function(){ - - var config = { - title : this.titleText, - items : [{ - xtype : 'textfield', - fieldLabel : this.hgBinaryText, - name : 'hgBinary', - allowBlank : false, - helpText: this.hgBinaryHelpText - },{ - xtype : 'textfield', - fieldLabel : this.pythonBinaryText, - name : 'pythonBinary', - allowBlank : false, - helpText: this.pythonBinaryHelpText - },{ - xtype : 'textfield', - fieldLabel : this.pythonPathText, - name : 'pythonPath', - helpText: this.pythonPathHelpText - },{ - xtype: 'textfield', - name: 'repositoryDirectory', - fieldLabel: this.repositoryDirectoryText, - helpText: this.repositoryDirectoryHelpText, - allowBlank : false - },{ - xtype: 'textfield', - name: 'encoding', - fieldLabel: this.encodingText, - helpText: this.encodingHelpText, - allowBlank : false - },{ - xtype: 'checkbox', - name: 'useOptimizedBytecode', - fieldLabel: this.useOptimizedBytecodeText, - inputValue: 'true', - helpText: this.useOptimizedBytecodeHelpText - },{ - xtype: 'checkbox', - name: 'showRevisionInId', - fieldLabel: this.showRevisionInIdText, - inputValue: 'true', - helpText: this.showRevisionInIdHelpText - },{ - xtype: 'checkbox', - name: 'disabled', - fieldLabel: this.disabledText, - inputValue: 'true', - helpText: this.disabledHelpText - },{ - xtype: 'button', - text: this.configWizardText, - fieldLabel: this.configWizardLabelText, - handler: function(){ - var config = this.getForm().getValues(); - var wizard = new Sonia.hg.ConfigWizard({ - hgConfig: config - }); - wizard.on('finish', function(config){ - var self = Ext.getCmp('hgConfigForm'); - if ( config ){ - if (debug){ - console.debug( 'load config from wizard and submit to server' ); - } - self.loadConfig( self.el, 'config/repositories/hg/auto-configuration.json', 'POST', config ); - } else { - if (debug){ - console.debug( 'reload config' ); - } - self.onLoad(self.el); - } - }, this); - wizard.show(); - }, - scope: this - }] - }; - - Ext.apply(this, Ext.apply(this.initialConfig, config)); - Sonia.hg.ConfigPanel.superclass.initComponent.apply(this, arguments); - }, - - onSubmit: function(values){ - this.el.mask(this.submitText); - Ext.Ajax.request({ - url: restUrl + 'config/repositories/hg.json', - method: 'POST', - jsonData: values, - scope: this, - disableCaching: true, - success: function(){ - this.el.unmask(); - }, - failure: function(){ - this.el.unmask(); - alert('failure'); - } - }); - }, - - onLoad: function(el){ - this.loadConfig(el, 'config/repositories/hg.json', 'GET'); - }, - - loadConfig: function(el, url, method, config){ - var tid = setTimeout( function(){ el.mask(this.loadingText); }, 100); - Ext.Ajax.request({ - url: restUrl + url, - method: method, - jsonData: config, - scope: this, - disableCaching: true, - success: function(response){ - var obj = Ext.decode(response.responseText); - this.load(obj); - clearTimeout(tid); - el.unmask(); - }, - failure: function(){ - el.unmask(); - clearTimeout(tid); - alert('failure'); - } - }); - } - -}); - -Ext.reg("hgConfigPanel", Sonia.hg.ConfigPanel); - -// i18n - -if ( i18n && i18n.country === 'de' ){ - - Ext.override(Sonia.hg.ConfigPanel, { - - // labels - titleText: 'Mercurial Einstellungen', - hgBinaryText: 'HG Pfad', - pythonBinaryText: 'Python Pfad', - pythonPathText: 'Python Modul Suchpfad', - repositoryDirectoryText: 'Repository-Verzeichnis', - useOptimizedBytecodeText: 'Optimierter Bytecode (.pyo)', - autoConfigText: 'Einstellungen automatisch laden', - autoConfigLabelText: 'Automatische Einstellung', - configWizardText: 'Konfigurations-Assistenten starten', - configWizardLabelText: 'Konfigurations-Assistent', - disabledText: 'Deaktivieren', - showRevisionInIdText: 'Zeige Revision an', - - // helpText - hgBinaryHelpText: 'Pfad zum "hg" Befehl.', - pythonBinaryHelpText: 'Pfad zum "python" Befehl.', - pythonPathHelpText: 'Python Modul Suchpfad (PYTHONPATH).', - repositoryDirectoryHelpText: 'Verzeichnis der Mercurial-Repositories.', - useOptimizedBytecodeHelpText: 'Optimierten Bytecode verwenden (python -O).', - disabledHelpText: 'Aktivieren oder deaktivieren des Mercurial Plugins.\n\ - Die Seite muss neu geladen werden wenn dieser Wert geändert wird.', - showRevisionInIdHelpText: 'Zeige die Revision als teil der NodeId an. \n\ - Der ApplicationServer muss neugestartet werden um zwischengespeicherte\n\ - Changesets zuändern.' - }); - -} - -// register information panel - -initCallbacks.push(function(main){ - main.registerInfoPanel('hg', { - checkoutTemplate: 'hg clone <a href="{0}" target="_blank">{0}</a>', - xtype: 'repositoryExtendedInfoPanel' - }); -}); - -// register config panel - -registerConfigPanel({ - id: 'hgConfigForm', - xtype : 'hgConfigPanel' -}); - -// register type icon - -Sonia.repository.typeIcons['hg'] = 'resources/images/icons/16x16/mercurial.png'; - -// override ChangesetViewerGrid to render changeset id's with revisions - -Ext.override(Sonia.repository.ChangesetViewerGrid, { - - isMercurialRepository: function(){ - return this.repository.type === 'hg'; - }, - - getChangesetId: function(id, record){ - if ( this.isMercurialRepository() ){ - var rev = Sonia.util.getProperty(record.get('properties'), 'hg.rev'); - if ( rev ){ - id = rev + ':' + id; - } - } - return id; - }, - - getParentIds: function(id, record){ - var parents = record.get('parents'); - if ( this.isMercurialRepository() ){ - if ( parents && parents.length > 0 ){ - var properties = record.get('properties'); - var rev = Sonia.util.getProperty(properties, 'hg.p1.rev'); - if (rev){ - parents[0] = rev + ':' + parents[0]; - } - if ( parents.length > 1 ){ - rev = Sonia.util.getProperty(properties, 'hg.p2.rev'); - if (rev){ - parents[1] = rev + ':' + parents[1]; - } - } - } - } - return parents; - } - -}); \ No newline at end of file diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 518f229011..1871200389 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -1,8 +1,8 @@ # # Copyright (c) 2010, Sebastian Sdorra -# All rights reserved. +# aLL rights reserved. # -# Redistribution and use in source and binary forms, with or without +# 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, @@ -32,61 +32,144 @@ Prints date, size and last message of files. """ -from mercurial import cmdutil,util + +from collections import defaultdict +from mercurial import scmutil cmdtable = {} -command = cmdutil.command(cmdtable) + +try: + from mercurial import registrar + command = registrar.command(cmdtable) +except (AttributeError, ImportError): + # Fallback to hg < 4.3 support + from mercurial import cmdutil + command = cmdutil.command(cmdtable) + +try: + from mercurial.utils import dateutil + _parsedate = dateutil.parsedate +except ImportError: + # compat with hg < 4.6 + from mercurial import util + _parsedate = util.parsedate + +FILE_MARKER = '<files>' + +class File_Collector: + + def __init__(self, recursive = False): + self.recursive = recursive + self.structure = defaultdict(dict, ((FILE_MARKER, []),)) + + def collect(self, paths, path = "", dir_only = False): + for p in paths: + if p.startswith(path): + self.attach(self.extract_name_without_parent(path, p), self.structure, dir_only) + + def attach(self, branch, trunk, dir_only = False): + parts = branch.split('/', 1) + if len(parts) == 1: # branch is a file + if dir_only: + trunk[parts[0]] = defaultdict(dict, ((FILE_MARKER, []),)) + else: + trunk[FILE_MARKER].append(parts[0]) + else: + node, others = parts + if node not in trunk: + trunk[node] = defaultdict(dict, ((FILE_MARKER, []),)) + if self.recursive: + self.attach(others, trunk[node], dir_only) + + def extract_name_without_parent(self, parent, name_with_parent): + if len(parent) > 0: + name_without_parent = name_with_parent[len(parent):] + if name_without_parent.startswith("/"): + name_without_parent = name_without_parent[1:] + return name_without_parent + return name_with_parent + +class File_Object: + def __init__(self, directory, path): + self.directory = directory + self.path = path + self.children = [] + self.sub_repository = None + + def get_name(self): + parts = self.path.split("/") + return parts[len(parts) - 1] + + def get_parent(self): + idx = self.path.rfind("/") + if idx > 0: + return self.path[0:idx] + return "" + + def add_child(self, child): + self.children.append(child) + + def __getitem__(self, key): + return self.children[key] + + def __len__(self): + return len(self.children) + + def __repr__(self): + result = self.path + if self.directory: + result += "/" + return result + +class File_Walker: + + def __init__(self, sub_repositories, visitor): + self.visitor = visitor + self.sub_repositories = sub_repositories + + def create_file(self, path): + return File_Object(False, path) + + def create_directory(self, path): + directory = File_Object(True, path) + if path in self.sub_repositories: + directory.sub_repository = self.sub_repositories[path] + return directory + + def visit_file(self, path): + file = self.create_file(path) + self.visit(file) + + def visit_directory(self, path): + file = self.create_directory(path) + self.visit(file) + + def visit(self, file): + self.visitor.visit(file) + + def create_path(self, parent, path): + if len(parent) > 0: + return parent + "/" + path + return path + + def walk(self, structure, parent = ""): + for key, value in structure.iteritems(): + if key == FILE_MARKER: + if value: + for v in value: + self.visit_file(self.create_path(parent, v)) + else: + self.visit_directory(self.create_path(parent, key)) + if isinstance(value, dict): + self.walk(value, self.create_path(parent, key)) + else: + self.visit_directory(self.create_path(parent, value)) class SubRepository: url = None revision = None -def removeTrailingSlash(path): - if path.endswith('/'): - path = path[0:-1] - return path - -def appendTrailingSlash(path): - if not path.endswith('/'): - path += '/' - return path - -def collectFiles(revCtx, path, files, directories, recursive): - length = 0 - paths = [] - mf = revCtx.manifest() - if path is "": - length = 1 - for f in mf: - paths.append(f) - else: - length = len(path.split('/')) + 1 - directory = path - if not directory.endswith('/'): - directory += '/' - - for f in mf: - if f.startswith(directory): - paths.append(f) - - if not recursive: - for p in paths: - parts = p.split('/') - depth = len(parts) - if depth is length: - file = revCtx[p] - files.append(file) - elif depth > length: - dirpath = '' - for i in range(0, length): - dirpath += parts[i] + '/' - if not dirpath in directories: - directories.append(dirpath) - else: - for p in paths: - files.append(revCtx[p]) - -def createSubRepositoryMap(revCtx): +def collect_sub_repositories(revCtx): subrepos = {} try: hgsub = revCtx.filectx('.hgsub').data().split('\n') @@ -98,7 +181,7 @@ def createSubRepositoryMap(revCtx): subrepos[parts[0].strip()] = subrepo except Exception: pass - + try: hgsubstate = revCtx.filectx('.hgsubstate').data().split('\n') for line in hgsubstate: @@ -109,32 +192,77 @@ def createSubRepositoryMap(revCtx): subrepo.revision = subrev except Exception: pass - + return subrepos - -def printSubRepository(ui, path, subrepository, transport): - format = '%s %s %s\n' - if transport: - format = 's%s\n%s %s\0' - ui.write( format % (appendTrailingSlash(path), subrepository.revision, subrepository.url)) - -def printDirectory(ui, path, transport): - format = '%s\n' - if transport: - format = 'd%s\0' - ui.write( format % path) - -def printFile(ui, repo, file, disableLastCommit, transport): - date = '0 0' - description = 'n/a' - if not disableLastCommit: - linkrev = repo[file.linkrev()] - date = '%d %d' % util.parsedate(linkrev.date()) - description = linkrev.description() - format = '%s %i %s %s\n' - if transport: - format = 'f%s\n%i %s %s\0' - ui.write( format % (file.path(), file.size(), date, description) ) + +class File_Printer: + + def __init__(self, ui, repo, revCtx, disableLastCommit, transport): + self.ui = ui + self.repo = repo + self.revCtx = revCtx + self.disableLastCommit = disableLastCommit + self.transport = transport + + def print_directory(self, path): + format = '%s/\n' + if self.transport: + format = 'd%s/\0' + self.ui.write( format % path) + + def print_file(self, path): + file = self.revCtx[path] + date = '0 0' + description = 'n/a' + if not self.disableLastCommit: + linkrev = self.repo[file.linkrev()] + date = '%d %d' % _parsedate(linkrev.date()) + description = linkrev.description() + format = '%s %i %s %s\n' + if self.transport: + format = 'f%s\n%i %s %s\0' + self.ui.write( format % (file.path(), file.size(), date, description) ) + + def print_sub_repository(self, path, subrepo): + format = '%s/ %s %s\n' + if self.transport: + format = 's%s/\n%s %s\0' + self.ui.write( format % (path, subrepo.revision, subrepo.url)) + + def visit(self, file): + if file.sub_repository: + self.print_sub_repository(file.path, file.sub_repository) + elif file.directory: + self.print_directory(file.path) + else: + self.print_file(file.path) + +class File_Viewer: + def __init__(self, revCtx, visitor): + self.revCtx = revCtx + self.visitor = visitor + self.sub_repositories = {} + self.recursive = False + + def remove_ending_slash(self, path): + if path.endswith("/"): + return path[:-1] + return path + + def view(self, path = ""): + manifest = self.revCtx.manifest() + if len(path) > 0 and path in manifest: + self.visitor.visit(File_Object(False, path)) + else: + p = self.remove_ending_slash(path) + + collector = File_Collector(self.recursive) + walker = File_Walker(self.sub_repositories, self.visitor) + + self.visitor.visit(File_Object(True, p)) + collector.collect(manifest, p) + collector.collect(self.sub_repositories.keys(), p, True) + walker.walk(collector.structure, p) @command('fileview', [ ('r', 'revision', 'tip', 'revision to print'), @@ -145,23 +273,12 @@ def printFile(ui, repo, file, disableLastCommit, transport): ('t', 'transport', False, 'format the output for command server'), ]) def fileview(ui, repo, **opts): - files = [] - directories = [] - revision = opts['revision'] - if revision == None: - revision = 'tip' - revCtx = repo[revision] - path = opts['path'] - if path.endswith('/'): - path = path[0:-1] - transport = opts['transport'] - collectFiles(revCtx, path, files, directories, opts['recursive']) - if not opts['disableSubRepositoryDetection']: - subRepositories = createSubRepositoryMap(revCtx) - for k, v in subRepositories.iteritems(): - if k.startswith(path): - printSubRepository(ui, k, v, transport) - for d in directories: - printDirectory(ui, d, transport) - for f in files: - printFile(ui, repo, f, opts['disableLastCommit'], transport) + revCtx = scmutil.revsingle(repo, opts["revision"]) + subrepos = {} + if not opts["disableSubRepositoryDetection"]: + subrepos = collect_sub_repositories(revCtx) + printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"]) + viewer = File_Viewer(revCtx, printer) + viewer.recursive = opts["recursive"] + viewer.sub_repositories = subrepos + viewer.view(opts["path"]) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py new file mode 100644 index 0000000000..2ce3989d58 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py @@ -0,0 +1,131 @@ +from fileview import File_Viewer, SubRepository +import unittest + +class DummyRevContext(): + + def __init__(self, mf): + self.mf = mf + + def manifest(self): + return self.mf + +class File_Object_Collector(): + + def __init__(self): + self.stack = [] + + def __getitem__(self, key): + if len(self.stack) == 0 and key == 0: + return self.last + return self.stack[key] + + def visit(self, file): + while len(self.stack) > 0: + current = self.stack[-1] + if file.get_parent() == current.path: + current.add_child(file) + break + else: + self.stack.pop() + if file.directory: + self.stack.append(file) + self.last = file + + +class Test_File_Viewer(unittest.TestCase): + + def test_single_file(self): + root = self.collect(["a.txt", "b.txt"], "a.txt") + self.assertFile(root, "a.txt") + + def test_simple(self): + root = self.collect(["a.txt", "b.txt"]) + self.assertFile(root[0], "a.txt") + self.assertFile(root[1], "b.txt") + + def test_recursive(self): + root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "", True) + self.assertChildren(root, ["a", "b", "f.txt", "c"]) + c = root[3] + self.assertDirectory(c, "c") + self.assertChildren(c, ["c/d.txt", "c/e.txt", "c/g"]) + g = c[2] + self.assertDirectory(g, "c/g") + self.assertChildren(g, ["c/g/h.txt"]) + + def test_recursive_with_path(self): + root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c", True) + self.assertDirectory(root, "c") + self.assertChildren(root, ["c/d.txt", "c/e.txt", "c/g"]) + g = root[2] + self.assertDirectory(g, "c/g") + self.assertChildren(g, ["c/g/h.txt"]) + + def test_recursive_with_deep_path(self): + root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c/g", True) + self.assertDirectory(root, "c/g") + self.assertChildren(root, ["c/g/h.txt"]) + + def test_non_recursive(self): + root = self.collect(["a.txt", "b.txt", "c/d.txt", "c/e.txt", "c/f/g.txt"]) + self.assertDirectory(root, "") + self.assertChildren(root, ["a.txt", "b.txt", "c"]) + c = root[2] + self.assertEmptyDirectory(c, "c") + + def test_non_recursive_with_path(self): + root = self.collect(["a.txt", "b.txt", "c/d.txt", "c/e.txt", "c/f/g.txt"], "c") + self.assertDirectory(root, "c") + self.assertChildren(root, ["c/d.txt", "c/e.txt", "c/f"]) + f = root[2] + self.assertEmptyDirectory(f, "c/f") + + def test_non_recursive_with_path_with_ending_slash(self): + root = self.collect(["c/d.txt"], "c/") + self.assertDirectory(root, "c") + self.assertFile(root[0], "c/d.txt") + + def test_with_sub_directory(self): + revCtx = DummyRevContext(["a.txt", "b/c.txt"]) + collector = File_Object_Collector() + viewer = File_Viewer(revCtx, collector) + sub_repositories = {} + sub_repositories["d"] = SubRepository() + sub_repositories["d"].url = "d" + sub_repositories["d"].revision = "42" + viewer.sub_repositories = sub_repositories + viewer.view() + + d = collector[0][2] + self.assertDirectory(d, "d") + + + def collect(self, paths, path = "", recursive = False): + revCtx = DummyRevContext(paths) + collector = File_Object_Collector() + + viewer = File_Viewer(revCtx, collector) + viewer.recursive = recursive + viewer.view(path) + + return collector[0] + + def assertChildren(self, parent, expectedPaths): + self.assertEqual(len(parent), len(expectedPaths)) + for idx,item in enumerate(parent.children): + self.assertEqual(item.path, expectedPaths[idx]) + + def assertFile(self, file, expectedPath): + self.assertEquals(file.path, expectedPath) + self.assertFalse(file.directory) + + def assertDirectory(self, file, expectedPath): + self.assertEquals(file.path, expectedPath) + self.assertTrue(file.directory) + + def assertEmptyDirectory(self, file, expectedPath): + self.assertDirectory(file, expectedPath) + self.assertTrue(len(file.children) == 0) + +if __name__ == '__main__': + unittest.main() diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py index 66d5fadc3c..aeb5d6d588 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py @@ -31,12 +31,31 @@ import os -from mercurial import demandimport +from mercurial import demandimport, ui as uimod, hg from mercurial.hgweb import hgweb, wsgicgi -repositoryPath = os.environ['SCM_REPOSITORY_PATH'] - demandimport.enable() -application = hgweb(repositoryPath) +try: + u = uimod.ui.load() +except AttributeError: + # For installations earlier than Mercurial 4.1 + u = uimod.ui() + +u.setconfig('web', 'push_ssl', 'false') +u.setconfig('web', 'allow_read', '*') +u.setconfig('web', 'allow_push', '*') + +u.setconfig('hooks', 'changegroup.scm', 'python:scmhooks.postHook') +u.setconfig('hooks', 'pretxnchangegroup.scm', 'python:scmhooks.preHook') + +# pass SCM_HTTP_POST_ARGS to enable experimental httppostargs protocol of mercurial +# SCM_HTTP_POST_ARGS is set by HgCGIServlet +# Issue 970: https://goo.gl/poascp +u.setconfig('experimental', 'httppostargs', os.environ['SCM_HTTP_POST_ARGS']) + +# open repository +# SCM_REPOSITORY_PATH contains the repository path and is set by HgCGIServlet +r = hg.repository(u, os.environ['SCM_REPOSITORY_PATH']) +application = hgweb(r) wsgicgi.launch(application) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py index e9a58d589f..637aa16331 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py @@ -40,26 +40,27 @@ import os, urllib, urllib2 baseUrl = os.environ['SCM_URL'] challenge = os.environ['SCM_CHALLENGE'] -credentials = os.environ['SCM_CREDENTIALS'] +token = os.environ['SCM_BEARER_TOKEN'] +repositoryId = os.environ['SCM_REPOSITORY_ID'] def printMessages(ui, msgs): for line in msgs: if line.startswith("_e") or line.startswith("_n"): line = line[2:]; - ui.warn(line); + ui.warn('%s\n' % line.rstrip()) def callHookUrl(ui, repo, hooktype, node): abort = True try: url = baseUrl + hooktype ui.debug( "send scm-hook to " + url + " and " + node + "\n" ) - data = urllib.urlencode({'node': node, 'challenge': challenge, 'credentials': credentials, 'repositoryPath': repo.root}) + data = urllib.urlencode({'node': node, 'challenge': challenge, 'token': token, 'repositoryPath': repo.root, 'repositoryId': repositoryId}) # open url but ignore proxy settings proxy_handler = urllib2.ProxyHandler({}) opener = urllib2.build_opener(proxy_handler) req = urllib2.Request(url, data) conn = opener.open(req) - if conn.code >= 200 and conn.code < 300: + if 200 <= conn.code < 300: ui.debug( "scm-hook " + hooktype + " success with status code " + str(conn.code) + "\n" ) printMessages(ui, conn) abort = False @@ -78,13 +79,13 @@ def callHookUrl(ui, repo, hooktype, node): printMessages(ui, msg.splitlines(True)) else: ui.warn( "ERROR: scm-hook failed with an unknown error\n" ) + ui.traceback() except ValueError: ui.warn( "scm-hook failed with an exception\n" ) + ui.traceback() return abort -def callback(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): - if pending != None: - pending() +def callback(ui, repo, hooktype, node=None): abort = True if node != None: if len(baseUrl) > 0: @@ -95,3 +96,28 @@ def callback(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs) else: ui.warn("changeset node is not available") return abort + +def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): + # older mercurial versions + if pending != None: + pending() + + # newer mercurial version + # we have to make in-memory changes visible to external process + # this does not happen automatically, because mercurial treat our hooks as internal hooks + # see hook.py at mercurial sources _exthook + try: + if repo is not None: + tr = repo.currenttransaction() + repo.dirstate.write(tr) + if tr and not tr.writepending(): + ui.warn("no pending write transaction found") + except AttributeError: + ui.debug("mercurial does not support currenttransation") + # do nothing + + return callback(ui, repo, hooktype, node) + +def postHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): + return callback(ui, repo, hooktype, node) + diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style index 73b3ec694b..2185c47a05 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style @@ -1,7 +1,8 @@ header = "%{pattern}" -changeset = "{rev}:{node}{author}\n{date|hgdate}\n{branch}\n{parents}close={ifeq(get(extras, 'close'),1,1,0)}\n{tags}{file_adds}{file_mods}{file_dels}\n{desc}\0" +changeset = "{rev}:{node}{author}\n{date|hgdate}\n{branch}\n{parents}{extras}\n{tags}{file_adds}{file_mods}{file_dels}\n{desc}\0" tag = "t {tag}\n" file_add = "a {file_add}\n" file_mod = "m {file_mod}\n" file_del = "d {file_del}\n" +extra = "{key}={value|stringescape}," footer = "%{pattern}" \ No newline at end of file diff --git a/scm-plugins/scm-hg-plugin/src/main/webapp/images/hg-logo.png b/scm-plugins/scm-hg-plugin/src/main/webapp/images/hg-logo.png new file mode 100644 index 0000000000..0f72285da6 Binary files /dev/null and b/scm-plugins/scm-hg-plugin/src/main/webapp/images/hg-logo.png differ diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java new file mode 100644 index 0000000000..1f88bfe665 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java @@ -0,0 +1,123 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; +import java.net.URISyntaxException; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class HgConfigAutoConfigurationResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @InjectMocks + private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper; + + @Mock + private HgRepositoryHandler repositoryHandler; + + @Mock + private Provider<HgConfigAutoConfigurationResource> resourceProvider; + + @Before + public void prepareEnvironment() { + HgConfigAutoConfigurationResource resource = + new HgConfigAutoConfigurationResource(dtoToConfigMapper, repositoryHandler); + + when(resourceProvider.get()).thenReturn(resource); + dispatcher.getRegistry().addSingletonResource( + new HgConfigResource(null, null, null, null, + resourceProvider, null)); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldSetDefaultConfigAndInstallHg() throws Exception { + MockHttpResponse response = put(null); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + + HgConfig actualConfig = captureConfig(); + assertFalse(actualConfig.isDisabled()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotSetDefaultConfigAndInstallHgWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); + + put(null); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldUpdateConfigAndInstallHg() throws Exception { + MockHttpResponse response = put("{\"disabled\":true}"); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + + HgConfig actualConfig = captureConfig(); + assertTrue(actualConfig.isDisabled()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotUpdateConfigAndInstallHgWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); + + put("{\"disabled\":true}"); + } + + private MockHttpResponse put(String content) throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.put("/" + HgConfigResource.HG_CONFIG_PATH_V2 + "/auto-configuration"); + + if (content != null) { + request + .contentType(HgVndMediaType.CONFIG) + .content(content.getBytes()); + } + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private HgConfig captureConfig() { + ArgumentCaptor<HgConfig> configCaptor = ArgumentCaptor.forClass(HgConfig.class); + verify(repositoryHandler).doAutoConfiguration(configCaptor.capture()); + return configCaptor.getValue(); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java new file mode 100644 index 0000000000..6e181f4886 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java @@ -0,0 +1,49 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.HgConfig; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class HgConfigDtoToHgConfigMapperTest { + + @InjectMocks + private HgConfigDtoToHgConfigMapperImpl mapper; + + @Test + public void shouldMapFields() { + HgConfigDto dto = createDefaultDto(); + HgConfig config = mapper.map(dto); + + assertTrue(config.isDisabled()); + + assertEquals("ABC", config.getEncoding()); + assertEquals("/etc/hg", config.getHgBinary()); + assertEquals("/py", config.getPythonBinary()); + assertEquals("/etc/", config.getPythonPath()); + assertTrue(config.isShowRevisionInId()); + assertTrue(config.isUseOptimizedBytecode()); + assertTrue(config.isDisableHookSSLValidation()); + assertTrue(config.isEnableHttpPostArgs()); + } + + private HgConfigDto createDefaultDto() { + HgConfigDto configDto = new HgConfigDto(); + configDto.setDisabled(true); + configDto.setEncoding("ABC"); + configDto.setHgBinary("/etc/hg"); + configDto.setPythonBinary("/py"); + configDto.setPythonPath("/etc/"); + configDto.setShowRevisionInId(true); + configDto.setUseOptimizedBytecode(true); + configDto.setDisableHookSSLValidation(true); + configDto.setEnableHttpPostArgs(true); + + return configDto; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java new file mode 100644 index 0000000000..d699a7b836 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java @@ -0,0 +1,65 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class HgConfigInIndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectNode root = objectMapper.createObjectNode(); + private final HgConfigInIndexResource hgConfigInIndexResource; + + public HgConfigInIndexResourceTest() { + root.put("_links", objectMapper.createObjectNode()); + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + hgConfigInIndexResource = new HgConfigInIndexResource(Providers.of(pathInfoStore), objectMapper); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void admin() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + hgConfigInIndexResource.enrich(context); + + assertEquals("/v2/config/hg", root.get("_links").get("hgConfig").get("href").asText()); + } + + @Test + @SubjectAware(username = "readOnly", password = "secret") + public void user() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + hgConfigInIndexResource.enrich(context); + + assertTrue(root.get("_links").iterator().hasNext()); + } + + @Test + public void anonymous() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + hgConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java new file mode 100644 index 0000000000..bcd9543d28 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java @@ -0,0 +1,118 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; +import java.net.URISyntaxException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class HgConfigInstallationsResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @InjectMocks + private HgConfigInstallationsToDtoMapper mapper; + + @Mock + private Provider<HgConfigInstallationsResource> resourceProvider; + + + @Before + public void prepareEnvironment() { + HgConfigInstallationsResource resource = new HgConfigInstallationsResource(mapper); + + when(resourceProvider.get()).thenReturn(resource); + dispatcher.getRegistry().addSingletonResource( + new HgConfigResource(null, null, null, null, + null, resourceProvider)); + + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetHgInstallations() throws Exception { + MockHttpResponse response = get("hg"); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String contentAsString = response.getContentAsString(); + assertThat(contentAsString).contains("{\"paths\":["); + assertThat(contentAsString).contains("hg"); + assertThat(contentAsString).doesNotContain("python"); + + assertThat(contentAsString).contains("\"self\":{\"href\":\"/v2/config/hg/installations/hg"); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetHgInstallationsWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); + + get("hg"); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetPythonInstallations() throws Exception { + MockHttpResponse response = get("python"); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String contentAsString = response.getContentAsString(); + assertThat(contentAsString).contains("{\"paths\":["); + assertThat(contentAsString).contains("python"); + + assertThat(contentAsString).contains("\"self\":{\"href\":\"/v2/config/hg/installations/python"); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetPythonInstallationsWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); + + get("python"); + } + + private MockHttpResponse get(String path) throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + HgConfigResource.HG_CONFIG_PATH_V2 + "/installations/" + path); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java new file mode 100644 index 0000000000..80f8ec32b1 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java @@ -0,0 +1,51 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class HgConfigInstallationsToDtoMapperTest { + + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @InjectMocks + private HgConfigInstallationsToDtoMapper mapper; + + private URI expectedBaseUri; + + private String expectedPath = "path"; + + @Before + public void init() { + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/installations/" + expectedPath); + } + + @Test + public void shouldMapFields() { + List<String> installations = Arrays.asList("/hg", "/bin/hg"); + + HgConfigInstallationsDto dto = mapper.map(installations, expectedPath); + + assertThat(dto.getPaths()).isEqualTo(installations); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java new file mode 100644 index 0000000000..473ddfe4b4 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java @@ -0,0 +1,207 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.installer.HgPackage; +import sonia.scm.installer.HgPackageReader; +import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.HgConfigTests.createPackage; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class HgConfigPackageResourceTest { + + public static final String URI = "/" + HgConfigResource.HG_CONFIG_PATH_V2 + "/packages"; + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = java.net.URI.create("/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @InjectMocks + private HgConfigPackagesToDtoMapperImpl mapper; + + @Mock + private HgRepositoryHandler repositoryHandler; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private HgPackageReader hgPackageReader; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private AdvancedHttpClient advancedHttpClient; + + @Mock + private Provider<HgConfigPackageResource> hgConfigPackageResourceProvider; + + @Mock + private HgPackage hgPackage; + + @Before + public void prepareEnvironment() { + setupResources(); + + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + + when(hgPackageReader.getPackages().getPackages()).thenReturn(createPackages()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetPackages() throws Exception { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String responseString = response.getContentAsString(); + ObjectNode responseJson = new ObjectMapper().readValue(responseString, ObjectNode.class); + + JsonNode packages = responseJson.get("packages"); + assertThat(packages).isNotNull(); + assertThat(packages).hasSize(2); + + JsonNode package1 = packages.get(0); + assertThat(package1.get("_links")).isNull(); + + JsonNode hgConfigTemplate = package1.get("hgConfigTemplate"); + assertThat(hgConfigTemplate).isNotNull(); + assertThat(hgConfigTemplate.get("_links")).isNull(); + + assertThat(responseString).contains("\"_links\":{\"self\":{\"href\":\"/v2/config/hg/packages"); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetPackagesWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); + + get(); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldInstallPackage() throws Exception { + String packgeId = "ourPackage"; + String url = "http://url"; + + setupPackageInstallation(packgeId, url); + when(advancedHttpClient.get(url).request().contentAsStream()) + .thenReturn(new ByteArrayInputStream("mockedFile".getBytes())); + + MockHttpResponse response = put(packgeId); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldHandleFailingInstallation() throws Exception { + String packgeId = "ourPackage"; + String url = "http://url"; + + setupPackageInstallation(packgeId, url); + when(advancedHttpClient.get(url).request().contentAsStream()) + .thenThrow(new IOException("mocked Exception")); + + MockHttpResponse response = put(packgeId); + assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.getStatus()); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldHandlePackagesThatAreNotFound() throws Exception { + String packageId = "this-package-does-not-ex"; + when(hgPackageReader.getPackage(packageId)).thenReturn(null); + MockHttpResponse response = put(packageId); + assertEquals(HttpServletResponse.SC_NOT_FOUND, response.getStatus()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotInstallPackageWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); + + put("don-t-care"); + } + + private List<HgPackage> createPackages() { + return Arrays.asList(createPackage(), new HgPackage()); + } + + private MockHttpResponse get() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get(URI); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private MockHttpResponse put(String pckgId) throws URISyntaxException { + String packgeIdParam = ""; + if (pckgId != null) { + packgeIdParam = "/" + pckgId; + } + MockHttpRequest request = MockHttpRequest.put(URI + packgeIdParam); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private void setupResources() { + HgConfigPackageResource hgConfigPackageResource = + new HgConfigPackageResource(hgPackageReader, advancedHttpClient, repositoryHandler, mapper); + + when(hgConfigPackageResourceProvider.get()).thenReturn(hgConfigPackageResource); + dispatcher.getRegistry().addSingletonResource( + new HgConfigResource(null, null, null, + hgConfigPackageResourceProvider, null, null)); + } + + private void setupPackageInstallation(String packgeId, String url) throws IOException { + when(hgPackage.getId()).thenReturn(packgeId); + when(hgPackageReader.getPackage(packgeId)).thenReturn(hgPackage); + when(repositoryHandler.getConfig()).thenReturn(new HgConfig()); + when(hgPackage.getHgConfigTemplate()).thenReturn(new HgConfig()); + when(hgPackage.getUrl()).thenReturn(url); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java new file mode 100644 index 0000000000..0b5d7b14d0 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java @@ -0,0 +1,66 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.installer.HgPackage; +import sonia.scm.installer.HgPackages; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.HgConfigTests.assertEqualsPackage; +import static sonia.scm.api.v2.resources.HgConfigTests.createPackage; + +@RunWith(MockitoJUnitRunner.class) +public class HgConfigPackagesToDtoMapperTest { + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @InjectMocks + private HgConfigPackagesToDtoMapperImpl mapper; + + private URI expectedBaseUri; + + @Before + public void init() { + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/packages"); + } + + @Test + public void shouldMapFields() { + HgPackages hgPackages = new HgPackages(); + hgPackages.setPackages(createPackages()); + + HgConfigPackagesDto dto = mapper.map(hgPackages); + + assertThat(dto.getPackages()).hasSize(2); + + HgConfigPackagesDto.HgConfigPackageDto hgPackageDto1 = dto.getPackages().get(0); + assertEqualsPackage(hgPackageDto1); + + HgConfigPackagesDto.HgConfigPackageDto hgPackageDto2 = dto.getPackages().get(1); + // Just verify a random field + assertThat(hgPackageDto2.getId()).isNull(); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + } + + + private List<HgPackage> createPackages() { + return Arrays.asList(createPackage(), new HgPackage()); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java new file mode 100644 index 0000000000..e0253ad86a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java @@ -0,0 +1,168 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class HgConfigResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + + @InjectMocks + private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @InjectMocks + private HgConfigToHgConfigDtoMapperImpl configToDtoMapper; + + @Mock + private HgRepositoryHandler repositoryHandler; + + @Mock + private Provider<HgConfigPackageResource> packagesResource; + + @Mock + private Provider<HgConfigAutoConfigurationResource> autoconfigResource; + + @Mock + private Provider<HgConfigInstallationsResource> installationsResource; + + @Before + public void prepareEnvironment() { + HgConfig gitConfig = createConfiguration(); + when(repositoryHandler.getConfig()).thenReturn(gitConfig); + HgConfigResource gitConfigResource = + new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource, + autoconfigResource, installationsResource); + dispatcher.getRegistry().addSingletonResource(gitConfigResource); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetHgConfig() throws URISyntaxException, IOException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String responseString = response.getContentAsString(); + ObjectNode responseJson = new ObjectMapper().readValue(responseString, ObjectNode.class); + + assertTrue(responseString.contains("\"disabled\":false")); + assertTrue(responseString.contains("\"self\":{\"href\":\"/v2/config/hg")); + assertTrue(responseString.contains("\"update\":{\"href\":\"/v2/config/hg")); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetHgConfigEvenWhenItsEmpty() throws URISyntaxException, UnsupportedEncodingException { + when(repositoryHandler.getConfig()).thenReturn(null); + + MockHttpResponse response = get(); + String responseString = response.getContentAsString(); + + assertTrue(responseString.contains("\"disabled\":false")); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetHgConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertFalse(response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config/hg")); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); + + get(); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldUpdateConfig() throws URISyntaxException { + MockHttpResponse response = put(); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); + + put(); + } + + private MockHttpResponse get() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + HgConfigResource.HG_CONFIG_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private MockHttpResponse put() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.put("/" + HgConfigResource.HG_CONFIG_PATH_V2) + .contentType(HgVndMediaType.CONFIG) + .content("{\"disabled\":true}".getBytes()); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private HgConfig createConfiguration() { + HgConfig config = new HgConfig(); + config.setDisabled(false); + return config; + } + +} + diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java new file mode 100644 index 0000000000..a3430aac43 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java @@ -0,0 +1,65 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.installer.HgPackage; +import sonia.scm.repository.HgConfig; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +class HgConfigTests { + + private HgConfigTests() { + } + + static HgConfig createConfiguration() { + HgConfig config = new HgConfig(); + config.setDisabled(true); + + config.setEncoding("ABC"); + config.setHgBinary("/etc/hg"); + config.setPythonBinary("/py"); + config.setPythonPath("/etc/"); + config.setShowRevisionInId(true); + config.setUseOptimizedBytecode(true); + + return config; + } + + static void assertEqualsConfiguration(HgConfigDto dto) { + assertTrue(dto.isDisabled()); + + assertEquals("ABC", dto.getEncoding()); + assertEquals("/etc/hg", dto.getHgBinary()); + assertEquals("/py", dto.getPythonBinary()); + assertEquals("/etc/", dto.getPythonPath()); + assertTrue(dto.isShowRevisionInId()); + assertTrue(dto.isUseOptimizedBytecode()); + } + + static HgPackage createPackage() { + HgPackage hgPackage= new HgPackage(); + hgPackage.setArch("arch"); + hgPackage.setId("1"); + hgPackage.setHgVersion("2"); + hgPackage.setPlatform("someOs"); + hgPackage.setPythonVersion("3"); + hgPackage.setSize(4); + hgPackage.setUrl("https://package"); + hgPackage.setHgConfigTemplate(createConfiguration()); + return hgPackage; + } + + static void assertEqualsPackage(HgConfigPackagesDto.HgConfigPackageDto dto) { + assertEquals("arch", dto.getArch()); + assertEquals("1", dto.getId()); + assertEquals("2", dto.getHgVersion()); + assertEquals("someOs", dto.getPlatform()); + assertEquals("3", dto.getPythonVersion()); + assertEquals(4, dto.getSize()); + assertEquals("https://package", dto.getUrl()); + + assertEqualsConfiguration(dto.getHgConfigTemplate()); + assertTrue(dto.getHgConfigTemplate().getLinks().isEmpty()); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java new file mode 100644 index 0000000000..d4bc8be549 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java @@ -0,0 +1,78 @@ +package sonia.scm.api.v2.resources; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.HgConfig; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.HgConfigTests.assertEqualsConfiguration; +import static sonia.scm.api.v2.resources.HgConfigTests.createConfiguration; + +@RunWith(MockitoJUnitRunner.class) +public class HgConfigToHgConfigDtoMapperTest { + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @InjectMocks + private HgConfigToHgConfigDtoMapperImpl mapper; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private URI expectedBaseUri; + + @Before + public void init() { + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2); + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @After + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapFields() { + HgConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:hg")).thenReturn(true); + HgConfigDto dto = mapper.map(config); + + assertEqualsConfiguration(dto); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); + } + + @Test + public void shouldMapFieldsWithoutUpdate() { + HgConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:hg")).thenReturn(false); + HgConfigDto dto = mapper.map(config); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertFalse(dto.getLinks().hasLink("update")); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgContextProviderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgContextProviderTest.java new file mode 100644 index 0000000000..de31e2b11e --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgContextProviderTest.java @@ -0,0 +1,87 @@ +package sonia.scm.repository; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.OutOfScopeException; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Scope; +import com.google.inject.servlet.RequestScoped; +import com.google.inject.util.Providers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class HgContextProviderTest { + + @Mock + private Scope scope; + + @Test + void shouldThrowNonOutOfScopeProvisionExceptions() { + Provider<HgContextRequestStore> provider = () -> { + throw new RuntimeException("something different"); + }; + + when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider); + + Injector injector = Guice.createInjector(new HgContextModule(scope)); + + assertThrows(ProvisionException.class, () -> injector.getInstance(HgContext.class)); + } + + @Test + void shouldCreateANewInstanceIfOutOfRequestScope() { + Provider<HgContextRequestStore> provider = () -> { + throw new OutOfScopeException("no request"); + }; + when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider); + + Injector injector = Guice.createInjector(new HgContextModule(scope)); + + HgContext contextOne = injector.getInstance(HgContext.class); + HgContext contextTwo = injector.getInstance(HgContext.class); + + assertThat(contextOne).isNotSameAs(contextTwo); + } + + @Test + void shouldInjectFromRequestScope() { + HgContextRequestStore requestStore = new HgContextRequestStore(); + Provider<HgContextRequestStore> provider = Providers.of(requestStore); + + when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider); + + Injector injector = Guice.createInjector(new HgContextModule(scope)); + + HgContext contextOne = injector.getInstance(HgContext.class); + HgContext contextTwo = injector.getInstance(HgContext.class); + + assertThat(contextOne).isSameAs(contextTwo); + } + + private static class HgContextModule extends AbstractModule { + + private Scope scope; + + private HgContextModule(Scope scope) { + this.scope = scope; + } + + @Override + protected void configure() { + bindScope(RequestScoped.class, scope); + bind(HgContextRequestStore.class); + bind(HgContext.class).toProvider(HgContextProvider.class); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java index c14e5f1b61..c2ed5af42e 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + * <p> * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * <p> * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * <p> * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,78 +24,78 @@ * 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. - * + * <p> * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.io.DefaultFileSystem; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.store.ConfigurationStoreFactory; -import static org.junit.Assert.*; +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ -import java.io.File; -import sonia.scm.store.ConfigurationStoreFactory; - /** - * * @author Sebastian Sdorra */ -public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase -{ +@RunWith(MockitoJUnitRunner.Silent.class) +public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { + + @Mock + private ConfigurationStoreFactory factory; + + @Mock + private com.google.inject.Provider<HgContext> provider; - /** - * Method description - * - * - * @param directory - */ @Override - protected void checkDirectory(File directory) - { + protected void checkDirectory(File directory) { File hgDirectory = new File(directory, ".hg"); assertTrue(hgDirectory.exists()); assertTrue(hgDirectory.isDirectory()); - - File hgrc = new File(hgDirectory, "hgrc"); - - assertTrue(hgrc.exists()); - assertTrue(hgrc.isFile()); - assertTrue(hgrc.length() > 0); } - /** - * Method description - * - * - * @param factory - * @param directory - * - * @return - */ + @Before + public void initFactory() { + when(factory.withType(any())).thenCallRealMethod(); + } + @Override - protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, - File directory) - { - HgRepositoryHandler handler = new HgRepositoryHandler(factory, - new DefaultFileSystem(), - new HgContextProvider()); + protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) { + HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null, null); handler.init(contextProvider); - handler.getConfig().setRepositoryDirectory(directory); - HgTestUtil.checkForSkip(handler); return handler; } + + @Test + public void getDirectory() { + HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null, null); + + HgConfig hgConfig = new HgConfig(); + hgConfig.setHgBinary("hg"); + hgConfig.setPythonBinary("python"); + repositoryHandler.setConfig(hgConfig); + + initRepository(); + File path = repositoryHandler.getDirectory(repository.getId()); + assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath()); + } } -//~--- non-JDK imports -------------------------------------------------------- diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java index 532038ff2d..ee5117b276 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java @@ -36,19 +36,20 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Assume; - import sonia.scm.SCMContext; -import sonia.scm.io.FileSystem; +import sonia.scm.TempDirRepositoryLocationResolver; import sonia.scm.store.InMemoryConfigurationStoreFactory; -import static org.mockito.Mockito.*; +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.nio.file.Path; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ -import java.io.File; - -import javax.servlet.http.HttpServletRequest; - /** * * @author Sebastian Sdorra @@ -95,19 +96,18 @@ public final class HgTestUtil * * @return */ - public static HgRepositoryHandler createHandler(File directory) - { + public static HgRepositoryHandler createHandler(File directory) { TempSCMContextProvider context = (TempSCMContextProvider) SCMContext.getContext(); context.setBaseDirectory(directory); - FileSystem fileSystem = mock(FileSystem.class); + RepositoryDAO repoDao = mock(RepositoryDAO.class); + RepositoryLocationResolver repositoryLocationResolver = new TempDirRepositoryLocationResolver(directory); HgRepositoryHandler handler = - new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), fileSystem, - new HgContextProvider()); - + new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null); + Path repoDir = directory.toPath(); handler.init(context); return handler; @@ -128,6 +128,7 @@ public final class HgTestUtil "http://localhost:8081/scm/hook/hg/"); when(hookManager.createUrl(any(HttpServletRequest.class))).thenReturn( "http://localhost:8081/scm/hook/hg/"); + when(hookManager.getCredentials()).thenReturn(""); return hookManager; } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/TempSCMContextProvider.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/TempSCMContextProvider.java index bc6794e5a5..32cf7b171c 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/TempSCMContextProvider.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/TempSCMContextProvider.java @@ -41,6 +41,7 @@ import sonia.scm.Stage; import java.io.File; import java.io.IOException; +import java.nio.file.Path; /** * @@ -49,32 +50,6 @@ import java.io.IOException; public class TempSCMContextProvider implements SCMContextProvider { - /** - * Method description - * - * - * @throws IOException - */ - @Override - public void close() throws IOException - { - - // do nothing - } - - /** - * Method description - * - */ - @Override - public void init() - { - - // do nothing - } - - //~--- get methods ---------------------------------------------------------- - /** * Method description * @@ -136,6 +111,11 @@ public class TempSCMContextProvider implements SCMContextProvider this.baseDirectory = baseDirectory; } + @Override + public Path resolve(Path path) { + return baseDirectory.toPath().resolve(path); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java index 6d78d0eb3c..dc943d476a 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java @@ -41,7 +41,7 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Changeset; import sonia.scm.repository.Tag; import sonia.scm.repository.spi.HookChangesetProvider; @@ -101,4 +101,4 @@ public class HgHookTagProviderTest { HookChangesetResponse response = new HookChangesetResponse(list); when(changesetProvider.handleRequest(Mockito.any(HookChangesetRequest.class))).thenReturn(response); } -} \ No newline at end of file +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java index 74d1b1f742..fa7371f840 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java @@ -53,7 +53,7 @@ public class HgBranchCommand implements BranchCommand public Branch branch(String name) throws IOException { com.aragost.javahg.commands.BranchCommand.on(repository).set(name); - return new Branch(name, repository.tip().getNode()); + return Branch.normalBranch(name, repository.tip().getNode()); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java index aa611a8ba4..b8967ed15b 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java @@ -32,11 +32,11 @@ package sonia.scm.repository.client.spi; import com.aragost.javahg.Repository; import com.google.common.collect.Lists; -import java.io.IOException; import sonia.scm.repository.Changeset; -import sonia.scm.repository.Modifications; import sonia.scm.repository.Person; +import java.io.IOException; + /** * Mercurial implementation of the {@link CommitCommand}. * @@ -70,9 +70,6 @@ public class HgCommitCommand implements CommitCommand changeset.setBranches(Lists.newArrayList(c.getBranch())); changeset.setTags(c.tags()); - changeset.setModifications( - new Modifications(c.getAddedFiles(), c.getModifiedFiles(), c.getDeletedFiles()) - ); return changeset; } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java index 73e0a1f9a4..263347ca00 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java @@ -32,9 +32,10 @@ package sonia.scm.repository.client.spi; import com.aragost.javahg.Repository; import com.aragost.javahg.commands.ExecutionException; -import java.io.IOException; import sonia.scm.repository.client.api.RepositoryClientException; +import java.io.IOException; + /** * Mercurial implementation of the {@link PushCommand}. * @@ -63,5 +64,10 @@ public class HgPushCommand implements PushCommand throw new RepositoryClientException("push to repository failed", ex); } } - + + @Override + public void pushTags() throws IOException { + push(); + } + } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientProvider.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientProvider.java index 583fe7f488..feb4887173 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientProvider.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientProvider.java @@ -32,10 +32,11 @@ package sonia.scm.repository.client.spi; import com.aragost.javahg.Repository; import com.google.common.collect.ImmutableSet; +import sonia.scm.repository.client.api.ClientCommand; + import java.io.File; import java.io.IOException; import java.util.Set; -import sonia.scm.repository.client.api.ClientCommand; /** * Mercurial implementation of the {@link RepositoryClientProvider}. @@ -47,7 +48,7 @@ public class HgRepositoryClientProvider extends RepositoryClientProvider private static final Set<ClientCommand> SUPPORTED_COMMANDS = ImmutableSet.of( ClientCommand.ADD, ClientCommand.REMOVE, ClientCommand.COMMIT, - ClientCommand.TAG, ClientCommand.BANCH, ClientCommand.PUSH + ClientCommand.TAG, ClientCommand.BRANCH, ClientCommand.PUSH ); private final Repository repository; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java index 3aa448bfca..639adf9a1b 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java @@ -32,7 +32,6 @@ package sonia.scm.repository.client.spi; import com.aragost.javahg.Repository; import com.google.common.base.Strings; -import java.io.IOException; import sonia.scm.repository.Tag; /** @@ -51,13 +50,16 @@ public class HgTagCommand implements TagCommand } @Override - public Tag tag(TagRequest request) throws IOException + public Tag tag(TagRequest request) { String rev = request.getRevision(); if ( Strings.isNullOrEmpty(rev) ){ rev = repository.tip().getNode(); } - com.aragost.javahg.commands.TagCommand.on(repository).rev(rev).execute(request.getName()); + com.aragost.javahg.commands.TagCommand.on(repository) + .rev(rev) + .user(request.getUserName()) + .execute(request.getName()); return new Tag(request.getName(), rev); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java index b5284c737c..d752047ba5 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java @@ -76,8 +76,7 @@ public class AbstractHgCommandTestBase extends ZippedRepositoryTestBase * @throws IOException */ @Before - public void initHgHandler() throws IOException - { + public void initHgHandler() throws IOException { this.handler = HgTestUtil.createHandler(tempFolder.newFolder()); HgTestUtil.checkForSkip(handler); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBlameCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBlameCommandTest.java index 0dea409589..8b46c5f290 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBlameCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBlameCommandTest.java @@ -36,17 +36,16 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; - import sonia.scm.repository.BlameLine; import sonia.scm.repository.BlameResult; -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -54,16 +53,8 @@ import java.io.IOException; public class HgBlameCommandTest extends AbstractHgCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetBlameResult() throws IOException, RepositoryException - { + public void testGetBlameResult() throws IOException { BlameCommandRequest request = new BlameCommandRequest(); request.setPath("a.txt"); @@ -87,17 +78,8 @@ public class HgBlameCommandTest extends AbstractHgCommandTestBase line.getAuthor().getMail()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetBlameResultWithRevision() - throws IOException, RepositoryException - { + public void testGetBlameResultWithRevision() throws IOException { BlameCommandRequest request = new BlameCommandRequest(); request.setPath("a.txt"); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java new file mode 100644 index 0000000000..4a41e469ac --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java @@ -0,0 +1,78 @@ +package sonia.scm.repository.spi; + +import com.aragost.javahg.commands.PullCommand; +import com.google.inject.util.Providers; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.repository.Branch; +import sonia.scm.repository.HgTestUtil; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.api.BranchRequest; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.web.HgRepositoryEnvironmentBuilder; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class HgBranchCommandTest extends AbstractHgCommandTestBase { + + private SimpleHgWorkdirFactory workdirFactory; + + @Before + public void initWorkdirFactory() { + HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder = + new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager()); + + workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder), new WorkdirProvider()) { + @Override + public void configure(PullCommand pullCommand) { + // we do not want to configure http hooks in this unit test + } + }; + } + + @Test + public void shouldCreateBranch() { + BranchRequest branchRequest = new BranchRequest(); + branchRequest.setNewBranch("new_branch"); + + Branch newBranch = new HgBranchCommand(cmdContext, repository, workdirFactory).branch(branchRequest); + + assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); + assertThat(cmdContext.open().changeset(newBranch.getRevision()).getParent1().getBranch()).isEqualTo("default"); + } + + @Test + public void shouldCreateBranchOnSpecificParent() { + BranchRequest branchRequest = new BranchRequest(); + branchRequest.setParentBranch("test-branch"); + branchRequest.setNewBranch("new_branch"); + + Branch newBranch = new HgBranchCommand(cmdContext, repository, workdirFactory).branch(branchRequest); + + assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); + assertThat(cmdContext.open().changeset(newBranch.getRevision()).getParent1().getBranch()).isEqualTo("test-branch"); + } + + @Test + public void shouldCloseBranch() { + String branchToBeClosed = "test-branch"; + + new HgBranchCommand(cmdContext, repository, workdirFactory).deleteOrClose(branchToBeClosed); + assertThat(readBranches()).filteredOn(b -> b.getName().equals(branchToBeClosed)).isEmpty(); + } + + @Test + public void shouldThrowInternalRepositoryException() { + String branchToBeClosed = "default"; + + new HgBranchCommand(cmdContext, repository, workdirFactory).deleteOrClose(branchToBeClosed); + assertThrows(InternalRepositoryException.class, () -> new HgBranchCommand(cmdContext, repository, workdirFactory).deleteOrClose(branchToBeClosed)); + } + + private List<Branch> readBranches() { + return new HgBranchesCommand(cmdContext, repository).getBranches(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index 0262dfc36b..2116d06a7a 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -33,40 +33,40 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import com.aragost.javahg.commands.LogCommand; import org.junit.Test; - import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +import java.util.Collection; -import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * * @author Sebastian Sdorra */ -public class HgBrowseCommandTest extends AbstractHgCommandTestBase -{ +public class HgBrowseCommandTest extends AbstractHgCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testBrowse() throws IOException, RepositoryException - { - List<FileObject> foList = getRootFromTip(new BrowseCommandRequest()); + public void testBrowseWithFilePath() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setPath("a.txt"); + FileObject file = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + assertEquals("a.txt", file.getName()); + assertFalse(file.isDirectory()); + assertTrue(file.getChildren().isEmpty()); + } + + @Test + public void testBrowse() throws IOException { + Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest()); FileObject a = getFileObject(foList, "a.txt"); FileObject c = getFileObject(foList, "c"); @@ -81,16 +81,21 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase assertEquals("c", c.getPath()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testBrowseSubDirectory() throws IOException, RepositoryException - { + public void testBrowseShouldResolveBranchForRevision() throws IOException { + String defaultBranchRevision = new LogCommand(cmdContext.open()).rev("default").single().getNode(); + + BrowseCommandRequest browseCommandRequest = new BrowseCommandRequest(); + browseCommandRequest.setRevision("default"); + + BrowserResult result = new HgBrowseCommand(cmdContext, + repository).getBrowserResult(browseCommandRequest); + + assertThat(result.getRevision()).isEqualTo(defaultBranchRevision); + } + + @Test + public void testBrowseSubDirectory() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setPath("c"); @@ -100,7 +105,9 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase assertNotNull(result); - List<FileObject> foList = result.getFiles(); + FileObject c = result.getFile(); + assertEquals("c", c.getName()); + Collection<FileObject> foList = c.getChildren(); assertNotNull(foList); assertFalse(foList.isEmpty()); @@ -137,21 +144,13 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase checkDate(e.getLastModified()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testDisableLastCommit() throws IOException, RepositoryException - { + public void testDisableLastCommit() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setDisableLastCommit(true); - List<FileObject> foList = getRootFromTip(request); + Collection<FileObject> foList = getRootFromTip(request); FileObject a = getFileObject(foList, "a.txt"); @@ -159,16 +158,8 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase assertNull(a.getLastModified()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testRecursive() throws IOException, RepositoryException - { + public void testRecursive() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setRecursive(true); @@ -178,11 +169,16 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase assertNotNull(result); - List<FileObject> foList = result.getFiles(); + FileObject root = result.getFile(); + Collection<FileObject> foList = root.getChildren(); assertNotNull(foList); assertFalse(foList.isEmpty()); - assertEquals(5, foList.size()); + assertEquals(4, foList.size()); + + FileObject c = getFileObject(foList, "c"); + assertTrue(c.isDirectory()); + assertEquals(2, c.getChildren().size()); } //~--- get methods ---------------------------------------------------------- @@ -196,45 +192,22 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase * * @return */ - private FileObject getFileObject(List<FileObject> foList, String name) + private FileObject getFileObject(Collection<FileObject> foList, String name) { - FileObject a = null; - - for (FileObject f : foList) - { - if (name.equals(f.getName())) - { - a = f; - - break; - } - } - - assertNotNull(a); - - return a; + return foList.stream() + .filter(f -> name.equals(f.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("file " + name + " not found")); } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private List<FileObject> getRootFromTip(BrowseCommandRequest request) - throws IOException, RepositoryException - { + private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws IOException { BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); assertNotNull(result); - List<FileObject> foList = result.getFiles(); + FileObject root = result.getFile(); + Collection<FileObject> foList = root.getChildren(); assertNotNull(foList); assertFalse(foList.isEmpty()); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java index 49ef283c1a..9379fd42a8 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java @@ -33,36 +33,21 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import org.junit.Ignore; import org.junit.Test; - -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.NotFoundException; +import sonia.scm.repository.InternalRepositoryException; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; -/** - * - * @author Sebastian Sdorra - */ -public class HgCatCommandTest extends AbstractHgCommandTestBase -{ +import static org.junit.Assert.assertEquals; + +public class HgCatCommandTest extends AbstractHgCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testCat() throws IOException, RepositoryException - { + public void testCat() throws IOException { CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -70,48 +55,49 @@ public class HgCatCommandTest extends AbstractHgCommandTestBase assertEquals("a", execute(request)); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testSimpleCat() throws IOException, RepositoryException - { + public void testSimpleCat() throws IOException { CatCommandRequest request = new CatCommandRequest(); request.setPath("b.txt"); assertEquals("b", execute(request)); } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private String execute(CatCommandRequest request) - throws IOException, RepositoryException - { - String content = null; + @Test(expected = InternalRepositoryException.class) + public void testUnknownFile() throws IOException { + CatCommandRequest request = new CatCommandRequest(); + + request.setPath("unknown"); + execute(request); + } + + @Test(expected = NotFoundException.class) + @Ignore("detection of unknown revision in hg not yet implemented") + public void testUnknownRevision() throws IOException { + CatCommandRequest request = new CatCommandRequest(); + + request.setRevision("abc"); + request.setPath("a.txt"); + execute(request); + } + + @Test + public void testSimpleStream() throws IOException { + CatCommandRequest request = new CatCommandRequest(); + request.setPath("b.txt"); + + InputStream catResultStream = new HgCatCommand(cmdContext, repository).getCatResultStream(request); + + assertEquals('b', catResultStream.read()); + assertEquals('\n', catResultStream.read()); + assertEquals(-1, catResultStream.read()); + + catResultStream.close(); + } + + private String execute(CatCommandRequest request) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - try - { - new HgCatCommand(cmdContext, repository).getCatResult(request, baos); - } - finally - { - content = baos.toString().trim(); - } - - return content; + new HgCatCommand(cmdContext, repository).getCatResult(request, baos); + return baos.toString().trim(); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgIncomingCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgIncomingCommandTest.java index a8a5de156b..4d7b7c75d9 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgIncomingCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgIncomingCommandTest.java @@ -35,19 +35,18 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.aragost.javahg.Changeset; - import org.junit.Test; - import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.HgTestUtil; -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.InternalRepositoryException; import java.io.IOException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -60,12 +59,9 @@ public class HgIncomingCommandTest extends IncomingOutgoingTestBase * * * @throws IOException - * @throws RepositoryException */ @Test - public void testGetIncomingChangesets() - throws IOException, RepositoryException - { + public void testGetIncomingChangesets() throws IOException { writeNewFile(outgoing, outgoingDirectory, "a.txt", "Content of file a.txt"); writeNewFile(outgoing, outgoingDirectory, "b.txt", "Content of file b.txt"); @@ -89,16 +85,8 @@ public class HgIncomingCommandTest extends IncomingOutgoingTestBase assertChangesetsEqual(c2, cpr.getChangesets().get(1)); } - /** - * Method description - * - * - * @throws RepositoryException - */ @Test - public void testGetIncomingChangesetsWithEmptyRepository() - throws RepositoryException - { + public void testGetIncomingChangesetsWithEmptyRepository() { HgIncomingCommand cmd = createIncomingCommand(); IncomingCommandRequest request = new IncomingCommandRequest(); @@ -110,17 +98,8 @@ public class HgIncomingCommandTest extends IncomingOutgoingTestBase assertEquals(0, cpr.getTotal()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ - @Test(expected = RepositoryException.class) - public void testGetIncomingChangesetsWithUnrelatedRepository() - throws IOException, RepositoryException - { + @Test(expected = InternalRepositoryException.class) + public void testGetIncomingChangesetsWithUnrelatedRepository() throws IOException { writeNewFile(outgoing, outgoingDirectory, "a.txt", "Content of file a.txt"); writeNewFile(outgoing, outgoingDirectory, "b.txt", "Content of file b.txt"); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java index bae48180ce..07e9a0dec3 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java @@ -36,20 +36,21 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; - import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.Modifications; -import sonia.scm.repository.RepositoryException; - -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -57,16 +58,8 @@ import java.io.IOException; public class HgLogCommandTest extends AbstractHgCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAll() throws IOException, RepositoryException - { + public void testGetAll() { ChangesetPagingResult result = createComamnd().getChangesets(new LogCommandRequest()); @@ -75,16 +68,8 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase assertEquals(5, result.getChangesets().size()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllByPath() throws IOException, RepositoryException - { + public void testGetAllByPath() { LogCommandRequest request = new LogCommandRequest(); request.setPath("a.txt"); @@ -102,16 +87,23 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase result.getChangesets().get(2).getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllWithLimit() throws IOException, RepositoryException - { + public void testGetDefaultBranchInfo() { + LogCommandRequest request = new LogCommandRequest(); + + request.setPath("a.txt"); + + ChangesetPagingResult result = createComamnd().getChangesets(request); + + assertNotNull(result); + assertEquals(1, + result.getChangesets().get(0).getBranches().size()); + assertEquals("default", + result.getChangesets().get(0).getBranches().get(0)); + } + + @Test + public void testGetAllWithLimit() { LogCommandRequest request = new LogCommandRequest(); request.setPagingLimit(2); @@ -133,16 +125,8 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase assertEquals("542bf4893dd2ff58a0eb719551d75ddeb919608b", c2.getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllWithPaging() throws IOException, RepositoryException - { + public void testGetAllWithPaging() { LogCommandRequest request = new LogCommandRequest(); request.setPagingStart(1); @@ -165,48 +149,33 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase assertEquals("79b6baf49711ae675568e0698d730b97ef13e84a", c2.getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetCommit() throws IOException, RepositoryException - { + public void testGetCommit() throws IOException { HgLogCommand command = createComamnd(); + String revision = "a9bacaf1b7fa0cebfca71fed4e59ed69a6319427"; Changeset c = - command.getChangeset("a9bacaf1b7fa0cebfca71fed4e59ed69a6319427"); + command.getChangeset(revision, null); assertNotNull(c); - assertEquals("a9bacaf1b7fa0cebfca71fed4e59ed69a6319427", c.getId()); + assertEquals(revision, c.getId()); assertEquals("added a and b files", c.getDescription()); checkDate(c.getDate()); assertEquals("Douglas Adams", c.getAuthor().getName()); assertEquals("douglas.adams@hitchhiker.com", c.getAuthor().getMail()); assertEquals("added a and b files", c.getDescription()); + ModificationsCommand modificationsCommand = new HgModificationsCommand(cmdContext, repository); + Modifications modifications = modificationsCommand.getModifications(revision); - Modifications mods = c.getModifications(); - - assertNotNull(mods); - assertTrue("modified list should be empty", mods.getModified().isEmpty()); - assertTrue("removed list should be empty", mods.getRemoved().isEmpty()); - assertFalse("added list should not be empty", mods.getAdded().isEmpty()); - assertEquals(2, mods.getAdded().size()); - assertThat(mods.getAdded(), contains("a.txt", "b.txt")); + assertNotNull(modifications); + assertTrue("modified list should be empty", modifications.getModified().isEmpty()); + assertTrue("removed list should be empty", modifications.getRemoved().isEmpty()); + assertFalse("added list should not be empty", modifications.getAdded().isEmpty()); + assertEquals(2, modifications.getAdded().size()); + assertThat(modifications.getAdded(), contains("a.txt", "b.txt")); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetRange() throws IOException, RepositoryException - { + public void testGetRange() { LogCommandRequest request = new LogCommandRequest(); request.setStartChangeset("3049df33fdbb"); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java new file mode 100644 index 0000000000..eae8319b89 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java @@ -0,0 +1,111 @@ +package sonia.scm.repository.spi; + +import com.aragost.javahg.Changeset; +import com.aragost.javahg.commands.RemoveCommand; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.repository.HgTestUtil; +import sonia.scm.repository.Modifications; + +import java.io.File; +import java.util.function.Consumer; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class HgModificationsCommandTest extends IncomingOutgoingTestBase { + + + private HgModificationsCommand outgoingModificationsCommand; + + @Before + public void init() { + HgCommandContext outgoingContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, outgoingRepository, outgoingDirectory); + outgoingModificationsCommand = new HgModificationsCommand(outgoingContext, outgoingRepository); + } + + @Test + public void shouldReadAddedFiles() throws Exception { + String fileName = "a.txt"; + writeNewFile(outgoing, outgoingDirectory, fileName, "bal bla"); + Changeset changeset = commit(outgoing, "added a.txt"); + String revision = String.valueOf(changeset.getRevision()); + Consumer<Modifications> assertModifications = assertAddedFile(fileName); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + } + + @Test + public void shouldReadModifiedFiles() throws Exception { + String fileName = "a.txt"; + writeNewFile(outgoing, outgoingDirectory, fileName, "bal bla"); + commit(outgoing, "added a.txt"); + writeNewFile(outgoing, outgoingDirectory, fileName, "new content"); + Changeset changeset = commit(outgoing, "modified a.txt"); + String revision = String.valueOf(changeset.getRevision()); + Consumer<Modifications> assertModifications = assertModifiedFiles(fileName); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + } + + @Test + public void shouldReadRemovedFiles() throws Exception { + String fileName = "a.txt"; + writeNewFile(outgoing, outgoingDirectory, fileName, "bal bla"); + commit(outgoing, "added a.txt"); + File file = new File(outgoingDirectory, fileName); + file.delete(); + RemoveCommand.on(outgoing).execute(file); + Changeset changeset = commit(outgoing, "removed a.txt"); + String revision = String.valueOf(changeset.getRevision()); + Consumer<Modifications> assertModifications = assertRemovedFiles(fileName); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + } + + + Consumer<Modifications> assertRemovedFiles(String fileName) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(0); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(0); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(1) + .containsOnly(fileName); + }; + } + + + Consumer<Modifications> assertModifiedFiles(String file) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(0); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(1) + .containsOnly(file); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(0); + }; + } + + Consumer<Modifications> assertAddedFile(String addedFile) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(1) + .containsOnly(addedFile); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(0); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(0); + }; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java new file mode 100644 index 0000000000..862d2ab2ed --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java @@ -0,0 +1,164 @@ +package sonia.scm.repository.spi; + +import com.google.inject.util.Providers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.AlreadyExistsException; +import sonia.scm.NoChangesMadeException; +import sonia.scm.NotFoundException; +import sonia.scm.repository.HgHookManager; +import sonia.scm.repository.HgTestUtil; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.web.HgRepositoryEnvironmentBuilder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HgModifyCommandTest extends AbstractHgCommandTestBase { + + private HgModifyCommand hgModifyCommand; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void initHgModifyCommand() { + HgHookManager hookManager = HgTestUtil.createHookManager(); + HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager); + SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(environmentBuilder), new WorkdirProvider()) { + @Override + public void configure(com.aragost.javahg.commands.PullCommand pullCommand) { + // we do not want to configure http hooks in this unit test + } + }; + hgModifyCommand = new HgModifyCommand(cmdContext, workdirFactory + ); + } + + @Test + public void shouldRemoveFiles() { + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt")); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + String result = hgModifyCommand.execute(request); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(result); + } + + @Test + public void shouldCreateFilesWithoutOverwrite() throws IOException { + File testFile = temporaryFolder.newFile(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setCommitMessage("I found the answer"); + request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + String changeSet = hgModifyCommand.execute(request); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet); + assertThat(cmdContext.open().tip().getAddedFiles().size()).isEqualTo(1); + } + + @Test + public void shouldOverwriteExistingFiles() throws IOException { + File testFile = temporaryFolder.newFile(); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", testFile, true)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + String changeSet2 = hgModifyCommand.execute(request2); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet2); + assertThat(cmdContext.open().tip().getModifiedFiles().size()).isEqualTo(1); + assertThat(cmdContext.open().tip().getModifiedFiles().get(0)).isEqualTo("a.txt"); + } + + @Test(expected = AlreadyExistsException.class) + public void shouldThrowFileAlreadyExistsException() throws IOException { + + File testFile = temporaryFolder.newFile(); + new FileOutputStream(testFile).write(21); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setCommitMessage("I found the answer"); + request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + hgModifyCommand.execute(request); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + hgModifyCommand.execute(request2); + } + + @Test + public void shouldModifyExistingFile() throws IOException { + File testFile = temporaryFolder.newFile("a.txt"); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.ModifyFileRequest("a.txt", testFile)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + String changeSet2 = hgModifyCommand.execute(request2); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet2); + assertThat(cmdContext.open().tip().getModifiedFiles().size()).isEqualTo(1); + assertThat(cmdContext.open().tip().getModifiedFiles().get(0)).isEqualTo(testFile.getName()); + } + + @Test(expected = NotFoundException.class) + public void shouldThrowNotFoundExceptionIfFileDoesNotExist() throws IOException { + File testFile = temporaryFolder.newFile("Answer.txt"); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.ModifyFileRequest("Answer.txt", testFile)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + hgModifyCommand.execute(request2); + } + + @Test(expected = NullPointerException.class) + public void shouldThrowNPEIfAuthorIsMissing() throws IOException { + File testFile = temporaryFolder.newFile(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setCommitMessage("I found the answer"); + hgModifyCommand.execute(request); + } + + @Test(expected = NullPointerException.class) + public void shouldThrowNPEIfCommitMessageIsMissing() throws IOException { + File testFile = temporaryFolder.newFile(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + hgModifyCommand.execute(request); + } + + @Test(expected = NoChangesMadeException.class) + public void shouldThrowNoChangesMadeExceptionIfEmptyLocalChangesetAfterRequest() { + hgModifyCommand.execute(new ModifyCommandRequest()); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgOutgoingCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgOutgoingCommandTest.java index 7a43df98bc..354481b9b3 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgOutgoingCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgOutgoingCommandTest.java @@ -35,20 +35,18 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.aragost.javahg.Changeset; - import org.junit.Test; - import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.HgTestUtil; -import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.InternalRepositoryException; + +import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; //~--- JDK imports ------------------------------------------------------------ -import java.io.IOException; - /** * * @author Sebastian Sdorra @@ -56,17 +54,8 @@ import java.io.IOException; public class HgOutgoingCommandTest extends IncomingOutgoingTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetOutgoingChangesets() - throws IOException, RepositoryException - { + public void testGetOutgoingChangesets() throws IOException { writeNewFile(outgoing, outgoingDirectory, "a.txt", "Content of file a.txt"); writeNewFile(outgoing, outgoingDirectory, "b.txt", "Content of file b.txt"); @@ -90,16 +79,8 @@ public class HgOutgoingCommandTest extends IncomingOutgoingTestBase assertChangesetsEqual(c2, cpr.getChangesets().get(1)); } - /** - * Method description - * - * - * @throws RepositoryException - */ @Test - public void testGetOutgoingChangesetsWithEmptyRepository() - throws RepositoryException - { + public void testGetOutgoingChangesetsWithEmptyRepository() { HgOutgoingCommand cmd = createOutgoingCommand(); OutgoingCommandRequest request = new OutgoingCommandRequest(); @@ -111,17 +92,8 @@ public class HgOutgoingCommandTest extends IncomingOutgoingTestBase assertEquals(0, cpr.getTotal()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ - @Test(expected = RepositoryException.class) - public void testGetOutgoingChangesetsWithUnrelatedRepository() - throws IOException, RepositoryException - { + @Test(expected = InternalRepositoryException.class) + public void testGetOutgoingChangesetsWithUnrelatedRepository() throws IOException { writeNewFile(outgoing, outgoingDirectory, "a.txt", "Content of file a.txt"); writeNewFile(outgoing, outgoingDirectory, "b.txt", "Content of file b.txt"); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java index c45d0af2da..6c654b0e54 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java @@ -41,32 +41,30 @@ import com.aragost.javahg.Repository; import com.aragost.javahg.RepositoryConfiguration; import com.aragost.javahg.commands.AddCommand; import com.aragost.javahg.commands.CommitCommand; - import com.google.common.base.Charsets; import com.google.common.io.Files; - import org.junit.Before; import org.junit.Rule; import org.junit.rules.TemporaryFolder; - import sonia.scm.AbstractTestBase; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgContext; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgTestUtil; +import sonia.scm.repository.RepositoryPathNotFoundException; import sonia.scm.user.User; import sonia.scm.user.UserTestData; import sonia.scm.util.MockUtil; -import static org.junit.Assert.*; - -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -81,8 +79,7 @@ public abstract class IncomingOutgoingTestBase extends AbstractTestBase * @throws IOException */ @Before - public void initHgHandler() throws IOException - { + public void initHgHandler() throws IOException, RepositoryPathNotFoundException { HgRepositoryHandler temp = HgTestUtil.createHandler(tempFolder.newFolder()); HgTestUtil.checkForSkip(temp); @@ -90,18 +87,16 @@ public abstract class IncomingOutgoingTestBase extends AbstractTestBase incomingDirectory = tempFolder.newFolder("incoming"); outgoingDirectory = tempFolder.newFolder("outgoing"); - incomingRepository = new sonia.scm.repository.Repository("1", "hg", - "incoming"); - outgoingRepository = new sonia.scm.repository.Repository("2", "hg", - "outgoing"); + incomingRepository = new sonia.scm.repository.Repository("1", "hg", "space", "incoming"); + outgoingRepository = new sonia.scm.repository.Repository("2", "hg", "space", "outgoing"); incoming = Repository.create(createConfig(temp), incomingDirectory); outgoing = Repository.create(createConfig(temp), outgoingDirectory); handler = mock(HgRepositoryHandler.class); - when(handler.getDirectory(incomingRepository)).thenReturn( + when(handler.getDirectory(incomingRepository.getId())).thenReturn( incomingDirectory); - when(handler.getDirectory(outgoingRepository)).thenReturn( + when(handler.getDirectory(outgoingRepository.getId())).thenReturn( outgoingDirectory); when(handler.getConfig()).thenReturn(temp.getConfig()); when(handler.getHgContext()).thenReturn(new HgContext()); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java new file mode 100644 index 0000000000..efe9983951 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java @@ -0,0 +1,36 @@ +package sonia.scm.web; + +import org.junit.Test; +import sonia.scm.repository.HgRepositoryHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static sonia.scm.web.HgHookCallbackServlet.PARAM_REPOSITORYID; + +public class HgHookCallbackServletTest { + + @Test + public void shouldExtractCorrectRepositoryId() throws ServletException, IOException { + HgRepositoryHandler handler = mock(HgRepositoryHandler.class); + HgHookCallbackServlet servlet = new HgHookCallbackServlet(null, handler, null, null); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + when(request.getContextPath()).thenReturn("http://example.com/scm"); + when(request.getRequestURI()).thenReturn("http://example.com/scm/hook/hg/pretxnchangegroup"); + String path = "/tmp/hg/12345"; + when(request.getParameter(PARAM_REPOSITORYID)).thenReturn(path); + + servlet.doPost(request, response); + + verify(response, never()).sendError(anyInt()); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java index 01e01cf302..f9bc77bbda 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java @@ -31,21 +31,30 @@ package sonia.scm.web; -import javax.servlet.http.HttpServletRequest; +import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; - import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import static org.mockito.Mockito.*; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.RepositoryProvider; +import javax.servlet.http.HttpServletRequest; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; +import static sonia.scm.web.WireProtocolRequestMockFactory.CMDS_HEADS_KNOWN_NODES; +import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.BOOKMARKS; +import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.PHASES; + /** * Unit tests for {@link HgPermissionFilter}. - * + * * @author Sebastian Sdorra */ @RunWith(MockitoJUnitRunner.class) @@ -56,13 +65,37 @@ public class HgPermissionFilterTest { @Mock private ScmConfiguration configuration; - + @Mock private RepositoryProvider repositoryProvider; - + + @Mock + private HgRepositoryHandler hgRepositoryHandler; + + private WireProtocolRequestMockFactory wireProtocol = new WireProtocolRequestMockFactory("/scm/hg/repo"); + @InjectMocks private HgPermissionFilter filter; - + + @Before + public void setUp() { + when(hgRepositoryHandler.getConfig()).thenReturn(new HgConfig()); + } + + /** + * Tests {@link HgPermissionFilter#wrapRequestIfRequired(HttpServletRequest)}. + */ + @Test + public void testWrapRequestIfRequired() { + assertSame(request, filter.wrapRequestIfRequired(request)); + + HgConfig hgConfig = new HgConfig(); + hgConfig.setEnableHttpPostArgs(true); + when(hgRepositoryHandler.getConfig()).thenReturn(hgConfig); + + assertThat(filter.wrapRequestIfRequired(request), is(instanceOf(HgServletRequest.class))); + } + /** * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)}. */ @@ -73,7 +106,7 @@ public class HgPermissionFilterTest { assertFalse(isWriteRequest("HEAD")); assertFalse(isWriteRequest("TRACE")); assertFalse(isWriteRequest("OPTIONS")); - + // write methods assertTrue(isWriteRequest("POST")); assertTrue(isWriteRequest("PUT")); @@ -81,8 +114,121 @@ public class HgPermissionFilterTest { assertTrue(isWriteRequest("KA")); } + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with enabled httppostargs option. + */ + @Test + public void testIsWriteRequestWithEnabledHttpPostArgs() { + HgConfig config = new HgConfig(); + config.setEnableHttpPostArgs(true); + when(hgRepositoryHandler.getConfig()).thenReturn(config); + + assertFalse(isWriteRequest("POST")); + assertFalse(isWriteRequest("POST", "heads")); + assertTrue(isWriteRequest("POST", "unbundle")); + } + private boolean isWriteRequest(String method) { + return isWriteRequest(method, "capabilities"); + } + + private boolean isWriteRequest(String method, String command) { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getQueryString()).thenReturn("cmd=" + command); when(request.getMethod()).thenReturn(method); return filter.isWriteRequest(request); } -} \ No newline at end of file + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * fresh clone of a repository. + */ + @Test + public void testIsWriteRequestWithClone() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES)); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + } + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * push of a single changeset. + */ + @Test + public void testIsWriteRequestWithSingleChangesetPush() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("c0ceccb3b2f0f5c977ff32b9337519e5f37942c2"))); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsWriteRequest(wireProtocol.unbundle(261L, "686173686564+6768033e216468247bd031a0a2d9876d79818f8f")); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsWriteRequest(wireProtocol.pushkey("c0ceccb3b2f0f5c977ff32b9337519e5f37942c2&namespace=phases&new=0&old=1")); + } + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * push to a single changeset. + */ + @Test + public void testIsWriteRequestWithMultipleChangesetsPush() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("ef5993bb4abb32a0565c347844c6d939fc4f4b98"))); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsReadRequest(wireProtocol.branchmap()); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsWriteRequest(wireProtocol.unbundle(746L, "686173686564+95373ca7cd5371cb6c49bb755ee451d9ec585845")); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsWriteRequest(wireProtocol.pushkey("ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1")); + } + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * push of multiple branches to a new repository. + */ + @Test + public void testIsWriteRequestWithMutlipleBranchesToNewRepositoryPush() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("ef5993bb4abb32a0565c347844c6d939fc4f4b98"))); + assertIsReadRequest(wireProtocol.known("c0ceccb3b2f0f5c977ff32b9337519e5f37942c2+187ddf37e237c370514487a0bb1a226f11a780b3+b5914611f84eae14543684b2721eec88b0edac12+8b63a323606f10c86b30465570c2574eb7a3a989")); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsWriteRequest(wireProtocol.unbundle(913L, "686173686564+6768033e216468247bd031a0a2d9876d79818f8f")); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsWriteRequest(wireProtocol.pushkey("ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1")); + } + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a + * push of a bookmark. + */ + @Test + public void testIsWriteRequestWithBookmarkPush() { + assertIsReadRequest(wireProtocol.capabilities()); + assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("ef5993bb4abb32a0565c347844c6d939fc4f4b98"))); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS)); + assertIsReadRequest(wireProtocol.listkeys(PHASES)); + assertIsWriteRequest(wireProtocol.pushkey("markone&namespace=bookmarks&new=ef5993bb4abb32a0565c347844c6d939fc4f4b98&old=")); + } + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a write request hidden in a batch GET + * request. + * + * @see <a href="https://goo.gl/poascp">Issue #970</a> + */ + @Test + public void testIsWriteRequestWithBookmarkPushInABatch() { + assertIsWriteRequest(wireProtocol.batch("pushkey key=markthree,namespace=bookmarks,new=187ddf37e237c370514487a0bb1a226f11a780b3,old=")); + } + + private void assertIsReadRequest(HttpServletRequest request) { + assertFalse(filter.isWriteRequest(request)); + } + + private void assertIsWriteRequest(HttpServletRequest request) { + assertTrue(filter.isWriteRequest(request)); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java new file mode 100644 index 0000000000..51b0a050fc --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java @@ -0,0 +1,50 @@ +package sonia.scm.web; + +import com.google.common.base.Charsets; +import com.google.common.io.ByteStreams; +import org.junit.Test; + +import javax.servlet.ServletInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class HgServletInputStreamTest { + + @Test + public void testReadAndCapture() throws IOException { + SampleServletInputStream original = new SampleServletInputStream("trillian.mcmillian@hitchhiker.com"); + HgServletInputStream hgServletInputStream = new HgServletInputStream(original); + + byte[] prefix = hgServletInputStream.readAndCapture(8); + assertEquals("trillian", new String(prefix, Charsets.US_ASCII)); + + byte[] wholeBytes = ByteStreams.toByteArray(hgServletInputStream); + assertEquals("trillian.mcmillian@hitchhiker.com", new String(wholeBytes, Charsets.US_ASCII)); + } + + @Test(expected = IllegalStateException.class) + public void testReadAndCaptureCalledTwice() throws IOException { + SampleServletInputStream original = new SampleServletInputStream("trillian.mcmillian@hitchhiker.com"); + HgServletInputStream hgServletInputStream = new HgServletInputStream(original); + + hgServletInputStream.readAndCapture(1); + hgServletInputStream.readAndCapture(1); + } + + private static class SampleServletInputStream extends ServletInputStream { + + private ByteArrayInputStream input; + + private SampleServletInputStream(String data) { + input = new ByteArrayInputStream(data.getBytes()); + } + + @Override + public int read() { + return input.read(); + } + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java new file mode 100644 index 0000000000..d1f5124b3a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java @@ -0,0 +1,114 @@ +package sonia.scm.web; + +import com.google.common.collect.Lists; + +import javax.servlet.http.HttpServletRequest; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import static org.mockito.Mockito.*; + +public class WireProtocolRequestMockFactory { + + public enum Namespace { + PHASES, BOOKMARKS; + } + + public static final String CMDS_HEADS_KNOWN_NODES = "heads+%3Bknown+nodes%3D"; + + private String repositoryPath; + + public WireProtocolRequestMockFactory(String repositoryPath) { + this.repositoryPath = repositoryPath; + } + + public HttpServletRequest capabilities() { + return base("GET", "cmd=capabilities"); + } + + public HttpServletRequest listkeys(Namespace namespace) { + HttpServletRequest request = base("GET", "cmd=capabilities"); + header(request, "vary", "X-HgArg-1"); + header(request, "x-hgarg-1", namespaceValue(namespace)); + return request; + } + + public HttpServletRequest branchmap() { + return base("GET", "cmd=branchmap"); + } + + public HttpServletRequest batch(String... args) { + HttpServletRequest request = base("GET", "cmd=batch"); + args(request, "cmds", args); + return request; + } + + public HttpServletRequest unbundle(long contentLength, String... heads) { + HttpServletRequest request = base("POST", "cmd=unbundle"); + header(request, "Content-Length", String.valueOf(contentLength)); + args(request, "heads", heads); + return request; + } + + public HttpServletRequest pushkey(String... keys) { + HttpServletRequest request = base("POST", "cmd=pushkey"); + args(request, "key", keys); + return request; + } + + public HttpServletRequest known(String... nodes) { + HttpServletRequest request = base("GET", "cmd=known"); + args(request, "nodes", nodes); + return request; + } + + private void args(HttpServletRequest request, String prefix, String[] values) { + List<String> headers = Lists.newArrayList(); + + StringBuilder vary = new StringBuilder(); + for ( int i=0; i<values.length; i++ ) { + String header = "X-HgArg-" + (i+1); + + if (i>0) { + vary.append(","); + } + + vary.append(header); + headers.add(header); + + header(request, header, prefix + "=" + values[i]); + } + header(request, "Vary", vary.toString()); + + when(request.getHeaderNames()).thenReturn(Collections.enumeration(headers)); + } + + private HttpServletRequest base(String method, String queryStringValue) { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getRequestURI()).thenReturn(repositoryPath); + when(request.getMethod()).thenReturn(method); + + queryString(request, queryStringValue); + + header(request, "Accept", "application/mercurial-0.1"); + header(request, "Accept-Encoding", "identity"); + header(request, "User-Agent", "mercurial/proto-1.0 (Mercurial 4.3.1)"); + return request; + } + + private void queryString(HttpServletRequest request, String queryString) { + when(request.getQueryString()).thenReturn(queryString); + } + + private void header(HttpServletRequest request, String header, String value) { + when(request.getHeader(header)).thenReturn(value); + } + + private String namespaceValue(Namespace namespace) { + return "namespace=" + namespace.toString().toLowerCase(Locale.ENGLISH); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java new file mode 100644 index 0000000000..9237127c88 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2018, 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; + +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link WireProtocol}. + */ +@RunWith(MockitoJUnitRunner.class) +public class WireProtocolTest { + + @Mock + private HttpServletRequest request; + + @Test + public void testIsWriteRequestOnPost() { + assertIsWriteRequest("capabilities", "unbundle"); + } + + @Test + public void testIsWriteRequest() { + assertIsWriteRequest("unbundle"); + assertIsWriteRequest("capabilities", "unbundle"); + assertIsWriteRequest("capabilities", "postkeys"); + assertIsReadRequest(); + assertIsReadRequest("capabilities"); + assertIsReadRequest("capabilities", "branches", "branchmap"); + } + + private void assertIsWriteRequest(String... commands) { + List<String> cmdList = Lists.newArrayList(commands); + assertTrue(WireProtocol.isWriteRequest(cmdList)); + } + + private void assertIsReadRequest(String... commands) { + List<String> cmdList = Lists.newArrayList(commands); + assertFalse(WireProtocol.isWriteRequest(cmdList)); + } + + @Test + public void testGetCommandsOf() { + expectQueryCommand("capabilities", "cmd=capabilities"); + expectQueryCommand("unbundle", "cmd=unbundle"); + expectQueryCommand("unbundle", "prefix=stuff&cmd=unbundle"); + expectQueryCommand("unbundle", "cmd=unbundle&suffix=stuff"); + expectQueryCommand("unbundle", "prefix=stuff&cmd=unbundle&suffix=stuff"); + expectQueryCommand("unbundle", "bool=&cmd=unbundle"); + expectQueryCommand("unbundle", "bool&cmd=unbundle"); + expectQueryCommand("unbundle", "prefix=stu==ff&cmd=unbundle"); + } + + @Test + public void testGetCommandsOfWithHgArgsPost() throws IOException { + when(request.getMethod()).thenReturn("POST"); + when(request.getQueryString()).thenReturn("cmd=batch"); + when(request.getIntHeader("X-HgArgs-Post")).thenReturn(29); + when(request.getHeaderNames()).thenReturn(Collections.enumeration(Lists.newArrayList("X-HgArgs-Post"))); + when(request.getInputStream()).thenReturn(new BufferedServletInputStream("cmds=lheads+%3Bknown+nodes%3D")); + + List<String> commands = WireProtocol.commandsOf(new HgServletRequest(request)); + assertThat(commands, contains("batch", "lheads", "known")); + } + + @Test + public void testGetCommandsOfWithBatch() { + prepareBatch("cmds=heads ;known nodes,ef5993bb4abb32a0565c347844c6d939fc4f4b98"); + List<String> commands = WireProtocol.commandsOf(request); + assertThat(commands, contains("batch", "heads", "known")); + } + + @Test + public void testGetCommandsOfWithBatchEncoded() { + prepareBatch("cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98"); + List<String> commands = WireProtocol.commandsOf(request); + assertThat(commands, contains("batch", "heads", "known")); + } + + @Test + public void testGetCommandsOfWithBatchAndMutlipleLines() { + prepareBatch( + "cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98", + "cmds=unbundle; postkeys", + "cmds= branchmap p1=r2,p2=r4; listkeys" + ); + List<String> commands = WireProtocol.commandsOf(request); + assertThat(commands, contains("batch", "heads", "known", "unbundle", "postkeys", "branchmap", "listkeys")); + } + + private void prepareBatch(String... args) { + when(request.getQueryString()).thenReturn("cmd=batch"); + List<String> headers = Lists.newArrayList(); + for (int i=0; i<args.length; i++) { + String header = "X-HgArg-" + (i+1); + headers.add(header); + when(request.getHeader(header)).thenReturn(args[i]); + } + when(request.getHeaderNames()).thenReturn(Collections.enumeration(headers)); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetCommandsOfWithMultipleCommandsInQueryString() { + when(request.getQueryString()).thenReturn("cmd=abc&cmd=def"); + WireProtocol.commandsOf(request); + } + + @Test + public void testGetCommandsOfWithoutCmdInQueryString() { + when(request.getQueryString()).thenReturn("abc=def&123=456"); + assertTrue(WireProtocol.commandsOf(request).isEmpty()); + } + + @Test + public void testGetCommandsOfWithEmptyQueryString() { + when(request.getQueryString()).thenReturn(""); + assertTrue(WireProtocol.commandsOf(request).isEmpty()); + } + + @Test + public void testGetCommandsOfWithNullQueryString() { + assertTrue(WireProtocol.commandsOf(request).isEmpty()); + } + + private void expectQueryCommand(String expected, String queryString) { + when(request.getQueryString()).thenReturn(queryString); + List<String> commands = WireProtocol.commandsOf(request); + assertEquals(1, commands.size()); + assertTrue(commands.contains(expected)); + } + + private static class BufferedServletInputStream extends ServletInputStream { + + private ByteArrayInputStream input; + + BufferedServletInputStream(String content) { + this.input = new ByteArrayInputStream(content.getBytes(Charsets.US_ASCII)); + } + + @Override + public int read() { + return input.read(); + } + + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini new file mode 100644 index 0000000000..d8083a04c9 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -0,0 +1,11 @@ +[users] +readOnly = secret, reader +writeOnly = secret, writer +readWrite = secret, readerWriter +admin = secret, admin + +[roles] +reader = configuration:read:hg +writer = configuration:write:hg +readerWriter = configuration:*:hg +admin = * diff --git a/scm-plugins/scm-hg-plugin/tsconfig.json b/scm-plugins/scm-hg-plugin/tsconfig.json new file mode 100644 index 0000000000..7cfe32efe6 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@scm-manager/tsconfig", + "include": [ + "./src/main/js" + ] +} diff --git a/scm-plugins/scm-legacy-plugin/package.json b/scm-plugins/scm-legacy-plugin/package.json new file mode 100644 index 0000000000..2e8ed49862 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/package.json @@ -0,0 +1,24 @@ +{ + "name": "@scm-manager/scm-legacy-plugin", + "private": true, + "version": "2.0.0-SNAPSHOT", + "license": "BSD-3-Clause", + "main": "./src/main/js/index.tsx", + "scripts": { + "build": "ui-scripts plugin", + "watch": "ui-scripts plugin-watch", + "typecheck": "tsc" + }, + "babel": { + "presets": [ + "@scm-manager/babel-preset" + ] + }, + "jest": { + "preset": "@scm-manager/jest-preset" + }, + "prettier": "@scm-manager/prettier-config", + "dependencies": { + "@scm-manager/ui-plugins": "^2.0.0-SNAPSHOT" + } +} diff --git a/scm-plugins/scm-legacy-plugin/pom.xml b/scm-plugins/scm-legacy-plugin/pom.xml index 2243e24258..1a12234014 100644 --- a/scm-plugins/scm-legacy-plugin/pom.xml +++ b/scm-plugins/scm-legacy-plugin/pom.xml @@ -6,7 +6,44 @@ <artifactId>scm-plugins</artifactId> <version>2.0.0-SNAPSHOT</version> </parent> + <artifactId>scm-legacy-plugin</artifactId> + <description>Support migrated repository urls and v1 passwords</description> <version>2.0.0-SNAPSHOT</version> <packaging>smp</packaging> + + <dependencies> + + <!-- servlet api --> + + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <version>${servlet.version}</version> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>javax.ws.rs</groupId> + <artifactId>jsr311-api</artifactId> + <version>1.1.1</version> + <scope>compile</scope> + </dependency> + + </dependencies> + + <build> + <plugins> + + <plugin> + <groupId>sonia.scm.maven</groupId> + <artifactId>smp-maven-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <corePlugin>true</corePlugin> + </configuration> + </plugin> + + </plugins> + </build> </project> diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java new file mode 100644 index 0000000000..0914f92e25 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java @@ -0,0 +1,38 @@ +package sonia.scm.legacy; + +import com.google.inject.Inject; +import sonia.scm.api.v2.resources.Enrich; +import sonia.scm.api.v2.resources.HalAppender; +import sonia.scm.api.v2.resources.HalEnricher; +import sonia.scm.api.v2.resources.HalEnricherContext; +import sonia.scm.api.v2.resources.Index; +import sonia.scm.api.v2.resources.LinkBuilder; +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.plugin.Extension; + +import javax.inject.Provider; + +@Extension +@Enrich(Index.class) +public class LegacyIndexHalEnricher implements HalEnricher { + + private Provider<ScmPathInfoStore> scmPathInfoStoreProvider; + + @Inject + public LegacyIndexHalEnricher(Provider<ScmPathInfoStore> scmPathInfoStoreProvider) { + this.scmPathInfoStoreProvider = scmPathInfoStoreProvider; + } + + private String createLink() { + return new LinkBuilder(scmPathInfoStoreProvider.get().get(), LegacyRepositoryService.class) + .method("getNameAndNamespaceForRepositoryId") + .parameters("REPOID") + .href() + .replace("REPOID", "{id}"); + } + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + appender.appendLink("nameAndNamespace", createLink()); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java new file mode 100644 index 0000000000..e8ec437248 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java @@ -0,0 +1,13 @@ +package sonia.scm.legacy; + +import com.google.inject.servlet.ServletModule; +import sonia.scm.plugin.Extension; + +@Extension +public class LegacyModule extends ServletModule { + + @Override + protected void configureServlets() { + filter("/*").through(RepositoryLegacyProtocolRedirectFilter.class); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java new file mode 100644 index 0000000000..ca6dea0898 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java @@ -0,0 +1,22 @@ +package sonia.scm.legacy; + +import sonia.scm.Priority; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.filter.Filters; +import sonia.scm.filter.WebElement; +import sonia.scm.web.UserAgentParser; +import sonia.scm.web.WebTokenGenerator; +import sonia.scm.web.filter.HttpProtocolServletAuthenticationFilterBase; + +import javax.inject.Inject; +import java.util.Set; + +@Priority(Filters.PRIORITY_AUTHENTICATION) +@WebElement(value = "/git/*", morePatterns = {"/hg/*", "/svn/*"}) +public class LegacyProtocolServletAuthenticationFilter extends HttpProtocolServletAuthenticationFilterBase { + + @Inject + public LegacyProtocolServletAuthenticationFilter(ScmConfiguration configuration, Set<WebTokenGenerator> tokenGenerators, UserAgentParser userAgentParser) { + super(configuration, tokenGenerators, userAgentParser); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java new file mode 100644 index 0000000000..d6b923a927 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java @@ -0,0 +1,43 @@ +package sonia.scm.legacy; + +import com.google.inject.Inject; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import sonia.scm.NotFoundException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("v2/legacy/repository") +public class LegacyRepositoryService { + + private RepositoryManager repositoryManager; + + @Inject + public LegacyRepositoryService(RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + } + + @GET + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository:read:global\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public NamespaceAndNameDto getNameAndNamespaceForRepositoryId(@PathParam("id") String repositoryId) { + Repository repo = repositoryManager.get(repositoryId); + if (repo == null) { + throw new NotFoundException(Repository.class, repositoryId); + } + return new NamespaceAndNameDto(repo.getName(), repo.getNamespace()); + } +} + diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java new file mode 100644 index 0000000000..2f7aae2dae --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java @@ -0,0 +1,11 @@ +package sonia.scm.legacy; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class NamespaceAndNameDto { + private String name; + private String namespace; +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java new file mode 100644 index 0000000000..6319af04f6 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java @@ -0,0 +1,180 @@ +package sonia.scm.legacy; + +import sonia.scm.Priority; +import sonia.scm.filter.Filters; +import sonia.scm.migration.MigrationDAO; +import sonia.scm.migration.MigrationInfo; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.web.filter.HttpFilter; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.util.Arrays.asList; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.apache.commons.lang.StringUtils.isEmpty; + +@Priority(Filters.PRIORITY_BASEURL) +@Singleton +public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { + + private final ProtocolBasedLegacyRepositoryInfo info; + private final RepositoryDAO repositoryDao; + + @Inject + public RepositoryLegacyProtocolRedirectFilter(MigrationDAO migrationDAO, RepositoryDAO repositoryDao) { + this.info = load(migrationDAO); + this.repositoryDao = repositoryDao; + } + + private static ProtocolBasedLegacyRepositoryInfo load(MigrationDAO migrationDAO) { + return new ProtocolBasedLegacyRepositoryInfo(migrationDAO.getAll()); + } + + @Override + protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + new Worker(request, response, chain).doFilter(); + } + + private class Worker { + private final HttpServletRequest request; + private final HttpServletResponse response; + private final FilterChain chain; + + private Worker(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { + this.request = request; + this.response = response; + this.chain = chain; + } + + void doFilter() throws IOException, ServletException { + String servletPath = getServletPathWithoutLeadingSlash(); + String[] pathElements = servletPath.split("/"); + if (pathElements.length > 0) { + checkPathElements(servletPath, pathElements); + } else { + noRedirect(); + } + } + + private void checkPathElements(String servletPath, String[] pathElements) throws IOException, ServletException { + Optional<MigrationInfo> migrationInfo = info.findRepository(asList(pathElements)); + if (migrationInfo.isPresent()) { + doRedirect(servletPath, migrationInfo.get()); + } else { + noRedirect(); + } + } + + private void doRedirect(String servletPath, MigrationInfo migrationInfo) throws IOException, ServletException { + if (repositoryDao.get(migrationInfo.getId()) == null) { + noRedirect(); + } else { + String furtherPath = servletPath.substring(migrationInfo.getProtocol().length() + 1 + migrationInfo.getOriginalRepositoryName().length()); + String queryString = request.getQueryString(); + if (isEmpty(queryString)) { + redirectWithoutQueryParameters(migrationInfo, furtherPath); + } else { + redirectWithQueryParameters(migrationInfo, furtherPath, queryString); + } + } + } + + private void redirectWithoutQueryParameters(MigrationInfo migrationInfo, String furtherPath) throws IOException { + response.sendRedirect(String.format("%s/repo/%s/%s%s", request.getContextPath(), migrationInfo.getNamespace(), migrationInfo.getName(), furtherPath)); + } + + private void redirectWithQueryParameters(MigrationInfo migrationInfo, String furtherPath, String queryString) throws IOException { + response.sendRedirect(String.format("%s/repo/%s/%s%s?%s", request.getContextPath(), migrationInfo.getNamespace(), migrationInfo.getName(), furtherPath, queryString)); + } + + private void noRedirect() throws IOException, ServletException { + chain.doFilter(request, response); + } + + private String getServletPathWithoutLeadingSlash() { + String servletPath = request.getServletPath(); + if (servletPath.startsWith("/")) { + return servletPath.substring(1); + } else { + return servletPath; + } + } + } + + private static class ProtocolBasedLegacyRepositoryInfo { + + private final Map<String, LegacyRepositoryInfoCollection> infosForProtocol = new HashMap<>(); + + ProtocolBasedLegacyRepositoryInfo(Collection<MigrationInfo> all) { + all.forEach(this::add); + } + + private void add(MigrationInfo migrationInfo) { + String protocol = migrationInfo.getProtocol(); + LegacyRepositoryInfoCollection legacyRepositoryInfoCollection = infosForProtocol.computeIfAbsent(protocol, x -> new LegacyRepositoryInfoCollection()); + legacyRepositoryInfoCollection.add(migrationInfo); + } + + private Optional<MigrationInfo> findRepository(List<String> pathElements) { + String protocol = pathElements.get(0); + if (!isProtocol(protocol)) { + return empty(); + } + return infosForProtocol.get(protocol).findRepository(removeFirstElement(pathElements)); + } + + boolean isProtocol(String protocol) { + return infosForProtocol.containsKey(protocol); + } + } + + private static class LegacyRepositoryInfoCollection { + + private final Map<String, MigrationInfo> repositories = new HashMap<>(); + private final Map<String, LegacyRepositoryInfoCollection> next = new HashMap<>(); + + Optional<MigrationInfo> findRepository(List<String> pathElements) { + String firstPathElement = pathElements.get(0); + if (repositories.containsKey(firstPathElement)) { + return of(repositories.get(firstPathElement)); + } else if (next.containsKey(firstPathElement)) { + return next.get(firstPathElement).findRepository(removeFirstElement(pathElements)); + } else { + return empty(); + } + } + + private void add(MigrationInfo migrationInfo) { + String originalRepositoryName = migrationInfo.getOriginalRepositoryName(); + List<String> originalRepositoryNameParts = asList(originalRepositoryName.split("/")); + add(migrationInfo, originalRepositoryNameParts); + } + + private void add(MigrationInfo migrationInfo, List<String> originalRepositoryNameParts) { + if (originalRepositoryNameParts.isEmpty()) { + throw new IllegalArgumentException("cannot handle empty name"); + } else if (originalRepositoryNameParts.size() == 1) { + repositories.put(originalRepositoryNameParts.get(0), migrationInfo); + } else { + LegacyRepositoryInfoCollection subCollection = next.computeIfAbsent(originalRepositoryNameParts.get(0), x -> new LegacyRepositoryInfoCollection()); + subCollection.add(migrationInfo, removeFirstElement(originalRepositoryNameParts)); + } + } + } + + private static <T> List<T> removeFirstElement(List<T> originalRepositoryNameParts) { + return originalRepositoryNameParts.subList(1, originalRepositoryNameParts.size()); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.tsx b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.tsx new file mode 100644 index 0000000000..8795e00756 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +class DummyComponent extends React.Component<{}> { + render() { + return <></>; + } +} + +export default DummyComponent; diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/index.tsx b/scm-plugins/scm-legacy-plugin/src/main/js/index.tsx new file mode 100644 index 0000000000..418c3e7b21 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/js/index.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { withRouter, RouteComponentProps } from "react-router-dom"; +import { binder } from "@scm-manager/ui-extensions"; +import { apiClient, ErrorBoundary, ErrorNotification, ProtectedRoute } from "@scm-manager/ui-components"; +import DummyComponent from "./DummyComponent"; +import { Links, Link } from "@scm-manager/ui-types"; + +type Props = RouteComponentProps & { + authenticated?: boolean; + links: Links; + + //context objects + history: History; +}; + +type State = { + error?: Error; +}; + +class LegacyRepositoryRedirect extends React.Component<Props, State> { + constructor(props: Props, state: State) { + super(props, state); + this.state = { + error: undefined + }; + } + + handleError = (error: Error) => { + this.setState({ + error + }); + }; + + redirectLegacyRepository() { + const { history, links } = this.props; + // eslint-disable-next-line no-restricted-globals + if (location.href && location.href.includes("#diffPanel;")) { + // eslint-disable-next-line no-restricted-globals + const splittedUrl = location.href.split(";"); + const repoId = splittedUrl[1]; + const changeSetId = splittedUrl[2]; + + const namespaceAndNameLink = links.nameAndNamespace as Link; + + apiClient + .get(namespaceAndNameLink.href.replace("{id}", repoId)) + .then(response => response.json()) + .then(payload => history.push("/repo/" + payload.namespace + "/" + payload.name + "/changeset/" + changeSetId)) + .catch(this.handleError); + } + } + + render() { + const { authenticated } = this.props; + const { error } = this.state; + + if (error) { + return ( + <section className="section"> + <div className="container"> + <ErrorBoundary> + <ErrorNotification error={error} /> + </ErrorBoundary> + </div> + </section> + ); + } + + return ( + <> + {authenticated ? ( + this.redirectLegacyRepository() + ) : ( + <ProtectedRoute path="/index.html" component={DummyComponent} authenticated={authenticated} /> + )} + </> + ); + } +} + +binder.bind("main.route", withRouter(LegacyRepositoryRedirect)); diff --git a/scm-plugins/scm-legacy-plugin/src/main/resources/META-INF/scm/plugin.xml b/scm-plugins/scm-legacy-plugin/src/main/resources/META-INF/scm/plugin.xml index f8a3c8c7b4..b9236845ca 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/resources/META-INF/scm/plugin.xml +++ b/scm-plugins/scm-legacy-plugin/src/main/resources/META-INF/scm/plugin.xml @@ -46,11 +46,17 @@ <scm-version>2</scm-version> <information> - <author>Sebastian Sdorra</author> + <displayName>Legacy</displayName> + <author>Cloudogu GmbH</author> + <category>Legacy Support</category> </information> <conditions> <min-version>${project.parent.version}</min-version> </conditions> + <resources> + <script>assets/scm-legacy-plugin.bundle.js</script> + </resources> + </plugin> diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRealmTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRealmTest.java index 327f03a6e5..f163e8c9a0 100644 --- a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRealmTest.java +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRealmTest.java @@ -45,7 +45,7 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.group.GroupDAO; import sonia.scm.security.BearerToken; diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java new file mode 100644 index 0000000000..a28f87dbf8 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java @@ -0,0 +1,43 @@ +package sonia.scm.legacy; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.NotFoundException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class LegacyRepositoryServiceTest { + + @Mock + private RepositoryManager repositoryManager; + + private LegacyRepositoryService legacyRepositoryService; + private final Repository repository = new Repository("abc123", "git", "space", "repo"); + + @Before + public void init() { + legacyRepositoryService = new LegacyRepositoryService(repositoryManager); + } + + @Test + public void findRepositoryNameSpaceAndNameForRepositoryId() { + when(repositoryManager.get(any(String.class))).thenReturn(repository); + NamespaceAndNameDto namespaceAndName = legacyRepositoryService.getNameAndNamespaceForRepositoryId("abc123"); + assertThat(namespaceAndName.getName()).isEqualToIgnoringCase("repo"); + assertThat(namespaceAndName.getNamespace()).isEqualToIgnoringCase("space"); + } + + @Test(expected = NotFoundException.class) + public void shouldGetNotFoundExceptionIfRepositoryNotExists() throws NotFoundException { + when(repositoryManager.get(any(String.class))).thenReturn(null); + legacyRepositoryService.getNameAndNamespaceForRepositoryId("456def"); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java new file mode 100644 index 0000000000..bd70cb9dfc --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java @@ -0,0 +1,103 @@ +package sonia.scm.legacy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.migration.MigrationDAO; +import sonia.scm.migration.MigrationInfo; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryDAO; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static java.util.Collections.singletonList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryLegacyProtocolRedirectFilterTest { + + @Mock + MigrationDAO migrationDAO; + @Mock + RepositoryDAO repositoryDao; + @Mock + HttpServletRequest request; + @Mock + HttpServletResponse response; + @Mock + FilterChain filterChain; + + @BeforeEach + void initRequest() { + lenient().when(request.getContextPath()).thenReturn("/scm"); + lenient().when(request.getQueryString()).thenReturn(""); + } + + @Test + void shouldNotRedirectForEmptyMigrationList() throws IOException, ServletException { + when(request.getServletPath()).thenReturn("/git/old/name"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + } + + @Test + void shouldRedirectForExistingRepository() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryDao.get("id")).thenReturn(new Repository()); + when(request.getServletPath()).thenReturn("/git/old/name"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(response).sendRedirect("/scm/repo/namespace/name"); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void shouldRedirectForExistingRepositoryWithFurtherPathElements() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryDao.get("id")).thenReturn(new Repository()); + when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(response).sendRedirect("/scm/repo/namespace/name/info/refs"); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void shouldRedirectWithQueryParameters() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryDao.get("id")).thenReturn(new Repository()); + when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); + when(request.getQueryString()).thenReturn("parameter=value"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(response).sendRedirect("/scm/repo/namespace/name/info/refs?parameter=value"); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void shouldNotRedirectWhenRepositoryHasBeenDeleted() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryDao.get("id")).thenReturn(null); + when(request.getServletPath()).thenReturn("/git/old/name"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); + + verify(response, never()).sendRedirect(any()); + verify(filterChain).doFilter(request, response); + } +} diff --git a/scm-plugins/scm-legacy-plugin/tsconfig.json b/scm-plugins/scm-legacy-plugin/tsconfig.json new file mode 100644 index 0000000000..7cfe32efe6 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@scm-manager/tsconfig", + "include": [ + "./src/main/js" + ] +} diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json new file mode 100644 index 0000000000..c7b0c125b3 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/package.json @@ -0,0 +1,24 @@ +{ + "name": "@scm-manager/scm-svn-plugin", + "private": true, + "version": "2.0.0-SNAPSHOT", + "license": "BSD-3-Clause", + "main": "./src/main/js/index.ts", + "scripts": { + "build": "ui-scripts plugin", + "watch": "ui-scripts plugin-watch", + "typecheck": "tsc" + }, + "babel": { + "presets": [ + "@scm-manager/babel-preset" + ] + }, + "jest": { + "preset": "@scm-manager/jest-preset" + }, + "prettier": "@scm-manager/prettier-config", + "dependencies": { + "@scm-manager/ui-plugins": "^2.0.0-SNAPSHOT" + } +} diff --git a/scm-plugins/scm-svn-plugin/pom.xml b/scm-plugins/scm-svn-plugin/pom.xml index d0f93671af..8ed7a7bef4 100644 --- a/scm-plugins/scm-svn-plugin/pom.xml +++ b/scm-plugins/scm-svn-plugin/pom.xml @@ -2,7 +2,7 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> - + <parent> <artifactId>scm-plugins</artifactId> <groupId>sonia.scm.plugins</groupId> @@ -10,13 +10,12 @@ </parent> <artifactId>scm-svn-plugin</artifactId> - <version>2.0.0-SNAPSHOT</version> - <name>scm-svn-plugin</name> <packaging>smp</packaging> <url>https://bitbucket.org/sdorra/scm-manager</url> <description>Plugin for the version control system Subversion</description> <dependencies> + <dependency> <groupId>sonia.svnkit</groupId> <artifactId>svnkit</artifactId> @@ -28,19 +27,28 @@ </exclusion> </exclusions> </dependency> - + <dependency> <groupId>sonia.svnkit</groupId> <artifactId>svnkit-dav</artifactId> <version>${svnkit.version}</version> </dependency> + </dependencies> - - <!-- create test jar --> - + <build> <plugins> - + + <plugin> + <groupId>sonia.scm.maven</groupId> + <artifactId>smp-maven-plugin</artifactId> + <configuration> + <corePlugin>true</corePlugin> + </configuration> + </plugin> + + <!-- create test jar --> + <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> @@ -53,24 +61,24 @@ </execution> </executions> </plugin> - + </plugins> </build> <repositories> - + <repository> <id>maven.tmatesoft.com</id> <name>tmatesoft release repository</name> <url>https://maven.tmatesoft.com/content/repositories/releases</url> </repository> - + <repository> <id>maven.scm-manager.org</id> <name>scm-manager release repository</name> <url>http://maven.scm-manager.org/nexus/content/groups/public</url> </repository> - + </repositories> </project> diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/rest/resources/SvnConfigResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/rest/resources/SvnConfigResource.java deleted file mode 100644 index cf824bcb46..0000000000 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/rest/resources/SvnConfigResource.java +++ /dev/null @@ -1,125 +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.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.SvnConfig; -import sonia.scm.repository.SvnRepositoryHandler; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; - -/** - * - * @author Sebastian Sdorra - */ -@Singleton -@Path("config/repositories/svn") -@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) -public class SvnConfigResource -{ - - /** - * Constructs ... - * - * - * @param repositoryManager - */ - @Inject - public SvnConfigResource(RepositoryManager repositoryManager) - { - repositoryHandler = (SvnRepositoryHandler) repositoryManager.getHandler( - SvnRepositoryHandler.TYPE_NAME); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @GET - public SvnConfig getConfig() - { - SvnConfig config = repositoryHandler.getConfig(); - - if (config == null) - { - config = new SvnConfig(); - repositoryHandler.setConfig(config); - } - - return config; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param uriInfo - * @param config - * - * @return - */ - @POST - @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public Response setConfig(@Context UriInfo uriInfo, SvnConfig config) - { - repositoryHandler.setConfig(config); - repositoryHandler.storeConfig(); - - return Response.created(uriInfo.getRequestUri()).build(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private SvnRepositoryHandler repositoryHandler; -} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java new file mode 100644 index 0000000000..879e92a186 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java @@ -0,0 +1,25 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import sonia.scm.repository.Compatibility; + +@NoArgsConstructor +@Getter +@Setter +public class SvnConfigDto extends HalRepresentation { + + private boolean disabled; + + private boolean enabledGZip; + private Compatibility compatibility; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapper.java new file mode 100644 index 0000000000..f996c49248 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapper.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import sonia.scm.repository.SvnConfig; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class SvnConfigDtoToSvnConfigMapper { + public abstract SvnConfig map(SvnConfigDto dto); +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java new file mode 100644 index 0000000000..918d38a346 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java @@ -0,0 +1,41 @@ +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.repository.SvnConfig; +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 SvnConfigInIndexResource extends JsonEnricherBase { + + private final Provider<ScmPathInfoStore> scmPathInfoStore; + + @Inject + public SvnConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.read(SvnConfig.PERMISSION).isPermitted()) { + String svnConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), SvnConfigResource.class) + .method("get") + .parameters() + .href(); + + JsonNode svnConfigRefNode = createObject(singletonMap("href", value(svnConfigUrl))); + + addPropertyNode(context.getResponseEntity().get("_links"), "svnConfig", svnConfigRefNode); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java new file mode 100644 index 0000000000..b12785dca9 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java @@ -0,0 +1,92 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.SvnConfig; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.web.SvnVndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +/** + * RESTful Web Service Resource to manage the configuration of the svn plugin. + */ +@Path(SvnConfigResource.SVN_CONFIG_PATH_V2) +public class SvnConfigResource { + + static final String SVN_CONFIG_PATH_V2 = "v2/config/svn"; + private final SvnConfigDtoToSvnConfigMapper dtoToConfigMapper; + private final SvnConfigToSvnConfigDtoMapper configToDtoMapper; + private final SvnRepositoryHandler repositoryHandler; + + @Inject + public SvnConfigResource(SvnConfigDtoToSvnConfigMapper dtoToConfigMapper, SvnConfigToSvnConfigDtoMapper configToDtoMapper, + SvnRepositoryHandler repositoryHandler) { + this.dtoToConfigMapper = dtoToConfigMapper; + this.configToDtoMapper = configToDtoMapper; + this.repositoryHandler = repositoryHandler; + } + + /** + * Returns the svn config. + */ + @GET + @Path("") + @Produces(SvnVndMediaType.SVN_CONFIG) + @TypeHint(SvnConfigDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:svn\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get() { + + SvnConfig config = repositoryHandler.getConfig(); + + if (config == null) { + config = new SvnConfig(); + repositoryHandler.setConfig(config); + } + + ConfigurationPermissions.read(config).check(); + + return Response.ok(configToDtoMapper.map(config)).build(); + } + + /** + * Modifies the svn config. + * + * @param configDto new configuration object + */ + @PUT + @Path("") + @Consumes(SvnVndMediaType.SVN_CONFIG) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:svn\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response update(SvnConfigDto configDto) { + + SvnConfig config = dtoToConfigMapper.map(configDto); + + ConfigurationPermissions.write(config).check(); + + repositoryHandler.setConfig(config); + repositoryHandler.storeConfig(); + + return Response.noContent().build(); + } + +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java new file mode 100644 index 0000000000..c160280822 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java @@ -0,0 +1,41 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.SvnConfig; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class SvnConfigToSvnConfigDtoMapper extends BaseMapper<SvnConfig, SvnConfigDto> { + + @Inject + private ScmPathInfoStore scmPathInfoStore; + + @AfterMapping + void appendLinks(SvnConfig config, @MappingTarget SvnConfigDto target) { + Links.Builder linksBuilder = linkingTo().self(self()); + if (ConfigurationPermissions.write(config).isPermitted()) { + linksBuilder.single(link("update", update())); + } + target.add(linksBuilder.build()); + } + + private String self() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), SvnConfigResource.class); + return linkBuilder.method("get").parameters().href(); + } + + private String update() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), SvnConfigResource.class); + return linkBuilder.method("update").parameters().href(); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java index c4b0af57a1..5fe5c0815d 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java @@ -33,12 +33,11 @@ package sonia.scm.repository; -//~--- JDK imports ------------------------------------------------------------ - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; /** * @@ -46,9 +45,12 @@ import javax.xml.bind.annotation.XmlRootElement; */ @XmlRootElement(name = "config") @XmlAccessorType(XmlAccessType.FIELD) -public class SvnConfig extends SimpleRepositoryConfig +public class SvnConfig extends RepositoryConfig { + @SuppressWarnings("WeakerAccess") // This might be needed for permission checking + public static final String PERMISSION = "svn"; + /** * Method description * @@ -108,4 +110,11 @@ public class SvnConfig extends SimpleRepositoryConfig /** Field description */ private Compatibility compatibility = Compatibility.NONE; + + @Override + @XmlTransient // Only for permission checks, don't serialize to XML + public String getId() { + // Don't change this without migrating SCM permission configuration! + return PERMISSION; + } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java new file mode 100644 index 0000000000..ab01393b29 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java @@ -0,0 +1,33 @@ +package sonia.scm.repository; + +import sonia.scm.ContextEntry; +import sonia.scm.io.INIConfiguration; +import sonia.scm.io.INIConfigurationReader; +import sonia.scm.io.INIConfigurationWriter; +import sonia.scm.io.INISection; + +import java.io.File; +import java.io.IOException; + +class SvnConfigHelper { + + private static final String CONFIG_FILE_NAME = "scm-manager.conf"; + private static final String CONFIG_SECTION_SCMM = "scmm"; + private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; + + void writeRepositoryId(Repository repository, File directory) throws IOException { + INISection iniSection = new INISection(CONFIG_SECTION_SCMM); + iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId()); + INIConfiguration iniConfiguration = new INIConfiguration(); + iniConfiguration.addSection(iniSection); + new INIConfigurationWriter().write(iniConfiguration, new File(directory, CONFIG_FILE_NAME)); + } + + String getRepositoryId(File directory) { + try { + return new INIConfigurationReader().read(new File(directory, CONFIG_FILE_NAME)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID); + } catch (IOException e) { + throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnModificationHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnModificationHandler.java deleted file mode 100644 index f7cb60e3df..0000000000 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnModificationHandler.java +++ /dev/null @@ -1,88 +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 org.tmatesoft.svn.core.SVNException; -import org.tmatesoft.svn.core.wc.admin.ISVNChangeEntryHandler; -import org.tmatesoft.svn.core.wc.admin.SVNChangeEntry; - -/** - * - * @author Sebastian Sdorra - */ -public class SvnModificationHandler implements ISVNChangeEntryHandler -{ - - /** - * Constructs ... - * - * - * @param changeset - */ - public SvnModificationHandler(Changeset changeset) - { - this.changeset = changeset; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param entry - * - * @throws SVNException - */ - @Override - public void handleEntry(SVNChangeEntry entry) throws SVNException - { - Modifications modification = changeset.getModifications(); - - if (modification == null) - { - modification = new Modifications(); - changeset.setModifications(modification); - } - - SvnUtil.appendModification(modification, entry); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Changeset changeset; -} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java index dcadd5c1c6..f3be47e0c2 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java @@ -37,10 +37,8 @@ package sonia.scm.repository; import com.google.inject.Inject; import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.internal.io.fs.FSHooks; @@ -48,20 +46,20 @@ import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; import org.tmatesoft.svn.util.SVNDebugLog; - -import sonia.scm.Type; -import sonia.scm.io.FileSystem; import sonia.scm.logging.SVNKitLogger; import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.SvnRepositoryServiceProvider; +import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; -import sonia.scm.repository.spi.HookEventFacade; -import sonia.scm.store.ConfigurationStoreFactory; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -73,43 +71,28 @@ public class SvnRepositoryHandler extends AbstractSimpleRepositoryHandler<SvnConfig> { - /** Field description */ public static final String PROPERTY_UUID = "svn.uuid"; - /** Field description */ - public static final String RESOURCE_VERSION = - "sonia/scm/version/scm-svn-plugin"; + public static final String RESOURCE_VERSION = "sonia/scm/version/scm-svn-plugin"; - /** Field description */ public static final String TYPE_DISPLAYNAME = "Subversion"; - /** Field description */ public static final String TYPE_NAME = "svn"; - /** Field description */ - public static final Type TYPE = new RepositoryType(TYPE_NAME, + public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, SvnRepositoryServiceProvider.COMMANDS); - /** the logger for SvnRepositoryHandler */ private static final Logger logger = LoggerFactory.getLogger(SvnRepositoryHandler.class); - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param storeFactory - * @param fileSystem - * @param repositoryManager - */ @Inject - public SvnRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, - HookEventFacade eventFacade) + public SvnRepositoryHandler(ConfigurationStoreFactory storeFactory, + HookEventFacade eventFacade, + RepositoryLocationResolver repositoryLocationResolver, + PluginLoader pluginLoader) { - super(storeFactory, fileSystem); + super(storeFactory, repositoryLocationResolver, pluginLoader); // register logger SVNDebugLog.setDefaultLog(new SVNKitLogger()); @@ -129,60 +112,26 @@ public class SvnRepositoryHandler } } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ @Override public ImportHandler getImportHandler() { return new SvnImportHandler(this); } - /** - * Method description - * - * - * @return - */ @Override - public Type getType() + public RepositoryType getType() { return TYPE; } - /** - * Method description - * - * - * @return - */ @Override public String getVersionInformation() { return getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION); } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * @param directory - * - * @throws IOException - * @throws RepositoryException - */ @Override - protected void create(Repository repository, File directory) - throws RepositoryException, IOException - { + protected void create(Repository repository, File directory) throws InternalRepositoryException { Compatibility comp = config.getCompatibility(); if (logger.isDebugEnabled()) @@ -229,7 +178,8 @@ public class SvnRepositoryHandler } catch (SVNException ex) { - throw new RepositoryException(ex); + logger.error("could not create svn repository", ex); + throw new InternalRepositoryException(entity(repository), "could not create repository", ex); } finally { @@ -262,4 +212,13 @@ public class SvnRepositoryHandler { return SvnConfig.class; } + + @Override + protected void postCreate(Repository repository, File directory) throws IOException { + new SvnConfigHelper().writeRepositoryId(repository, directory); + } + + String getRepositoryId(File directory) { + return new SvnConfigHelper().getRepositoryId(directory); + } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHook.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHook.java index 1de3b2aecb..c0a440f8d1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHook.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHook.java @@ -37,7 +37,6 @@ package sonia.scm.repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.SVNCancelException; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNErrorMessage; @@ -45,21 +44,19 @@ import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.internal.io.fs.FSHook; import org.tmatesoft.svn.core.internal.io.fs.FSHookEvent; import org.tmatesoft.svn.core.internal.io.fs.FSHooks; - import sonia.scm.repository.spi.AbstractSvnHookChangesetProvider; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.SvnHookContextProvider; import sonia.scm.repository.spi.SvnPostReceiveHookChangesetProvier; import sonia.scm.repository.spi.SvnPreReceiveHookChangesetProvier; import sonia.scm.util.AssertUtil; -import sonia.scm.util.IOUtil; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -73,16 +70,7 @@ public class SvnRepositoryHook implements FSHook //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * - * @param hookEventFacade - * @param handler - */ - public SvnRepositoryHook(HookEventFacade hookEventFacade, - SvnRepositoryHandler handler) + public SvnRepositoryHook(HookEventFacade hookEventFacade, SvnRepositoryHandler handler) { this.hookEventFacade = hookEventFacade; this.handler = handler; @@ -166,12 +154,10 @@ public class SvnRepositoryHook implements FSHook { try { - String name = getRepositoryName(directory); - - name = IOUtil.trimSeperatorChars(name); + String repositoryId = getRepositoryId(directory); //J- - hookEventFacade.handle(SvnRepositoryHandler.TYPE_NAME, name) + hookEventFacade.handle(repositoryId) .fireHookEvent( changesetProvider.getType(), new SvnHookContextProvider(changesetProvider) @@ -202,18 +188,16 @@ public class SvnRepositoryHook implements FSHook * * @throws IOException */ - private String getRepositoryName(File directory) throws IOException + private String getRepositoryId(File directory) { AssertUtil.assertIsNotNull(directory); - - return RepositoryUtil.getRepositoryName(handler, directory); + return handler.getRepositoryId(directory); } //~--- fields --------------------------------------------------------------- - /** Field description */ - private SvnRepositoryHandler handler; - /** Field description */ private HookEventFacade hookEventFacade; + + private final SvnRepositoryHandler handler; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java index 6cc5eb5da9..b4be68a51f 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java @@ -38,10 +38,8 @@ package sonia.scm.repository; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.io.Closeables; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.SVNLogEntryPath; @@ -51,21 +49,20 @@ import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil; import org.tmatesoft.svn.core.internal.util.SVNXMLUtil; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.wc.SVNClientManager; -import org.tmatesoft.svn.core.wc.admin.SVNChangeEntry; - import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; - import java.util.List; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -108,30 +105,37 @@ public final class SvnUtil //~--- methods -------------------------------------------------------------- - /** - * TODO: type replaced - * - * - * @param modifications - * @param entry - */ - public static void appendModification(Modifications modifications, - SVNLogEntryPath entry) - { - appendModification(modifications, entry.getType(), entry.getPath()); + public static long parseRevision(String v, Repository repository) { + long result = -1l; + + if (!Strings.isNullOrEmpty(v)) + { + try + { + result = Long.parseLong(v); + } + catch (NumberFormatException ex) + { + throw notFound(entity("Revision", v).in(repository)); + } + } + + return result; } - /** - * Method description - * - * - * @param modifications - * @param entry - */ - public static void appendModification(Modifications modifications, - SVNChangeEntry entry) - { - appendModification(modifications, entry.getType(), entry.getPath()); + + public static Modifications createModifications(SVNLogEntry entry, String revision) { + Modifications modifications = new Modifications(); + modifications.setRevision(revision); + Map<String, SVNLogEntryPath> changeMap = entry.getChangedPaths(); + + if (Util.isNotEmpty(changeMap)) { + + for (SVNLogEntryPath e : changeMap.values()) { + appendModification(modifications, e.getType(), e.getPath()); + } + } + return modifications; } /** @@ -215,19 +219,6 @@ public final class SvnUtil { changeset.getParents().add(String.valueOf(revision - 1)); } - - Map<String, SVNLogEntryPath> changeMap = entry.getChangedPaths(); - - if (Util.isNotEmpty(changeMap)) - { - Modifications modifications = changeset.getModifications(); - - for (SVNLogEntryPath e : changeMap.values()) - { - appendModification(modifications, e); - } - } - return changeset; } @@ -351,21 +342,8 @@ public final class SvnUtil } } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param revision - * - * @return - * - * @throws RepositoryException - */ - public static long getRevisionNumber(String revision) - throws RepositoryException - { + public static long getRevisionNumber(String revision, Repository repository) { + // REVIEW Bei SVN wird ohne Revision die -1 genommen, was zu einem Fehler führt long revisionNumber = -1; if (Util.isNotEmpty(revision)) @@ -376,7 +354,7 @@ public final class SvnUtil } catch (NumberFormatException ex) { - throw new RepositoryException("given revision is not a svnrevision"); + throw notFound(entity("Revision", revision).in(repository)); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV2UpdateStep.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV2UpdateStep.java new file mode 100644 index 0000000000..2423040f60 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV2UpdateStep.java @@ -0,0 +1,56 @@ +package sonia.scm.repository; + +import sonia.scm.migration.UpdateException; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Path; + +import static sonia.scm.version.Version.parse; + +@Extension +public class SvnV2UpdateStep implements UpdateStep { + + private final RepositoryLocationResolver locationResolver; + private final UpdateStepRepositoryMetadataAccess<Path> repositoryMetadataAccess; + + @Inject + public SvnV2UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess<Path> repositoryMetadataAccess) { + this.locationResolver = locationResolver; + this.repositoryMetadataAccess = repositoryMetadataAccess; + } + + @Override + public void doUpdate() { + locationResolver.forClass(Path.class).forAllLocations( + (repositoryId, path) -> { + Repository repository = repositoryMetadataAccess.read(path); + if (isSvnDirectory(repository)) { + try { + new SvnConfigHelper().writeRepositoryId(repository, path.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY).toFile()); + } catch (IOException e) { + throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e); + } + } + } + ); + } + + private boolean isSvnDirectory(Repository repository) { + return SvnRepositoryHandler.TYPE_NAME.equals(repository.getType()); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.plugin.svn"; + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkDirFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkDirFactory.java new file mode 100644 index 0000000000..6ae7715ded --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkDirFactory.java @@ -0,0 +1,9 @@ +package sonia.scm.repository; + +import sonia.scm.repository.spi.SvnContext; +import sonia.scm.repository.util.WorkdirFactory; + +import java.io.File; + +public interface SvnWorkDirFactory extends WorkdirFactory<File, File, SvnContext> { +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java new file mode 100644 index 0000000000..70615698b8 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java @@ -0,0 +1,1256 @@ +package sonia.scm.repository.spi; + +import de.regnis.q.sequence.line.diff.QDiffGenerator; +import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory; +import de.regnis.q.sequence.line.diff.QDiffManager; +import de.regnis.q.sequence.line.diff.QDiffUniGenerator; +import org.tmatesoft.svn.core.SVNErrorCode; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNMergeRangeList; +import org.tmatesoft.svn.core.SVNProperties; +import org.tmatesoft.svn.core.SVNProperty; +import org.tmatesoft.svn.core.SVNPropertyValue; +import org.tmatesoft.svn.core.internal.util.SVNHashMap; +import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; +import org.tmatesoft.svn.core.internal.util.SVNPathUtil; +import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; +import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback; +import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; +import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; +import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback; +import org.tmatesoft.svn.core.wc.ISVNOptions; +import org.tmatesoft.svn.core.wc.SVNDiffOptions; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import org.tmatesoft.svn.util.SVNLogType; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * This is a copy of the SvnDiffGenerator class from the patched SVNKit library used in SCM-Manager + * (a fork of SVNKit from TMate Software (http://svnkit.com/)). + * + * The original class can be found here: https://bitbucket.org/sdorra/svnkit/src/default/svnkit/src/main/java/org/tmatesoft/svn/core/internal/wc2/ng/SvnDiffGenerator.java + * + * The folowing modifications are applied when using the git format + * <ul> + * <li> + * remove the svn header + * </li> + * <li> + * use the git diff code 100644 on new file and deleted file actions + * </li> + * <li> + * remove the labels in the added and deleted file headers, eg. <code>[+++ a/a.txt (revision 4)]</code> is + * replaced with <code>[+++ a/a.txt]</code> + * </li> + * </ul> + */ +@SuppressWarnings("all") +public class SCMSvnDiffGenerator implements ISvnDiffGenerator { + + protected static final String WC_REVISION_LABEL = "(working copy)"; + protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________"; + protected static final String HEADER_SEPARATOR = "==================================================================="; + protected static final String HEADER_ENCODING = "UTF-8"; + + private SvnTarget originalTarget1; + private SvnTarget originalTarget2; + private SvnTarget baseTarget; + private SvnTarget relativeToTarget; + private SvnTarget repositoryRoot; + private String encoding; + private byte[] eol; + private boolean useGitFormat; + private boolean forcedBinaryDiff; + + private boolean diffDeleted; + private boolean diffAdded; + private List<String> rawDiffOptions; + private boolean forceEmpty; + + private Set<String> visitedPaths; + private String externalDiffCommand; + private SVNDiffOptions diffOptions; + private boolean fallbackToAbsolutePath; + private ISVNOptions options; + private boolean propertiesOnly; + private boolean ignoreProperties; + + private String getDisplayPath(SvnTarget target) { + String relativePath; + if (baseTarget == null) { + relativePath = null; + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = baseTarget.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) { + String relativePath; + if (repositoryRoot == null) { + relativePath = null; + } else { + if (repositoryRoot.isFile() == target.isFile()) { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = repositoryRoot.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = new File("").getAbsolutePath(); + relativePath = getRelativePath(targetString, baseTargetString); + } + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativePath(String targetString, String baseTargetString) { + if (targetString != null) { + targetString = targetString.replace(File.separatorChar, '/'); + } + if (baseTargetString != null) { + baseTargetString = baseTargetString.replace(File.separatorChar, '/'); + } + + final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString); + if (pathAsChild != null) { + return pathAsChild; + } + if (targetString.equals(baseTargetString)) { + return ""; + } + return null; + } + + private String getChildPath(String path, String relativeToPath) { + if (relativeToTarget == null) { + return null; + } + + String relativePath = getRelativePath(path, relativeToPath); + if (relativePath == null) { + return path; + } + + if (relativePath.length() > 0) { + return relativePath; + } + + if (relativeToPath.equals(path)) { + return "."; + } + + return null; + } + + public SCMSvnDiffGenerator() { + this.originalTarget1 = null; + this.originalTarget2 = null; + this.visitedPaths = new HashSet<String>(); + this.diffDeleted = true; + this.diffAdded = true; + } + + public void setBaseTarget(SvnTarget baseTarget) { + this.baseTarget = baseTarget; + } + + public void setUseGitFormat(boolean useGitFormat) { + this.useGitFormat = useGitFormat; + } + + public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) { + this.originalTarget1 = originalTarget1; + this.originalTarget2 = originalTarget2; + } + + public void setRelativeToTarget(SvnTarget relativeToTarget) { + this.relativeToTarget = relativeToTarget; + } + + public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) { + //anchors are not used + } + + public void setRepositoryRoot(SvnTarget repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + public void setForceEmpty(boolean forceEmpty) { + this.forceEmpty = forceEmpty; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return encoding; + } + + public String getGlobalEncoding() { + ISVNOptions options = getOptions(); + + if (options != null && options instanceof DefaultSVNOptions) { + DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options; + return defaultOptions.getGlobalCharset(); + } + return null; + } + + public void setEOL(byte[] eol) { + this.eol = eol; + } + + public byte[] getEOL() { + return eol; + } + + public boolean isForcedBinaryDiff() { + return forcedBinaryDiff; + } + + public void setForcedBinaryDiff(boolean forcedBinaryDiff) { + this.forcedBinaryDiff = forcedBinaryDiff; + } + + public boolean isPropertiesOnly() { + return propertiesOnly; + } + + public void setPropertiesOnly(boolean propertiesOnly) { + this.propertiesOnly = propertiesOnly; + } + + public boolean isIgnoreProperties() { + return ignoreProperties; + } + + public void setIgnoreProperties(boolean ignoreProperties) { + this.ignoreProperties = ignoreProperties; + } + + public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isIgnoreProperties()) { + return; + } + if (dirWasAdded && !isDiffAdded()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (displayPath == null || displayPath.length() == 0) { + displayPath = "."; + } + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + boolean showDiffHeader = !visitedPaths.contains(displayPath); + if (showDiffHeader) { + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified); + visitedPaths.add(displayPath); + if (useGitFormat) { + displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + if (shouldStopDisplaying) { + return; + } + +// if (useGitFormat) { +// String copyFromPath = null; +// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified; +// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1); +// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2); +// displayGitDiffHeader(outputStream, operationKind, +// getRelativeToRootPath(target, originalTarget1), +// getRelativeToRootPath(target, originalTarget2), +// copyFromPath); +// } + + if (useGitFormat) { + displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null); + } else { + displayHeaderFields(outputStream, label1, label2); + } + } + + displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream); + + displayPropDiffValues(outputStream, propChanges, originalProps); + } + + private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''", + displayPath, relativeToPath); + SVNErrorManager.error(errorMessage, SVNLogType.CLIENT); + } + + private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + + try { + displayString(outputStream, "--- "); + displayFirstGitLabelPath(outputStream, path1, revision1, operation); + displayEOL(outputStream); + displayString(outputStream, "+++ "); + displaySecondGitLabelPath(outputStream, path2, revision2, operation); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private String adjustRelativeToReposRoot(String targetString) { + if (repositoryRoot != null) { + String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString(); + String relativePath = getRelativePath(targetString, repositoryRootString); + return relativePath == null ? "" : relativePath; + } + return targetString; + } + + private String computeLabel(String targetString, String originalTargetString) { + if (originalTargetString.length() == 0) { + return targetString; + } else if (originalTargetString.charAt(0) == '/') { + return targetString + "\t(..." + originalTargetString + ")"; + } else { + return targetString + "\t(.../" + originalTargetString + ")"; + } + } + + public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isPropertiesOnly()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean leftIsBinary = false; + boolean rightIsBinary = false; + + if (mimeType1 != null) { + leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1); + } + if (mimeType2 != null) { + rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2); + } + + if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) { + boolean shouldStopDisplaying = false; + if (!useGitFormat) { + shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + } else { + String path1 = getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + displayGitDiffHeader(outputStream, operation,path1,path2,null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + if (!useGitFormat){ + displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary); + } + + return; + } + + final String diffCommand = getExternalDiffCommand(); + if (diffCommand != null) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2); + } else { + internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2); + } + } + + private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException { + displayCannotDisplayFileMarkedBinary(outputStream); + + if (leftIsBinary && !rightIsBinary) { + displayMimeType(outputStream, mimeType1); + } else if (!leftIsBinary && rightIsBinary) { + displayMimeType(outputStream, mimeType2); + } else if (leftIsBinary && rightIsBinary) { + if (mimeType1.equals(mimeType2)) { + displayMimeType(outputStream, mimeType1); + } else { + displayMimeTypes(outputStream, mimeType1, mimeType2); + } + } + } + + private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException { + String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath); + if (file2 == null && !isDiffDeleted()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + if (file1 == null && !isDiffAdded()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath); + + RandomAccessFile is1 = null; + RandomAccessFile is2 = null; + try { + is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1); + is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2); + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + final String diffHeader; + if (forceEmpty || useGitFormat) { + displayString(outputStream, header); + diffHeader = headerFields; + + visitedPaths.add(displayPath); + } else { + diffHeader = header + headerFields; + } + QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader); + EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream); + QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator); + if (emptyDetectionOutputStream.isSomethingWritten()) { + visitedPaths.add(displayPath); + } + emptyDetectionOutputStream.flush(); + } catch (IOException e) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage()); + SVNErrorManager.error(err, e, SVNLogType.DEFAULT); + } finally { + SVNFileUtil.closeFile(is1); + SVNFileUtil.closeFile(is2); + } + } + + private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (useGitFormat) { + displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath); + } else { + displayHeaderFields(byteArrayOutputStream, label1, label2); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (!useGitFormat) { + // display the svn header only if the git format is not required + displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation); + } else { + displayGitDiffHeader(byteArrayOutputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + copyFromPath); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException { + final List<String> args = new ArrayList<String>(); + args.add(diffCommand); + if (rawDiffOptions != null) { + args.addAll(rawDiffOptions); + } else { + Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection(); + args.addAll(svnDiffOptionsCollection); + args.add("-u"); + } + + if (label1 != null) { + args.add("-L"); + args.add(label1); + } + + if (label2 != null) { + args.add("-L"); + args.add(label2); + } + + boolean tmpFile1 = false; + boolean tmpFile2 = false; + if (file1 == null) { + file1 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile1 = true; + } + if (file2 == null) { + file2 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile2 = true; + } + + String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/'); + String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/'); + + args.add(file1Path); + args.add(file2Path); + try { + final Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + + SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true, + new ISVNReturnValueCallback() { + + public void handleReturnValue(int returnValue) throws SVNException { + if (returnValue != 0 && returnValue != 1) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, + "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)}); + SVNErrorManager.error(err, SVNLogType.DEFAULT); + } + } + + public void handleChar(char ch) throws SVNException { + try { + writer.write(ch); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } + } + + public boolean isHandleProgramOutput() { + return true; + } + }); + + writer.flush(); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } finally { + try { + if (tmpFile1) { + SVNFileUtil.deleteFile(file1); + } + if (tmpFile2) { + SVNFileUtil.deleteFile(file2); + } + } catch (SVNException e) { + // skip + } + } + } + + private String getExternalDiffCommand() { + return externalDiffCommand; + } + + private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = "); + displayString(outputStream, mimeType); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = ("); + displayString(outputStream, mimeType1); + displayString(outputStream, ", "); + displayString(outputStream, mimeType2); + displayString(outputStream, ")"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException { + try { + displayString(outputStream, "Cannot display: file marked as a binary type."); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void ensureEncodingAndEOLSet() { + if (getEOL() == null) { + setEOL(SVNProperty.EOL_LF_BYTES); + } + if (getEncoding() == null) { + final ISVNOptions options = getOptions(); + if (options != null && options.getNativeCharset() != null) { + setEncoding(options.getNativeCharset()); + } else { + setEncoding("UTF-8"); + } + } + } + + private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException { + for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) { + String name = (String) changedPropNames.next(); + SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null; + SVNPropertyValue newValue = diff.getSVNPropertyValue(name); + String headerFormat = null; + + if (originalValue == null) { + headerFormat = "Added: "; + } else if (newValue == null) { + headerFormat = "Deleted: "; + } else { + headerFormat = "Modified: "; + } + + try { + displayString(outputStream, (headerFormat + name)); + displayEOL(outputStream); + if (SVNProperty.MERGE_INFO.equals(name)) { + displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString()); + continue; + } + + byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding()); + byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding()); + + if (originalValueBytes == null) { + originalValueBytes = new byte[0]; + } else { + originalValueBytes = maybeAppendEOL(originalValueBytes); + } + + boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 && + (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] || + newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]); + + if (newValueBytes == null) { + newValueBytes = new byte[0]; + } else { + newValueBytes = maybeAppendEOL(newValueBytes); + } + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##"); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + QDiffGenerator generator = new QDiffUniGenerator(properties, ""); + Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes), + null, writer, generator); + writer.flush(); + if (!newValueHadEol) { + displayString(outputStream, "\\ No newline at end of property"); + displayEOL(outputStream); + } + } catch (IOException e) { + wrapException(e); + } + } + + } + + private byte[] maybeAppendEOL(byte[] buffer) { + if (buffer.length == 0) { + return buffer; + } + + byte lastByte = buffer[buffer.length - 1]; + if (lastByte == SVNProperty.EOL_CR_BYTES[0]) { + return buffer; + } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) { + final byte[] newBuffer = new byte[buffer.length + getEOL().length]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length); + return newBuffer; + } else { + return buffer; + } + } + + private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("a/" + copyFromPath, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("a/" + copyFromPath, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("b/" + path2, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath); + } + } + + private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + // 100644 is the mode code used from git for new and deleted file mode + displayString(outputStream, "new file mode 100644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + // 100644 is the mode code used from git for new and deleted file mode + displayString(outputStream, "deleted file mode 100644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "copy from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "copy to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "rename from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "rename to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException { + displayGitPath(outputStream, path1, "a/"); + } + + private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException { + displayGitPath(outputStream, path2, "b/"); + } + + private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "a/"; + if (operation == SvnDiffCallback.OperationKind.Added) { + path1 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix); + } + + private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "b/"; + if (operation == SvnDiffCallback.OperationKind.Deleted) { + path2 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix); + } + + private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix) throws IOException { + displayString(outputStream, pathPrefix); + displayString(outputStream, path1); + } + + private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) { + String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor); + return getLabel(adjustedPath, revision); + } + + private String getAdjustedPath(String displayPath, String path1, String commonAncestor) { + String adjustedPath = getRelativePath(path1, commonAncestor); + + if (adjustedPath == null || adjustedPath.length() == 0) { + adjustedPath = displayPath; + } else if (adjustedPath.charAt(0) == '/') { + adjustedPath = displayPath + "\t(..." + adjustedPath + ")"; + } else { + adjustedPath = displayPath + "\t(.../" + adjustedPath + ")"; + } + return adjustedPath; + //TODO: respect relativeToDir + } + + protected String getLabel(String path, String revToken) { + if (useGitFormat){ + // the label in the git format contains only the path + return path; + } + revToken = revToken == null ? WC_REVISION_LABEL : revToken; + return path + "\t" + revToken; + } + + protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException { + try { + if (deleted && !isDiffDeleted()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (deleted)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + if (added && !isDiffAdded()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (added)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + displayString(os, "Index: "); + displayString(os, path); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return false; + } catch (IOException e) { + wrapException(e); + } + return false; + } + + protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException { + try { + displayString(os, "--- "); + displayString(os, label1); + displayEOL(os); + displayString(os, "+++ "); + displayString(os, label2); + displayEOL(os); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException { + try { + displayEOL(outputStream); + displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path))); + displayEOL(outputStream); + displayString(outputStream, PROPERTIES_SEPARATOR); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) { + if (value == null) { + return null; + } + if (value.isString()) { + try { + return value.getString().getBytes(encoding); + } catch (UnsupportedEncodingException e) { + return value.getString().getBytes(); + } + } + return value.getBytes(); + } + + private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException { + Map oldMergeInfo = null; + Map newMergeInfo = null; + if (oldValue != null) { + oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null); + } + if (newValue != null) { + newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null); + } + + Map deleted = new TreeMap(); + Map added = new TreeMap(); + SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true); + + for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path); + displayString(outputStream, (" Reverse-merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + + for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path); + displayString(outputStream, (" Merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + } + + private boolean useLocalFileSeparatorChar() { + return true; + } + + public boolean isDiffDeleted() { + return diffDeleted; + } + + public boolean isDiffAdded() { + return diffAdded; + } + + private void wrapException(IOException e) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e); + SVNErrorManager.error(errorMessage, e, SVNLogType.WC); + } + + private void displayString(OutputStream outputStream, String s) throws IOException { + outputStream.write(s.getBytes(HEADER_ENCODING)); + } + + private void displayEOL(OutputStream os) throws IOException { + os.write(getEOL()); + } + + public SVNDiffOptions getDiffOptions() { + if (diffOptions == null) { + diffOptions = new SVNDiffOptions(); + } + return diffOptions; + } + + public void setExternalDiffCommand(String externalDiffCommand) { + this.externalDiffCommand = externalDiffCommand; + } + + public void setRawDiffOptions(List<String> rawDiffOptions) { + this.rawDiffOptions = rawDiffOptions; + } + + public void setDiffOptions(SVNDiffOptions diffOptions) { + this.diffOptions = diffOptions; + } + + public void setDiffDeleted(boolean diffDeleted) { + this.diffDeleted = diffDeleted; + } + + public void setDiffAdded(boolean diffAdded) { + this.diffAdded = diffAdded; + } + + public void setBasePath(File absoluteFile) { + setBaseTarget(SvnTarget.fromFile(absoluteFile)); + } + + public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) { + this.fallbackToAbsolutePath = fallbackToAbsolutePath; + } + + public void setOptions(ISVNOptions options) { + this.options = options; + } + + public ISVNOptions getOptions() { + return options; + } + + private class EmptyDetectionOutputStream extends OutputStream { + + private final OutputStream outputStream; + private boolean somethingWritten; + + public EmptyDetectionOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + this.somethingWritten = false; + } + + public boolean isSomethingWritten() { + return somethingWritten; + } + + @Override + public void write(int c) throws IOException { + somethingWritten = true; + outputStream.write(c); + } + + @Override + public void write(byte[] bytes) throws IOException { + somethingWritten = bytes.length > 0; + outputStream.write(bytes); + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + somethingWritten = length > 0; + outputStream.write(bytes, offset, length); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactory.java new file mode 100644 index 0000000000..bb9976b4a9 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactory.java @@ -0,0 +1,63 @@ +package sonia.scm.repository.spi; + +import org.apache.commons.lang.exception.CloneFailedException; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNURL; +import org.tmatesoft.svn.core.wc2.SvnCheckout; +import org.tmatesoft.svn.core.wc2.SvnOperationFactory; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import sonia.scm.repository.Repository; +import sonia.scm.repository.SvnWorkDirFactory; +import sonia.scm.repository.util.SimpleWorkdirFactory; +import sonia.scm.repository.util.WorkdirProvider; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; + +public class SimpleSvnWorkDirFactory extends SimpleWorkdirFactory<File, File, SvnContext> implements SvnWorkDirFactory { + + @Inject + public SimpleSvnWorkDirFactory(WorkdirProvider workdirProvider) { + super(workdirProvider); + } + + @Override + protected Repository getScmRepository(SvnContext context) { + return context.getRepository(); + } + + @Override + protected ParentAndClone<File, File> cloneRepository(SvnContext context, File workingCopy, String initialBranch) { + + final SvnOperationFactory svnOperationFactory = new SvnOperationFactory(); + + SVNURL source; + try { + source = SVNURL.fromFile(context.getDirectory()); + } catch (SVNException ex) { + throw new CloneFailedException(ex.getMessage()); + } + + try { + final SvnCheckout checkout = svnOperationFactory.createCheckout(); + checkout.setSingleTarget(SvnTarget.fromFile(workingCopy)); + checkout.setSource(SvnTarget.fromURL(source)); + checkout.run(); + } catch (SVNException ex) { + throw new CloneFailedException(ex.getMessage()); + } finally { + svnOperationFactory.dispose(); + } + + return new ParentAndClone<>(context.getDirectory(), workingCopy); + } + + @Override + protected void closeRepository(File workingCopy) { + } + + @Override + protected void closeWorkdirInternal(File workdir) { + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBlameCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBlameCommand.java index 329f2672e1..b589734d03 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBlameCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBlameCommand.java @@ -35,7 +35,6 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.Lists; - import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; @@ -43,21 +42,18 @@ import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; import org.tmatesoft.svn.core.wc.SVNLogClient; import org.tmatesoft.svn.core.wc.SVNRevision; - import sonia.scm.repository.BlameLine; import sonia.scm.repository.BlameResult; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.SvnBlameHandler; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; -import java.io.IOException; - import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -65,37 +61,13 @@ import java.util.List; public class SvnBlameCommand extends AbstractSvnCommand implements BlameCommand { - /** - * Constructs ... - * - * - * - * @param context - * @param repository - * @param repositoryDirectory - */ public SvnBlameCommand(SvnContext context, Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override - public BlameResult getBlameResult(BlameCommandRequest request) - throws IOException, RepositoryException - { + public BlameResult getBlameResult(BlameCommandRequest request) { String path = request.getPath(); String revision = request.getRevision(); List<BlameLine> blameLines = Lists.newArrayList(); @@ -120,13 +92,13 @@ public class SvnBlameCommand extends AbstractSvnCommand implements BlameCommand SVNLogClient svnLogClient = new SVNLogClient(svnManager, null); svnLogClient.doAnnotate(svnurl, SVNRevision.UNDEFINED, - SVNRevision.create(1L), endRevision, + SVNRevision.create(1l), endRevision, new SvnBlameHandler(svnRepository, path, blameLines)); } catch (SVNException ex) { - throw new RepositoryException("could not create blame result", ex); + throw new InternalRepositoryException(repository, "could not create blame result", ex); } return new BlameResult(blameLines.size(), blameLines); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 1464d7eb96..99dae0e77b 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -35,32 +35,29 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.collect.Lists; - +import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.SVNDirEntry; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNNodeKind; import org.tmatesoft.svn.core.SVNProperties; import org.tmatesoft.svn.core.SVNProperty; import org.tmatesoft.svn.core.io.SVNRepository; - import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.SubRepository; import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import java.util.Collection; -import java.util.List; + +import static org.tmatesoft.svn.core.SVNErrorCode.FS_NO_SUCH_REVISION; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -76,45 +73,19 @@ public class SvnBrowseCommand extends AbstractSvnCommand private static final Logger logger = LoggerFactory.getLogger(SvnBrowseCommand.class); - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param context - * @param repository - * @param repositoryDirectory - */ SvnBrowseCommand(SvnContext context, Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override @SuppressWarnings("unchecked") - public BrowserResult getBrowserResult(BrowseCommandRequest request) - throws IOException, RepositoryException - { - String path = request.getPath(); - long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision()); + public BrowserResult getBrowserResult(BrowseCommandRequest request) { + String path = Strings.nullToEmpty(request.getPath()); + long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision(), repository); if (logger.isDebugEnabled()) { - logger.debug("browser repository {} in path {} at revision {}", repository.getName(), path, revisionNumber); + logger.debug("browser repository {} in path \"{}\" at revision {}", repository.getName(), path, revisionNumber); } BrowserResult result = null; @@ -122,33 +93,27 @@ public class SvnBrowseCommand extends AbstractSvnCommand try { SVNRepository svnRepository = open(); - Collection<SVNDirEntry> entries = - svnRepository.getDir(Util.nonNull(path), revisionNumber, null, - (Collection) null); - List<FileObject> children = Lists.newArrayList(); - String basePath = createBasePath(path); - if (request.isRecursive()) - { - browseRecursive(svnRepository, revisionNumber, request, children, - entries, basePath); - } - else - { - for (SVNDirEntry entry : entries) - { - children.add(createFileObject(request, svnRepository, revisionNumber, - entry, basePath)); - - } + if (revisionNumber == -1) { + revisionNumber = svnRepository.getLatestRevision(); } - result = new BrowserResult(); - result.setRevision(String.valueOf(revisionNumber)); - result.setFiles(children); + SVNDirEntry rootEntry = svnRepository.info(path, revisionNumber); + FileObject root = createFileObject(request, svnRepository, revisionNumber, rootEntry, path); + root.setPath(path); + + if (root.isDirectory()) { + traverse(svnRepository, revisionNumber, request, root, createBasePath(path)); + } + + + result = new BrowserResult(String.valueOf(revisionNumber), root); } catch (SVNException ex) { + if (FS_NO_SUCH_REVISION.equals(ex.getErrorMessage().getErrorCode())) { + throw notFound(entity("Revision", Long.toString(revisionNumber)).in(this.repository)); + } logger.error("could not open repository", ex); } @@ -157,52 +122,24 @@ public class SvnBrowseCommand extends AbstractSvnCommand //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param svnRepository - * @param revisionNumber - * @param request - * @param children - * @param entries - * @param basePath - * - * @throws SVNException - */ @SuppressWarnings("unchecked") - private void browseRecursive(SVNRepository svnRepository, - long revisionNumber, BrowseCommandRequest request, - List<FileObject> children, Collection<SVNDirEntry> entries, String basePath) + private void traverse(SVNRepository svnRepository, long revisionNumber, BrowseCommandRequest request, + FileObject parent, String basePath) throws SVNException { + Collection<SVNDirEntry> entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null); for (SVNDirEntry entry : entries) { - FileObject fo = createFileObject(request, svnRepository, revisionNumber, - entry, basePath); + FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); - children.add(fo); + parent.addChild(child); - if (fo.isDirectory()) - { - Collection<SVNDirEntry> subEntries = - svnRepository.getDir(Util.nonNull(fo.getPath()), revisionNumber, - null, (Collection) null); - - browseRecursive(svnRepository, revisionNumber, request, children, - subEntries, createBasePath(fo.getPath())); + if (child.isDirectory() && request.isRecursive()) { + traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); } } } - /** - * Method description - * - * - * @param path - * - * @return - */ private String createBasePath(String path) { String basePath = Util.EMPTY_STRING; @@ -220,23 +157,12 @@ public class SvnBrowseCommand extends AbstractSvnCommand return basePath; } - /** - * Method description - * - * - * - * - * @param request - * @param repository - * @param revision - * @param entry - * @param path - * - * @return - */ private FileObject createFileObject(BrowseCommandRequest request, SVNRepository repository, long revision, SVNDirEntry entry, String path) { + if (entry == null) { + throw notFound(entity("Path", path).in("Revision", Long.toString(revision)).in(this.repository)); + } FileObject fileObject = new FileObject(); fileObject.setName(entry.getName()); @@ -264,15 +190,6 @@ public class SvnBrowseCommand extends AbstractSvnCommand return fileObject; } - /** - * Method description - * - * - * @param repository - * @param revision - * @param entry - * @param fileObject - */ private void fetchExternalsProperty(SVNRepository repository, long revision, SVNDirEntry entry, FileObject fileObject) { @@ -293,7 +210,7 @@ public class SvnBrowseCommand extends AbstractSvnCommand } catch (SVNException ex) { - logger.error("could not fetch file properties"); + logger.error("could not fetch file properties", ex); } } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBundleCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBundleCommand.java index f0811c2c74..c3ce87decf 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBundleCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBundleCommand.java @@ -37,25 +37,22 @@ package sonia.scm.repository.spi; import com.google.common.io.ByteSink; import com.google.common.io.Closeables; - import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNRevision; import org.tmatesoft.svn.core.wc.admin.SVNAdminClient; - import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.SvnUtil; import sonia.scm.repository.api.BundleResponse; -import static com.google.common.base.Preconditions.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; import java.io.OutputStream; +import static com.google.common.base.Preconditions.checkNotNull; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra <s.sdorra@gmail.com> @@ -64,31 +61,11 @@ public class SvnBundleCommand extends AbstractSvnCommand implements BundleCommand { - /** - * Constructs ... - * - * - * @param context - * @param repository - */ public SvnBundleCommand(SvnContext context, Repository repository) { super(context, repository); } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param adminClient - * @param repository - * @param target - * - * @throws IOException - * @throws SVNException - */ private static void dump(SVNAdminClient adminClient, File repository, ByteSink target) throws SVNException, IOException @@ -98,7 +75,7 @@ public class SvnBundleCommand extends AbstractSvnCommand try { outputStream = target.openBufferedStream(); - adminClient.doDump(repository, outputStream, SVNRevision.create(-1L), + adminClient.doDump(repository, outputStream, SVNRevision.create(-1l), SVNRevision.HEAD, false, false); } finally @@ -107,21 +84,8 @@ public class SvnBundleCommand extends AbstractSvnCommand } } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override - public BundleResponse bundle(BundleCommandRequest request) - throws IOException, RepositoryException - { + public BundleResponse bundle(BundleCommandRequest request) throws IOException { ByteSink archive = checkNotNull(request.getArchive(), "archive is required"); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java index af08ea929e..9ee43b2259 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java @@ -37,22 +37,26 @@ package sonia.scm.repository.spi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNProperties; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.admin.SVNLookClient; - +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.SvnUtil; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.io.OutputStream; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -68,15 +72,6 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * - * @param context - * @param repository - * @param repositoryDirectory - */ SvnCatCommand(SvnContext context, Repository repository) { super(context, repository); @@ -84,20 +79,8 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * @param output - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void getCatResult(CatCommandRequest request, OutputStream output) - throws IOException, RepositoryException - { + public void getCatResult(CatCommandRequest request, OutputStream output) { if (logger.isDebugEnabled()) { logger.debug("try to get content for {}", request); @@ -114,26 +97,23 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand else { - long revisionNumber = SvnUtil.getRevisionNumber(revision); + long revisionNumber = SvnUtil.getRevisionNumber(revision, repository); getCatFromRevision(request, output, revisionNumber); } } - /** - * Method description - * - * - * @param request - * @param output - * @param revision - * - * @throws RepositoryException - */ - private void getCatFromRevision(CatCommandRequest request, - OutputStream output, long revision) - throws RepositoryException - { + @Override + public InputStream getCatResultStream(CatCommandRequest request) { + // There seems to be no method creating an input stream as a result, so + // we have no other possibility then to copy the content into a buffer and + // stream it from there. + ByteArrayOutputStream output = new ByteArrayOutputStream(); + getCatResult(request, output); + return new ByteArrayInputStream(output.toByteArray()); + } + + private void getCatFromRevision(CatCommandRequest request, OutputStream output, long revision) { logger.debug("try to read content from revision {} and path {}", revision, request.getPath()); @@ -146,24 +126,22 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand } catch (SVNException ex) { - throw new RepositoryException("could not get content from revision", ex); + handleSvnException(request, ex); } } - /** - * Method description - * - * - * @param request - * @param output - * @param txn - * - * @throws RepositoryException - */ - private void getCatFromTransaction(CatCommandRequest request, - OutputStream output, String txn) - throws RepositoryException - { + private void handleSvnException(CatCommandRequest request, SVNException ex) { + int svnErrorCode = ex.getErrorMessage().getErrorCode().getCode(); + if (SVNErrorCode.FS_NOT_FOUND.getCode() == svnErrorCode) { + throw notFound(entity("Path", request.getPath()).in("Revision", request.getRevision()).in(repository)); + } else if (SVNErrorCode.FS_NO_SUCH_REVISION.getCode() == svnErrorCode) { + throw notFound(entity("Revision", request.getRevision()).in(repository)); + } else { + throw new InternalRepositoryException(repository, "could not get content from revision", ex); + } + } + + private void getCatFromTransaction(CatCommandRequest request, OutputStream output, String txn) { logger.debug("try to read content from transaction {} and path {}", txn, request.getPath()); @@ -179,8 +157,7 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand } catch (SVNException ex) { - throw new RepositoryException("could not get content from transaction", - ex); + throw new InternalRepositoryException(repository, "could not get content from transaction", ex); } finally { diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java index 3a04ec1a2d..f8f6986c59 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java @@ -42,113 +42,57 @@ import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; +import sonia.scm.repository.Repository; import sonia.scm.repository.SvnUtil; //~--- JDK imports ------------------------------------------------------------ import java.io.Closeable; import java.io.File; -import java.io.IOException; /** * * @author Sebastian Sdorra */ -public class SvnContext implements Closeable -{ +public class SvnContext implements Closeable { - /** - * the logger for SvnContext - */ - private static final Logger logger = - LoggerFactory.getLogger(SvnContext.class); + private static final Logger LOG = LoggerFactory.getLogger(SvnContext.class); - //~--- constructors --------------------------------------------------------- + private final Repository repository; + private final File directory; - /** - * Constructs ... - * - * - * @param directory - */ - public SvnContext(File directory) - { + private SVNRepository svnRepository; + + public SvnContext(Repository repository, File directory) { + this.repository = repository; this.directory = directory; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @throws IOException - */ - @Override - public void close() throws IOException - { - if (logger.isTraceEnabled()) - { - logger.trace("close svn repository {}", directory); - } - - SvnUtil.closeSession(repository); - } - - /** - * Method description - * - * - * @return - * - * @throws SVNException - */ - public SVNURL createUrl() throws SVNException - { - return SVNURL.fromFile(directory); - } - - /** - * Method description - * - * - * @return - * - * @throws SVNException - */ - public SVNRepository open() throws SVNException - { - if (repository == null) - { - if (logger.isTraceEnabled()) - { - logger.trace("open svn repository {}", directory); - } - - repository = SVNRepositoryFactory.create(createUrl()); - } - + public Repository getRepository() { return repository; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public File getDirectory() - { + public File getDirectory() { return directory; } - //~--- fields --------------------------------------------------------------- + public SVNURL createUrl() throws SVNException { + return SVNURL.fromFile(directory); + } - /** Field description */ - private File directory; + public SVNRepository open() throws SVNException { + if (svnRepository == null) { + LOG.trace("open svn repository {}", directory); + svnRepository = SVNRepositoryFactory.create(createUrl()); + } + + return svnRepository; + } + + @Override + public void close() { + LOG.trace("close svn repository {}", directory); + SvnUtil.closeSession(svnRepository); + } - /** Field description */ - private SVNRepository repository; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java index f7af17326d..3bbde02844 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java @@ -1,19 +1,19 @@ -/** +/* * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + * <p> * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * <p> * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * <p> * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,48 +24,39 @@ * 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. - * + * <p> * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Preconditions; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.SVNDepth; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; -import org.tmatesoft.svn.core.wc.DefaultSVNDiffGenerator; -import org.tmatesoft.svn.core.wc.ISVNDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnNewDiffGenerator; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNDiffClient; import org.tmatesoft.svn.core.wc.SVNRevision; - +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.SvnUtil; +import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.DiffFormat; import sonia.scm.util.Util; //~--- JDK imports ------------------------------------------------------------ -import java.io.IOException; -import java.io.OutputStream; - /** * * @author Sebastian Sdorra */ -public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand -{ +public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand { /** * the logger for SvnDiffCommand @@ -73,87 +64,39 @@ public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand private static final Logger logger = LoggerFactory.getLogger(SvnDiffCommand.class); - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param context - * @param repository - * @param repositoryDirectory - */ - public SvnDiffCommand(SvnContext context, Repository repository) - { + public SvnDiffCommand(SvnContext context, Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param output - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) - throws IOException, RepositoryException - { - if (logger.isDebugEnabled()) - { - logger.debug("create diff for {}", request); - } - + public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) { + logger.debug("create diff for {}", request); Preconditions.checkNotNull(request, "request is required"); - Preconditions.checkNotNull(output, "outputstream is required"); String path = request.getPath(); - SVNClientManager clientManager = null; + return output -> { + SVNClientManager clientManager = null; + try { + SVNURL svnurl = context.createUrl(); + if (Util.isNotEmpty(path)) { + svnurl = svnurl.appendPath(path, true); + } + clientManager = SVNClientManager.newInstance(); + SVNDiffClient diffClient = clientManager.getDiffClient(); + diffClient.setDiffGenerator(new SvnNewDiffGenerator(new SCMSvnDiffGenerator())); - try - { - SVNURL svnurl = context.createUrl(); + long currentRev = SvnUtil.getRevisionNumber(request.getRevision(), repository); - if (Util.isNotEmpty(path)) - { - svnurl = svnurl.appendPath(path, true); + diffClient.setGitDiffFormat(request.getFormat() == DiffFormat.GIT); + + diffClient.doDiff(svnurl, SVNRevision.HEAD, + SVNRevision.create(currentRev - 1), SVNRevision.create(currentRev), + SVNDepth.INFINITY, false, output); + } catch (SVNException ex) { + throw new InternalRepositoryException(repository, "could not create diff", ex); + } finally { + SvnUtil.dispose(clientManager); } - - clientManager = SVNClientManager.newInstance(); - - SVNDiffClient diffClient = clientManager.getDiffClient(); - ISVNDiffGenerator diffGenerator = diffClient.getDiffGenerator(); - - if (diffGenerator == null) - { - diffGenerator = new DefaultSVNDiffGenerator(); - } - - diffGenerator.setDiffAdded(true); - diffGenerator.setDiffDeleted(true); - diffClient.setDiffGenerator(diffGenerator); - - long currentRev = SvnUtil.getRevisionNumber(request.getRevision()); - - diffClient.setGitDiffFormat(request.getFormat() == DiffFormat.GIT); - - diffClient.doDiff(svnurl, SVNRevision.HEAD, - SVNRevision.create(currentRev - 1), SVNRevision.create(currentRev), - SVNDepth.INFINITY, false, output); - } - catch (SVNException ex) - { - throw new RepositoryException("could not create diff", ex); - } - finally - { - SvnUtil.dispose(clientManager); - } + }; } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java index 41b44415a3..0cc8687154 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java @@ -37,33 +37,26 @@ package sonia.scm.repository.spi; import com.google.common.base.Strings; import com.google.common.collect.Lists; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.ISVNLogEntryHandler; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.io.SVNRepository; - import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import java.util.Collection; import java.util.List; -/** - * - * @author Sebastian Sdorra - */ +import static sonia.scm.repository.SvnUtil.parseRevision; + +//~--- JDK imports ------------------------------------------------------------ + public class SvnLogCommand extends AbstractSvnCommand implements LogCommand { @@ -73,17 +66,6 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand private static final Logger logger = LoggerFactory.getLogger(SvnLogCommand.class); - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param context - * @param repository - * @param repositoryDirectory - */ SvnLogCommand(SvnContext context, Repository repository) { super(context, repository); @@ -91,22 +73,9 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param revision - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override @SuppressWarnings("unchecked") - public Changeset getChangeset(String revision) - throws IOException, RepositoryException - { + public Changeset getChangeset(String revision, LogCommandRequest request) { Changeset changeset = null; if (logger.isDebugEnabled()) @@ -116,7 +85,7 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand try { - long revisioNumber = parseRevision(revision); + long revisioNumber = parseRevision(revision, repository); SVNRepository repo = open(); Collection<SVNLogEntry> entries = repo.log(null, null, revisioNumber, revisioNumber, true, true); @@ -128,28 +97,15 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand } catch (SVNException ex) { - throw new RepositoryException("could not open repository", ex); + throw new InternalRepositoryException(repository, "could not open repository", ex); } return changeset; } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ @Override @SuppressWarnings("unchecked") - public ChangesetPagingResult getChangesets(LogCommandRequest request) - throws IOException, RepositoryException - { + public ChangesetPagingResult getChangesets(LogCommandRequest request) { if (logger.isDebugEnabled()) { logger.debug("fetch changesets for {}", request); @@ -158,8 +114,8 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand ChangesetPagingResult changesets = null; int start = request.getPagingStart(); int limit = request.getPagingLimit(); - long startRevision = parseRevision(request.getStartChangeset()); - long endRevision = parseRevision(request.getEndChangeset()); + long startRevision = parseRevision(request.getStartChangeset(), repository); + long endRevision = parseRevision(request.getEndChangeset(), repository); String[] pathArray = null; if (!Strings.isNullOrEmpty(request.getPath())) @@ -183,43 +139,12 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand } catch (SVNException ex) { - throw new RepositoryException("could not open repository", ex); + throw new InternalRepositoryException(repository, "could not open repository", ex); } return changesets; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param v - * - * @return - * - * @throws RepositoryException - */ - private long parseRevision(String v) throws RepositoryException - { - long result = -1L; - - if (!Strings.isNullOrEmpty(v)) - { - try - { - result = Long.parseLong(v); - } - catch (NumberFormatException ex) - { - throw new RepositoryException( - String.format("could not convert revision %s", v), ex); - } - } - - return result; - } //~--- get methods ---------------------------------------------------------- @@ -252,7 +177,7 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand new ChangesetCollector(changesets)); } - return new ChangesetPagingResult((int) (latest + 1L), changesets); + return new ChangesetPagingResult((int) (latest + 1l), changesets); } /** @@ -279,7 +204,7 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand long endRev = Math.max(endRevision, 0); long maxRev = repo.getLatestRevision(); - if (startRevision >= 0L) + if (startRevision >= 0l) { startRev = startRevision; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java new file mode 100644 index 0000000000..580bc0b77d --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java @@ -0,0 +1,71 @@ +package sonia.scm.repository.spi; + +import lombok.extern.slf4j.Slf4j; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNLogEntry; +import org.tmatesoft.svn.core.io.SVNRepository; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.admin.SVNLookClient; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Modifications; +import sonia.scm.repository.Repository; +import sonia.scm.repository.SvnUtil; +import sonia.scm.util.Util; + +import java.util.Collection; + +@Slf4j +public class SvnModificationsCommand extends AbstractSvnCommand implements ModificationsCommand { + + SvnModificationsCommand(SvnContext context, Repository repository) { + super(context, repository); + } + + @Override + public Modifications getModifications(String revisionOrTransactionId) { + Modifications modifications; + try { + if (SvnUtil.isTransactionEntryId(revisionOrTransactionId)) { + modifications = getModificationsFromTransaction(SvnUtil.getTransactionId(revisionOrTransactionId)); + } else { + modifications = getModificationFromRevision(revisionOrTransactionId); + } + return modifications; + } catch (SVNException ex) { + throw new InternalRepositoryException( + repository, + "failed to get svn modifications for " + revisionOrTransactionId, + ex + ); + } + } + + @SuppressWarnings("unchecked") + private Modifications getModificationFromRevision(String revision) throws SVNException { + log.debug("get svn modifications from revision: {}", revision); + long revisionNumber = SvnUtil.getRevisionNumber(revision, repository); + SVNRepository repo = open(); + Collection<SVNLogEntry> entries = repo.log(null, null, revisionNumber, + revisionNumber, true, true); + if (Util.isNotEmpty(entries)) { + return SvnUtil.createModifications(entries.iterator().next(), revision); + } + return null; + } + + private Modifications getModificationsFromTransaction(String transaction) throws SVNException { + log.debug("get svn modifications from transaction: {}", transaction); + final Modifications modifications = new Modifications(); + SVNLookClient client = SVNClientManager.newInstance().getLookClient(); + client.doGetChanged(context.getDirectory(), transaction, + e -> SvnUtil.appendModification(modifications, e.getType(), e.getPath()), true); + + return modifications; + } + + @Override + public Modifications getModifications(ModificationsCommandRequest request) { + return getModifications(request.getRevision()); + } + +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java new file mode 100644 index 0000000000..8475cf7c48 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java @@ -0,0 +1,118 @@ +package sonia.scm.repository.spi; + +import org.tmatesoft.svn.core.SVNCommitInfo; +import org.tmatesoft.svn.core.SVNDepth; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.SVNWCClient; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.SvnWorkDirFactory; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +public class SvnModifyCommand implements ModifyCommand { + + private SvnContext context; + private SvnWorkDirFactory workDirFactory; + private Repository repository; + + SvnModifyCommand(SvnContext context, Repository repository, SvnWorkDirFactory workDirFactory) { + this.context = context; + this.repository = repository; + this.workDirFactory = workDirFactory; + } + + @Override + public String execute(ModifyCommandRequest request) { + SVNClientManager clientManager = SVNClientManager.newInstance(); + try (WorkingCopy<File, File> workingCopy = workDirFactory.createWorkingCopy(context, null)) { + File workingDirectory = workingCopy.getDirectory(); + modifyWorkingDirectory(request, clientManager, workingDirectory); + return commitChanges(clientManager, workingDirectory, request.getCommitMessage()); + } + } + + private String commitChanges(SVNClientManager clientManager, File workingDirectory, String commitMessage) { + try { + SVNCommitInfo svnCommitInfo = clientManager.getCommitClient().doCommit( + new File[]{workingDirectory}, + false, + commitMessage, + null, + null, + false, + true, + SVNDepth.INFINITY + ); + return String.valueOf(svnCommitInfo.getNewRevision()); + } catch (SVNException e) { + throw new InternalRepositoryException(repository, "could not commit changes on repository"); + } + } + + private void modifyWorkingDirectory(ModifyCommandRequest request, SVNClientManager clientManager, File workingDirectory) { + for (ModifyCommandRequest.PartialRequest partialRequest : request.getRequests()) { + try { + SVNWCClient wcClient = clientManager.getWCClient(); + partialRequest.execute(new ModifyWorker(wcClient, workingDirectory)); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "could not read files from repository"); + } + } + } + + private class ModifyWorker implements ModifyWorkerHelper { + private final SVNWCClient wcClient; + private final File workingDirectory; + + private ModifyWorker(SVNWCClient wcClient, File workingDirectory) { + this.wcClient = wcClient; + this.workingDirectory = workingDirectory; + } + + @Override + public void doScmDelete(String toBeDeleted) { + try { + wcClient.doDelete(new File(workingDirectory, toBeDeleted), true, true, false); + } catch (SVNException e) { + throw new InternalRepositoryException(repository, "could not delete file from repository"); + } + } + + @Override + public void addFileToScm(String name, Path file) { + try { + wcClient.doAdd( + file.toFile(), + true, + false, + true, + SVNDepth.INFINITY, + false, + true + ); + } catch (SVNException e) { + throw new InternalRepositoryException(repository, "could not add file to repository"); + } + } + + @Override + public File getWorkDir() { + return workingDirectory; + } + + @Override + public Repository getRepository() { + return repository; + } + + @Override + public String getBranch() { + return null; + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnPreReceiveHookChangesetProvier.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnPreReceiveHookChangesetProvier.java index 93fb88f841..e4efdb71ab 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnPreReceiveHookChangesetProvier.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnPreReceiveHookChangesetProvier.java @@ -37,22 +37,19 @@ package sonia.scm.repository.spi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.wc.ISVNOptions; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNWCUtil; import org.tmatesoft.svn.core.wc.admin.SVNLookClient; - import sonia.scm.repository.Changeset; import sonia.scm.repository.RepositoryHookType; -import sonia.scm.repository.SvnModificationHandler; import sonia.scm.repository.SvnUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -123,10 +120,6 @@ public class SvnPreReceiveHookChangesetProvier { changeset = SvnUtil.createChangeset(entry); changeset.setId(SvnUtil.createTransactionEntryId(transaction)); - - clientManager.doGetChanged(repositoryDirectory, transaction, - new SvnModificationHandler(changeset), true); - } else if (logger.isWarnEnabled()) { diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java index 24180bfe9e..b12b787122 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java @@ -33,19 +33,15 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.collect.ImmutableSet; import com.google.common.io.Closeables; - import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.SvnWorkDirFactory; import sonia.scm.repository.api.Command; -//~--- JDK imports ------------------------------------------------------------ - +import javax.inject.Inject; import java.io.IOException; - import java.util.Set; /** @@ -59,24 +55,19 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider //J- public static final Set<Command> COMMANDS = ImmutableSet.of( Command.BLAME, Command.BROWSE, Command.CAT, Command.DIFF, - Command.LOG, Command.BUNDLE, Command.UNBUNDLE + Command.LOG, Command.BUNDLE, Command.UNBUNDLE, Command.MODIFY ); //J+ //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param handler - * @param repository - */ + @Inject SvnRepositoryServiceProvider(SvnRepositoryHandler handler, - Repository repository) + Repository repository, SvnWorkDirFactory workdirFactory) { this.repository = repository; - this.context = new SvnContext(handler.getDirectory(repository)); + this.context = new SvnContext(repository, handler.getDirectory(repository.getId())); + this.workDirFactory = workdirFactory; } //~--- methods -------------------------------------------------------------- @@ -167,6 +158,14 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider return new SvnLogCommand(context, repository); } + public ModificationsCommand getModificationsCommand() { + return new SvnModificationsCommand(context, repository); + } + + public ModifyCommand getModifyCommand() { + return new SvnModifyCommand(context, repository, workDirFactory); + } + /** * Method description * @@ -198,4 +197,6 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider /** Field description */ private final Repository repository; + + private final SvnWorkDirFactory workDirFactory; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java index c46c6722be..05097ff111 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java @@ -32,64 +32,32 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.Inject; - import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.SvnWorkDirFactory; -/** - * - * @author Sebastian Sdorra - */ @Extension -public class SvnRepositoryServiceResolver implements RepositoryServiceResolver -{ +public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { - /** Field description */ - public static final String TYPE = "svn"; + private SvnRepositoryHandler handler; + private SvnWorkDirFactory workdirFactory; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param handler - */ @Inject - public SvnRepositoryServiceResolver(SvnRepositoryHandler handler) - { + public SvnRepositoryServiceResolver(SvnRepositoryHandler handler, SvnWorkDirFactory workdirFactory) { this.handler = handler; + this.workdirFactory = workdirFactory; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * - * @return - */ @Override - public SvnRepositoryServiceProvider reslove(Repository repository) - { + public SvnRepositoryServiceProvider resolve(Repository repository) { SvnRepositoryServiceProvider provider = null; - if (TYPE.equalsIgnoreCase(repository.getType())) - { - provider = new SvnRepositoryServiceProvider(handler, repository); + if (SvnRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { + provider = new SvnRepositoryServiceProvider(handler, repository, workdirFactory); } return provider; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private SvnRepositoryHandler handler; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java deleted file mode 100644 index 1d9581f7c1..0000000000 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java +++ /dev/null @@ -1,102 +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.repository.SvnUtil; -import sonia.scm.util.HttpUtil; -import sonia.scm.web.filter.AuthenticationFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * - * @author Sebastian Sdorra - */ -@Priority(Filters.PRIORITY_AUTHENTICATION) -@WebElement(value = SvnServletModule.PATTERN_SVN) -public class SvnBasicAuthenticationFilter extends AuthenticationFilter -{ - - /** - * Constructs ... - * - * - * @param configuration - * @param webTokenGenerators - */ - @Inject - public SvnBasicAuthenticationFilter(ScmConfiguration configuration, Set<WebTokenGenerator> webTokenGenerators) { - super(configuration, webTokenGenerators); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Sends unauthorized instead of forbidden for svn clients, because the - * svn client prompts again for authentication. - * - * - * @param request http request - * @param response http response - * - * @throws IOException - */ - @Override - protected void sendFailedAuthenticationError(HttpServletRequest request, - HttpServletResponse response) - throws IOException - { - if (SvnUtil.isSvnClient(request)) - { - HttpUtil.sendUnauthorized(response, configuration.getRealmDescription()); - } - else - { - super.sendFailedAuthenticationError(request, response); - } - } -} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnCollectionRenderer.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnCollectionRenderer.java index 2087ed8e10..2808c2b384 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnCollectionRenderer.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnCollectionRenderer.java @@ -38,8 +38,10 @@ import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.Ordering; import com.google.inject.Inject; import com.google.inject.Provider; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.tmatesoft.svn.core.SVNDirEntry; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; @@ -48,23 +50,23 @@ import org.tmatesoft.svn.core.internal.server.dav.CollectionRenderer; import org.tmatesoft.svn.core.internal.server.dav.DAVPathUtil; import org.tmatesoft.svn.core.internal.server.dav.DAVResource; import org.tmatesoft.svn.core.internal.server.dav.DAVResourceURI; + import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryProvider; import sonia.scm.template.Template; import sonia.scm.template.TemplateEngine; import sonia.scm.template.TemplateEngineFactory; -import sonia.scm.url.RepositoryUrlProvider; -import sonia.scm.url.UrlProvider; -import sonia.scm.url.UrlProviderFactory; import sonia.scm.util.HttpUtil; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ + import java.io.IOException; import java.io.StringWriter; -import java.util.List; -//~--- JDK imports ------------------------------------------------------------ +import java.util.Iterator; +import java.util.List; +import javax.servlet.http.HttpServletRequest; /** * @@ -169,30 +171,23 @@ public class SvnCollectionRenderer implements CollectionRenderer entries.add(new DirectoryEntry("..", parent, true)); } - for (final Object o : resource.getEntries()) { - SVNDirEntry entry = (SVNDirEntry) o; + for (Iterator iterator = resource.getEntries().iterator(); + iterator.hasNext(); ) + { + SVNDirEntry entry = (SVNDirEntry) iterator.next(); entries.add(new DirectoryEntry(resource, entry)); } - UrlProvider urlProvider = createUrlProvider(); - //J- return new RepositoryWrapper( - urlProvider.getRepositoryUrlProvider(), repositoryProvider.get(), resource, new DirectoryOrdering().immutableSortedCopy(entries.build()) ); //J+ } - - private UrlProvider createUrlProvider() { - String baseUrl = getBaseUrl(); - logger.trace("render subversion collection with base url: {}", baseUrl); - return UrlProviderFactory.createUrlProvider(baseUrl, UrlProviderFactory.TYPE_WUI); - } - + private String getBaseUrl() { return HttpUtil.getCompleteUrl(requestProvider.get()); } @@ -388,15 +383,12 @@ public class SvnCollectionRenderer implements CollectionRenderer * * * - * @param repositoryUrlProvider * @param repository * @param resource * @param entries */ - public RepositoryWrapper(RepositoryUrlProvider repositoryUrlProvider, - Repository repository, DAVResource resource, List<DirectoryEntry> entries) + public RepositoryWrapper(Repository repository, DAVResource resource, List<DirectoryEntry> entries) { - this.repositoryUrlProvider = repositoryUrlProvider; this.repository = repository; this.resource = resource; this.entries = entries; @@ -404,17 +396,6 @@ public class SvnCollectionRenderer implements CollectionRenderer //~--- get methods -------------------------------------------------------- - /** - * Method description - * - * - * @return - */ - public String getCommitViewLink() - { - return repositoryUrlProvider.getChangesetUrl(repository.getId(), 0, 20); - } - /** * Method description * @@ -448,18 +429,6 @@ public class SvnCollectionRenderer implements CollectionRenderer return repository; } - /** - * Method description - * - * - * @return - */ - public String getSourceViewLink() - { - return repositoryUrlProvider.getBrowseUrl(repository.getId(), - resource.getResourceURI().getPath(), null); - } - //~--- fields ------------------------------------------------------------- /** Field description */ @@ -468,9 +437,6 @@ public class SvnCollectionRenderer implements CollectionRenderer /** Field description */ private final Repository repository; - /** Field description */ - private final RepositoryUrlProvider repositoryUrlProvider; - /** Field description */ private final DAVResource resource; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVConfig.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVConfig.java index b220737ecb..a544e8051b 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVConfig.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVConfig.java @@ -292,7 +292,7 @@ public class SvnDAVConfig extends DAVConfig if (repository != null) { - directory = handler.getDirectory(repository); + directory = handler.getDirectory(repository.getId()); } return directory; diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java index db22857595..92d01db5a1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java @@ -33,39 +33,32 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.Inject; import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.internal.server.dav.DAVConfig; import org.tmatesoft.svn.core.internal.server.dav.DAVServlet; - import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryRequestListenerUtil; import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.util.AssertUtil; import sonia.scm.util.HttpUtil; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; /** * * @author Sebastian Sdorra */ @Singleton -public class SvnDAVServlet extends DAVServlet +public class SvnDAVServlet extends DAVServlet implements ScmProviderHttpServlet { /** Field description */ @@ -114,28 +107,18 @@ public class SvnDAVServlet extends DAVServlet * @throws ServletException */ @Override - public void service(HttpServletRequest request, HttpServletResponse response) + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { - Repository repository = repositoryProvider.get(); - - if (repository != null) - { - if (repositoryRequestListenerUtil.callListeners(request, response, - repository)) - { - super.service(new SvnHttpServletRequestWrapper(request, - repositoryProvider), response); - } - else if (logger.isDebugEnabled()) - { - logger.debug("request aborted by repository request listener"); - } - } - else + if (repositoryRequestListenerUtil.callListeners(request, response, + repository)) { super.service(new SvnHttpServletRequestWrapper(request, - repositoryProvider), response); + repository), response); + } + else if (logger.isDebugEnabled()) + { + logger.debug("request aborted by repository request listener"); } } @@ -167,18 +150,11 @@ public class SvnDAVServlet extends DAVServlet extends HttpServletRequestWrapper { - /** - * Constructs ... - * - * - * @param request - * @param repositoryProvider - */ public SvnHttpServletRequestWrapper(HttpServletRequest request, - RepositoryProvider repositoryProvider) + Repository repository) { super(request); - this.repositoryProvider = repositoryProvider; + this.repository = repository; } //~--- get methods -------------------------------------------------------- @@ -215,8 +191,6 @@ public class SvnDAVServlet extends DAVServlet AssertUtil.assertIsNotEmpty(pathInfo); - Repository repository = repositoryProvider.get(); - if (repository != null) { if (pathInfo.startsWith(HttpUtil.SEPARATOR_PATH)) @@ -224,7 +198,7 @@ public class SvnDAVServlet extends DAVServlet pathInfo = pathInfo.substring(1); } - pathInfo = pathInfo.substring(repository.getName().length()); + pathInfo = pathInfo.substring(repository.getNamespace().length() + 1 + repository.getName().length()); } return pathInfo; @@ -240,7 +214,6 @@ public class SvnDAVServlet extends DAVServlet public String getServletPath() { String servletPath = super.getServletPath(); - Repository repository = repositoryProvider.get(); if (repository != null) { @@ -249,7 +222,7 @@ public class SvnDAVServlet extends DAVServlet servletPath = servletPath.concat(HttpUtil.SEPARATOR_PATH); } - servletPath = servletPath.concat(repository.getName()); + servletPath = servletPath + repository.getNamespace() + "/" + repository.getName(); } return servletPath; @@ -284,10 +257,9 @@ public class SvnDAVServlet extends DAVServlet //~--- fields ------------------------------------------------------------- /** Field description */ - private final RepositoryProvider repositoryProvider; + private final Repository repository; } - //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServletProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServletProvider.java new file mode 100644 index 0000000000..d221504256 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServletProvider.java @@ -0,0 +1,23 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletProvider; + +import javax.inject.Provider; + +public class SvnDAVServletProvider extends ScmProviderHttpServletProvider { + + @Inject + private Provider<SvnDAVServlet> servletProvider; + + public SvnDAVServletProvider() { + super(SvnRepositoryHandler.TYPE_NAME); + } + + @Override + protected ScmProviderHttpServlet getRootServlet() { + return servletProvider.get(); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java index 65afefbb28..4352299ed5 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java @@ -32,110 +32,51 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; -import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import sonia.scm.filter.GZipFilter; +import sonia.scm.filter.GZipFilterConfig; +import sonia.scm.filter.GZipResponseWrapper; +import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.util.WebUtil; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; -/** - * - * @author Sebastian Sdorra - */ -@Singleton -public class SvnGZipFilter extends GZipFilter -{ +class SvnGZipFilter implements ScmProviderHttpServlet { - /** - * the logger for SvnGZipFilter - */ - private static final Logger logger = - LoggerFactory.getLogger(SvnGZipFilter.class); + private static final Logger logger = LoggerFactory.getLogger(SvnGZipFilter.class); - //~--- constructors --------------------------------------------------------- + private final SvnRepositoryHandler handler; + private final ScmProviderHttpServlet delegate; - /** - * Constructs ... - * - * - * @param handler - */ - @Inject - public SvnGZipFilter(SvnRepositoryHandler handler) - { + private GZipFilterConfig config = new GZipFilterConfig(); + + SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate) { this.handler = handler; + this.delegate = delegate; + config.setBufferResponse(false); } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param filterConfig - * - * @throws ServletException - */ @Override - public void init(FilterConfig filterConfig) throws ServletException - { - super.init(filterConfig); - getConfig().setBufferResponse(false); - } - - /** - * Method description - * - * - * @param request - * @param response - * @param chain - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void doFilter(HttpServletRequest request, - HttpServletResponse response, FilterChain chain) - throws IOException, ServletException - { - if (handler.getConfig().isEnabledGZip()) - { - if (logger.isTraceEnabled()) - { - logger.trace("encode svn request with gzip"); - } - - super.doFilter(request, response, chain); - } - else - { - if (logger.isTraceEnabled()) - { - logger.trace("skip gzip encoding"); - } - - chain.doFilter(request, response); + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { + if (handler.getConfig().isEnabledGZip() && WebUtil.isGzipSupported(request)) { + logger.trace("compress svn response with gzip"); + GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response, config); + delegate.service(request, wrappedResponse, repository); + wrappedResponse.finishResponse(); + } else { + logger.trace("skip gzip encoding"); + delegate.service(request, response, repository); } } - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private SvnRepositoryHandler handler; + @Override + public void init(ServletConfig config) throws ServletException { + delegate.init(config); + } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilterFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilterFactory.java new file mode 100644 index 0000000000..e2774106fa --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilterFactory.java @@ -0,0 +1,28 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +@Extension +public class SvnGZipFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final SvnRepositoryHandler handler; + + @Inject + public SvnGZipFilterFactory(SvnRepositoryHandler handler) { + this.handler = handler; + } + + @Override + public boolean handlesScmType(String type) { + return SvnRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new SvnGZipFilter(handler, delegate); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java index 30ef3e94c0..2c0a1e65ff 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java @@ -33,37 +33,24 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.collect.ImmutableSet; -import com.google.inject.Inject; - import sonia.scm.ClientMessages; -import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; -import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.ScmSvnErrorCode; import sonia.scm.repository.SvnUtil; -import sonia.scm.web.filter.ProviderPermissionFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.web.filter.PermissionFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; /** * * @author Sebastian Sdorra */ -@Priority(Filters.PRIORITY_AUTHORIZATION) -@WebElement(value = SvnServletModule.PATTERN_SVN) -public class SvnPermissionFilter extends ProviderPermissionFilter +public class SvnPermissionFilter extends PermissionFilter { /** Field description */ @@ -77,13 +64,10 @@ public class SvnPermissionFilter extends ProviderPermissionFilter * Constructs ... * * @param configuration - * @param repository */ - @Inject - public SvnPermissionFilter(ScmConfiguration configuration, - RepositoryProvider repository) + public SvnPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) { - super(configuration, repository); + super(configuration, delegate); } //~--- methods -------------------------------------------------------------- @@ -132,7 +116,7 @@ public class SvnPermissionFilter extends ProviderPermissionFilter * @return */ @Override - protected boolean isWriteRequest(HttpServletRequest request) + public boolean isWriteRequest(HttpServletRequest request) { return WRITEMETHOD_SET.contains(request.getMethod().toUpperCase()); } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilterFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilterFactory.java new file mode 100644 index 0000000000..882cb8c54f --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilterFactory.java @@ -0,0 +1,30 @@ +package sonia.scm.web; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +import javax.inject.Inject; + +@Extension +public class SvnPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final ScmConfiguration configuration; + + @Inject + public SvnPermissionFilterFactory(ScmConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public boolean handlesScmType(String type) { + return SvnRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new SvnPermissionFilter(configuration, delegate); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnScmProtocolProviderWrapper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnScmProtocolProviderWrapper.java new file mode 100644 index 0000000000..ba7d5e875a --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnScmProtocolProviderWrapper.java @@ -0,0 +1,74 @@ +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.SvnRepositoryHandler; +import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; +import sonia.scm.repository.spi.ScmProviderHttpServlet; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import java.util.Enumeration; + +@Singleton +@Extension +public class SvnScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper { + + public static final String PARAMETER_SVN_PARENTPATH = "SVNParentPath"; + + @Override + public String getType() { + return SvnRepositoryHandler.TYPE_NAME; + } + + @Inject + public SvnScmProtocolProviderWrapper(SvnDAVServletProvider servletProvider, Provider<ScmPathInfoStore> uriInfoStore, ScmConfiguration scmConfiguration) { + super(servletProvider, uriInfoStore, scmConfiguration); + } + + @Override + protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException { + + super.initializeServlet(new SvnConfigEnhancer(config), httpServlet); + } + + private static class SvnConfigEnhancer implements ServletConfig { + + private final ServletConfig originalConfig; + + private SvnConfigEnhancer(ServletConfig originalConfig) { + this.originalConfig = originalConfig; + } + + @Override + public String getServletName() { + return originalConfig.getServletName(); + } + + @Override + public ServletContext getServletContext() { + return originalConfig.getServletContext(); + } + + @Override + /** + * Overridden to return the systems temp directory for the key {@link PARAMETER_SVN_PARENTPATH}. + */ + public String getInitParameter(String key) { + if (PARAMETER_SVN_PARENTPATH.equals(key)) { + return System.getProperty("java.io.tmpdir"); + } + return originalConfig.getInitParameter(key); + } + + @Override + public Enumeration getInitParameterNames() { + return originalConfig.getInitParameterNames(); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java index b55588969f..b4f0aaf920 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java @@ -33,48 +33,25 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.servlet.ServletModule; - +import org.mapstruct.factory.Mappers; +import sonia.scm.api.v2.resources.SvnConfigDtoToSvnConfigMapper; +import sonia.scm.api.v2.resources.SvnConfigToSvnConfigDtoMapper; import sonia.scm.plugin.Extension; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.HashMap; -import java.util.Map; +import sonia.scm.repository.SvnWorkDirFactory; +import sonia.scm.repository.spi.SimpleSvnWorkDirFactory; /** * * @author Sebastian Sdorra */ @Extension -public class SvnServletModule extends ServletModule -{ +public class SvnServletModule extends ServletModule { - /** Field description */ - public static final String PARAMETER_SVN_PARENTPATH = "SVNParentPath"; - - /** Field description */ - public static final String PATTERN_SVN = "/svn/*"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ @Override - protected void configureServlets() - { - filter(PATTERN_SVN).through(SvnGZipFilter.class); - filter(PATTERN_SVN).through(SvnBasicAuthenticationFilter.class); - filter(PATTERN_SVN).through(SvnPermissionFilter.class); - - Map<String, String> parameters = new HashMap<String, String>(); - - parameters.put(PARAMETER_SVN_PARENTPATH, - System.getProperty("java.io.tmpdir")); - serve(PATTERN_SVN).with(SvnDAVServlet.class, parameters); + protected void configureServlets() { + bind(SvnConfigDtoToSvnConfigMapper.class).to(Mappers.getMapper(SvnConfigDtoToSvnConfigMapper.class).getClass()); + bind(SvnConfigToSvnConfigDtoMapper.class).to(Mappers.getMapper(SvnConfigToSvnConfigDtoMapper.class).getClass()); + bind(SvnWorkDirFactory.class).to(SimpleSvnWorkDirFactory.class); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnVndMediaType.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnVndMediaType.java new file mode 100644 index 0000000000..1e294df1fe --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnVndMediaType.java @@ -0,0 +1,8 @@ +package sonia.scm.web; + +public class SvnVndMediaType { + public static final String SVN_CONFIG = VndMediaType.PREFIX + "svnConfig" + VndMediaType.SUFFIX; + + private SvnVndMediaType() { + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.tsx b/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.tsx new file mode 100644 index 0000000000..f5b6941562 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { withTranslation, WithTranslation } from "react-i18next"; +import { Repository } from "@scm-manager/ui-types"; +import { repositories } from "@scm-manager/ui-components"; + +type Props = WithTranslation & { + repository: Repository; +}; + +class ProtocolInformation extends React.Component<Props> { + render() { + const { repository, t } = this.props; + const href = repositories.getProtocolLinkByType(repository, "http"); + if (!href) { + return null; + } + return ( + <div> + <h4>{t("scm-svn-plugin.information.checkout")}</h4> + <pre> + <code>svn checkout {href}</code> + </pre> + </div> + ); + } +} + +export default withTranslation("plugins")(ProtocolInformation); diff --git a/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.tsx b/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.tsx new file mode 100644 index 0000000000..1a2bc347e7 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Image } from "@scm-manager/ui-components"; + +type Props = {}; + +class SvnAvatar extends React.Component<Props> { + render() { + return <Image src="/images/svn-logo.gif" alt="Subversion Logo" />; + } +} + +export default SvnAvatar; diff --git a/scm-plugins/scm-svn-plugin/src/main/js/SvnConfigurationForm.tsx b/scm-plugins/scm-svn-plugin/src/main/js/SvnConfigurationForm.tsx new file mode 100644 index 0000000000..31ffdbd78c --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/js/SvnConfigurationForm.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { withTranslation, WithTranslation } from "react-i18next"; +import { Links } from "@scm-manager/ui-types"; +import { Checkbox, Select } from "@scm-manager/ui-components"; + +type Configuration = { + compatibility: string; + enabledGZip: boolean; + _links: Links; +}; + +type Props = WithTranslation & { + initialConfiguration: Configuration; + readOnly: boolean; + + onConfigurationChange: (p1: Configuration, p2: boolean) => void; +}; + +type State = Configuration; + +class SvnConfigurationForm extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + ...props.initialConfiguration + }; + } + + handleChange = (value: any, name?: string) => { + if (!name) { + throw new Error("required name not set"); + } + this.setState( + // @ts-ignore + { + [name]: value + }, + () => this.props.onConfigurationChange(this.state, true) + ); + }; + + compatibilityOptions = (values: string[]) => { + const options = []; + for (const value of values) { + options.push(this.compatibilityOption(value)); + } + return options; + }; + + compatibilityOption = (value: string) => { + return { + value, + label: this.props.t("scm-svn-plugin.config.compatibility-values." + value.toLowerCase()) + }; + }; + + render() { + const { readOnly, t } = this.props; + const compatibilityOptions = this.compatibilityOptions(["NONE", "PRE14", "PRE15", "PRE16", "PRE17", "WITH17"]); + + return ( + <> + <Select + name="compatibility" + label={t("scm-svn-plugin.config.compatibility")} + helpText={t("scm-svn-plugin.config.compatibilityHelpText")} + value={this.state.compatibility} + options={compatibilityOptions} + onChange={this.handleChange} + /> + <Checkbox + name="enabledGZip" + label={t("scm-svn-plugin.config.enabledGZip")} + helpText={t("scm-svn-plugin.config.enabledGZipHelpText")} + checked={this.state.enabledGZip} + onChange={this.handleChange} + disabled={readOnly} + /> + </> + ); + } +} + +export default withTranslation("plugins")(SvnConfigurationForm); diff --git a/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.tsx b/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.tsx new file mode 100644 index 0000000000..0093449045 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Title, Configuration } from "@scm-manager/ui-components"; +import SvnConfigurationForm from "./SvnConfigurationForm"; + +type Props = WithTranslation & { + link: string; +}; + +class SvnGlobalConfiguration extends React.Component<Props> { + render() { + const { link, t } = this.props; + return ( + <div> + <Title title={t("scm-svn-plugin.config.title")} /> + <Configuration link={link} render={(props: any) => <SvnConfigurationForm {...props} />} /> + </div> + ); + } +} + +export default withTranslation("plugins")(SvnGlobalConfiguration); diff --git a/scm-plugins/scm-svn-plugin/src/main/js/index.ts b/scm-plugins/scm-svn-plugin/src/main/js/index.ts new file mode 100644 index 0000000000..122e1bbf3d --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/js/index.ts @@ -0,0 +1,16 @@ +import { binder } from "@scm-manager/ui-extensions"; +import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components"; +import ProtocolInformation from "./ProtocolInformation"; +import SvnAvatar from "./SvnAvatar"; +import SvnGlobalConfiguration from "./SvnGlobalConfiguration"; + +const svnPredicate = (props: any) => { + return props.repository && props.repository.type === "svn"; +}; + +binder.bind("repos.repository-details.information", ProtocolInformation, svnPredicate); +binder.bind("repos.repository-avatar", SvnAvatar, svnPredicate); + +// bind global configuration + +cfgBinder.bindGlobal("/svn", "scm-svn-plugin.config.link", "svnConfig", SvnGlobalConfiguration); diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml new file mode 100644 index 0000000000..602b1606e6 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, Sebastian Sdorra + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of SCM-Manager; nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.dd7s + + http://bitbucket.org/sdorra/scm-manager + + +--> +<permissions> + + <permission> + <value>configuration:read,write:svn</value> + </permission> + <permission> + <value>repository:svn:*</value> + </permission> + +</permissions> diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/plugin.xml b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/plugin.xml index 86b4f4f843..14e77b42a4 100644 --- a/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/plugin.xml +++ b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/plugin.xml @@ -46,22 +46,18 @@ <scm-version>2</scm-version> <information> - <author>Sebastian Sdorra</author> - <category>Subversion</category> - <tags> - <tag>subversion</tag> - <tag>scm</tag> - <tag>vcs</tag> - <tag>svn</tag> - </tags> + <displayName>Subversion</displayName> + <author>Cloudogu GmbH</author> + <category>Source Code Management</category> + <avatarUrl>/images/svn-logo.gif</avatarUrl> </information> <conditions> <min-version>${project.parent.version}</min-version> </conditions> - + <resources> - <script>/sonia/scm/svn.config.js</script> + <script>assets/scm-svn-plugin.bundle.js</script> </resources> -</plugin> \ No newline at end of file +</plugin> diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/repository-permissions.xml b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/repository-permissions.xml new file mode 100644 index 0000000000..7c7cd48b79 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/repository-permissions.xml @@ -0,0 +1,7 @@ +<repository-permissions> + <verbs> + <verb>svn</verb> + </verbs> + <roles> + </roles> +</repository-permissions> diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json new file mode 100644 index 0000000000..1b27a23564 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json @@ -0,0 +1,52 @@ +{ + "scm-svn-plugin": { + "information": { + "checkout": "Repository auschecken" + }, + "config": { + "link": "Subversion", + "title": "Subversion Konfiguration", + "compatibility": "Version Kompatibilität", + "compatibilityHelpText": "Gibt an, mit welcher Subversion Version die Repositories kompatibel sind.", + "compatibility-values": { + "none": "Keine Kompatibilität", + "pre14": "Vor 1.4 kompatibel", + "pre15": "Vor 1.5 kompatibel", + "pre16": "Vor 1.6 kompatibel", + "pre17": "Vor 1.7 kompatibel", + "with17": "Mit 1.7 kompatibel" + }, + "enabledGZip": "GZip Compression aktivieren", + "enabledGZipHelpText": "Aktiviert GZip Kompression für SVN Responses", + "disabled": "Deaktiviert", + "disabledHelpText": "Aktiviert oder deaktiviert das SVN Plugin", + "required": "Dieser Konfigurationswert wird benötigt" + } + }, + "permissions": { + "configuration": { + "read,write": { + "svn": { + "displayName": "Subversion Konfiguration ändern", + "description": "Darf die Subversion Konfiguration verändern" + } + } + }, + "repository": { + "svn": { + "*": { + "displayName": "Repository-spezifische Subversion Konfiguration ändern", + "description": "Darf die Subversion Konfiguration für alle Repositories verändern." + } + } + } + }, + "verbs": { + "repository": { + "svn": { + "displayName": "Subversion konfigurieren", + "description": "Darf die Subversion Konfiguration für dieses Repository verändern." + } + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json new file mode 100644 index 0000000000..0d487e1f3d --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json @@ -0,0 +1,52 @@ +{ + "scm-svn-plugin": { + "information": { + "checkout": "Checkout repository" + }, + "config": { + "link": "Subversion", + "title": "Subversion Configuration", + "compatibility": "Version Compatibility", + "compatibilityHelpText": "Specifies with which Subversion version repositories are compatible.", + "compatibility-values": { + "none": "No compatibility", + "pre14": "Pre 1.4 Compatible", + "pre15": "Pre 1.5 Compatible", + "pre16": "Pre 1.6 Compatible", + "pre17": "Pre 1.7 Compatible", + "with17": "With 1.7 Compatible" + }, + "enabledGZip": "Enable GZip Compression", + "enabledGZipHelpText": "Enable GZip compression for SVN responses.", + "disabled": "Disabled", + "disabledHelpText": "Enable or disable the SVN plugin", + "required": "This configuration value is required" + } + }, + "permissions": { + "configuration": { + "read,write": { + "svn": { + "displayName": "Modify Subversion configuration", + "description": "May modify the Subversion configuration" + } + } + }, + "repository": { + "svn": { + "*": { + "displayName": "Modify repository specific Subversion configuration", + "description": "May change the Subversion configuration for repositories" + } + } + } + }, + "verbs": { + "repository": { + "svn": { + "displayName": "configure Subversion", + "description": "May change the Subversion configuration for this repository" + } + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/sonia/scm/svn.config.js b/scm-plugins/scm-svn-plugin/src/main/resources/sonia/scm/svn.config.js deleted file mode 100644 index 4d59f41807..0000000000 --- a/scm-plugins/scm-svn-plugin/src/main/resources/sonia/scm/svn.config.js +++ /dev/null @@ -1,159 +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 - * - */ - -Ext.ns("Sonia.svn"); - -Sonia.svn.ConfigPanel = Ext.extend(Sonia.config.SimpleConfigForm, { - - // labels - titleText: 'Subversion Settings', - repositoryDirectoryText: 'Repository directory', - noneCompatibility: 'No compatibility modus', - pre14CompatibleText: 'Pre 1.4 Compatible', - pre15CompatibleText: 'Pre 1.5 Compatible', - pre16CompatibleText: 'Pre 1.6 Compatible', - pre17CompatibleText: 'Pre 1.7 Compatible', - with17CompatibleText: 'With 1.7 Compatible', - enableGZipText: 'Enable GZip Encoding', - disabledText: 'Disabled', - - // helpTexts - repositoryDirectoryHelpText: 'Location of the Suberversion repositories.', - disabledHelpText: 'Enable or disable the Subversion plugin.\n\ - Note you have to reload the page, after changing this value.', - enableGZipHelpText: 'Enable GZip encoding for svn responses.', - - initComponent: function(){ - - var config = { - title : this.titleText, - configUrl: restUrl + 'config/repositories/svn.json', - items : [{ - xtype: 'textfield', - name: 'repositoryDirectory', - fieldLabel: this.repositoryDirectoryText, - helpText: this.repositoryDirectoryHelpText, - allowBlank : false - },{ - xtype: 'radiogroup', - name: 'compatibility', - columns: 1, - items: [{ - boxLabel: this.noneCompatibility, - inputValue: 'NONE', - name: 'compatibility' - },{ - boxLabel: this.pre14CompatibleText, - inputValue: 'PRE14', - name: 'compatibility' - },{ - boxLabel: this.pre15CompatibleText, - inputValue: 'PRE15', - name: 'compatibility' - },{ - boxLabel: this.pre16CompatibleText, - inputValue: 'PRE16', - name: 'compatibility' - },{ - boxLabel: this.pre17CompatibleText, - inputValue: 'PRE17', - name: 'compatibility' - },{ - boxLabel: this.with17CompatibleText, - inputValue: 'WITH17', - name: 'compatibility' - }] - },{ - xtype: 'checkbox', - name: 'enable-gzip', - fieldLabel: this.enableGZipText, - inputValue: 'true', - helpText: this.enableGZipHelpText - },{ - xtype: 'checkbox', - name: 'disabled', - fieldLabel: this.disabledText, - inputValue: 'true', - helpText: this.disabledHelpText - }] - }; - - Ext.apply(this, Ext.apply(this.initialConfig, config)); - Sonia.svn.ConfigPanel.superclass.initComponent.apply(this, arguments); - } - -}); - -Ext.reg("svnConfigPanel", Sonia.svn.ConfigPanel); - -// i18n - -if ( i18n && i18n.country === 'de' ){ - - Ext.override(Sonia.svn.ConfigPanel, { - - // labels - titleText: 'Subversion Einstellungen', - repositoryDirectoryText: 'Repository-Verzeichnis', - noneCompatibility: 'Kein Kompatiblitätsmodus', - pre14CompatibleText: 'Mit Versionen vor 1.4 kompatibel', - pre15CompatibleText: 'Mit Versionen vor 1.5 kompatibel', - pre16CompatibleText: 'Mit Versionen vor 1.6 kompatibel', - pre17CompatibleText: 'Mit Versionen vor 1.7 kompatibel', - with17CompatibleText: 'Mit Version 1.7 kompatibel', - disabledText: 'Deaktivieren', - - // helpTexts - repositoryDirectoryHelpText: 'Verzeichnis der Subversion-Repositories.', - disabledHelpText: 'Aktivieren oder deaktivieren des Subversion Plugins.\n\ - Die Seite muss neu geladen werden wenn dieser Wert geändert wird.' - }); - -} - -// register information panel - -initCallbacks.push(function(main){ - main.registerInfoPanel('svn', { - checkoutTemplate: 'svn checkout <a href="{0}" target="_blank">{0}</a>', - xtype: 'repositoryExtendedInfoPanel' - }); -}); - -// register panel - -registerConfigPanel({ - xtype : 'svnConfigPanel' -}); - -// register type icon - -Sonia.repository.typeIcons['svn'] = 'resources/images/icons/16x16/subversion.png'; diff --git a/scm-plugins/scm-svn-plugin/src/main/webapp/images/svn-logo.gif b/scm-plugins/scm-svn-plugin/src/main/webapp/images/svn-logo.gif new file mode 100644 index 0000000000..b8b45c093e Binary files /dev/null and b/scm-plugins/scm-svn-plugin/src/main/webapp/images/svn-logo.gif differ diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java new file mode 100644 index 0000000000..27ca6d5635 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java @@ -0,0 +1,38 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.Compatibility; +import sonia.scm.repository.SvnConfig; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class SvnConfigDtoToSvnConfigMapperTest { + + @InjectMocks + private SvnConfigDtoToSvnConfigMapperImpl mapper; + + @Test + public void shouldMapFields() { + SvnConfigDto dto = createDefaultDto(); + SvnConfig config = mapper.map(dto); + + assertTrue(config.isDisabled()); + + assertEquals(Compatibility.PRE15, config.getCompatibility()); + assertTrue(config.isEnabledGZip()); + } + + private SvnConfigDto createDefaultDto() { + SvnConfigDto configDto = new SvnConfigDto(); + configDto.setDisabled(true); + configDto.setCompatibility(Compatibility.PRE15); + configDto.setEnabledGZip(true); + + return configDto; + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java new file mode 100644 index 0000000000..5d4fa36fe6 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java @@ -0,0 +1,65 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class SvnConfigInIndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectNode root = objectMapper.createObjectNode(); + private final SvnConfigInIndexResource svnConfigInIndexResource; + + public SvnConfigInIndexResourceTest() { + root.put("_links", objectMapper.createObjectNode()); + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + svnConfigInIndexResource = new SvnConfigInIndexResource(Providers.of(pathInfoStore), objectMapper); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void admin() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + svnConfigInIndexResource.enrich(context); + + assertEquals("/v2/config/svn", root.get("_links").get("svnConfig").get("href").asText()); + } + + @Test + @SubjectAware(username = "readOnly", password = "secret") + public void user() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + svnConfigInIndexResource.enrich(context); + + assertTrue(root.get("_links").iterator().hasNext()); + } + + @Test + public void anonymous() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + svnConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java new file mode 100644 index 0000000000..f7ccf039b2 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java @@ -0,0 +1,156 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.SvnConfig; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.web.SvnVndMediaType; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class SvnConfigResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + + @InjectMocks + private SvnConfigDtoToSvnConfigMapperImpl dtoToConfigMapper; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @InjectMocks + private SvnConfigToSvnConfigDtoMapperImpl configToDtoMapper; + + @Mock + private SvnRepositoryHandler repositoryHandler; + + @Before + public void prepareEnvironment() { + SvnConfig gitConfig = createConfiguration(); + when(repositoryHandler.getConfig()).thenReturn(gitConfig); + SvnConfigResource gitConfigResource = new SvnConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler); + dispatcher.getRegistry().addSingletonResource(gitConfigResource); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetSvnConfig() throws URISyntaxException, IOException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String responseString = response.getContentAsString(); + ObjectNode responseJson = new ObjectMapper().readValue(responseString, ObjectNode.class); + + assertTrue(responseString.contains("\"disabled\":false")); + assertTrue(responseString.contains("\"self\":{\"href\":\"/v2/config/svn")); + assertTrue(responseString.contains("\"update\":{\"href\":\"/v2/config/svn")); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetSvnConfigEvenWhenItsEmpty() throws URISyntaxException, IOException { + when(repositoryHandler.getConfig()).thenReturn(null); + + MockHttpResponse response = get(); + String responseString = response.getContentAsString(); + + assertTrue(responseString.contains("\"disabled\":false")); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetSvnConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertFalse(response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config/svn")); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:read:svn]"); + + get(); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldUpdateConfig() throws URISyntaxException { + MockHttpResponse response = put(); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:write:svn]"); + + put(); + } + + private MockHttpResponse get() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + SvnConfigResource.SVN_CONFIG_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private MockHttpResponse put() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.put("/" + SvnConfigResource.SVN_CONFIG_PATH_V2) + .contentType(SvnVndMediaType.SVN_CONFIG) + .content("{\"disabled\":true}".getBytes()); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private SvnConfig createConfiguration() { + SvnConfig config = new SvnConfig(); + config.setDisabled(false); + return config; + } + +} + diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java new file mode 100644 index 0000000000..07ead15322 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java @@ -0,0 +1,92 @@ +package sonia.scm.api.v2.resources; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.Compatibility; +import sonia.scm.repository.SvnConfig; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SvnConfigToSvnConfigDtoMapperTest { + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ScmPathInfoStore scmPathInfoStore; + + @InjectMocks + private SvnConfigToSvnConfigDtoMapperImpl mapper; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private URI expectedBaseUri; + + @Before + public void init() { + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(SvnConfigResource.SVN_CONFIG_PATH_V2); + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @After + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapFields() { + SvnConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:svn")).thenReturn(true); + SvnConfigDto dto = mapper.map(config); + + assertTrue(dto.isDisabled()); + + assertEquals(Compatibility.PRE15, dto.getCompatibility()); + assertTrue(dto.isEnabledGZip()); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); + } + + @Test + public void shouldMapFieldsWithoutUpdate() { + SvnConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:svn")).thenReturn(false); + SvnConfigDto dto = mapper.map(config); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertFalse(dto.getLinks().hasLink("update")); + } + + private SvnConfig createConfiguration() { + SvnConfig config = new SvnConfig(); + config.setDisabled(true); + + config.setCompatibility(Compatibility.PRE15); + config.setEnabledGZip(true); + + return config; + } + +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java index af80e27bcf..932ff007e0 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + * <p> * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * <p> * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * <p> * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,42 +24,56 @@ * 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. - * + * <p> * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.io.DefaultFileSystem; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import org.junit.Test; +import org.mockito.Mock; +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.repository.spi.HookEventFacade; +import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; -import sonia.scm.store.ConfigurationStoreFactory; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +//~--- JDK imports ------------------------------------------------------------ /** * * @author Sebastian Sdorra */ -public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase -{ +public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { + + @Mock + private ConfigurationStoreFactory factory; + + @Mock + private com.google.inject.Provider<RepositoryManager> repositoryManagerProvider; + + private HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); + + private HookEventFacade facade = new HookEventFacade(repositoryManagerProvider, hookContextFactory); - /** - * Method description - * - * - * @param directory - */ @Override - protected void checkDirectory(File directory) - { + protected void postSetUp() throws IOException, RepositoryPathNotFoundException { + initMocks(this); + super.postSetUp(); + } + + @Override + protected void checkDirectory(File directory) { File format = new File(directory, "format"); assertTrue(format.exists()); @@ -71,31 +85,33 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase assertTrue(db.isDirectory()); } - /** - * Method description - * - * - * @param factory - * @param directory - * - * @return - */ @Override protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, - File directory) - { - SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, - new DefaultFileSystem(), null); + RepositoryLocationResolver locationResolver, + File directory) { + SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, locationResolver, null); handler.init(contextProvider); SvnConfig config = new SvnConfig(); - config.setRepositoryDirectory(directory); - // TODO fix event bus exception handler.setConfig(config); return handler; } + + @Test + public void getDirectory() { + when(factory.withType(any())).thenCallRealMethod(); + SvnRepositoryHandler repositoryHandler = new SvnRepositoryHandler(factory, + facade, locationResolver, null); + + SvnConfig svnConfig = new SvnConfig(); + repositoryHandler.setConfig(svnConfig); + + initRepository(); + File path = repositoryHandler.getDirectory(repository.getId()); + assertEquals(repoPath.toString()+File.separator+ RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath()); + } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnAddCommand.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnAddCommand.java deleted file mode 100644 index 110211ef8b..0000000000 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnAddCommand.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2014, 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.client.spi; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -/** - * Add files to subversion repository. - * - * @author Sebastian Sdorra - * @since 1.51 - */ -public final class SvnAddCommand extends SvnFileCommand implements AddCommand { - - SvnAddCommand(File workingCopy, List<File> pendingFiles) { - super(workingCopy, pendingFiles); - } - - @Override - public void add(String path) throws IOException { - append(path); - } - -} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnChangeWorker.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnChangeWorker.java new file mode 100644 index 0000000000..302ef3e8aa --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnChangeWorker.java @@ -0,0 +1,143 @@ +package sonia.scm.repository.client.spi; + +import org.tmatesoft.svn.core.SVNCommitInfo; +import org.tmatesoft.svn.core.SVNDepth; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.SVNRevision; +import org.tmatesoft.svn.core.wc.SVNWCClient; +import org.tmatesoft.svn.core.wc2.SvnCommit; +import org.tmatesoft.svn.core.wc2.SvnLog; +import org.tmatesoft.svn.core.wc2.SvnRevisionRange; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.SvnUtil; +import sonia.scm.repository.client.api.RepositoryClientException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +class SvnChangeWorker { + + private final File workingCopy; + private final List<File> addedFiles = new ArrayList<>(); + private final List<File> removedFiles = new ArrayList<>(); + + public SvnChangeWorker(File workingCopy) { + this.workingCopy = workingCopy; + } + + public AddCommand addCommand() { + return new SvnAddCommand(); + } + + public RemoveCommand removeCommand() { + return new SvnRemoveCommand(); + } + + public CommitCommand commitCommand(SVNClientManager client) { + return new SvnCommitCommand(client); + } + + private class SvnAddCommand implements AddCommand { + @Override + public void add(String path) throws IOException { + addedFiles.add(toFile(path)); + } + } + + private class SvnRemoveCommand implements RemoveCommand { + @Override + public void remove(String path) throws IOException { + removedFiles.add(toFile(path)); + } + } + + private class SvnCommitCommand implements CommitCommand { + private final SVNClientManager client; + + private SvnCommitCommand(SVNClientManager client) { + this.client = client; + } + + @Override + public Changeset commit(CommitRequest request) throws IOException { + SVNWCClient wClient = client.getWCClient(); + + // add files + if (!addedFiles.isEmpty()){ + try { + wClient.doAdd(addedFiles.toArray(new File[0]), true, false, false, + SVNDepth.INFINITY, false, false, false); + addedFiles.clear(); + + } catch (SVNException ex) { + throw new RepositoryClientException("failed to add files", ex); + } + } + + // remove files + try { + Iterator<File> removeIt = removedFiles.iterator(); + while (removeIt.hasNext()) { + File file = removeIt.next(); + wClient.doDelete(file, false, true, false); + removeIt.remove(); + } + } catch (SVNException ex) { + throw new RepositoryClientException("failed to remove files", ex); + } + + SvnTarget workingCopyTarget = SvnTarget.fromFile(workingCopy); + Changeset changeset; + SVNCommitInfo info; + + // commit files + try { + SvnCommit commit = client.getOperationFactory().createCommit(); + commit.setDepth(SVNDepth.INFINITY); + commit.setCommitMessage(request.getMessage()); + commit.setSingleTarget(workingCopyTarget); + + info = commit.run(); + + SVNErrorMessage msg = info.getErrorMessage(); + if (msg != null) { + throw new IOException(msg.getFullMessage()); + } + + } catch (SVNException ex) { + throw new RepositoryClientException("failed to commit", ex); + } + + // get log for commit + try { + SVNRevision revision = SVNRevision.create(info.getNewRevision()); + + SvnLog log = client.getOperationFactory().createLog(); + log.addRange(SvnRevisionRange.create(revision, revision)); + log.setSingleTarget(workingCopyTarget); + + changeset = SvnUtil.createChangeset(log.run()); + } catch (SVNException ex) { + throw new RepositoryClientException("failed to create log entry for last commit", ex); + } + + return changeset; + } + } + + + protected File toFile(String path) throws FileNotFoundException { + File file = new File(workingCopy, path); + if (!file.exists()) { + throw new FileNotFoundException("could not find file ".concat(path)); + } + return file; + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnCommitCommand.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnCommitCommand.java deleted file mode 100644 index 3142a7be51..0000000000 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnCommitCommand.java +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (c) 2014, 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.client.spi; - -import java.io.File; -import java.io.IOException; -import java.util.Iterator; -import java.util.List; -import org.tmatesoft.svn.core.SVNCommitInfo; -import org.tmatesoft.svn.core.SVNDepth; -import org.tmatesoft.svn.core.SVNErrorMessage; -import org.tmatesoft.svn.core.SVNException; -import org.tmatesoft.svn.core.wc.SVNClientManager; -import org.tmatesoft.svn.core.wc.SVNRevision; -import org.tmatesoft.svn.core.wc.SVNWCClient; -import org.tmatesoft.svn.core.wc2.SvnCommit; -import org.tmatesoft.svn.core.wc2.SvnLog; -import org.tmatesoft.svn.core.wc2.SvnRevisionRange; -import org.tmatesoft.svn.core.wc2.SvnTarget; - -import sonia.scm.repository.Changeset; -import sonia.scm.repository.SvnUtil; -import sonia.scm.repository.client.api.RepositoryClientException; - -/** - * - * @author Sebastian Sdorra - */ -public class SvnCommitCommand implements CommitCommand { - - private final SVNClientManager client; - private final File workingCopy; - private final List<File> addedFiles; - private final List<File> removedFiles; - - SvnCommitCommand(SVNClientManager client, File workingCopy, List<File> addedFiles, List<File> removedFiles) { - this.client = client; - this.workingCopy = workingCopy; - this.addedFiles = addedFiles; - this.removedFiles = removedFiles; - } - - @Override - public Changeset commit(CommitRequest request) throws IOException { - SVNWCClient wClient = client.getWCClient(); - - // add files - try { - wClient.doAdd(addedFiles.toArray(new File[0]), true, false, false, - SVNDepth.INFINITY, false, false, false); - addedFiles.clear(); - - } catch (SVNException ex) { - throw new RepositoryClientException("failed to add files", ex); - } - - // remove files - try { - Iterator<File> removeIt = removedFiles.iterator(); - while (removeIt.hasNext()) { - File file = removeIt.next(); - wClient.doDelete(file, false, true, false); - removeIt.remove(); - } - } catch (SVNException ex) { - throw new RepositoryClientException("failed to remove files", ex); - } - - SvnTarget workingCopyTarget = SvnTarget.fromFile(workingCopy); - Changeset changeset; - SVNCommitInfo info; - - // commit files - try { - SvnCommit commit = client.getOperationFactory().createCommit(); - commit.setDepth(SVNDepth.INFINITY); - commit.setCommitMessage(request.getMessage()); - commit.setSingleTarget(workingCopyTarget); - - info = commit.run(); - - SVNErrorMessage msg = info.getErrorMessage(); - if (msg != null) { - throw new IOException(msg.getFullMessage()); - } - - } catch (SVNException ex) { - throw new RepositoryClientException("failed to commit", ex); - } - - // get log for commit - try { - SVNRevision revision = SVNRevision.create(info.getNewRevision()); - - SvnLog log = client.getOperationFactory().createLog(); - log.addRange(SvnRevisionRange.create(revision, revision)); - log.setSingleTarget(workingCopyTarget); - - changeset = SvnUtil.createChangeset(log.run()); - } catch (SVNException ex) { - throw new RepositoryClientException("failed to create log entry for last commit", ex); - } - - return changeset; - } - -} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnFileCommand.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnFileCommand.java deleted file mode 100644 index 528923a0d8..0000000000 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnFileCommand.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2014, 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.client.spi; - -import java.io.File; -import java.io.FileNotFoundException; - -import java.util.List; - -/** - * Abstract file based svn command. - * - * @author Sebastian Sdorra - * @since 1.51 - */ -public abstract class SvnFileCommand { - - private final File workingCopy; - private final List<File> pendingFiles; - - protected SvnFileCommand(File workingCopy, List<File> pendingFiles) { - this.workingCopy = workingCopy; - this.pendingFiles = pendingFiles; - } - - protected void append(String path) throws FileNotFoundException { - File file = new File(workingCopy, path); - if (!file.exists()) { - throw new FileNotFoundException("could not find file ".concat(path)); - } - pendingFiles.add(file); - } - -} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnRemoveCommand.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnRemoveCommand.java deleted file mode 100644 index 38e7f70de9..0000000000 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnRemoveCommand.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2014, 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.client.spi; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -/** - * Remove files from subversion repository. - * - * @author Sebastian Sdorra - * @since 1.51 - */ -public class SvnRemoveCommand extends SvnFileCommand implements RemoveCommand { - - SvnRemoveCommand(File workingCopy, List<File> pendingFiles) { - super(workingCopy, pendingFiles); - } - - @Override - public void remove(String path) throws IOException { - append(path); - } - -} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnRepositoryClientProvider.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnRepositoryClientProvider.java index 9c5424d660..2624ec9e84 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnRepositoryClientProvider.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnRepositoryClientProvider.java @@ -35,8 +35,6 @@ import org.tmatesoft.svn.core.wc.SVNClientManager; import sonia.scm.repository.client.api.ClientCommand; import java.io.File; -import java.util.ArrayList; -import java.util.List; import java.util.Set; /** @@ -54,27 +52,27 @@ public class SvnRepositoryClientProvider extends RepositoryClientProvider { private final SVNClientManager client; private final File workingCopy; - private final List<File> addedFiles = new ArrayList<>(); - private final List<File> removedFiles = new ArrayList<>(); - + private final SvnChangeWorker changeWorker; + SvnRepositoryClientProvider(SVNClientManager client, File workingCopy) { + changeWorker = new SvnChangeWorker(workingCopy); this.client = client; this.workingCopy = workingCopy; } @Override - public SvnAddCommand getAddCommand() { - return new SvnAddCommand(workingCopy, addedFiles); + public AddCommand getAddCommand() { + return changeWorker.addCommand(); } @Override - public SvnRemoveCommand getRemoveCommand() { - return new SvnRemoveCommand(workingCopy, removedFiles); + public RemoveCommand getRemoveCommand() { + return changeWorker.removeCommand(); } @Override - public SvnCommitCommand getCommitCommand() { - return new SvnCommitCommand(client, workingCopy, addedFiles, removedFiles); + public CommitCommand getCommitCommand() { + return changeWorker.commitCommand(client); } @Override diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/AbstractSvnCommandTestBase.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/AbstractSvnCommandTestBase.java index 81b55e5db3..a0a9e9c77d 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/AbstractSvnCommandTestBase.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/AbstractSvnCommandTestBase.java @@ -72,7 +72,7 @@ public class AbstractSvnCommandTestBase extends ZippedRepositoryTestBase { if (context == null) { - context = new SvnContext(repositoryDirectory); + context = new SvnContext(repository, repositoryDirectory); } return context; diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactoryTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactoryTest.java new file mode 100644 index 0000000000..8beae9e0ed --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactoryTest.java @@ -0,0 +1,77 @@ +package sonia.scm.repository.spi; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tmatesoft.svn.core.SVNException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SimpleSvnWorkDirFactoryTest extends AbstractSvnCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + // keep this so that it will not be garbage collected (Transport keeps this in a week reference) + private WorkdirProvider workdirProvider; + + @Before + public void initWorkDirProvider() throws IOException { + workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); + } + + @Test + public void shouldCheckoutLatestRevision() throws SVNException, IOException { + SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + + try (WorkingCopy<File, File> workingCopy = factory.createWorkingCopy(createContext(), null)) { + assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")) + .exists() + .isFile() + .hasContent("a and b\nline for blame test"); + } + } + + @Test + public void cloneFromPoolshouldNotBeReused() { + SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + + File firstDirectory; + try (WorkingCopy<File, File> workingCopy = factory.createWorkingCopy(createContext(), null)) { + firstDirectory = workingCopy.getDirectory(); + } + try (WorkingCopy<File, File> workingCopy = factory.createWorkingCopy(createContext(), null)) { + File secondDirectory = workingCopy.getDirectory(); + assertThat(secondDirectory).isNotEqualTo(firstDirectory); + } + } + + @Test + public void shouldDeleteCloneOnClose() { + SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + + File directory; + File workingRepository; + try (WorkingCopy<File, File> workingCopy = factory.createWorkingCopy(createContext(), null)) { + directory = workingCopy.getDirectory(); + workingRepository = workingCopy.getWorkingRepository(); + + } + assertThat(directory).doesNotExist(); + assertThat(workingRepository).doesNotExist(); + } + + @Test + public void shouldReturnRepository() { + SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + Repository scmRepository = factory.getScmRepository(createContext()); + assertThat(scmRepository).isSameAs(repository); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBlameCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBlameCommandTest.java index e320dd3251..ec4f375df4 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBlameCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBlameCommandTest.java @@ -35,17 +35,15 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; - import sonia.scm.repository.BlameLine; import sonia.scm.repository.BlameResult; -import sonia.scm.repository.RepositoryException; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; //~--- JDK imports ------------------------------------------------------------ -import java.io.IOException; - /** * * @author Sebastian Sdorra @@ -53,15 +51,8 @@ import java.io.IOException; public class SvnBlameCommandTest extends AbstractSvnCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetBlameResult() throws IOException, RepositoryException + public void testGetBlameResult() { BlameCommandRequest request = new BlameCommandRequest(); @@ -85,17 +76,8 @@ public class SvnBlameCommandTest extends AbstractSvnCommandTestBase assertNull(line.getAuthor().getMail()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetBlameResultWithRevision() - throws IOException, RepositoryException - { + public void testGetBlameResultWithRevision() { BlameCommandRequest request = new BlameCommandRequest(); request.setPath("a.txt"); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 369d179fed..d3e6a98558 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -33,21 +33,18 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import org.junit.Test; - import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +import java.util.Collection; -import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * @@ -56,17 +53,19 @@ import java.util.List; public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testBrowse() throws IOException, RepositoryException - { - List<FileObject> foList = getRootFromTip(new BrowseCommandRequest()); + public void testBrowseWithFilePath() { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setPath("a.txt"); + FileObject file = createCommand().getBrowserResult(request).getFile(); + assertEquals("a.txt", file.getName()); + assertFalse(file.isDirectory()); + assertTrue(file.getChildren().isEmpty()); + } + + @Test + public void testBrowse() { + Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest()); FileObject a = getFileObject(foList, "a.txt"); FileObject c = getFileObject(foList, "c"); @@ -87,11 +86,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase * * * @throws IOException - * @throws RepositoryException */ @Test - public void testBrowseSubDirectory() throws IOException, RepositoryException - { + public void testBrowseSubDirectory() { BrowseCommandRequest request = new BrowseCommandRequest(); request.setPath("c"); @@ -100,7 +97,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertNotNull(result); - List<FileObject> foList = result.getFiles(); + Collection<FileObject> foList = result.getFile().getChildren(); assertNotNull(foList); assertFalse(foList.isEmpty()); @@ -137,21 +134,13 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase checkDate(e.getLastModified()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testDisableLastCommit() throws IOException, RepositoryException - { + public void testDisableLastCommit() { BrowseCommandRequest request = new BrowseCommandRequest(); request.setDisableLastCommit(true); - List<FileObject> foList = getRootFromTip(request); + Collection<FileObject> foList = getRootFromTip(request); FileObject a = getFileObject(foList, "a.txt"); @@ -160,23 +149,23 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase } @Test - public void testRecursive() throws IOException, RepositoryException - { + public void testRecursive() { BrowseCommandRequest request = new BrowseCommandRequest(); request.setRecursive(true); BrowserResult result = createCommand().getBrowserResult(request); assertNotNull(result); - List<FileObject> foList = result.getFiles(); + Collection<FileObject> foList = result.getFile().getChildren(); assertNotNull(foList); assertFalse(foList.isEmpty()); - assertEquals(4, foList.size()); - - for ( FileObject fo : foList ){ - System.out.println(fo); - } + assertEquals(2, foList.size()); + + FileObject c = getFileObject(foList, "c"); + assertEquals("c", c.getName()); + assertTrue(c.isDirectory()); + assertEquals(2, c.getChildren().size()); } /** @@ -201,44 +190,20 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase * * @return */ - private FileObject getFileObject(List<FileObject> foList, String name) + private FileObject getFileObject(Collection<FileObject> foList, String name) { - FileObject a = null; - - for (FileObject f : foList) - { - if (name.equals(f.getName())) - { - a = f; - - break; - } - } - - assertNotNull(a); - - return a; + return foList.stream() + .filter(f -> name.equals(f.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("file " + name + " not found")); } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private List<FileObject> getRootFromTip(BrowseCommandRequest request) - throws IOException, RepositoryException - { + private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) { BrowserResult result = createCommand().getBrowserResult(request); assertNotNull(result); - List<FileObject> foList = result.getFiles(); + Collection<FileObject> foList = result.getFile().getChildren(); assertNotNull(foList); assertFalse(foList.isEmpty()); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBundleCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBundleCommandTest.java index 88e3eddf02..9e7e40810c 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBundleCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBundleCommandTest.java @@ -37,23 +37,22 @@ package sonia.scm.repository.spi; import com.google.common.io.ByteSink; import com.google.common.io.Files; - import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; - -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.api.BundleResponse; -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -61,15 +60,8 @@ import java.io.IOException; public class SvnBundleCommandTest extends AbstractSvnCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testBundle() throws IOException, RepositoryException + public void testBundle() throws IOException { File file = temp.newFile(); ByteSink sink = Files.asByteSink(file); @@ -78,9 +70,9 @@ public class SvnBundleCommandTest extends AbstractSvnCommandTestBase repository).bundle(req); assertThat(res, notNullValue()); - assertThat(res.getChangesetCount(), is(5L)); + assertThat(res.getChangesetCount(), is(5l)); assertTrue("file does not exists", file.exists()); - assertThat(file.length(), greaterThan(0L)); + assertThat(file.length(), greaterThan(0l)); } //~--- fields --------------------------------------------------------------- diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java index 198a55edf3..e899c63b19 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java @@ -32,36 +32,28 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Rule; import org.junit.Test; - -import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import org.junit.rules.ExpectedException; +import sonia.scm.NotFoundException; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; -/** - * - * @author Sebastian Sdorra - */ -public class SvnCatCommandTest extends AbstractSvnCommandTestBase -{ +import static org.junit.Assert.assertEquals; + +//~--- JDK imports ------------------------------------------------------------ + +public class SvnCatCommandTest extends AbstractSvnCommandTestBase { + + @Rule + public final ExpectedException expectedException = ExpectedException.none(); - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testCat() throws IOException, RepositoryException - { + public void testCat() { CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -69,36 +61,74 @@ public class SvnCatCommandTest extends AbstractSvnCommandTestBase assertEquals("a", execute(request)); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testSimpleCat() throws IOException, RepositoryException - { + public void testSimpleCat() { CatCommandRequest request = new CatCommandRequest(); request.setPath("c/d.txt"); assertEquals("d", execute(request)); } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private String execute(CatCommandRequest request) - throws IOException, RepositoryException - { + @Test + public void testUnknownFile() { + CatCommandRequest request = new CatCommandRequest(); + + request.setPath("unknown"); + request.setRevision("1"); + + expectedException.expect(new BaseMatcher<Object>() { + @Override + public void describeTo(Description description) { + description.appendText("expected NotFoundException for path"); + } + + @Override + public boolean matches(Object item) { + return "Path".equals(((NotFoundException)item).getContext().get(0).getType()); + } + }); + + execute(request); + } + + @Test + public void testUnknownRevision() { + CatCommandRequest request = new CatCommandRequest(); + + request.setPath("a.txt"); + request.setRevision("42"); + + expectedException.expect(new BaseMatcher<Object>() { + @Override + public void describeTo(Description description) { + description.appendText("expected NotFoundException for revision"); + } + + @Override + public boolean matches(Object item) { + return "Revision".equals(((NotFoundException)item).getContext().get(0).getType()); + } + }); + + execute(request); + } + + @Test + public void testSimpleStream() throws IOException { + CatCommandRequest request = new CatCommandRequest(); + request.setPath("a.txt"); + request.setRevision("1"); + + InputStream catResultStream = new SvnCatCommand(createContext(), repository).getCatResultStream(request); + + assertEquals('a', catResultStream.read()); + assertEquals('\n', catResultStream.read()); + assertEquals(-1, catResultStream.read()); + + catResultStream.close(); + } + + private String execute(CatCommandRequest request) { String content = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java index a3942bd0f4..15198a19ed 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java @@ -35,18 +35,17 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; - import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.Modifications; -import sonia.scm.repository.RepositoryException; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; //~--- JDK imports ------------------------------------------------------------ -import java.io.IOException; - /** * * @author Sebastian Sdorra @@ -54,16 +53,8 @@ import java.io.IOException; public class SvnLogCommandTest extends AbstractSvnCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAll() throws IOException, RepositoryException - { + public void testGetAll() { ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); @@ -72,16 +63,8 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase assertEquals(6, result.getChangesets().size()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllByPath() throws IOException, RepositoryException - { + public void testGetAllByPath() { LogCommandRequest request = new LogCommandRequest(); request.setPath("a.txt"); @@ -96,16 +79,8 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase assertEquals("1", result.getChangesets().get(2).getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllWithLimit() throws IOException, RepositoryException - { + public void testGetAllWithLimit() { LogCommandRequest request = new LogCommandRequest(); request.setPagingLimit(2); @@ -127,16 +102,8 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase assertEquals("4", c2.getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllWithPaging() throws IOException, RepositoryException - { + public void testGetAllWithPaging() { LogCommandRequest request = new LogCommandRequest(); request.setPagingStart(1); @@ -159,17 +126,9 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase assertEquals("3", c2.getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetCommit() throws IOException, RepositoryException - { - Changeset c = createCommand().getChangeset("3"); + public void testGetCommit() { + Changeset c = createCommand().getChangeset("3", null); assertNotNull(c); assertEquals("3", c.getId()); @@ -177,27 +136,19 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase checkDate(c.getDate()); assertEquals("perfect", c.getAuthor().getName()); assertNull("douglas.adams@hitchhiker.com", c.getAuthor().getMail()); + SvnModificationsCommand modificationsCommand = new SvnModificationsCommand(createContext(), repository); + Modifications modifications = modificationsCommand.getModifications("3"); - Modifications mods = c.getModifications(); - - assertNotNull(mods); - assertEquals(1, mods.getModified().size()); - assertEquals(1, mods.getRemoved().size()); - assertTrue("added list should be empty", mods.getAdded().isEmpty()); - assertEquals("a.txt", mods.getModified().get(0)); - assertEquals("b.txt", mods.getRemoved().get(0)); + assertNotNull(modifications); + assertEquals(1, modifications.getModified().size()); + assertEquals(1, modifications.getRemoved().size()); + assertTrue("added list should be empty", modifications.getAdded().isEmpty()); + assertEquals("a.txt", modifications.getModified().get(0)); + assertEquals("b.txt", modifications.getRemoved().get(0)); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetRange() throws IOException, RepositoryException - { + public void testGetRange() { LogCommandRequest request = new LogCommandRequest(); request.setStartChangeset("2"); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java new file mode 100644 index 0000000000..456971d9a2 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java @@ -0,0 +1,89 @@ +package sonia.scm.repository.spi; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.AlreadyExistsException; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { + + private SvnModifyCommand svnModifyCommand; + private SvnContext context; + private SimpleSvnWorkDirFactory workDirFactory; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void initSvnModifyCommand() { + context = createContext(); + workDirFactory = new SimpleSvnWorkDirFactory(new WorkdirProvider(context.getDirectory())); + svnModifyCommand = new SvnModifyCommand(context, createRepository(), workDirFactory); + } + + @Test + public void shouldRemoveFiles() { + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt")); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + svnModifyCommand.execute(request); + WorkingCopy<File, File> workingCopy = workDirFactory.createWorkingCopy(context, null); + assertThat(new File(workingCopy.getWorkingRepository().getAbsolutePath() + "/a.txt")).doesNotExist(); + assertThat(new File(workingCopy.getWorkingRepository().getAbsolutePath() + "/c")).exists(); + } + + @Test + public void shouldAddNewFile() throws IOException { + File testfile = temporaryFolder.newFile("Test123"); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Test123", testfile, false)); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + svnModifyCommand.execute(request); + + WorkingCopy<File, File> workingCopy = workDirFactory.createWorkingCopy(context, null); + assertThat(new File(workingCopy.getWorkingRepository(), "Test123")).exists(); + } + + @Test + public void shouldThrowFileAlreadyExistsException() throws IOException { + File testfile = temporaryFolder.newFile("a.txt"); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", testfile, false)); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + assertThrows(AlreadyExistsException.class, () -> svnModifyCommand.execute(request)); + } + + @Test + public void shouldUpdateExistingFile() throws IOException { + File testfile = temporaryFolder.newFile("a.txt"); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", testfile, true)); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + svnModifyCommand.execute(request); + + WorkingCopy<File, File> workingCopy = workDirFactory.createWorkingCopy(context, null); + assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")).exists(); + assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")).hasContent(""); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java index d0142151a1..283a65449b 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java @@ -34,26 +34,22 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.io.Files; - import org.junit.Test; - import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; - -import sonia.scm.repository.RepositoryException; import sonia.scm.repository.SvnUtil; import sonia.scm.repository.api.UnbundleResponse; -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -61,17 +57,9 @@ import java.io.IOException; public class SvnUnbundleCommandTest extends AbstractSvnCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - * @throws SVNException - */ @Test public void testUnbundle() - throws IOException, RepositoryException, SVNException + throws IOException, SVNException { File bundle = bundle(); SvnContext ctx = createEmptyContext(); @@ -87,24 +75,15 @@ public class SvnUnbundleCommandTest extends AbstractSvnCommandTestBase //J+ assertThat(res, notNullValue()); - assertThat(res.getChangesetCount(), is(5L)); + assertThat(res.getChangesetCount(), is(5l)); SVNRepository repo = ctx.open(); - assertThat(repo.getLatestRevision(), is(5L)); + assertThat(repo.getLatestRevision(), is(5l)); SvnUtil.closeSession(repo); } - /** - * Method description - * - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private File bundle() throws IOException, RepositoryException + private File bundle() throws IOException { File file = tempFolder.newFile(); @@ -137,6 +116,6 @@ public class SvnUnbundleCommandTest extends AbstractSvnCommandTestBase SVNRepositoryFactory.createLocalRepository(folder, true, true); - return new SvnContext(folder); + return new SvnContext(repository, folder); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini new file mode 100644 index 0000000000..fe84723e0a --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -0,0 +1,11 @@ +[users] +readOnly = secret, reader +writeOnly = secret, writer +readWrite = secret, readerWriter +admin = secret, admin + +[roles] +reader = configuration:read:svn +writer = configuration:write:svn +readerWriter = configuration:*:svn +admin = * diff --git a/scm-plugins/scm-svn-plugin/tsconfig.json b/scm-plugins/scm-svn-plugin/tsconfig.json new file mode 100644 index 0000000000..7cfe32efe6 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@scm-manager/tsconfig", + "include": [ + "./src/main/js" + ] +} diff --git a/scm-server/pom.xml b/scm-server/pom.xml index a20dbd5857..74f6466821 100644 --- a/scm-server/pom.xml +++ b/scm-server/pom.xml @@ -9,6 +9,7 @@ <version>2.0.0-SNAPSHOT</version> </parent> + <groupId>sonia.scm</groupId> <artifactId>scm-server</artifactId> <version>2.0.0-SNAPSHOT</version> <name>scm-server</name> @@ -63,7 +64,6 @@ <assembleDirectory>${exploded.directory}</assembleDirectory> <repoPath>lib</repoPath> <repositoryLayout>flat</repositoryLayout> - <includeConfigurationDirectoryInClasspath>true</includeConfigurationDirectoryInClasspath> <daemons> <daemon> @@ -140,7 +140,6 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> - <version>2.3</version> <configuration> <descriptors> <descriptor>src/main/assembly/scm-server-app.xml</descriptor> @@ -306,8 +305,8 @@ </profiles> <properties> - <commons.daemon.version>1.0.15</commons.daemon.version> - <commons.daemon.native.version>1.0.15.1</commons.daemon.native.version> + <commons.daemon.version>1.1.0</commons.daemon.version> + <commons.daemon.native.version>1.1.0</commons.daemon.native.version> <exploded.directory>${project.build.directory}/appassembler/commons-daemon/scm-server</exploded.directory> </properties> diff --git a/scm-server/src/main/conf/server-config.xml b/scm-server/src/main/conf/server-config.xml index c591b1a7a2..ba6ee1b01e 100644 --- a/scm-server/src/main/conf/server-config.xml +++ b/scm-server/src/main/conf/server-config.xml @@ -48,11 +48,9 @@ <Set name="requestHeaderSize">16384</Set> <Set name="responseHeaderSize">16384</Set> - <!-- forwarding <Call name="addCustomizer"> <Arg><New class="org.eclipse.jetty.server.ForwardedRequestCustomizer"/></Arg> </Call> - --> </New> <!-- diff --git a/scm-test/pom.xml b/scm-test/pom.xml index f9762d46f1..bddbcf2c85 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -9,6 +9,7 @@ <version>2.0.0-SNAPSHOT</version> </parent> + <groupId>sonia.scm</groupId> <artifactId>scm-test</artifactId> <version>2.0.0-SNAPSHOT</version> <name>scm-test</name> @@ -26,23 +27,19 @@ <artifactId>scm-core</artifactId> <version>2.0.0-SNAPSHOT</version> </dependency> - - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <version>${junit.version}</version> - </dependency> - + <dependency> <groupId>com.github.sdorra</groupId> <artifactId>shiro-unit</artifactId> - <version>1.0.0</version> + <!-- scm-test is test scoped in other modules and they might need shiro unit for their tests. --> + <scope>compile</scope> </dependency> <dependency> <groupId>org.mockito</groupId> - <artifactId>mockito-all</artifactId> - <version>${mokito.version}</version> + <artifactId>mockito-core</artifactId> + <scope>compile</scope> + <version>${mockito.version}</version> </dependency> <dependency> @@ -50,7 +47,7 @@ <artifactId>slf4j-simple</artifactId> <version>${slf4j.version}</version> </dependency> - + </dependencies> <!-- for svnkit and jgit --> diff --git a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java index 13cde0391e..48831fb670 100644 --- a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java +++ b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java @@ -46,10 +46,15 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; +import sonia.scm.io.DefaultFileSystem; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.util.IOUtil; import sonia.scm.util.MockUtil; import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; //~--- JDK imports ------------------------------------------------------------ @@ -66,10 +71,29 @@ import java.util.logging.Logger; public class AbstractTestBase { - /** Field description */ private static ThreadState subjectThreadState; - //~--- methods -------------------------------------------------------------- + protected SCMContextProvider contextProvider; + + private File tempDirectory; + + protected DefaultFileSystem fileSystem; + + protected RepositoryDAO repositoryDAO = mock(RepositoryDAO.class); + protected RepositoryLocationResolver repositoryLocationResolver; + + @Before + public void setUpTest() throws Exception + { + tempDirectory = new File(System.getProperty("java.io.tmpdir"), + UUID.randomUUID().toString()); + assertTrue(tempDirectory.mkdirs()); + contextProvider = MockUtil.getSCMContextProvider(tempDirectory); + fileSystem = new DefaultFileSystem(); + InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver(); + repositoryLocationResolver = new TempDirRepositoryLocationResolver(tempDirectory); + postSetUp(); + } /** * Method description @@ -165,25 +189,6 @@ public class AbstractTestBase } } - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @throws Exception - */ - @Before - public void setUpTest() throws Exception - { - tempDirectory = new File(System.getProperty("java.io.tmpdir"), - UUID.randomUUID().toString()); - assertTrue(tempDirectory.mkdirs()); - contextProvider = MockUtil.getSCMContextProvider(tempDirectory); - postSetUp(); - } - - //~--- methods -------------------------------------------------------------- /** * Clears Shiro's thread state, ensuring the thread remains clean for @@ -250,11 +255,4 @@ public class AbstractTestBase subjectThreadState.bind(); } - //~--- fields --------------------------------------------------------------- - - /** Field description */ - protected SCMContextProvider contextProvider; - - /** Field description */ - private File tempDirectory; } diff --git a/scm-test/src/main/java/sonia/scm/ManagerTestBase.java b/scm-test/src/main/java/sonia/scm/ManagerTestBase.java index 24bd2a0ebb..3990b05df9 100644 --- a/scm-test/src/main/java/sonia/scm/ManagerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/ManagerTestBase.java @@ -33,33 +33,48 @@ package sonia.scm; -import java.io.IOException; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.util.MockUtil; +import java.io.File; +import java.io.IOException; + +import static org.mockito.Mockito.mock; + /** * * @author Sebastian Sdorra * * @param <T> - * @param <E> */ -public abstract class ManagerTestBase<T extends ModelObject, E extends Exception> +public abstract class ManagerTestBase<T extends ModelObject> { @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); protected SCMContextProvider contextProvider; - - protected Manager<T, E> manager; - + protected RepositoryLocationResolver locationResolver; + + protected Manager<T> manager; + + protected File temp ; + @Before public void setUp() throws IOException { - contextProvider = MockUtil.getSCMContextProvider(tempFolder.newFolder()); + if (temp == null){ + temp = tempFolder.newFolder(); + } + contextProvider = MockUtil.getSCMContextProvider(temp); + InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(); + RepositoryDAO repoDao = mock(RepositoryDAO.class); + locationResolver = new TempDirRepositoryLocationResolver(temp); manager = createManager(); manager.init(contextProvider); } @@ -75,6 +90,6 @@ public abstract class ManagerTestBase<T extends ModelObject, E extends Exception * * @return */ - protected abstract Manager<T, E> createManager(); + protected abstract Manager<T> createManager(); } diff --git a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java new file mode 100644 index 0000000000..e172b53ba2 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java @@ -0,0 +1,41 @@ +package sonia.scm; + +import sonia.scm.repository.BasicRepositoryLocationResolver; + +import java.io.File; +import java.nio.file.Path; +import java.util.function.BiConsumer; + +public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationResolver { + private final File tempDirectory; + + public TempDirRepositoryLocationResolver(File tempDirectory) { + super(Path.class); + this.tempDirectory = tempDirectory; + } + + @Override + protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) { + return new RepositoryLocationResolverInstance<T>() { + @Override + public T getLocation(String repositoryId) { + return (T) tempDirectory.toPath(); + } + + @Override + public T createLocation(String repositoryId) { + return (T) tempDirectory.toPath(); + } + + @Override + public void setLocation(String repositoryId, T location) { + throw new UnsupportedOperationException("not implemented for tests"); + } + + @Override + public void forAllLocations(BiConsumer<String, T> consumer) { + consumer.accept("id", (T) tempDirectory.toPath()); + } + }; + } +} diff --git a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java index 0a85231da0..ccaeee8631 100644 --- a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java +++ b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + * <p> * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * <p> * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * <p> * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,113 +24,75 @@ * 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. - * + * <p> * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.Type; -import sonia.scm.io.DefaultFileSystem; +import com.google.common.collect.Sets; +import sonia.scm.AlreadyExistsException; +import sonia.scm.store.ConfigurationStoreFactory; + +import javax.xml.bind.annotation.XmlRootElement; +import java.io.File; +import java.util.HashSet; +import java.util.Set; //~--- JDK imports ------------------------------------------------------------ -import java.io.File; -import java.io.IOException; -import sonia.scm.store.ConfigurationStoreFactory; - /** - * * @author Sebastian Sdorra */ public class DummyRepositoryHandler - extends AbstractSimpleRepositoryHandler<SimpleRepositoryConfig> -{ + extends AbstractSimpleRepositoryHandler<DummyRepositoryHandler.DummyRepositoryConfig> { - /** Field description */ public static final String TYPE_DISPLAYNAME = "Dummy"; - /** Field description */ public static final String TYPE_NAME = "dummy"; - /** Field description */ - public static final Type TYPE = new Type(TYPE_NAME, TYPE_DISPLAYNAME); + public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, Sets.newHashSet()); - //~--- constructors --------------------------------------------------------- + private final Set<String> existingRepoNames = new HashSet<>(); - /** - * Constructs ... - * - * - * @param storeFactory - */ - public DummyRepositoryHandler(ConfigurationStoreFactory storeFactory) - { - super(storeFactory, new DefaultFileSystem()); + public DummyRepositoryHandler(ConfigurationStoreFactory storeFactory, RepositoryLocationResolver repositoryLocationResolver) { + super(storeFactory, repositoryLocationResolver, null); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ @Override - public Type getType() - { + public RepositoryType getType() { return TYPE; } - //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param repository - * @param directory - * - * @throws IOException - * @throws RepositoryException - */ @Override - protected void create(Repository repository, File directory) - throws RepositoryException, IOException - { - - // do nothing + protected void create(Repository repository, File directory) { + String key = repository.getNamespace() + "/" + repository.getName(); + if (existingRepoNames.contains(key)) { + throw new AlreadyExistsException(repository); + } else { + existingRepoNames.add(key); + } } - /** - * Method description - * - * - * @return - */ @Override - protected SimpleRepositoryConfig createInitialConfig() - { - return new SimpleRepositoryConfig(); + protected DummyRepositoryConfig createInitialConfig() { + return new DummyRepositoryConfig(); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ @Override - protected Class<SimpleRepositoryConfig> getConfigClass() - { - return SimpleRepositoryConfig.class; + protected Class<DummyRepositoryConfig> getConfigClass() { + return DummyRepositoryConfig.class; + } + + @XmlRootElement(name = "config") + public static class DummyRepositoryConfig extends RepositoryConfig { + @Override + public String getId() { + return TYPE_NAME; + } } } diff --git a/scm-test/src/main/java/sonia/scm/repository/RepositoryBuilder.java b/scm-test/src/main/java/sonia/scm/repository/RepositoryBuilder.java new file mode 100644 index 0000000000..9c8ceb4762 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/RepositoryBuilder.java @@ -0,0 +1,49 @@ +package sonia.scm.repository; + +public class RepositoryBuilder { + + private String id = "id-" + ++nextID; + private String contact = "test@example.com"; + private String description = ""; + private String namespace = "test"; + private String name = "name"; + private String type = "git"; + + private static int nextID = 0; + + public RepositoryBuilder type(String type) { + this.type = type; + return this; + } + + public RepositoryBuilder contact(String contact) { + this.contact = contact; + return this; + } + + public RepositoryBuilder namespace(String namespace) { + this.namespace = namespace; + return this; + } + + public RepositoryBuilder name(String name) { + this.name = name; + return this; + } + + public RepositoryBuilder description(String description) { + this.description = description; + return this; + } + + public Repository build() { + Repository repository = new Repository(); + repository.setId(id); + repository.setType(type); + repository.setContact(contact); + repository.setNamespace(namespace); + repository.setName(name); + repository.setDescription(description); + return repository; + } +} diff --git a/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java b/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java index d5d443fe31..82c6e24108 100644 --- a/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java +++ b/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java @@ -6,13 +6,13 @@ * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE @@ -26,154 +26,75 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository; -/** - * - * @author Sebastian Sdorra - */ -public final class RepositoryTestData -{ +public final class RepositoryTestData { - /** - * Constructs ... - * - */ - private RepositoryTestData() {} + public static final String NAMESPACE = "hitchhiker"; + public static final String MAIL_DOMAIN = "@hitchhiker.com"; - //~--- methods -------------------------------------------------------------- + private RepositoryTestData() { + } - /** - * Method description - * - * - * @return - */ - public static Repository create42Puzzle() - { + public static Repository create42Puzzle() { return create42Puzzle(DummyRepositoryHandler.TYPE_NAME); } - /** - * Method description - * - * - * @param type - * - * @return - */ - public static Repository create42Puzzle(String type) - { - Repository repository = new Repository(); - - repository.setType(type); - repository.setContact("douglas.adams@hitchhiker.com"); - repository.setName("42Puzzle"); - repository.setDescription("The 42 Puzzle"); - - return repository; + public static Repository create42Puzzle(String type) { + return new RepositoryBuilder() + .type(type) + .contact("douglas.adams" + MAIL_DOMAIN) + .name("42Puzzle") + .namespace(NAMESPACE) + .description("The 42 Puzzle") + .build(); } - /** - * Method description - * - * - * @return - */ - public static Repository createHappyVerticalPeopleTransporter() - { + public static Repository createHappyVerticalPeopleTransporter() { return createHappyVerticalPeopleTransporter( DummyRepositoryHandler.TYPE_NAME); } - /** - * Method description - * - * - * - * @param type - * @return - */ - public static Repository createHappyVerticalPeopleTransporter(String type) - { - Repository happyVerticalPeopleTransporter = new Repository(); - - happyVerticalPeopleTransporter.setType(type); - happyVerticalPeopleTransporter.setContact( - "zaphod.beeblebrox@hitchhiker.com"); - happyVerticalPeopleTransporter.setName("happyVerticalPeopleTransporter"); - happyVerticalPeopleTransporter.setDescription( - "Happy Vertical People Transporter"); - - return happyVerticalPeopleTransporter; + public static Repository createHappyVerticalPeopleTransporter(String type) { + return new RepositoryBuilder() + .type(type) + .contact("zaphod.beeblebrox" + MAIL_DOMAIN) + .name("happyVerticalPeopleTransporter") + .namespace(NAMESPACE) + .description("Happy Vertical People Transporter") + .build(); } - /** - * Method description - * - * - * @return - */ - public static Repository createHeartOfGold() - { + public static Repository createHeartOfGold() { return createHeartOfGold(DummyRepositoryHandler.TYPE_NAME); } - /** - * Method description - * - * - * - * @param type - * @return - */ - public static Repository createHeartOfGold(String type) - { - Repository heartOfGold = new Repository(); - - heartOfGold.setType(type); - heartOfGold.setContact("zaphod.beeblebrox@hitchhiker.com"); - heartOfGold.setName("HeartOfGold"); - heartOfGold.setDescription( - "Heart of Gold is the first prototype ship to successfully utilise the revolutionary Infinite Improbability Drive"); - - return heartOfGold; + public static Repository createHeartOfGold(String type) { + return new RepositoryBuilder() + .type(type) + .contact("zaphod.beeblebrox" + MAIL_DOMAIN) + .name("HeartOfGold") + .namespace(NAMESPACE) + .description( + "Heart of Gold is the first prototype ship to successfully utilise the revolutionary Infinite Improbability Drive") + .build(); } - /** - * Method description - * - * - * @return - */ - public static Repository createRestaurantAtTheEndOfTheUniverse() - { + public static Repository createRestaurantAtTheEndOfTheUniverse() { return createRestaurantAtTheEndOfTheUniverse( DummyRepositoryHandler.TYPE_NAME); } - /** - * Method description - * - * - * @param type - * - * @return - */ - public static Repository createRestaurantAtTheEndOfTheUniverse(String type) - { - Repository repository = new Repository(); - - repository.setType(type); - repository.setContact("douglas.adams@hitchhiker.com"); - repository.setName("RestaurantAtTheEndOfTheUniverse"); - repository.setDescription("The Restaurant at the End of the Universe"); - - return repository; + public static Repository createRestaurantAtTheEndOfTheUniverse(String type) { + return new RepositoryBuilder() + .type(type) + .contact("douglas.adams" + MAIL_DOMAIN) + .name("RestaurantAtTheEndOfTheUniverse") + .namespace(NAMESPACE) + .description("The Restaurant at the End of the Universe") + .build(); } } diff --git a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java index 19af367a1d..615169b58c 100644 --- a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + * <p> * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * <p> * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * <p> * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,182 +24,102 @@ * 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. - * + * <p> * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; - +import org.mockito.stubbing.Answer; import sonia.scm.AbstractTestBase; +import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.InMemoryConfigurationStoreFactory; import sonia.scm.util.IOUtil; -import static org.junit.Assert.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ -import java.io.File; -import java.io.IOException; -import sonia.scm.store.ConfigurationStoreFactory; - /** - * * @author Sebastian Sdorra */ -public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase -{ +public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { + + + protected RepositoryDAO repoDao = mock(RepositoryDAO.class); + protected Path repoPath; + protected Repository repository; - /** - * Method description - * - * - * @param directory - */ protected abstract void checkDirectory(File directory); - /** - * Method description - * - * - * @param factory - * @param directory - * - * @return - */ protected abstract RepositoryHandler createRepositoryHandler( - ConfigurationStoreFactory factory, File directory); + ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) throws IOException, RepositoryPathNotFoundException; - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testCreate() throws RepositoryException, IOException - { + public void testCreate() { createRepository(); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ - @Test(expected = RepositoryAlreadyExistsException.class) - public void testCreateExisitingRepository() - throws RepositoryException, IOException - { - createRepository(); - createRepository(); - } - - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ - @Test - public void testCreateResourcePath() throws RepositoryException, IOException - { - Repository repository = createRepository(); - String path = handler.createResourcePath(repository); - - assertNotNull(path); - assertTrue(path.trim().length() > 0); - assertTrue(path.contains(repository.getName())); - } - - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ - @Test - public void testDelete() throws RepositoryException, IOException - { - Repository repository = createRepository(); - - handler.delete(repository); - - File directory = new File(baseDirectory, repository.getName()); - - assertFalse(directory.exists()); - } - - /** - * Method description - * - * - * @throws Exception - */ @Override - protected void postSetUp() throws Exception - { + protected void postSetUp() throws IOException, RepositoryPathNotFoundException { InMemoryConfigurationStoreFactory storeFactory = new InMemoryConfigurationStoreFactory(); baseDirectory = new File(contextProvider.getBaseDirectory(), "repositories"); IOUtil.mkdirs(baseDirectory); - handler = createRepositoryHandler(storeFactory, baseDirectory); + + locationResolver = mock(RepositoryLocationResolver.class); + + RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class); + when(locationResolver.create(any())).thenReturn(instanceMock); + when(locationResolver.supportsLocationType(any())).thenReturn(true); + Answer<Object> pathAnswer = ic -> { + String id = ic.getArgument(0); + return baseDirectory.toPath().resolve(id); + }; + when(instanceMock.getLocation(anyString())).then(pathAnswer); + when(instanceMock.createLocation(anyString())).then(pathAnswer); + + handler = createRepositoryHandler(storeFactory, locationResolver, baseDirectory); } - /** - * Method description - * - * - * @throws Exception - */ @Override - protected void preTearDown() throws Exception - { - if (handler != null) - { + protected void preTearDown() throws Exception { + if (handler != null) { handler.close(); } } - /** - * Method description - * - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private Repository createRepository() throws RepositoryException, IOException - { - Repository repository = RepositoryTestData.createHeartOfGold(); + private void createRepository() { + File nativeRepoDirectory = initRepository(); handler.create(repository); - File directory = new File(baseDirectory, repository.getName()); - - assertTrue(directory.exists()); - assertTrue(directory.isDirectory()); - checkDirectory(directory); - - return repository; + assertTrue(nativeRepoDirectory.exists()); + assertTrue(nativeRepoDirectory.isDirectory()); + checkDirectory(nativeRepoDirectory); } - //~--- fields --------------------------------------------------------------- + protected File initRepository() { + repository = RepositoryTestData.createHeartOfGold(); + File repoDirectory = new File(baseDirectory, repository.getId()); + repoPath = repoDirectory.toPath(); +// when(repoDao.getPath(repository.getId())).thenReturn(repoPath); + return new File(repoDirectory, RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); + } - /** Field description */ protected File baseDirectory; + protected RepositoryLocationResolver locationResolver; - /** Field description */ private RepositoryHandler handler; } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/AddCommandBuilder.java b/scm-test/src/main/java/sonia/scm/repository/client/api/AddCommandBuilder.java index 2add6c73dd..510d2fac11 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/AddCommandBuilder.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/AddCommandBuilder.java @@ -32,97 +32,38 @@ package sonia.scm.repository.client.api; -//~--- non-JDK imports -------------------------------------------------------- - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.client.spi.AddCommand; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; /** - * - * @author Sebastian Sdorra * @since 1.18 */ -public final class AddCommandBuilder -{ +public final class AddCommandBuilder { - /** - * the logger for AddCommandBuilder - */ - private static final Logger logger = - LoggerFactory.getLogger(AddCommandBuilder.class); + private static final Logger logger = LoggerFactory.getLogger(AddCommandBuilder.class); - //~--- constructors --------------------------------------------------------- + private final AddCommand command; - /** - * Constructs ... - * - * - * @param directory - * @param command - */ - AddCommandBuilder(AddCommand command) - { + AddCommandBuilder(AddCommand command) { this.command = command; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param path - * @param pathes - * - * @return - * - * @throws IOException - */ - public AddCommandBuilder add(String path, String... pathes) throws IOException - { - add(path); - - if (Util.isNotEmpty(pathes)) - { - for (String p : pathes) - { - add(p); - } + public AddCommandBuilder add(String... paths) throws IOException { + for (String p : paths) { + add(p); } - return this; } - /** - * Method description - * - * - * @param path - * - * @throws IOException - */ - private void add(String path) throws IOException - { - if (Util.isNotEmpty(path)) - { - if (logger.isDebugEnabled()) - { - logger.debug("add path {}", path); - } + private void add(String path) throws IOException { + if (Util.isNotEmpty(path)) { + logger.debug("add path {}", path); command.add(path); } } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private AddCommand command; } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/ClientCommand.java b/scm-test/src/main/java/sonia/scm/repository/client/api/ClientCommand.java index 219767fe6c..b5de4ee5f7 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/ClientCommand.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/ClientCommand.java @@ -39,5 +39,5 @@ package sonia.scm.repository.client.api; */ public enum ClientCommand { - ADD, REMOVE, COMMIT, PUSH, TAG, BANCH + ADD, REMOVE, COMMIT, PUSH, TAG, BRANCH } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/PushCommandBuilder.java b/scm-test/src/main/java/sonia/scm/repository/client/api/PushCommandBuilder.java index b3b7619e7b..06b59e4ba0 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/PushCommandBuilder.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/PushCommandBuilder.java @@ -36,13 +36,12 @@ package sonia.scm.repository.client.api; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.client.spi.PushCommand; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -88,6 +87,14 @@ public final class PushCommandBuilder command.push(); } + public void pushTags() throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("push tag changes back to main repository"); + } + + command.pushTags(); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/RemoveCommandBuilder.java b/scm-test/src/main/java/sonia/scm/repository/client/api/RemoveCommandBuilder.java index 23696ae10d..4c3a2517ce 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/RemoveCommandBuilder.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/RemoveCommandBuilder.java @@ -32,98 +32,37 @@ package sonia.scm.repository.client.api; -//~--- non-JDK imports -------------------------------------------------------- - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.client.spi.RemoveCommand; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; /** - * - * @author Sebastian Sdorra * @since 1.18 */ -public final class RemoveCommandBuilder -{ +public final class RemoveCommandBuilder { - /** - * the logger for RemoveCommandBuilder - */ - private static final Logger logger = - LoggerFactory.getLogger(RemoveCommandBuilder.class); + private static final Logger logger = LoggerFactory.getLogger(RemoveCommandBuilder.class); - //~--- constructors --------------------------------------------------------- + private final RemoveCommand command; - /** - * Constructs ... - * - * - * @param directory - * @param command - */ - RemoveCommandBuilder(RemoveCommand command) - { + RemoveCommandBuilder(RemoveCommand command) { this.command = command; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param path - * @param pathes - * - * @return - * - * @throws IOException - */ - public RemoveCommandBuilder remove(String path, String... pathes) - throws IOException - { - remove(path); - - if (Util.isNotEmpty(pathes)) - { - for (String p : pathes) - { - remove(p); - } + public RemoveCommandBuilder remove(String... paths) throws IOException { + for (String p : paths) { + remove(p); } - return this; } - /** - * Method description - * - * - * @param path - * - * @throws IOException - */ - private void remove(String path) throws IOException - { - if (Util.isNotEmpty(path)) - { - if (logger.isDebugEnabled()) - { - logger.debug("add path {}", path); - } - + private void remove(String path) throws IOException { + if (Util.isNotEmpty(path)) { + logger.debug("add path {}", path); command.remove(path); } } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private RemoveCommand command; } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClient.java b/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClient.java index 664c5a9446..7f18e4fe22 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClient.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClient.java @@ -29,191 +29,75 @@ * */ - - package sonia.scm.repository.client.api; -//~--- non-JDK imports -------------------------------------------------------- - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.client.spi.RepositoryClientProvider; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Closeable; import java.io.File; -/** - * - * @author Sebastian Sdorra - * @since 1.18 - */ -public final class RepositoryClient implements Closeable -{ +public final class RepositoryClient implements Closeable { - /** - * the logger for RepositoryClient - */ - private static final Logger logger = - LoggerFactory.getLogger(RepositoryClient.class); + private static final Logger logger = LoggerFactory.getLogger(RepositoryClient.class); - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param directory - * @param clientProvider - */ RepositoryClient(RepositoryClientProvider clientProvider) { this.clientProvider = clientProvider; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ @Override - public void close() - { - if (logger.isTraceEnabled()) - { - logger.trace("close client provider"); - } + public void close() { + logger.trace("close client provider"); IOUtil.close(clientProvider); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public AddCommandBuilder getAddCommand() - { - if (logger.isTraceEnabled()) - { - logger.trace("create add command"); - } + public AddCommandBuilder getAddCommand() { + logger.trace("create add command"); return new AddCommandBuilder(clientProvider.getAddCommand()); } - /** - * Method description - * - * - * @return - */ - public BranchCommandBuilder getBranchCommand() - { - if (logger.isTraceEnabled()) - { - logger.trace("create branch command"); - } + public BranchCommandBuilder getBranchCommand() { + logger.trace("create branch command"); return new BranchCommandBuilder(clientProvider.getBranchCommand()); } - /** - * Method description - * - * - * @return - */ - public CommitCommandBuilder getCommitCommand() - { - if (logger.isTraceEnabled()) - { - logger.trace("create commit command"); - } + public CommitCommandBuilder getCommitCommand() { + logger.trace("create commit command"); return new CommitCommandBuilder(clientProvider.getCommitCommand()); } - /** - * Method description - * - * - * @return - */ - public PushCommandBuilder getPushCommand() - { - if (logger.isTraceEnabled()) - { - logger.trace("create push command"); - } + public PushCommandBuilder getPushCommand() { + logger.trace("create push command"); return new PushCommandBuilder(clientProvider.getPushCommand()); } - /** - * Method description - * - * - * @return - */ - public RemoveCommandBuilder getRemoveCommand() - { - if (logger.isTraceEnabled()) - { - logger.trace("create remove command"); - } + public RemoveCommandBuilder getRemoveCommand() { + logger.trace("create remove command"); return new RemoveCommandBuilder(clientProvider.getRemoveCommand()); } - /** - * Method description - * - * - * @return - */ - public TagCommandBuilder getTagCommand() - { - if (logger.isTraceEnabled()) - { - logger.trace("create tag command"); - } + public TagCommandBuilder getTagCommand() { + logger.trace("create tag command"); return new TagCommandBuilder(clientProvider.getTagCommand()); } - /** - * Returns the working copy of the repository. - * - * @return working copy - * @since 1.51 - */ public File getWorkingCopy() { return clientProvider.getWorkingCopy(); } - /** - * Method description - * - * - * @param command - * - * @return - */ - public boolean isCommandSupported(ClientCommand command) - { + public boolean isCommandSupported(ClientCommand command) { return clientProvider.getSupportedClientCommands().contains(command); } - //~--- fields --------------------------------------------------------------- - - /** Field description */ private final RepositoryClientProvider clientProvider; } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClientFactory.java b/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClientFactory.java index 17f1818e33..8dec6ec524 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClientFactory.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClientFactory.java @@ -123,6 +123,12 @@ public final class RepositoryClientFactory password, workingCopy)); } + public RepositoryClient create(String type, String url, File workingCopy) + throws IOException + { + return new RepositoryClient(getProvider(type).create(url, null, null, workingCopy)); + } + //~--- get methods ---------------------------------------------------------- /** diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/TagCommandBuilder.java b/scm-test/src/main/java/sonia/scm/repository/client/api/TagCommandBuilder.java index 8b42817aa8..1d153dee0e 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/TagCommandBuilder.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/TagCommandBuilder.java @@ -36,15 +36,14 @@ package sonia.scm.repository.client.api; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.Tag; import sonia.scm.repository.client.spi.TagCommand; import sonia.scm.repository.client.spi.TagRequest; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -84,9 +83,10 @@ public final class TagCommandBuilder * * @throws IOException */ - public Tag tag(String name) throws IOException + public Tag tag(String name, String username) throws IOException { request.setName(name); + request.setUsername(username); if (logger.isDebugEnabled()) { diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/CommitRequest.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/CommitRequest.java index e278604366..2138fef5cc 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/spi/CommitRequest.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/CommitRequest.java @@ -36,7 +36,6 @@ package sonia.scm.repository.client.spi; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; - import sonia.scm.repository.Person; /** @@ -107,9 +106,9 @@ public final class CommitRequest { //J- return MoreObjects.toStringHelper(this) - .add("author", author) - .add("message", message) - .toString(); + .add("author", author) + .add("message", message) + .toString(); //J+ } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/PushCommand.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/PushCommand.java index 5e85ed0e5e..be8fd94718 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/spi/PushCommand.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/PushCommand.java @@ -44,11 +44,7 @@ import java.io.IOException; public interface PushCommand { - /** - * Method description - * - * - * @throws IOException - */ - public void push() throws IOException; + void push() throws IOException; + + void pushTags() throws IOException; } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/RepositoryClientProvider.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/RepositoryClientProvider.java index dd32ee066f..6cb40411e1 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/spi/RepositoryClientProvider.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/RepositoryClientProvider.java @@ -37,14 +37,13 @@ package sonia.scm.repository.client.spi; import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.ClientCommandNotSupportedException; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Closeable; import java.io.File; import java.io.IOException; - import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -93,7 +92,7 @@ public abstract class RepositoryClientProvider implements Closeable */ public BranchCommand getBranchCommand() { - throw new ClientCommandNotSupportedException(ClientCommand.BANCH); + throw new ClientCommandNotSupportedException(ClientCommand.BRANCH); } /** diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/TagRequest.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/TagRequest.java index 3138a4331e..53abf0f8f8 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/spi/TagRequest.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/TagRequest.java @@ -36,7 +36,6 @@ package sonia.scm.repository.client.spi; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; - /** * * @author Sebastian Sdorra @@ -92,6 +91,7 @@ public final class TagRequest { this.name = null; this.revision = null; + this.username = null; } /** @@ -105,9 +105,10 @@ public final class TagRequest { //J- return MoreObjects.toStringHelper(this) - .add("revision", revision) - .add("name", name) - .toString(); + .add("revision", revision) + .add("name", name) + .add("username", username) + .toString(); //J+ } @@ -135,6 +136,10 @@ public final class TagRequest this.revision = revision; } + public void setUsername(String username) { + this.username = username; + } + //~--- get methods ---------------------------------------------------------- /** @@ -159,6 +164,10 @@ public final class TagRequest return revision; } + public String getUserName() { + return username; + } + //~--- fields --------------------------------------------------------------- /** Field description */ @@ -166,4 +175,6 @@ public final class TagRequest /** Field description */ private String revision; + + private String username; } diff --git a/scm-test/src/main/java/sonia/scm/repository/spi/ZippedRepositoryTestBase.java b/scm-test/src/main/java/sonia/scm/repository/spi/ZippedRepositoryTestBase.java index c7a3b8f4e9..99f91c7926 100644 --- a/scm-test/src/main/java/sonia/scm/repository/spi/ZippedRepositoryTestBase.java +++ b/scm-test/src/main/java/sonia/scm/repository/spi/ZippedRepositoryTestBase.java @@ -153,7 +153,12 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase */ private void extract(File folder) throws IOException { - URL url = Resources.getResource(getZippedRepositoryResource()); + String zippedRepositoryResource = getZippedRepositoryResource(); + extract(folder, zippedRepositoryResource); + } + + public static void extract(File targetFolder, String zippedRepositoryResource) throws IOException { + URL url = Resources.getResource(zippedRepositoryResource); ZipInputStream zip = null; try @@ -164,7 +169,7 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase while (entry != null) { - File file = new File(folder, entry.getName()); + File file = new File(targetFolder, entry.getName()); File parent = file.getParentFile(); if (!parent.exists()) diff --git a/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java index 48504feaf2..f3f252053d 100644 --- a/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java @@ -35,22 +35,24 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.io.ByteStreams; - import org.junit.Before; import org.junit.Test; - import sonia.scm.AbstractTestBase; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.RepositoryTestData; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; - import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -58,12 +60,6 @@ import java.util.List; public abstract class BlobStoreTestBase extends AbstractTestBase { - /** - * Method description - * - * - * @return - */ protected abstract BlobStoreFactory createBlobStoreFactory(); /** @@ -73,7 +69,10 @@ public abstract class BlobStoreTestBase extends AbstractTestBase @Before public void createBlobStore() { - store = createBlobStoreFactory().getBlobStore("test"); + store = createBlobStoreFactory() + .withName("test") + .forRepository(RepositoryTestData.createHeartOfGold()) + .build(); store.clear(); } diff --git a/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java index 8d3a63717a..140bd54e65 100644 --- a/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java @@ -32,12 +32,13 @@ package sonia.scm.store; +import sonia.scm.repository.Repository; + /** * * @author Sebastian Sdorra */ -public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestBase -{ +public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestBase { /** * Method description @@ -48,17 +49,20 @@ public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestB protected abstract ConfigurationEntryStoreFactory createConfigurationStoreFactory(); //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ @Override - protected ConfigurationEntryStore<StoreObject> getDataStore() - { - return createConfigurationStoreFactory().getStore(StoreObject.class, - "test"); + protected ConfigurationEntryStore getDataStore(Class type) { + return this.createConfigurationStoreFactory() + .withType(type) + .withName(storeName) + .build(); + } + + @Override + protected ConfigurationEntryStore getDataStore(Class type, Repository repository) { + return this.createConfigurationStoreFactory() + .withType(type) + .withName(repoStoreName) + .forRepository(repository) + .build(); } } diff --git a/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java index 3129d3a339..39ce021715 100644 --- a/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java @@ -33,6 +33,13 @@ package sonia.scm.store; +import org.junit.Test; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * * @author Sebastian Sdorra @@ -48,17 +55,29 @@ public abstract class DataStoreTestBase extends KeyValueStoreTestBase */ protected abstract DataStoreFactory createDataStoreFactory(); + //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - */ - @Override - protected DataStore<StoreObject> getDataStore() + + + + @Test + public void shouldStoreRepositorySpecificData() { - return createDataStoreFactory().getStore(StoreObject.class, "test"); + DataStoreFactory dataStoreFactory = createDataStoreFactory(); + StoreObject obj = new StoreObject("test-1"); + Repository repository = RepositoryTestData.createHeartOfGold(); + + DataStore<StoreObject> store = dataStoreFactory + .withType(StoreObject.class) + .withName("test") + .forRepository(repository) + .build(); + + String id = store.put(obj); + + assertNotNull(id); + + assertEquals(obj, store.get(id)); } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java new file mode 100644 index 0000000000..40124dd717 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java @@ -0,0 +1,52 @@ +package sonia.scm.store; + +import com.google.common.base.Predicate; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +public class InMemoryConfigurationEntryStore<V> implements ConfigurationEntryStore<V> { + + private final Map<String, V> values = new HashMap<>(); + + @Override + public Collection<V> getMatchingValues(Predicate<V> predicate) { + return values.values().stream().filter(predicate).collect(Collectors.toList()); + } + + @Override + public String put(V item) { + String key = UUID.randomUUID().toString(); + values.put(key, item); + return key; + } + + @Override + public void put(String id, V item) { + values.put(id, item); + } + + @Override + public Map<String, V> getAll() { + return Collections.unmodifiableMap(values); + } + + @Override + public void clear() { + values.clear(); + } + + @Override + public void remove(String id) { + values.remove(id); + } + + @Override + public V get(String id) { + return values.get(id); + } +} diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java new file mode 100644 index 0000000000..1578a20751 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java @@ -0,0 +1,23 @@ +package sonia.scm.store; + +import java.util.HashMap; +import java.util.Map; + +public class InMemoryConfigurationEntryStoreFactory implements ConfigurationEntryStoreFactory { + + private final Map<String, InMemoryConfigurationEntryStore> stores = new HashMap<>(); + + public static InMemoryConfigurationEntryStoreFactory create() { + return new InMemoryConfigurationEntryStoreFactory(); + } + + @Override + public <T> ConfigurationEntryStore<T> getStore(TypedStoreParameters<T> storeParameters) { + String name = storeParameters.getName(); + return get(name); + } + + public <T> InMemoryConfigurationEntryStore<T> get(String name) { + return stores.computeIfAbsent(name, x -> new InMemoryConfigurationEntryStore()); + } +} diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java index d5e9474ff5..7f5d2a2b52 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java @@ -35,16 +35,36 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- +import java.util.HashMap; +import java.util.Map; + /** * In memory configuration store factory for testing purposes. + * + * Use {@link #create()} to get a store that creates the same store on each request. * * @author Sebastian Sdorra */ public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory { + private final Map<String, InMemoryConfigurationStore> stores = new HashMap<>(); + + public static InMemoryConfigurationStoreFactory create() { + return new InMemoryConfigurationStoreFactory(); + } + @Override - public <T> ConfigurationStore<T> getStore(Class<T> type, String name) - { - return new InMemoryConfigurationStore<>(); + public ConfigurationStore getStore(TypedStoreParameters storeParameters) { + String name = storeParameters.getName(); + String id = storeParameters.getRepositoryId(); + return get(name, id); + } + + public ConfigurationStore get(String name, String id) { + return stores.computeIfAbsent(buildKey(name, id), x -> new InMemoryConfigurationStore()); + } + + private String buildKey(String name, String id) { + return id == null? name: name + "-" + id; } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryDataStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryDataStore.java new file mode 100644 index 0000000000..06198d89bf --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryDataStore.java @@ -0,0 +1,53 @@ +package sonia.scm.store; + +import sonia.scm.security.KeyGenerator; +import sonia.scm.security.UUIDKeyGenerator; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * In memory store implementation of {@link DataStore}. + * + * @author Sebastian Sdorra + * + * @param <T> type of stored object + */ +public class InMemoryDataStore<T> implements DataStore<T> { + + private final Map<String, T> store = new HashMap<>(); + private KeyGenerator generator = new UUIDKeyGenerator(); + + @Override + public String put(T item) { + String key = generator.createKey(); + store.put(key, item); + return key; + } + + @Override + public void put(String id, T item) { + store.put(id, item); + } + + @Override + public Map<String, T> getAll() { + return Collections.unmodifiableMap(store); + } + + @Override + public void clear() { + store.clear(); + } + + @Override + public void remove(String id) { + store.remove(id); + } + + @Override + public T get(String id) { + return store.get(id); + } +} diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryDataStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryDataStoreFactory.java new file mode 100644 index 0000000000..b0e95e9f9c --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryDataStoreFactory.java @@ -0,0 +1,26 @@ +package sonia.scm.store; + +/** + * In memory configuration store factory for testing purposes. + * + * @author Sebastian Sdorra + */ +public class InMemoryDataStoreFactory implements DataStoreFactory { + + private InMemoryDataStore store; + + public InMemoryDataStoreFactory() { + } + + public InMemoryDataStoreFactory(InMemoryDataStore store) { + this.store = store; + } + + @Override + public <T> DataStore<T> getStore(TypedStoreParameters<T> storeParameters) { + if (store != null) { + return store; + } + return new InMemoryDataStore<>(); + } +} diff --git a/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java index 0abad4f558..a54b58178f 100644 --- a/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java @@ -38,6 +38,8 @@ import org.junit.Before; import org.junit.Test; import sonia.scm.AbstractTestBase; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -56,13 +58,21 @@ import java.util.Map; public abstract class KeyValueStoreTestBase extends AbstractTestBase { + protected Repository repository = RepositoryTestData.createHeartOfGold(); + protected DataStore<StoreObject> store; + protected DataStore<StoreObject> repoStore; + protected String repoStoreName = "testRepoStore"; + protected String storeName = "testStore"; + /** * Method description * * * @return */ - protected abstract DataStore<StoreObject> getDataStore(); + protected abstract <STORE_OBJECT> DataStore<STORE_OBJECT> getDataStore(Class<STORE_OBJECT> type , Repository repository); + protected abstract <STORE_OBJECT> DataStore<STORE_OBJECT> getDataStore(Class<STORE_OBJECT> type ); + //~--- methods -------------------------------------------------------------- @@ -73,8 +83,10 @@ public abstract class KeyValueStoreTestBase extends AbstractTestBase @Before public void before() { - store = getDataStore(); + store = getDataStore(StoreObject.class); + repoStore = getDataStore(StoreObject.class, repository); store.clear(); + repoStore.clear(); } /** @@ -215,8 +227,5 @@ public abstract class KeyValueStoreTestBase extends AbstractTestBase assertNull(store.get("2")); } - //~--- fields --------------------------------------------------------------- - /** Field description */ - private DataStore<StoreObject> store; } diff --git a/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java index c39efa3ffe..ef806c79f8 100644 --- a/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java +++ b/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java @@ -35,10 +35,11 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; - import sonia.scm.AbstractTestBase; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; //~--- JDK imports ------------------------------------------------------------ @@ -65,8 +66,7 @@ public abstract class StoreTestBase extends AbstractTestBase @Test public void testGet() { - ConfigurationStore<StoreObject> store = createStoreFactory().getStore(StoreObject.class, - "test"); + ConfigurationStore<StoreObject> store = createStoreFactory().withType(StoreObject.class).withName("test").build(); assertNotNull(store); @@ -82,8 +82,7 @@ public abstract class StoreTestBase extends AbstractTestBase @Test public void testSet() { - ConfigurationStore<StoreObject> store = createStoreFactory().getStore(StoreObject.class, - "test"); + ConfigurationStore<StoreObject> store = createStoreFactory().withType(StoreObject.class).withName("test").build(); assertNotNull(store); diff --git a/scm-test/src/main/java/sonia/scm/update/UpdateStepTestUtil.java b/scm-test/src/main/java/sonia/scm/update/UpdateStepTestUtil.java new file mode 100644 index 0000000000..51899cdbee --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/update/UpdateStepTestUtil.java @@ -0,0 +1,54 @@ +package sonia.scm.update; + +import com.google.common.io.Resources; +import org.mockito.Mockito; +import sonia.scm.SCMContextProvider; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +public class UpdateStepTestUtil { + + private final SCMContextProvider contextProvider; + + private final Path tempDir; + + public UpdateStepTestUtil(Path tempDir) { + this.tempDir = tempDir; + contextProvider = Mockito.mock(SCMContextProvider.class); + lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); + lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString())); + } + + public SCMContextProvider getContextProvider() { + return contextProvider; + } + + public void copyConfigFile(String fileName) throws IOException { + Path configDir = tempDir.resolve("config"); + Files.createDirectories(configDir); + copyTestDatabaseFile(configDir, fileName); + } + + public void copyConfigFile(String fileName, String targetFileName) throws IOException { + Path configDir = tempDir.resolve("config"); + Files.createDirectories(configDir); + copyTestDatabaseFile(configDir, fileName, targetFileName); + } + + private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException { + Path targetFileName = Paths.get(fileName).getFileName(); + copyTestDatabaseFile(configDir, fileName, targetFileName.toString()); + } + + private void copyTestDatabaseFile(Path configDir, String fileName, String targetFileName) throws IOException { + URL url = Resources.getResource(fileName); + Files.copy(url.openStream(), configDir.resolve(targetFileName)); + } +} diff --git a/scm-test/src/main/java/sonia/scm/update/V1PropertyDaoTestUtil.java b/scm-test/src/main/java/sonia/scm/update/V1PropertyDaoTestUtil.java new file mode 100644 index 0000000000..44020ca8a1 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/update/V1PropertyDaoTestUtil.java @@ -0,0 +1,40 @@ +package sonia.scm.update; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Arrays.stream; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class V1PropertyDaoTestUtil { + + private final V1PropertyDAO propertyDAO = mock(V1PropertyDAO.class); + + public V1PropertyDAO getPropertyDAO() { + return propertyDAO; + } + + public void mockRepositoryProperties(PropertiesForRepository... mockedPropertiesForRepositories) { + Map<String, V1Properties> map = new HashMap<>(); + stream(mockedPropertiesForRepositories).forEach(p -> map.put(p.repositoryId, p.asProperties())); + V1PropertyReader.Instance v1PropertyReader = new MapBasedPropertyReaderInstance(map); + when(propertyDAO.getProperties(argThat(argument -> argument instanceof RepositoryV1PropertyReader))).thenReturn(v1PropertyReader); + } + + public static class PropertiesForRepository { + private final String repositoryId; + private final Map<String, String> properties; + + public PropertiesForRepository(String repositoryId, Map<String, String> properties) { + this.repositoryId = repositoryId; + this.properties = properties; + } + + V1Properties asProperties() { + return new V1Properties(properties.entrySet().stream().map(e -> new V1Property(e.getKey(), e.getValue())).collect(Collectors.toList())); + } + } +} diff --git a/scm-test/src/main/java/sonia/scm/user/UserManagerTestBase.java b/scm-test/src/main/java/sonia/scm/user/UserManagerTestBase.java index e86edaba56..3c684f58c9 100644 --- a/scm-test/src/main/java/sonia/scm/user/UserManagerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/user/UserManagerTestBase.java @@ -37,46 +37,31 @@ package sonia.scm.user; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; - import org.junit.Test; - +import sonia.scm.AlreadyExistsException; import sonia.scm.Manager; import sonia.scm.ManagerTestBase; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; +import sonia.scm.NotFoundException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.UUID; -/** - * - * @author Sebastian Sdorra - */ -public abstract class UserManagerTestBase - extends ManagerTestBase<User, UserException> -{ +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +//~--- JDK imports ------------------------------------------------------------ + +public abstract class UserManagerTestBase extends ManagerTestBase<User> { - /** Field description */ public static final int THREAD_COUNT = 10; - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ @Test - public void testCreate() throws UserException, IOException - { + public void testCreate() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -87,16 +72,8 @@ public abstract class UserManagerTestBase assertUserEquals(zaphod, otherUser); } - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ - @Test(expected = UserAlreadyExistsException.class) - public void testCreateExisting() throws UserException, IOException - { + @Test(expected = AlreadyExistsException.class) + public void testCreateExisting() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -107,16 +84,8 @@ public abstract class UserManagerTestBase manager.create(sameUser); } - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ @Test - public void testDelete() throws UserException, IOException - { + public void testDelete() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -125,29 +94,13 @@ public abstract class UserManagerTestBase assertNull(manager.get("zaphod")); } - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ - @Test(expected = UserNotFoundException.class) - public void testDeleteNotFound() throws UserException, IOException - { + @Test(expected = NotFoundException.class) + public void testDeleteNotFound() { manager.delete(UserTestData.createDent()); } - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ @Test - public void testGet() throws UserException, IOException - { + public void testGet() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -160,16 +113,8 @@ public abstract class UserManagerTestBase assertEquals("Zaphod Beeblebrox", zaphod.getDisplayName()); } - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ @Test - public void testGetAll() throws UserException, IOException - { + public void testGetAll() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -233,16 +178,8 @@ public abstract class UserManagerTestBase assertEquals(reference.getDisplayName(), "Tricia McMillan"); } - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ @Test - public void testModify() throws UserException, IOException - { + public void testModify() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -256,33 +193,15 @@ public abstract class UserManagerTestBase assertEquals(otherUser.getDisplayName(), "Tricia McMillan"); } - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ - @Test(expected = UserException.class) - public void testModifyNotExisting() throws UserException, IOException - { + @Test(expected = NotFoundException.class) + public void testModifyNotExisting() { manager.modify(UserTestData.createZaphod()); } - /** - * Method description - * - * - * @throws IOException - * @throws InterruptedException - * @throws UserException - */ @Test - public void testMultiThreaded() - throws UserException, IOException, InterruptedException - { + public void testMultiThreaded() throws InterruptedException { int initialSize = manager.getAll().size(); - List<MultiThreadTester> testers = new ArrayList<>(); + List<MultiThreadTester> testers = new ArrayList<MultiThreadTester>(); for (int i = 0; i < THREAD_COUNT; i++) { @@ -301,7 +220,7 @@ public abstract class UserManagerTestBase while (!fin) { - Thread.sleep(100L); + Thread.sleep(100l); fin = true; for (MultiThreadTester tester : testers) @@ -316,16 +235,8 @@ public abstract class UserManagerTestBase assertTrue((initialSize + THREAD_COUNT) == manager.getAll().size()); } - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ @Test - public void testRefresh() throws UserException, IOException - { + public void testRefresh() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -335,26 +246,11 @@ public abstract class UserManagerTestBase assertEquals(zaphod.getDisplayName(), "Zaphod Beeblebrox"); } - /** - * Method description - * - * - * @throws IOException - * @throws UserException - */ - @Test(expected = UserNotFoundException.class) - public void testRefreshNotFound() throws UserException, IOException - { + @Test(expected = NotFoundException.class) + public void testRefreshNotFound(){ manager.refresh(UserTestData.createDent()); } - /** - * Method description - * - * - * @param user - * @param otherUser - */ private void assertUserEquals(User user, User otherUser) { assertEquals(user.getName(), otherUser.getName()); @@ -363,35 +259,16 @@ public abstract class UserManagerTestBase assertEquals(user.getPassword(), otherUser.getPassword()); } - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 2010-11-23 - * @author Sebastian Sdorra - */ private static class MultiThreadTester implements Runnable { - /** - * Constructs ... - * - * - * @param userManager - */ - public MultiThreadTester(Manager<User, UserException> userManager) + public MultiThreadTester(Manager<User> userManager) { this.manager = userManager; } //~--- methods ------------------------------------------------------------ - /** - * Method description - * - */ @Override public void run() { @@ -410,17 +287,7 @@ public abstract class UserManagerTestBase finished = true; } - /** - * Method description - * - * - * @return - * - * @throws IOException - * @throws UserException - */ - private User createUser() throws UserException, IOException - { + private User createUser() { String id = UUID.randomUUID().toString(); User user = new User(id, id.concat(" displayName"), id.concat("@mail.com")); @@ -430,18 +297,7 @@ public abstract class UserManagerTestBase return user; } - /** - * Method description - * - * - * @param user - * - * @throws IOException - * @throws UserException - */ - private void modifyAndDeleteUser(User user) - throws UserException, IOException - { + private void modifyAndDeleteUser(User user) { String name = user.getName(); String nd = name.concat(" new displayname"); @@ -463,6 +319,6 @@ public abstract class UserManagerTestBase private boolean finished = false; /** Field description */ - private Manager<User, UserException> manager; + private Manager<User> manager; } } diff --git a/scm-test/src/main/java/sonia/scm/user/UserTestData.java b/scm-test/src/main/java/sonia/scm/user/UserTestData.java index 67a2d33b0b..eb5e8d6e28 100644 --- a/scm-test/src/main/java/sonia/scm/user/UserTestData.java +++ b/scm-test/src/main/java/sonia/scm/user/UserTestData.java @@ -111,8 +111,9 @@ public final class UserTestData */ public static User createTrillian() { - return new User("trillian", "Tricia McMillan", - "tricia.mcmillan@hitchhiker.com"); + User user = new User("trillian", "Tricia McMillan", "tricia.mcmillan@hitchhiker.com"); + user.setType("xml"); + return user; } /** @@ -123,7 +124,8 @@ public final class UserTestData */ public static User createZaphod() { - return new User("zaphod", "Zaphod Beeblebrox", - "zaphod.beeblebrox@hitchhiker.com"); + User user = new User("zaphod", "Zaphod Beeblebrox", "zaphod.beeblebrox@hitchhiker.com"); + user.setType("xml"); + return user; } } diff --git a/scm-test/src/main/java/sonia/scm/util/MockUtil.java b/scm-test/src/main/java/sonia/scm/util/MockUtil.java index 1d15477472..4345b4d225 100644 --- a/scm-test/src/main/java/sonia/scm/util/MockUtil.java +++ b/scm-test/src/main/java/sonia/scm/util/MockUtil.java @@ -41,21 +41,28 @@ import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject.Builder; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + import sonia.scm.SCMContextProvider; -import sonia.scm.security.Role; import sonia.scm.user.User; import sonia.scm.user.UserTestData; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.File; -import java.util.Arrays; -import java.util.List; - import static org.mockito.Mockito.*; //~--- JDK imports ------------------------------------------------------------ +import java.io.File; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import sonia.scm.security.Role; + /** * * @author Sebastian Sdorra @@ -90,7 +97,12 @@ public final class MockUtil when(subject.isAuthenticated()).thenReturn(Boolean.TRUE); when(subject.isPermitted(anyListOf(Permission.class))).then( - invocation -> { + new Answer<Boolean[]>() + { + + @Override + public Boolean[] answer(InvocationOnMock invocation) throws Throwable + { List<Permission> permissions = (List<Permission>) invocation.getArguments()[0]; Boolean[] returnArray = new Boolean[permissions.size()]; @@ -98,13 +110,13 @@ public final class MockUtil Arrays.fill(returnArray, Boolean.TRUE); return returnArray; - }); + } + }); when(subject.isPermitted(any(Permission.class))).thenReturn(Boolean.TRUE); when(subject.isPermitted(any(String.class))).thenReturn(Boolean.TRUE); when(subject.isPermittedAll(anyCollectionOf(Permission.class))).thenReturn( Boolean.TRUE); when(subject.isPermittedAll()).thenReturn(Boolean.TRUE); - when(subject.hasRole(Role.ADMIN)).thenReturn(Boolean.TRUE); when(subject.hasRole(Role.USER)).thenReturn(Boolean.TRUE); PrincipalCollection collection = mock(PrincipalCollection.class); @@ -200,7 +212,11 @@ public final class MockUtil { SCMContextProvider provider = mock(SCMContextProvider.class); - when(provider.getBaseDirectory()).thenReturn(directory); + lenient().when(provider.getBaseDirectory()).thenReturn(directory); + lenient().when(provider.resolve(any(Path.class))).then(ic -> { + Path p = ic.getArgument(0); + return directory.toPath().resolve(p); + }); return provider; } diff --git a/scm-ui/README.md b/scm-ui/README.md new file mode 100644 index 0000000000..b5d407a69b --- /dev/null +++ b/scm-ui/README.md @@ -0,0 +1,27 @@ +# scm-ui + +## VSCode Plugins + +* EditorConfig for VS Code +* Flow Language Support +* Prettier - Code formatter +* Project Snippets +* Debugger for Chrome + +```bash +code --install-extension EditorConfig.EditorConfig +code --install-extension flowtype.flow-for-vscode +code --install-extension esbenp.prettier-vscode +code --install-extension rebornix.project-snippets + +# debugging with chrome browser +code --install-extension msjsdiag.debugger-for-chrome +``` + +## Install pre-commit hook + +```bash +echo "" >> .hg/hgrc +echo "[hooks]" >> .hg/hgrc +echo "pre-commit = cd scm-ui && yarn run pre-commit" >> .hg/hgrc +``` diff --git a/scm-ui/babel-preset/index.js b/scm-ui/babel-preset/index.js new file mode 100644 index 0000000000..54c71e3741 --- /dev/null +++ b/scm-ui/babel-preset/index.js @@ -0,0 +1,12 @@ +module.exports = () => ({ + presets: [ + require("@babel/preset-env"), + require("@babel/preset-flow"), + require("@babel/preset-react"), + require("@babel/preset-typescript") + ], + plugins: [ + require("@babel/plugin-proposal-class-properties"), + require("@babel/plugin-proposal-optional-chaining") + ] +}); diff --git a/scm-ui/babel-preset/package.json b/scm-ui/babel-preset/package.json new file mode 100644 index 0000000000..4751670f65 --- /dev/null +++ b/scm-ui/babel-preset/package.json @@ -0,0 +1,22 @@ +{ + "name": "@scm-manager/babel-preset", + "version": "2.0.0-SNAPSHOT", + "license": "BSD-3-Clause", + "description": "Babel configuration for scm-manager and its plugins", + "main": "index.js", + "author": "Sebastian Sdorra <s.sdorra@cloudogu.com>", + "private": false, + "prettier": "@scm-manager/prettier-config", + "dependencies": { + "@babel/core": "^7.6.3", + "@babel/plugin-proposal-class-properties": "^7.5.5", + "@babel/plugin-proposal-optional-chaining": "^7.6.0", + "@babel/preset-env": "^7.6.3", + "@babel/preset-flow": "^7.0.0", + "@babel/preset-react": "^7.6.3", + "@babel/preset-typescript": "^7.6.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/scm-ui/eslint-config/index.js b/scm-ui/eslint-config/index.js new file mode 100644 index 0000000000..b3d8eb7bba --- /dev/null +++ b/scm-ui/eslint-config/index.js @@ -0,0 +1,33 @@ +module.exports = { + extends: [ + "react-app", + "plugin:prettier/recommended", + "plugin:flowtype/recommended" + ], + rules: { + semi: ["error", "always"], + quotes: ["error", "double"], + "jsx-a11y/href-no-hash": [0], + "flowtype/no-types-missing-file-annotation": 2, + "no-console": "error" + }, + overrides: [ + { + files: ["*.ts", "*.tsx"], + parser: "@typescript-eslint/parser", + extends: [ + "react-app", + "plugin:prettier/recommended", + "plugin:@typescript-eslint/recommended" + ], + rules: { + semi: ["error", "always"], + quotes: ["error", "double"], + "jsx-a11y/href-no-hash": [0], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/ban-ts-ignore": "warn", + "no-console": "error" + } + } + ] +}; diff --git a/scm-ui/eslint-config/package.json b/scm-ui/eslint-config/package.json new file mode 100644 index 0000000000..b819ad5bd5 --- /dev/null +++ b/scm-ui/eslint-config/package.json @@ -0,0 +1,27 @@ +{ + "name": "@scm-manager/eslint-config", + "version": "2.0.0-SNAPSHOT", + "description": "ESLint configuration for scm-manager and its plugins", + "main": "index.js", + "author": "Sebastian Sdorra <s.sdorra@gmail.com>", + "license": "MIT", + "private": false, + "prettier": "@scm-manager/prettier-config", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^2.4.0", + "@typescript-eslint/parser": "^2.4.0", + "babel-eslint": "^10.0.3", + "eslint": "^6.5.1", + "eslint-config-prettier": "^6.4.0", + "eslint-config-react-app": "^5.0.2", + "eslint-plugin-flowtype": "^4.3.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-react": "^7.16.0", + "eslint-plugin-react-hooks": "^2.1.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/scm-ui/jest-preset/jest-preset.js b/scm-ui/jest-preset/jest-preset.js new file mode 100644 index 0000000000..ffdeac244b --- /dev/null +++ b/scm-ui/jest-preset/jest-preset.js @@ -0,0 +1,60 @@ +const path = require("path"); +const mockDirectory = path.resolve(__dirname, "src", "__mocks__"); +const findName = require("./src/findName"); +const findTarget = require("./src/findTarget"); + +// Set timezone for tests, this is required to get same date values +// accross diferent machines such ci-server and dev box. +// We have to set the timezone as soon as possible, because Date will +// cache the value. +// @see https://stackoverflow.com/questions/56261381/how-do-i-set-a-timezone-in-my-jest-config +process.env.TZ = "Europe/Berlin"; + +const root = process.cwd(); +const name = findName(root); +const target = findTarget(root); +const reportDirectory = path.join(target, "jest-reports"); + +module.exports = { + rootDir: root, + roots: [ + root + ], + testPathDirs: [ + path.join(root, "src") + ], + transform: { + "^.+\\.(ts|tsx|js)$": "@scm-manager/jest-preset" + }, + transformIgnorePatterns: [ + "node_modules/(?!(@scm-manager)/)" + ], + moduleNameMapper: { + "\\.(png|svg|jpg|gif|woff2?|eot|ttf)$": path.join( + mockDirectory, + "fileMock.js" + ), + "\\.(css|scss|sass)$": path.join(mockDirectory, "styleMock.js") + }, + setupFiles: [path.resolve(__dirname, "src", "setup.js")], + collectCoverage: true, + collectCoverageFrom: [ + "src/**/*.{ts,tsx,js,jsx}" + ], + coverageDirectory: path.join(reportDirectory, "coverage-" + name), + coveragePathIgnorePatterns: [ + "src/tests/.*", + "src/testing/.*" + ], + reporters: [ + "default", + [ + "jest-junit", + { + suiteName: name + " tests", + outputDirectory: reportDirectory, + outputName: "TEST-" + name + ".xml" + } + ] + ] +}; diff --git a/scm-ui/jest-preset/package.json b/scm-ui/jest-preset/package.json new file mode 100644 index 0000000000..c9efe9fe38 --- /dev/null +++ b/scm-ui/jest-preset/package.json @@ -0,0 +1,19 @@ +{ + "name": "@scm-manager/jest-preset", + "version": "2.0.0-SNAPSHOT", + "description": "Jest presets for SCM-Manager and its plugins", + "main": "src/index.js", + "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", + "license": "BSD-3-Clause", + "private": false, + "prettier": "@scm-manager/prettier-config", + "dependencies": { + "babel-jest": "^24.9.0", + "babel-plugin-require-context-hook": "^1.0.0", + "jest": "^24.9.0", + "jest-junit": "^8.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/scm-ui/jest-preset/src/__mocks__/fileMock.js b/scm-ui/jest-preset/src/__mocks__/fileMock.js new file mode 100644 index 0000000000..0a445d0600 --- /dev/null +++ b/scm-ui/jest-preset/src/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = "test-file-stub"; diff --git a/scm-ui/jest-preset/src/__mocks__/styleMock.js b/scm-ui/jest-preset/src/__mocks__/styleMock.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/scm-ui/jest-preset/src/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/scm-ui/jest-preset/src/findName.js b/scm-ui/jest-preset/src/findName.js new file mode 100644 index 0000000000..1307369261 --- /dev/null +++ b/scm-ui/jest-preset/src/findName.js @@ -0,0 +1,15 @@ +const path = require("path"); +const fs = require("fs"); + +function findName(directory) { + const packageJSON = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), {encoding: "UTF-8"})); + + let name = packageJSON.name; + const orgaIndex = name.indexOf("/"); + if (orgaIndex > 0) { + return name.substring(orgaIndex + 1); + } + return name; +} + +module.exports = findName; diff --git a/scm-ui/jest-preset/src/findTarget.js b/scm-ui/jest-preset/src/findTarget.js new file mode 100644 index 0000000000..6ebf189cc5 --- /dev/null +++ b/scm-ui/jest-preset/src/findTarget.js @@ -0,0 +1,16 @@ +const fs = require("fs"); +const path = require("path"); + +function findMavenModuleRoot(directory) { + if (fs.existsSync(path.join(directory, "pom.xml"))) { + return directory; + } + return findMavenModuleRoot(path.resolve(directory, "..")); +} + +function findTarget(directory) { + const moduleRoot = findMavenModuleRoot(directory); + return path.join(moduleRoot, "target"); +} + +module.exports = findTarget; diff --git a/scm-ui/jest-preset/src/index.js b/scm-ui/jest-preset/src/index.js new file mode 100644 index 0000000000..74a2ac4f41 --- /dev/null +++ b/scm-ui/jest-preset/src/index.js @@ -0,0 +1,20 @@ +const babelJest = require("babel-jest"); +const transformer = babelJest.createTransformer({ + presets: ["@scm-manager/babel-preset"], + plugins: ["require-context-hook"], + babelrc: false, + configFile: false +}); + +module.exports = { + ...transformer, + process(src, filename) { + if ( + !filename.includes("node_modules") || + filename.includes("@scm-manager") + ) { + return transformer.process(...arguments); + } + return src; + } +}; diff --git a/scm-ui/jest-preset/src/setup.js b/scm-ui/jest-preset/src/setup.js new file mode 100644 index 0000000000..ffb0ddb8c0 --- /dev/null +++ b/scm-ui/jest-preset/src/setup.js @@ -0,0 +1,2 @@ +import registerRequireContextHook from "babel-plugin-require-context-hook/register"; +registerRequireContextHook(); diff --git a/scm-ui/pom.xml b/scm-ui/pom.xml new file mode 100644 index 0000000000..ffc727f10d --- /dev/null +++ b/scm-ui/pom.xml @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>sonia.scm</groupId> + <artifactId>scm</artifactId> + <version>2.0.0-SNAPSHOT</version> + </parent> + + <groupId>sonia.scm</groupId> + <artifactId>scm-ui</artifactId> + <packaging>war</packaging> + <version>2.0.0-SNAPSHOT</version> + <name>scm-ui</name> + + <properties> + <sonar.language>typescript</sonar.language> + <sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources> + <sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions> + <sonar.coverage.exclusions>**/*.test.js,src/tests/**</sonar.coverage.exclusions> + <sonar.typescript.jstest.reportsPath>target/jest-reports</sonar.typescript.jstest.reportsPath> + <sonar.typescript.lcov.reportPaths>target/jest-reports/coverage-ui-extensions/lcov.info,target/jest-reports/coverage-ui-components/lcov.info,target/jest-reports/coverage-ui-webapp/lcov.info</sonar.typescript.lcov.reportPaths> + </properties> + + <build> + <finalName>scm-ui</finalName> + <plugins> + + <plugin> + <groupId>com.github.sdorra</groupId> + <artifactId>buildfrontend-maven-plugin</artifactId> + <configuration> + <workingDirectory>${basedir}/..</workingDirectory> + <node> + <version>${nodejs.version}</version> + </node> + <pkgManager> + <type>YARN</type> + <version>${yarn.version}</version> + </pkgManager> + <script>run</script> + </configuration> + <executions> + <execution> + <id>install</id> + <phase>process-resources</phase> + <goals> + <goal>install</goal> + </goals> + </execution> + <execution> + <id>typecheck</id> + <phase>process-resources</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <script>typecheck</script> + </configuration> + </execution> + <execution> + <id>build</id> + <phase>compile</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <script>build</script> + </configuration> + </execution> + <execution> + <id>test</id> + <phase>test</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <script>test</script> + <skip>${skipTests}</skip> + </configuration> + </execution> + <execution> + <id>deploy</id> + <phase>deploy</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <script>deploy</script> + <args> + <arg>${project.version}</arg> + </args> + </configuration> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-war-plugin</artifactId> + <version>3.1.0</version> + <configuration> + <failOnMissingWebXml>false</failOnMissingWebXml> + <webResources> + <resource> + <directory>target/assets</directory> + <targetPath>assets</targetPath> + </resource> + <resource> + <directory>ui-webapp/public</directory> + </resource> + </webResources> + </configuration> + </plugin> + + </plugins> + </build> +</project> diff --git a/scm-ui/prettier-config/index.js b/scm-ui/prettier-config/index.js new file mode 100644 index 0000000000..50f8192e96 --- /dev/null +++ b/scm-ui/prettier-config/index.js @@ -0,0 +1,3 @@ +module.exports = { + printWidth: 120 +}; diff --git a/scm-ui/prettier-config/package.json b/scm-ui/prettier-config/package.json new file mode 100644 index 0000000000..584081645c --- /dev/null +++ b/scm-ui/prettier-config/package.json @@ -0,0 +1,15 @@ +{ + "name": "@scm-manager/prettier-config", + "version": "2.0.0-SNAPSHOT", + "license": "BSD-3-Clause", + "description": "Prettier configuration", + "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", + "private": false, + "main": "index.js", + "dependencies": { + "prettier": "^1.18.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/scm-ui/tsconfig/package.json b/scm-ui/tsconfig/package.json new file mode 100644 index 0000000000..4ee8613469 --- /dev/null +++ b/scm-ui/tsconfig/package.json @@ -0,0 +1,15 @@ +{ + "name": "@scm-manager/tsconfig", + "version": "2.0.0-SNAPSHOT", + "license": "BSD-3-Clause", + "description": "TypeScript configuration", + "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", + "private": false, + "main": "tsconfig.json", + "dependencies": { + "typescript": "^3.6.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/scm-ui/tsconfig/tsconfig.json b/scm-ui/tsconfig/tsconfig.json new file mode 100644 index 0000000000..a2450080eb --- /dev/null +++ b/scm-ui/tsconfig/tsconfig.json @@ -0,0 +1,64 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + "allowJs": true, /* Allow javascript files to be compiled. */ + "checkJs": true, /* Report errors in .js files. */ + "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "skipLibCheck": true + } +} diff --git a/scm-ui/ui-components/.storybook/addons.js b/scm-ui/ui-components/.storybook/addons.js new file mode 100644 index 0000000000..300545cd8b --- /dev/null +++ b/scm-ui/ui-components/.storybook/addons.js @@ -0,0 +1 @@ +import "storybook-addon-i18next/register"; diff --git a/scm-ui/ui-components/.storybook/config.js b/scm-ui/ui-components/.storybook/config.js new file mode 100644 index 0000000000..5b9ed3d3a2 --- /dev/null +++ b/scm-ui/ui-components/.storybook/config.js @@ -0,0 +1,31 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import { addDecorator, configure } from "@storybook/react"; +import { withI18next } from "storybook-addon-i18next"; + +import "!style-loader!css-loader!sass-loader!../../ui-styles/src/scm.scss"; + +i18n.use(initReactI18next).init({ + whitelist: ["en", "de", "es"], + lng: "en", + fallbackLng: "en", + interpolation: { + escapeValue: false + }, + react: { + useSuspense: false + } +}); + +addDecorator( + withI18next({ + i18n, + languages: { + en: "English", + de: "Deutsch", + es: "Spanisch" + } + }) +); + +configure(require.context("../src", true, /\.stories\.tsx?$/), module); diff --git a/scm-ui/ui-components/.storybook/webpack.config.js b/scm-ui/ui-components/.storybook/webpack.config.js new file mode 100644 index 0000000000..947515f4d3 --- /dev/null +++ b/scm-ui/ui-components/.storybook/webpack.config.js @@ -0,0 +1,45 @@ +module.exports = { + module: { + rules: [ + { + parser: { + system: false, + systemjs: false + } + }, + { + test: /\.(js|ts|jsx|tsx)$/, + exclude: /node_modules/, + use: [ + { + loader: "babel-loader", + options: { + cacheDirectory: true, + presets: ["@scm-manager/babel-preset"] + } + } + ] + }, + { + test: /\.(css|scss|sass)$/i, + use: [ + // Creates `style` nodes from JS strings + "style-loader", + // Translates CSS into CommonJS + "css-loader", + // Compiles Sass to CSS + "sass-loader" + ] + }, + { + test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/, + use: ["file-loader"] + } + ] + }, + resolve: { + extensions: [ + ".ts", ".tsx", ".js", ".jsx", ".css", ".scss", ".json" + ] + } +}; diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json new file mode 100644 index 0000000000..e7266aae6f --- /dev/null +++ b/scm-ui/ui-components/package.json @@ -0,0 +1,74 @@ +{ + "name": "@scm-manager/ui-components", + "version": "2.0.0-SNAPSHOT", + "description": "UI Components for SCM-Manager and its plugins", + "main": "src/index.ts", + "files": [ + "dist", + "src" + ], + "repository": "https://bitbucket.org/sdorra/scm-manager", + "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", + "license": "BSD-3-Clause", + "scripts": { + "test": "jest", + "typecheck": "tsc", + "storybook": "start-storybook -s ../ui-webapp/public", + "storyshots": "jest --testPathPattern=\"storyshots.test.ts\" --collectCoverage=false", + "update-storyshots": "jest --testPathPattern=\"storyshots.test.ts\" --collectCoverage=false -u" + }, + "devDependencies": { + "@scm-manager/ui-tests": "^2.0.0-SNAPSHOT", + "@storybook/addon-actions": "^5.2.3", + "@storybook/addon-storyshots": "^5.2.3", + "@storybook/react": "^5.2.3", + "@types/classnames": "^2.2.9", + "@types/enzyme": "^3.10.3", + "@types/fetch-mock": "^7.3.1", + "@types/jest": "^24.0.19", + "@types/query-string": "5", + "@types/react": "^16.9.9", + "@types/react-dom": "^16.9.2", + "@types/react-router-dom": "^5.1.0", + "@types/react-select": "^2.0.19", + "@types/react-syntax-highlighter": "^11.0.1", + "@types/storybook__addon-storyshots": "^5.1.1", + "@types/styled-components": "^4.1.19", + "enzyme-context": "^1.1.2", + "enzyme-context-react-router-4": "^2.0.0", + "fetch-mock": "^7.5.1", + "flow-bin": "^0.109.0", + "flow-typed": "^2.5.1", + "raf": "^3.4.0", + "react-test-renderer": "^16.10.2", + "storybook-addon-i18next": "^1.2.1", + "typescript": "^3.6.4" + }, + "dependencies": { + "@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT", + "@scm-manager/ui-types": "^2.0.0-SNAPSHOT", + "classnames": "^2.2.6", + "date-fns": "^2.4.1", + "query-string": "5", + "react": "^16.8.6", + "react-diff-view": "^1.8.1", + "react-dom": "^16.8.6", + "react-i18next": "^10.13.1", + "react-markdown": "^4.0.6", + "react-router-dom": "^5.1.2", + "react-select": "^2.1.2", + "react-syntax-highlighter": "^11.0.2" + }, + "babel": { + "presets": [ + "@scm-manager/babel-preset" + ] + }, + "jest": { + "preset": "@scm-manager/jest-preset" + }, + "prettier": "@scm-manager/prettier-config", + "publishConfig": { + "access": "public" + } +} diff --git a/scm-ui/ui-components/src/Autocomplete.tsx b/scm-ui/ui-components/src/Autocomplete.tsx new file mode 100644 index 0000000000..7618464b57 --- /dev/null +++ b/scm-ui/ui-components/src/Autocomplete.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import { Async, AsyncCreatable } from "react-select"; +import { SelectValue } from "@scm-manager/ui-types"; +import LabelWithHelpIcon from "./forms/LabelWithHelpIcon"; +import { ActionMeta, ValueType } from "react-select/lib/types"; + +type Props = { + loadSuggestions: (p: string) => Promise<SelectValue[]>; + valueSelected: (p: SelectValue) => void; + label: string; + helpText?: string; + value?: SelectValue; + placeholder: string; + loadingMessage: string; + noOptionsMessage: string; + creatable?: boolean; +}; + +type State = {}; + +class Autocomplete extends React.Component<Props, State> { + static defaultProps = { + placeholder: "Type here", + loadingMessage: "Loading...", + noOptionsMessage: "No suggestion available" + }; + + handleInputChange = (newValue: ValueType<SelectValue>, action: ActionMeta) => { + this.selectValue(newValue as SelectValue); + }; + + selectValue = (value: SelectValue) => { + this.props.valueSelected(value); + }; + + // We overwrite this to avoid running into a bug (https://github.com/JedWatson/react-select/issues/2944) + isValidNewOption = ( + inputValue: string, + selectValue: ValueType<SelectValue>, + selectOptions: readonly SelectValue[] + ): boolean => { + const isNotDuplicated = !selectOptions.map(option => option.label).includes(inputValue); + const isNotEmpty = inputValue !== ""; + return isNotEmpty && isNotDuplicated; + }; + + render() { + const { + label, + helpText, + value, + placeholder, + loadingMessage, + noOptionsMessage, + loadSuggestions, + creatable + } = this.props; + return ( + <div className="field"> + <LabelWithHelpIcon label={label} helpText={helpText} /> + <div className="control"> + {creatable ? ( + <AsyncCreatable + cacheOptions + loadOptions={loadSuggestions} + onChange={this.handleInputChange} + value={value} + placeholder={placeholder} + loadingMessage={() => loadingMessage} + noOptionsMessage={() => noOptionsMessage} + isValidNewOption={this.isValidNewOption} + onCreateOption={value => { + this.selectValue({ + label: value, + value: { + id: value, + displayName: value + } + }); + }} + /> + ) : ( + <Async + cacheOptions + loadOptions={loadSuggestions} + onChange={this.handleInputChange} + value={value} + placeholder={placeholder} + loadingMessage={() => loadingMessage} + noOptionsMessage={() => noOptionsMessage} + /> + )} + </div> + </div> + ); + } +} + +export default Autocomplete; diff --git a/scm-ui/ui-components/src/BackendErrorNotification.tsx b/scm-ui/ui-components/src/BackendErrorNotification.tsx new file mode 100644 index 0000000000..5319243f79 --- /dev/null +++ b/scm-ui/ui-components/src/BackendErrorNotification.tsx @@ -0,0 +1,124 @@ +import React from "react"; +import { BackendError } from "./errors"; +import Notification from "./Notification"; + +import { WithTranslation, withTranslation } from "react-i18next"; + +type Props = WithTranslation & { + error: BackendError; +}; + +class BackendErrorNotification extends React.Component<Props> { + constructor(props: Props) { + super(props); + } + + render() { + return ( + <Notification type="danger"> + <div className="content"> + <p className="subtitle">{this.renderErrorName()}</p> + <p>{this.renderErrorDescription()}</p> + <p>{this.renderViolations()}</p> + {this.renderMetadata()} + </div> + </Notification> + ); + } + + renderErrorName = () => { + const { error, t } = this.props; + const translation = t("errors." + error.errorCode + ".displayName"); + if (translation === error.errorCode) { + return error.message; + } + return translation; + }; + + renderErrorDescription = () => { + const { error, t } = this.props; + const translation = t("errors." + error.errorCode + ".description"); + if (translation === error.errorCode) { + return ""; + } + return translation; + }; + + renderViolations = () => { + const { error, t } = this.props; + if (error.violations) { + return ( + <> + <p> + <strong>{t("errors.violations")}</strong> + </p> + <ul> + {error.violations.map((violation, index) => { + return ( + <li key={index}> + <strong>{violation.path}:</strong> {violation.message} + </li> + ); + })} + </ul> + </> + ); + } + }; + + renderMetadata = () => { + const { error, t } = this.props; + return ( + <> + {this.renderContext()} + {this.renderMoreInformationLink()} + <div className="level is-size-7"> + <div className="left"> + {t("errors.transactionId")} {error.transactionId} + </div> + <div className="right"> + {t("errors.errorCode")} {error.errorCode} + </div> + </div> + </> + ); + }; + + renderContext = () => { + const { error, t } = this.props; + if (error.context) { + return ( + <> + <p> + <strong>{t("errors.context")}</strong> + </p> + <ul> + {error.context.map((context, index) => { + return ( + <li key={index}> + <strong>{context.type}:</strong> {context.id} + </li> + ); + })} + </ul> + </> + ); + } + }; + + renderMoreInformationLink = () => { + const { error, t } = this.props; + if (error.url) { + return ( + <p> + {t("errors.moreInfo")}{" "} + <a href={error.url} target="_blank"> + {error.errorCode} + </a> + </p> + ); + } + }; +} + +export default withTranslation("plugins")(BackendErrorNotification); diff --git a/scm-ui/ui-components/src/BranchSelector.tsx b/scm-ui/ui-components/src/BranchSelector.tsx new file mode 100644 index 0000000000..970417fc3c --- /dev/null +++ b/scm-ui/ui-components/src/BranchSelector.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import classNames from "classnames"; +import styled from "styled-components"; +import { Branch } from "@scm-manager/ui-types"; +import DropDown from "./forms/DropDown"; + +type Props = { + branches: Branch[]; + selected: (branch?: Branch) => void; + selectedBranch?: string; + label: string; + disabled?: boolean; +}; + +type State = { + selectedBranch?: Branch; +}; + +const ZeroflexFieldLabel = styled.div` + flex-basis: inherit; + flex-grow: 0; +`; + +const MinWidthControl = styled.div` + min-width: 10rem; +`; + +const NoBottomMarginField = styled.div` + margin-bottom: 0 !important; +`; + +export default class BranchSelector extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = {}; + } + + componentDidMount() { + const { branches } = this.props; + if (branches) { + const selectedBranch = branches.find(branch => branch.name === this.props.selectedBranch); + this.setState({ + selectedBranch + }); + } + } + + render() { + const { branches, label, disabled } = this.props; + + if (branches) { + return ( + <div className={classNames("field", "is-horizontal")}> + <ZeroflexFieldLabel className={classNames("field-label", "is-normal")}> + <label className={classNames("label", "is-size-6")}>{label}</label> + </ZeroflexFieldLabel> + <div className="field-body"> + <NoBottomMarginField className={classNames("field", "is-narrow")}> + <MinWidthControl className="control"> + <DropDown + className="is-fullwidth" + options={branches.map(b => b.name)} + optionSelected={this.branchSelected} + disabled={!!disabled} + preselectedOption={this.state.selectedBranch ? this.state.selectedBranch.name : ""} + /> + </MinWidthControl> + </NoBottomMarginField> + </div> + </div> + ); + } else { + return null; + } + } + + branchSelected = (branchName: string) => { + const { branches, selected } = this.props; + + if (!branchName) { + this.setState({ + selectedBranch: undefined + }); + selected(undefined); + return; + } + const branch = branches.find(b => b.name === branchName); + + selected(branch); + this.setState({ + selectedBranch: branch + }); + }; +} diff --git a/scm-ui/ui-components/src/Breadcrumb.tsx b/scm-ui/ui-components/src/Breadcrumb.tsx new file mode 100644 index 0000000000..5fb34b79da --- /dev/null +++ b/scm-ui/ui-components/src/Breadcrumb.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { WithTranslation, withTranslation } from "react-i18next"; +import classNames from "classnames"; +import styled from "styled-components"; +import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; +import { Branch, Repository } from "@scm-manager/ui-types"; +import Icon from "./Icon"; + +type Props = WithTranslation & { + repository: Repository; + branch: Branch; + defaultBranch: Branch; + revision: string; + path: string; + baseUrl: string; + sources: File; +}; + +const FlexStartNav = styled.nav` + flex: 1; +`; + +const HomeIcon = styled(Icon)` + line-height: 1.5rem; +`; + +const ActionWrapper = styled.div` + align-self: center; + padding-right: 1rem; +`; + +class Breadcrumb extends React.Component<Props> { + renderPath() { + const { revision, path, baseUrl } = this.props; + + if (path) { + const paths = path.split("/"); + const map = paths.map((path, index) => { + const currPath = paths.slice(0, index + 1).join("/"); + if (paths.length - 1 === index) { + return ( + <li className="is-active" key={index}> + <Link to="#" aria-current="page"> + {path} + </Link> + </li> + ); + } + return ( + <li key={index}> + <Link to={baseUrl + "/" + revision + "/" + currPath}>{path}</Link> + </li> + ); + }); + return map; + } + return null; + } + + render() { + const { repository, baseUrl, branch, defaultBranch, sources, revision, path, t } = this.props; + + let homeUrl = baseUrl + "/"; + if (revision) { + homeUrl += encodeURIComponent(revision) + "/"; + } + + return ( + <> + <div className="is-flex"> + <FlexStartNav className={classNames("breadcrumb", "sources-breadcrumb")} aria-label="breadcrumbs"> + <ul> + <li> + <Link to={homeUrl}> + <HomeIcon title={t("breadcrumb.home")} name="home" color="inherit" /> + </Link> + </li> + {this.renderPath()} + </ul> + </FlexStartNav> + {binder.hasExtension("repos.sources.actionbar") && ( + <ActionWrapper> + <ExtensionPoint + name="repos.sources.actionbar" + props={{ + baseUrl, + revision, + branch: branch ? branch : defaultBranch, + path, + sources, + repository + }} + renderAll={true} + /> + </ActionWrapper> + )} + </div> + <hr className="is-marginless" /> + </> + ); + } +} + +export default withTranslation("commons")(Breadcrumb); diff --git a/scm-ui/ui-components/src/CardColumn.tsx b/scm-ui/ui-components/src/CardColumn.tsx new file mode 100644 index 0000000000..fd84b9b11e --- /dev/null +++ b/scm-ui/ui-components/src/CardColumn.tsx @@ -0,0 +1,95 @@ +import React, { ReactNode } from "react"; +import classNames from "classnames"; +import styled from "styled-components"; +import { Link } from "react-router-dom"; + +type Props = { + title: string; + description: string; + avatar: ReactNode; + contentRight?: ReactNode; + footerLeft: ReactNode; + footerRight: ReactNode; + link?: string; + action?: () => void; + className?: string; +}; + +const NoEventWrapper = styled.article` + position: relative; + pointer-events: none; + z-index: 1; +`; + +const AvatarWrapper = styled.figure` + margin-top: 0.8em; + margin-left: 1em !important; +`; + +const FlexFullHeight = styled.div` + flex-direction: column; + justify-content: space-around; + align-self: stretch; +`; + +const FooterWrapper = styled.div` + padding-bottom: 1rem; +`; + +const ContentLeft = styled.div` + margin-bottom: 0 !important; + overflow: hidden; +`; + +const ContentRight = styled.div` + margin-left: auto; +`; + +export default class CardColumn extends React.Component<Props> { + createLink = () => { + const { link, action } = this.props; + if (link) { + return <Link className="overlay-column" to={link} />; + } else if (action) { + return ( + <a + className="overlay-column" + onClick={e => { + e.preventDefault(); + action(); + }} + href="#" + /> + ); + } + return null; + }; + + render() { + const { avatar, title, description, contentRight, footerLeft, footerRight, className } = this.props; + const link = this.createLink(); + return ( + <> + {link} + <NoEventWrapper className={classNames("media", className)}> + <AvatarWrapper className="media-left">{avatar}</AvatarWrapper> + <FlexFullHeight className={classNames("media-content", "text-box", "is-flex")}> + <div className="is-flex"> + <ContentLeft className="content"> + <p className="shorten-text is-marginless"> + <strong>{title}</strong> + </p> + <p className="shorten-text">{description}</p> + </ContentLeft> + <ContentRight>{contentRight}</ContentRight> + </div> + <FooterWrapper className={classNames("level", "is-flex")}> + <div className="level-left is-hidden-mobile">{footerLeft}</div> + <div className="level-right is-mobile is-marginless">{footerRight}</div> + </FooterWrapper> + </FlexFullHeight> + </NoEventWrapper> + </> + ); + } +} diff --git a/scm-ui/ui-components/src/CardColumnGroup.tsx b/scm-ui/ui-components/src/CardColumnGroup.tsx new file mode 100644 index 0000000000..de76140479 --- /dev/null +++ b/scm-ui/ui-components/src/CardColumnGroup.tsx @@ -0,0 +1,78 @@ +import React, { ReactNode } from "react"; +import classNames from "classnames"; +import styled from "styled-components"; + +type Props = { + name: string; + elements: ReactNode[]; +}; + +type State = { + collapsed: boolean; +}; + +const Container = styled.div` + margin-bottom: 1em; +`; + +const Wrapper = styled.div` + padding: 0 0.75rem; +`; + +export default class CardColumnGroup extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + collapsed: false + }; + } + + toggleCollapse = () => { + this.setState(prevState => ({ + collapsed: !prevState.collapsed + })); + }; + + isLastEntry = (array: ReactNode[], index: number) => { + return index === array.length - 1; + }; + + isLengthOdd = (array: ReactNode[]) => { + return array.length % 2 !== 0; + }; + + isFullSize = (array: ReactNode[], index: number) => { + return this.isLastEntry(array, index) && this.isLengthOdd(array); + }; + + render() { + const { name, elements } = this.props; + const { collapsed } = this.state; + + const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; + let content = null; + if (!collapsed) { + content = elements.map((entry, index) => { + const fullColumnWidth = this.isFullSize(elements, index); + const sizeClass = fullColumnWidth ? "is-full" : "is-half"; + return ( + <div className={classNames("box", "box-link-shadow", "column", "is-clipped", sizeClass)} key={index}> + {entry} + </div> + ); + }); + } + return ( + <Container> + <h2> + <span className={classNames("is-size-4", "has-cursor-pointer")} onClick={this.toggleCollapse}> + <i className={classNames("fa", icon)} /> {name} + </span> + </h2> + <hr /> + <Wrapper className={classNames("columns", "card-columns", "is-multiline")}>{content}</Wrapper> + <div className="is-clearfix" /> + </Container> + ); + } +} diff --git a/scm-ui/ui-components/src/DateFromNow.stories.tsx b/scm-ui/ui-components/src/DateFromNow.stories.tsx new file mode 100644 index 0000000000..eee92c2681 --- /dev/null +++ b/scm-ui/ui-components/src/DateFromNow.stories.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import DateFromNow from "./DateFromNow"; +import { storiesOf } from "@storybook/react"; + +const baseProps = { + timeZone: "Europe/Berlin", + baseDate: "2019-10-12T13:56:42+02:00" +}; + +storiesOf("DateFromNow", module).add("Default", () => ( + <div> + <p> + <DateFromNow date="2009-06-30T18:30:00+02:00" {...baseProps} /> + </p> + <p> + <DateFromNow date="2019-06-30T18:30:00+02:00" {...baseProps} /> + </p> + <p> + <DateFromNow date="2019-10-12T13:56:40+02:00" {...baseProps} /> + </p> + <p> + <DateFromNow date="2019-10-11T13:56:40+02:00" {...baseProps} /> + </p> + </div> +)); diff --git a/scm-ui/ui-components/src/DateFromNow.test.ts b/scm-ui/ui-components/src/DateFromNow.test.ts new file mode 100644 index 0000000000..b791b5019d --- /dev/null +++ b/scm-ui/ui-components/src/DateFromNow.test.ts @@ -0,0 +1,25 @@ +import { chooseLocale, supportedLocales } from "./DateFromNow"; + +describe("test choose locale", () => { + + it("should choose de", () => { + const locale = chooseLocale("de_DE", ["de", "en"]); + expect(locale).toBe(supportedLocales.de); + }); + + it("should choose de, even without language array", () => { + const locale = chooseLocale("de", []); + expect(locale).toBe(supportedLocales.de); + }); + + it("should choose es", () => { + const locale = chooseLocale("de", ["af", "be", "es"]); + expect(locale).toBe(supportedLocales.es); + }); + + it("should fallback en", () => { + const locale = chooseLocale("af", ["af", "be"]); + expect(locale).toBe(supportedLocales.en); + }); + +}); diff --git a/scm-ui/ui-components/src/DateFromNow.tsx b/scm-ui/ui-components/src/DateFromNow.tsx new file mode 100644 index 0000000000..4f2a70c7ee --- /dev/null +++ b/scm-ui/ui-components/src/DateFromNow.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { withTranslation, WithTranslation } from "react-i18next"; +import { formatDistance, format, parseISO, Locale } from "date-fns"; +import { enUS, de, es } from "date-fns/locale"; +import styled from "styled-components"; + +type LocaleMap = { + [key: string]: Locale; +}; + +type DateInput = Date | string; + +export const supportedLocales: LocaleMap = { + enUS, + en: enUS, + de, + es +}; + +type Props = WithTranslation & { + date?: DateInput; + timeZone?: string; + + /** + * baseDate is the date from which the distance is calculated, + * default is the current time (new Date()). This property + * is required to keep snapshots tests green over the time on + * ci server. + */ + baseDate?: DateInput; +}; + +type Options = { + addSuffix: boolean; + locale: Locale; + timeZone?: string; +}; + +const DateElement = styled.time` + border-bottom: 1px dotted rgba(219, 219, 219); + cursor: help; +`; + +export const chooseLocale = (language: string, languages?: string[]) => { + for (const lng of languages || []) { + const locale = supportedLocales[lng]; + if (locale) { + return locale; + } + } + + const locale = supportedLocales[language]; + if (locale) { + return locale; + } + + return enUS; +}; + +class DateFromNow extends React.Component<Props> { + getLocale = (): Locale => { + const { i18n } = this.props; + return chooseLocale(i18n.language, i18n.languages); + }; + + createOptions = () => { + const { timeZone } = this.props; + const options: Options = { + addSuffix: true, + locale: this.getLocale() + }; + if (timeZone) { + options.timeZone = timeZone; + } + return options; + }; + + toDate = (value: DateInput): Date => { + if (value instanceof Date) { + return value; + } + return parseISO(value); + }; + + getBaseDate = () => { + const { baseDate } = this.props; + if (baseDate) { + return this.toDate(baseDate); + } + return new Date(); + }; + + render() { + const { date } = this.props; + if (date) { + const isoDate = this.toDate(date); + const options = this.createOptions(); + const distance = formatDistance(isoDate, this.getBaseDate(), options); + const formatted = format(isoDate, "yyyy-MM-dd HH:mm:ss", options); + return <DateElement title={formatted}>{distance}</DateElement>; + } + return null; + } +} + +export default withTranslation()(DateFromNow); diff --git a/scm-ui/ui-components/src/ErrorBoundary.tsx b/scm-ui/ui-components/src/ErrorBoundary.tsx new file mode 100644 index 0000000000..ae5f93582a --- /dev/null +++ b/scm-ui/ui-components/src/ErrorBoundary.tsx @@ -0,0 +1,49 @@ +import React, { ReactNode } from "react"; +import ErrorNotification from "./ErrorNotification"; + +type Props = { + fallback?: React.ComponentType<any>; + children: ReactNode; +}; + +type ErrorInfo = { + componentStack: string; +}; + +type State = { + error?: Error; + errorInfo?: ErrorInfo; +}; + +class ErrorBoundary extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = {}; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Catch errors in any components below and re-render with error message + this.setState({ + error, + errorInfo + }); + } + + renderError = () => { + let FallbackComponent = this.props.fallback; + if (!FallbackComponent) { + FallbackComponent = ErrorNotification; + } + + return <FallbackComponent {...this.state} />; + }; + + render() { + const { error } = this.state; + if (error) { + return this.renderError(); + } + return this.props.children; + } +} +export default ErrorBoundary; diff --git a/scm-ui/ui-components/src/ErrorNotification.tsx b/scm-ui/ui-components/src/ErrorNotification.tsx new file mode 100644 index 0000000000..12d8066b63 --- /dev/null +++ b/scm-ui/ui-components/src/ErrorNotification.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { BackendError, ForbiddenError, UnauthorizedError } from "./errors"; +import Notification from "./Notification"; +import BackendErrorNotification from "./BackendErrorNotification"; + +type Props = WithTranslation & { + error?: Error; +}; + +class ErrorNotification extends React.Component<Props> { + render() { + const { t, error } = this.props; + if (error) { + if (error instanceof BackendError) { + return <BackendErrorNotification error={error} />; + } else if (error instanceof UnauthorizedError) { + return ( + <Notification type="danger"> + <strong>{t("errorNotification.prefix")}:</strong> {t("errorNotification.timeout")}{" "} + <a href="javascript:window.location.reload(true)">{t("errorNotification.loginLink")}</a> + </Notification> + ); + } else if (error instanceof ForbiddenError) { + return ( + <Notification type="danger"> + <strong>{t("errorNotification.prefix")}:</strong> {t("errorNotification.forbidden")} + </Notification> + ); + } else { + return ( + <Notification type="danger"> + <strong>{t("errorNotification.prefix")}:</strong> {error.message} + </Notification> + ); + } + } + return null; + } +} + +export default withTranslation("commons")(ErrorNotification); diff --git a/scm-ui/ui-components/src/ErrorPage.tsx b/scm-ui/ui-components/src/ErrorPage.tsx new file mode 100644 index 0000000000..6b5686d432 --- /dev/null +++ b/scm-ui/ui-components/src/ErrorPage.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import ErrorNotification from "./ErrorNotification"; +import { BackendError, ForbiddenError } from "./errors"; + +type Props = { + error: Error; + title: string; + subtitle: string; +}; + +class ErrorPage extends React.Component<Props> { + render() { + const { title, error } = this.props; + + return ( + <section className="section"> + <div className="box column is-4 is-offset-4 container"> + <h1 className="title">{title}</h1> + {this.renderSubtitle()} + <ErrorNotification error={error} /> + </div> + </section> + ); + } + + renderSubtitle = () => { + const { error, subtitle } = this.props; + if (error instanceof BackendError || error instanceof ForbiddenError) { + return null; + } + return <p className="subtitle">{subtitle}</p>; + }; +} + +export default ErrorPage; diff --git a/scm-ui/ui-components/src/FileSize.test.ts b/scm-ui/ui-components/src/FileSize.test.ts new file mode 100644 index 0000000000..993059477f --- /dev/null +++ b/scm-ui/ui-components/src/FileSize.test.ts @@ -0,0 +1,10 @@ +import FileSize from "./FileSize"; + +it("should format bytes", () => { + expect(FileSize.format(0)).toBe("0 B"); + expect(FileSize.format(160)).toBe("160 B"); + expect(FileSize.format(6304)).toBe("6.30 K"); + expect(FileSize.format(28792588)).toBe("28.79 M"); + expect(FileSize.format(1369510189)).toBe("1.37 G"); + expect(FileSize.format(42949672960)).toBe("42.95 G"); +}); diff --git a/scm-ui/ui-components/src/FileSize.tsx b/scm-ui/ui-components/src/FileSize.tsx new file mode 100644 index 0000000000..b56def8e09 --- /dev/null +++ b/scm-ui/ui-components/src/FileSize.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +type Props = { + bytes: number; +}; + +class FileSize extends React.Component<Props> { + static format(bytes: number) { + if (!bytes) { + return "0 B"; + } + + const units = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"]; + const i = Math.floor(Math.log(bytes) / Math.log(1000)); + + const size = i === 0 ? bytes : (bytes / 1000 ** i).toFixed(2); + return `${size} ${units[i]}`; + } + + render() { + const formattedBytes = FileSize.format(this.props.bytes); + return <span>{formattedBytes}</span>; + } +} + +export default FileSize; diff --git a/scm-ui/ui-components/src/GroupAutocomplete.tsx b/scm-ui/ui-components/src/GroupAutocomplete.tsx new file mode 100644 index 0000000000..ffaf09f626 --- /dev/null +++ b/scm-ui/ui-components/src/GroupAutocomplete.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import AutocompleteProps from "./UserGroupAutocomplete"; +import UserGroupAutocomplete from "./UserGroupAutocomplete"; + +class GroupAutocomplete extends React.Component<AutocompleteProps & WithTranslation> { + render() { + const { t } = this.props; + return ( + <UserGroupAutocomplete + label={t("autocomplete.group")} + noOptionsMessage={t("autocomplete.noGroupOptions")} + loadingMessage={t("autocomplete.loading")} + placeholder={t("autocomplete.groupPlaceholder")} + {...this.props} + /> + ); + } +} + +export default withTranslation("commons")(GroupAutocomplete); diff --git a/scm-ui/ui-components/src/Help.tsx b/scm-ui/ui-components/src/Help.tsx new file mode 100644 index 0000000000..16567afb9d --- /dev/null +++ b/scm-ui/ui-components/src/Help.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import classNames from "classnames"; +import styled from "styled-components"; +import Tooltip from "./Tooltip"; +import HelpIcon from "./HelpIcon"; + +type Props = { + message: string; + className?: string; +}; + +const HelpTooltip = styled(Tooltip)` + position: absolute; + padding-left: 3px; +`; + +export default class Help extends React.Component<Props> { + render() { + const { message, className } = this.props; + return ( + <HelpTooltip className={classNames("is-inline-block", className)} message={message}> + <HelpIcon /> + </HelpTooltip> + ); + } +} diff --git a/scm-ui/ui-components/src/HelpIcon.tsx b/scm-ui/ui-components/src/HelpIcon.tsx new file mode 100644 index 0000000000..d5c9ca7f84 --- /dev/null +++ b/scm-ui/ui-components/src/HelpIcon.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import Icon from "./Icon"; + +type Props = { + className?: string; +}; + +export default class HelpIcon extends React.Component<Props> { + render() { + const { className } = this.props; + return <Icon name="question-circle" color="blue-light" className={className} />; + } +} diff --git a/scm-ui/ui-components/src/Icon.tsx b/scm-ui/ui-components/src/Icon.tsx new file mode 100644 index 0000000000..800f270c90 --- /dev/null +++ b/scm-ui/ui-components/src/Icon.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import classNames from "classnames"; + +type Props = { + title?: string; + name: string; + color: string; + className?: string; +}; + +export default class Icon extends React.Component<Props> { + static defaultProps = { + color: "grey-light" + }; + + render() { + const { title, name, color, className } = this.props; + if (title) { + return <i title={title} className={classNames("fas", "fa-fw", "fa-" + name, `has-text-${color}`, className)} />; + } + return <i className={classNames("fas", "fa-" + name, `has-text-${color}`, className)} />; + } +} diff --git a/scm-ui/ui-components/src/Image.tsx b/scm-ui/ui-components/src/Image.tsx new file mode 100644 index 0000000000..9c36986458 --- /dev/null +++ b/scm-ui/ui-components/src/Image.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { withContextPath } from "./urls"; + +type Props = { + src: string; + alt: string; + className?: string; +}; + +class Image extends React.Component<Props> { + createImageSrc = () => { + const { src } = this.props; + if (src.startsWith("http")) { + return src; + } + return withContextPath(src); + }; + + render() { + const { alt, className } = this.props; + return <img className={className} src={this.createImageSrc()} alt={alt} />; + } +} + +export default Image; diff --git a/scm-ui/ui-components/src/LinkPaginator.tsx b/scm-ui/ui-components/src/LinkPaginator.tsx new file mode 100644 index 0000000000..a1f9f7b599 --- /dev/null +++ b/scm-ui/ui-components/src/LinkPaginator.tsx @@ -0,0 +1,123 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { PagedCollection } from "@scm-manager/ui-types"; +import { Button } from "./buttons"; + +type Props = WithTranslation & { + collection: PagedCollection; + page: number; + filter?: string; +}; + +class LinkPaginator extends React.Component<Props> { + addFilterToLink(link: string) { + const { filter } = this.props; + if (filter) { + return `${link}?q=${filter}`; + } + return link; + } + + renderFirstButton() { + return <Button className="pagination-link" label={"1"} disabled={false} link={this.addFilterToLink("1")} />; + } + + renderPreviousButton(className: string, label?: string) { + const { page } = this.props; + const previousPage = page - 1; + + return ( + <Button + className={className} + label={label ? label : previousPage.toString()} + disabled={!this.hasLink("prev")} + link={this.addFilterToLink(`${previousPage}`)} + /> + ); + } + + hasLink(name: string) { + const { collection } = this.props; + return collection._links[name]; + } + + renderNextButton(className: string, label?: string) { + const { page } = this.props; + const nextPage = page + 1; + return ( + <Button + className={className} + label={label ? label : nextPage.toString()} + disabled={!this.hasLink("next")} + link={this.addFilterToLink(`${nextPage}`)} + /> + ); + } + + renderLastButton() { + const { collection } = this.props; + return ( + <Button + className="pagination-link" + label={`${collection.pageTotal}`} + disabled={false} + link={this.addFilterToLink(`${collection.pageTotal}`)} + /> + ); + } + + separator() { + return <span className="pagination-ellipsis">…</span>; + } + + currentPage(page: number) { + return <Button className="pagination-link is-current" label={"" + page} disabled={true} />; + } + + pageLinks() { + const { collection } = this.props; + + const links = []; + const page = collection.page + 1; + const pageTotal = collection.pageTotal; + if (page > 1) { + links.push(this.renderFirstButton()); + } + if (page > 3) { + links.push(this.separator()); + } + if (page > 2) { + links.push(this.renderPreviousButton("pagination-link")); + } + + links.push(this.currentPage(page)); + + if (page + 1 < pageTotal) { + links.push(this.renderNextButton("pagination-link")); + } + if (page + 2 < pageTotal) links.push(this.separator()); + //if there exists pages between next and last + if (page < pageTotal) { + links.push(this.renderLastButton()); + } + return links; + } + render() { + const { collection, t } = this.props; + if (collection) { + return ( + <nav className="pagination is-centered" aria-label="pagination"> + {this.renderPreviousButton("pagination-previous", t("paginator.previous"))} + <ul className="pagination-list"> + {this.pageLinks().map((link, index) => { + return <li key={index}>{link}</li>; + })} + </ul> + {this.renderNextButton("pagination-next", t("paginator.next"))} + </nav> + ); + } + return null; + } +} +export default withTranslation("commons")(LinkPaginator); diff --git a/scm-ui/ui-components/src/Loading.stories.tsx b/scm-ui/ui-components/src/Loading.stories.tsx new file mode 100644 index 0000000000..4e77da40ff --- /dev/null +++ b/scm-ui/ui-components/src/Loading.stories.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { storiesOf } from "@storybook/react"; +import Loading from "./Loading"; + +storiesOf("Loading", module).add("Default", () => ( + <div> + <Loading /> + </div> +)); diff --git a/scm-ui/ui-components/src/Loading.tsx b/scm-ui/ui-components/src/Loading.tsx new file mode 100644 index 0000000000..ad1bff842f --- /dev/null +++ b/scm-ui/ui-components/src/Loading.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import styled from "styled-components"; +import Image from "./Image"; + +type Props = WithTranslation & { + message?: string; +}; + +const Wrapper = styled.div` + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 256px; +`; + +const FixedSizedImage = styled(Image)` + margin-bottom: 0.75rem; + width: 128px; + height: 128px; +`; + +class Loading extends React.Component<Props> { + render() { + const { message, t } = this.props; + return ( + <Wrapper className="is-flex"> + <FixedSizedImage src="/images/loading.svg" alt={t("loading.alt")} /> + <p className="has-text-centered">{message}</p> + </Wrapper> + ); + } +} + +export default withTranslation("commons")(Loading); diff --git a/scm-ui/ui-components/src/Logo.stories.tsx b/scm-ui/ui-components/src/Logo.stories.tsx new file mode 100644 index 0000000000..7e351117d8 --- /dev/null +++ b/scm-ui/ui-components/src/Logo.stories.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { storiesOf } from "@storybook/react"; +import styled from "styled-components"; +import Logo from "./Logo"; + +const Wrapper = styled.div` + padding: 2em; + background-color: black; + height: 100%; +`; + +storiesOf("Logo", module).add("Default", () => ( + <Wrapper> + <Logo /> + </Wrapper> +)); diff --git a/scm-ui/ui-components/src/Logo.tsx b/scm-ui/ui-components/src/Logo.tsx new file mode 100644 index 0000000000..13c445c7b2 --- /dev/null +++ b/scm-ui/ui-components/src/Logo.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import Image from "./Image"; + +class Logo extends React.Component<WithTranslation> { + render() { + const { t } = this.props; + return <Image src="/images/logo.png" alt={t("logo.alt")} />; + } +} + +export default withTranslation("commons")(Logo); diff --git a/scm-ui/ui-components/src/MailLink.tsx b/scm-ui/ui-components/src/MailLink.tsx new file mode 100644 index 0000000000..0f965927de --- /dev/null +++ b/scm-ui/ui-components/src/MailLink.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +type Props = { + address?: string; +}; + +class MailLink extends React.Component<Props> { + render() { + const { address } = this.props; + if (!address) { + return null; + } + return <a href={"mailto:" + address}>{address}</a>; + } +} + +export default MailLink; diff --git a/scm-ui/ui-components/src/MarkdownHeadingRenderer.test.ts b/scm-ui/ui-components/src/MarkdownHeadingRenderer.test.ts new file mode 100644 index 0000000000..1c3d0d56ed --- /dev/null +++ b/scm-ui/ui-components/src/MarkdownHeadingRenderer.test.ts @@ -0,0 +1,15 @@ +import React from "react"; +import { headingToAnchorId } from "./MarkdownHeadingRenderer"; + +describe("headingToAnchorId tests", () => { + it("should lower case the text", () => { + expect(headingToAnchorId("Hello")).toBe("hello"); + expect(headingToAnchorId("HeLlO")).toBe("hello"); + expect(headingToAnchorId("HELLO")).toBe("hello"); + }); + + it("should replace spaces with hyphen", () => { + expect(headingToAnchorId("awesome stuff")).toBe("awesome-stuff"); + expect(headingToAnchorId("a b c d e f")).toBe("a-b-c-d-e-f"); + }); +}); diff --git a/scm-ui/ui-components/src/MarkdownHeadingRenderer.tsx b/scm-ui/ui-components/src/MarkdownHeadingRenderer.tsx new file mode 100644 index 0000000000..af5a5a2def --- /dev/null +++ b/scm-ui/ui-components/src/MarkdownHeadingRenderer.tsx @@ -0,0 +1,43 @@ +import React, { ReactNode } from "react"; +import { withRouter, RouteComponentProps } from "react-router-dom"; +import { withContextPath } from "./urls"; + +/** + * Adds anchor links to markdown headings. + * + * @see <a href="https://github.com/rexxars/react-markdown/issues/69">Headings are missing anchors / ids</a> + */ + +type Props = RouteComponentProps & { + children: ReactNode; + level: number; +}; + +function flatten(text: string, child: any): any { + return typeof child === "string" ? text + child : React.Children.toArray(child.props.children).reduce(flatten, text); +} + +/** + * Turns heading text into a anchor id + * + * @VisibleForTesting + */ +export function headingToAnchorId(heading: string) { + return heading.toLowerCase().replace(/\W/g, "-"); +} + +function MarkdownHeadingRenderer(props: Props) { + const children = React.Children.toArray(props.children); + const heading = children.reduce(flatten, ""); + const anchorId = headingToAnchorId(heading); + const headingElement = React.createElement("h" + props.level, {}, props.children); + const href = withContextPath(props.location.pathname + "#" + anchorId); + + return ( + <a id={`${anchorId}`} className="anchor" href={href}> + {headingElement} + </a> + ); +} + +export default withRouter(MarkdownHeadingRenderer); diff --git a/scm-ui/ui-components/src/MarkdownView.stories.tsx b/scm-ui/ui-components/src/MarkdownView.stories.tsx new file mode 100644 index 0000000000..a7e71aac2b --- /dev/null +++ b/scm-ui/ui-components/src/MarkdownView.stories.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { storiesOf } from "@storybook/react"; +import MarkdownView from "./MarkdownView"; +import styled from "styled-components"; +import { MemoryRouter } from "react-router-dom"; + +import TestPage from "./__resources__/test-page.md"; +import MarkdownWithoutLang from "./__resources__/markdown-without-lang.md"; + +const Spacing = styled.div` + padding: 2em; +`; + +storiesOf("MarkdownView", module) + .addDecorator(story => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>) + .add("Default", () => ( + <Spacing> + <MarkdownView content={TestPage} skipHtml={false} /> + </Spacing> + )) + .add("Code without Lang", () => ( + <Spacing> + <MarkdownView content={MarkdownWithoutLang} skipHtml={false} /> + </Spacing> + )); diff --git a/scm-ui/ui-components/src/MarkdownView.tsx b/scm-ui/ui-components/src/MarkdownView.tsx new file mode 100644 index 0000000000..94930692b3 --- /dev/null +++ b/scm-ui/ui-components/src/MarkdownView.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { withRouter, RouteComponentProps } from "react-router-dom"; +// @ts-ignore +import Markdown from "react-markdown/with-html"; +import styled from "styled-components"; +import { binder } from "@scm-manager/ui-extensions"; +import SyntaxHighlighter from "./SyntaxHighlighter"; +import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; + +type Props = RouteComponentProps & { + content: string; + renderContext?: object; + renderers?: any; + skipHtml?: boolean; + enableAnchorHeadings?: boolean; +}; + +const MarkdownWrapper = styled.div` + > .content { + > h1, + h2, + h3, + h4, + h5, + h6 { + margin: 0.5rem 0; + font-size: 0.9rem; + } + > h1 { + font-weight: 700; + } + > h2 { + font-weight: 600; + } + > h3, + h4, + h5, + h6 { + font-weight: 500; + } + & strong { + font-weight: 500; + } + } +`; + +class MarkdownView extends React.Component<Props> { + static defaultProps: Partial<Props> = { + enableAnchorHeadings: false, + skipHtml: false + }; + + contentRef: HTMLDivElement | null | undefined; + + constructor(props: Props) { + super(props); + } + + componentDidUpdate() { + // we have to use componentDidUpdate, because we have to wait until all + // children are rendered and componentDidMount is called before the + // markdown content was rendered. + const hash = this.props.location.hash; + if (this.contentRef && hash) { + // we query only child elements, to avoid strange scrolling with multiple + // markdown elements on one page. + const element = this.contentRef.querySelector(hash); + if (element && element.scrollIntoView) { + element.scrollIntoView(); + } + } + } + + render() { + const { content, renderers, renderContext, enableAnchorHeadings, skipHtml } = this.props; + + const rendererFactory = binder.getExtension("markdown-renderer-factory"); + let rendererList = renderers; + + if (rendererFactory) { + rendererList = rendererFactory(renderContext); + } + + if (!rendererList) { + rendererList = {}; + } + + if (enableAnchorHeadings) { + rendererList.heading = MarkdownHeadingRenderer; + } + + if (!rendererList.code) { + rendererList.code = SyntaxHighlighter; + } + + return ( + <MarkdownWrapper ref={el => (this.contentRef = el)}> + <Markdown + className="content" + skipHtml={skipHtml} + escapeHtml={skipHtml} + source={content} + renderers={rendererList} + /> + </MarkdownWrapper> + ); + } +} + +export default withRouter(MarkdownView); diff --git a/scm-ui/ui-components/src/Notification.tsx b/scm-ui/ui-components/src/Notification.tsx new file mode 100644 index 0000000000..9125405303 --- /dev/null +++ b/scm-ui/ui-components/src/Notification.tsx @@ -0,0 +1,40 @@ +import React, { ReactNode } from "react"; +import classNames from "classnames"; + +type NotificationType = "primary" | "info" | "success" | "warning" | "danger" | "inherit"; + +type Props = { + type: NotificationType; + onClose?: () => void; + className?: string; + children?: ReactNode; +}; + +class Notification extends React.Component<Props> { + static defaultProps = { + type: "info" + }; + + renderCloseButton() { + const { onClose } = this.props; + if (onClose) { + return <button className="delete" onClick={onClose} />; + } + return ""; + } + + render() { + const { type, className, children } = this.props; + + const color = type !== "inherit" ? "is-" + type : ""; + + return ( + <div className={classNames("notification", color, className)}> + {this.renderCloseButton()} + {children} + </div> + ); + } +} + +export default Notification; diff --git a/scm-ui/ui-components/src/OverviewPageActions.tsx b/scm-ui/ui-components/src/OverviewPageActions.tsx new file mode 100644 index 0000000000..bb9aea0ee6 --- /dev/null +++ b/scm-ui/ui-components/src/OverviewPageActions.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { History } from "history"; +import { withRouter, RouteComponentProps } from "react-router-dom"; +import classNames from "classnames"; +import { Button, urls } from "./index"; +import { FilterInput } from "./forms"; + +type Props = RouteComponentProps & { + showCreateButton: boolean; + link: string; + label?: string; + + // context props + history: History; + location: any; +}; + +class OverviewPageActions extends React.Component<Props> { + render() { + const { history, location, link } = this.props; + return ( + <> + <FilterInput + value={urls.getQueryStringFromLocation(location)} + filter={filter => { + history.push(`/${link}/?q=${filter}`); + }} + /> + {this.renderCreateButton()} + </> + ); + } + + renderCreateButton() { + const { showCreateButton, link, label } = this.props; + if (showCreateButton) { + return ( + <div className={classNames("input-button", "control")}> + <Button label={label} link={`/${link}/create`} color="primary" /> + </div> + ); + } + return null; + } +} + +export default withRouter(OverviewPageActions); diff --git a/scm-ui/ui-components/src/Paginator.test.tsx b/scm-ui/ui-components/src/Paginator.test.tsx new file mode 100644 index 0000000000..69208bc920 --- /dev/null +++ b/scm-ui/ui-components/src/Paginator.test.tsx @@ -0,0 +1,256 @@ +import React from "react"; +import { mount, shallow } from "@scm-manager/ui-tests/enzyme-router"; +import "@scm-manager/ui-tests/i18n"; +import Paginator from "./Paginator"; + +xdescribe("paginator rendering tests", () => { + const dummyLink = { + href: "https://dummy" + }; + + it("should render all buttons but disabled, without links", () => { + const collection = { + page: 10, + pageTotal: 20, + _links: {}, + _embedded: {} + }; + + const paginator = shallow(<Paginator collection={collection} />); + const buttons = paginator.find("Button"); + expect(buttons.length).toBe(7); + buttons.forEach(button => { + // @ts-ignore ??? + expect(button.props.disabled).toBeTruthy(); + }); + }); + + it("should render buttons for first page", () => { + const collection = { + page: 0, + pageTotal: 148, + _links: { + first: dummyLink, + next: dummyLink, + last: dummyLink + }, + _embedded: {} + }; + + const paginator = shallow(<Paginator collection={collection} />); + const buttons = paginator.find("Button"); + expect(buttons.length).toBe(5); + + // previous button + expect(buttons.get(0).props.disabled).toBeTruthy(); + // last button + expect(buttons.get(1).props.disabled).toBeFalsy(); + // first button + const firstButton = buttons.get(2).props; + expect(firstButton.disabled).toBeTruthy(); + expect(firstButton.label).toBe(1); + + // next button + const nextButton = buttons.get(3).props; + expect(nextButton.disabled).toBeFalsy(); + expect(nextButton.label).toBe("2"); + + // last button + const lastButton = buttons.get(4).props; + expect(lastButton.disabled).toBeFalsy(); + expect(lastButton.label).toBe("148"); + }); + + it("should render buttons for second page", () => { + const collection = { + page: 1, + pageTotal: 148, + _links: { + first: dummyLink, + prev: dummyLink, + next: dummyLink, + last: dummyLink + }, + _embedded: {} + }; + + const paginator = shallow(<Paginator collection={collection} />); + const buttons = paginator.find("Button"); + expect(buttons.length).toBe(6); + + // previous button + expect(buttons.get(0).props.disabled).toBeFalsy(); + // last button + expect(buttons.get(1).props.disabled).toBeFalsy(); + // first button + const firstButton = buttons.get(2).props; + expect(firstButton.disabled).toBeFalsy(); + expect(firstButton.label).toBe("1"); + + // current button + const currentButton = buttons.get(3).props; + expect(currentButton.disabled).toBeTruthy(); + expect(currentButton.label).toBe(2); + + // next button + const nextButton = buttons.get(4).props; + expect(nextButton.disabled).toBeFalsy(); + expect(nextButton.label).toBe("3"); + + // last button + const lastButton = buttons.get(5).props; + expect(lastButton.disabled).toBeFalsy(); + expect(lastButton.label).toBe("148"); + }); + + it("should render buttons for last page", () => { + const collection = { + page: 147, + pageTotal: 148, + _links: { + first: dummyLink, + prev: dummyLink + }, + _embedded: {} + }; + + const paginator = shallow(<Paginator collection={collection} />); + const buttons = paginator.find("Button"); + expect(buttons.length).toBe(5); + + // previous button + expect(buttons.get(0).props.disabled).toBeFalsy(); + // last button + expect(buttons.get(1).props.disabled).toBeTruthy(); + // first button + const firstButton = buttons.get(2).props; + expect(firstButton.disabled).toBeFalsy(); + expect(firstButton.label).toBe("1"); + + // next button + const nextButton = buttons.get(3).props; + expect(nextButton.disabled).toBeFalsy(); + expect(nextButton.label).toBe("147"); + + // last button + const lastButton = buttons.get(4).props; + expect(lastButton.disabled).toBeTruthy(); + expect(lastButton.label).toBe(148); + }); + + it("should render buttons for penultimate page", () => { + const collection = { + page: 146, + pageTotal: 148, + _links: { + first: dummyLink, + prev: dummyLink, + next: dummyLink, + last: dummyLink + }, + _embedded: {} + }; + + const paginator = shallow(<Paginator collection={collection} />); + const buttons = paginator.find("Button"); + expect(buttons.length).toBe(6); + + // previous button + expect(buttons.get(0).props.disabled).toBeFalsy(); + // last button + expect(buttons.get(1).props.disabled).toBeFalsy(); + + // first button + const firstButton = buttons.get(2).props; + expect(firstButton.disabled).toBeFalsy(); + expect(firstButton.label).toBe("1"); + + const currentButton = buttons.get(3).props; + expect(currentButton.disabled).toBeFalsy(); + expect(currentButton.label).toBe("146"); + + // current button + const nextButton = buttons.get(4).props; + expect(nextButton.disabled).toBeTruthy(); + expect(nextButton.label).toBe(147); + + // last button + const lastButton = buttons.get(5).props; + expect(lastButton.disabled).toBeFalsy(); + expect(lastButton.label).toBe("148"); + }); + + it("should render buttons for a page in the middle", () => { + const collection = { + page: 41, + pageTotal: 148, + _links: { + first: dummyLink, + prev: dummyLink, + next: dummyLink, + last: dummyLink + }, + _embedded: {} + }; + + const paginator = shallow(<Paginator collection={collection} />); + const buttons = paginator.find("Button"); + expect(buttons.length).toBe(7); + + // previous button + expect(buttons.get(0).props.disabled).toBeFalsy(); + // next button + expect(buttons.get(1).props.disabled).toBeFalsy(); + + // first button + const firstButton = buttons.get(2).props; + expect(firstButton.disabled).toBeFalsy(); + expect(firstButton.label).toBe("1"); + + // previous Button + const previousButton = buttons.get(3).props; + expect(previousButton.disabled).toBeFalsy(); + expect(previousButton.label).toBe("41"); + + // current button + const currentButton = buttons.get(4).props; + expect(currentButton.disabled).toBeTruthy(); + expect(currentButton.label).toBe(42); + + // next button + const nextButton = buttons.get(5).props; + expect(nextButton.disabled).toBeFalsy(); + expect(nextButton.label).toBe("43"); + + // last button + const lastButton = buttons.get(6).props; + expect(lastButton.disabled).toBeFalsy(); + expect(lastButton.label).toBe("148"); + }); + + it("should call the function with the last previous url", () => { + const collection = { + page: 41, + pageTotal: 148, + _links: { + first: dummyLink, + prev: { + href: "https://www.scm-manager.org" + }, + next: dummyLink, + last: dummyLink + }, + _embedded: {} + }; + + let urlToOpen; + const callMe = (url: string) => { + urlToOpen = url; + }; + + const paginator = mount(<Paginator collection={collection} onPageChange={callMe} />); + paginator.find("Button.pagination-previous").simulate("click"); + + expect(urlToOpen).toBe("https://www.scm-manager.org"); + }); +}); diff --git a/scm-ui/ui-components/src/Paginator.tsx b/scm-ui/ui-components/src/Paginator.tsx new file mode 100644 index 0000000000..907a922ee2 --- /dev/null +++ b/scm-ui/ui-components/src/Paginator.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { PagedCollection, Link } from "@scm-manager/ui-types"; +import { Button } from "./buttons"; + +type Props = WithTranslation & { + collection: PagedCollection; + onPageChange?: (p: string) => void; +}; + +class Paginator extends React.Component<Props> { + isLinkUnavailable(linkType: string) { + return !this.props.collection || !this.props.collection._links[linkType]; + } + + createAction = (linkType: string) => () => { + const { collection, onPageChange } = this.props; + if (onPageChange) { + const link = collection._links[linkType] as Link; + if (link && link.href) { + onPageChange(link.href); + } + } + }; + + renderFirstButton() { + return this.renderPageButton(1, "first"); + } + + renderPreviousButton() { + const { t } = this.props; + return this.renderButton("pagination-previous", t("paginator.previous"), "prev"); + } + + renderNextButton() { + const { t } = this.props; + return this.renderButton("pagination-next", t("paginator.next"), "next"); + } + + renderLastButton() { + const { collection } = this.props; + return this.renderPageButton(collection.pageTotal, "last"); + } + + renderPageButton(page: number, linkType: string) { + return this.renderButton("pagination-link", page.toString(), linkType); + } + + renderButton(className: string, label: string, linkType: string) { + return ( + <Button + className={className} + label={label} + disabled={this.isLinkUnavailable(linkType)} + action={this.createAction(linkType)} + /> + ); + } + + seperator() { + return <span className="pagination-ellipsis">…</span>; + } + + currentPage(page: number) { + return <Button className="pagination-link is-current" label={"" + page} disabled={true} />; + } + + pageLinks() { + const { collection } = this.props; + + const links = []; + const page = collection.page + 1; + const pageTotal = collection.pageTotal; + if (page > 1) { + links.push(this.renderFirstButton()); + } + if (page > 3) { + links.push(this.seperator()); + } + if (page > 2) { + links.push(this.renderPageButton(page - 1, "prev")); + } + + links.push(this.currentPage(page)); + + if (page + 1 < pageTotal) { + links.push(this.renderPageButton(page + 1, "next")); + } + if (page + 2 < pageTotal) links.push(this.seperator()); + //if there exists pages between next and last + if (page < pageTotal) { + links.push(this.renderLastButton()); + } + return links; + } + render() { + return ( + <nav className="pagination is-centered" aria-label="pagination"> + {this.renderPreviousButton()} + {this.renderNextButton()} + <ul className="pagination-list"> + {this.pageLinks().map((link, index) => { + return <li key={index}>{link}</li>; + })} + </ul> + </nav> + ); + } +} +export default withTranslation("commons")(Paginator); diff --git a/scm-ui/ui-components/src/ProtectedRoute.tsx b/scm-ui/ui-components/src/ProtectedRoute.tsx new file mode 100644 index 0000000000..9950608212 --- /dev/null +++ b/scm-ui/ui-components/src/ProtectedRoute.tsx @@ -0,0 +1,35 @@ +import React, { Component } from "react"; +import { Route, Redirect, withRouter, RouteComponentProps, RouteProps } from "react-router-dom"; + +type Props = RouteComponentProps & + RouteProps & { + authenticated?: boolean; + }; + +class ProtectedRoute extends Component<Props> { + renderRoute = (Component: any, authenticated?: boolean) => { + return (routeProps: any) => { + if (authenticated) { + return <Component {...routeProps} />; + } else { + return ( + <Redirect + to={{ + pathname: "/login", + state: { + from: routeProps.location + } + }} + /> + ); + } + }; + }; + + render() { + const { component, authenticated, ...routeProps } = this.props; + return <Route {...routeProps} render={this.renderRoute(component, authenticated)} />; + } +} + +export default withRouter(ProtectedRoute); diff --git a/scm-ui/ui-components/src/StatePaginator.tsx b/scm-ui/ui-components/src/StatePaginator.tsx new file mode 100644 index 0000000000..bb471fb3a4 --- /dev/null +++ b/scm-ui/ui-components/src/StatePaginator.tsx @@ -0,0 +1,123 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { PagedCollection } from "@scm-manager/ui-types"; +import { Button } from "./index"; + +type Props = WithTranslation & { + collection: PagedCollection; + page: number; + updatePage: (p: number) => void; +}; + +class StatePaginator extends React.Component<Props> { + renderFirstButton() { + return <Button className="pagination-link" label={"1"} disabled={false} action={() => this.updateCurrentPage(1)} />; + } + + updateCurrentPage = (newPage: number) => { + this.props.updatePage(newPage); + }; + + renderPreviousButton(label?: string) { + const { page } = this.props; + const previousPage = page - 1; + + return ( + <Button + className="pagination-previous" + label={label ? label : previousPage.toString()} + disabled={!this.hasLink("prev")} + action={() => this.updateCurrentPage(previousPage)} + /> + ); + } + + hasLink(name: string) { + const { collection } = this.props; + return collection._links[name]; + } + + renderNextButton(label?: string) { + const { page } = this.props; + const nextPage = page + 1; + return ( + <Button + className="pagination-next" + label={label ? label : nextPage.toString()} + disabled={!this.hasLink("next")} + action={() => this.updateCurrentPage(nextPage)} + /> + ); + } + + renderLastButton() { + const { collection } = this.props; + return ( + <Button + className="pagination-link" + label={`${collection.pageTotal}`} + disabled={false} + action={() => this.updateCurrentPage(collection.pageTotal)} + /> + ); + } + + separator() { + return <span className="pagination-ellipsis">…</span>; + } + + currentPage(page: number) { + return ( + <Button + className="pagination-link is-current" + label={"" + page} + disabled={true} + action={() => this.updateCurrentPage(page)} + /> + ); + } + + pageLinks() { + const { collection } = this.props; + + const links = []; + const page = collection.page + 1; + const pageTotal = collection.pageTotal; + if (page > 1) { + links.push(this.renderFirstButton()); + } + if (page > 3) { + links.push(this.separator()); + } + if (page > 2) { + links.push(this.renderPreviousButton()); + } + + links.push(this.currentPage(page)); + + if (page + 1 < pageTotal) { + links.push(this.renderNextButton()); + } + if (page + 2 < pageTotal) links.push(this.separator()); + //if there exists pages between next and last + if (page < pageTotal) { + links.push(this.renderLastButton()); + } + return links; + } + render() { + const { t } = this.props; + return ( + <nav className="pagination is-centered" aria-label="pagination"> + {this.renderPreviousButton(t("paginator.previous"))} + <ul className="pagination-list"> + {this.pageLinks().map((link, index) => { + return <li key={index}>{link}</li>; + })} + </ul> + {this.renderNextButton(t("paginator.next"))} + </nav> + ); + } +} +export default withTranslation("commons")(StatePaginator); diff --git a/scm-ui/ui-components/src/SyntaxHighlighter.stories.tsx b/scm-ui/ui-components/src/SyntaxHighlighter.stories.tsx new file mode 100644 index 0000000000..35375a56a6 --- /dev/null +++ b/scm-ui/ui-components/src/SyntaxHighlighter.stories.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { storiesOf } from "@storybook/react"; +import styled from "styled-components"; +import SyntaxHighlighter from "./SyntaxHighlighter"; + +import JavaHttpServer from "./__resources__/HttpServer.java"; +import GoHttpServer from "./__resources__/HttpServer.go"; +import JsHttpServer from "./__resources__/HttpServer.js"; +import PyHttpServer from "./__resources__/HttpServer.py"; + +const Spacing = styled.div` + padding: 1em; +`; + +storiesOf("SyntaxHighlighter", module) + .add("Java", () => ( + <Spacing> + <SyntaxHighlighter language="java" value={JavaHttpServer} /> + </Spacing> + )) + .add("Go", () => ( + <Spacing> + <SyntaxHighlighter language="go" value={GoHttpServer} /> + </Spacing> + )) + .add("Javascript", () => ( + <Spacing> + <SyntaxHighlighter language="javascript" value={JsHttpServer} /> + </Spacing> + )) + .add("Python", () => ( + <Spacing> + <SyntaxHighlighter language="python" value={PyHttpServer} /> + </Spacing> + )); diff --git a/scm-ui/ui-components/src/SyntaxHighlighter.tsx b/scm-ui/ui-components/src/SyntaxHighlighter.tsx new file mode 100644 index 0000000000..8544f3db01 --- /dev/null +++ b/scm-ui/ui-components/src/SyntaxHighlighter.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +import { LightAsync as ReactSyntaxHighlighter } from "react-syntax-highlighter"; +// @ts-ignore +import { arduinoLight } from "react-syntax-highlighter/dist/cjs/styles/hljs"; + +type Props = { + language?: string; + value: string; +}; + +const defaultLanguage = "text"; + +class SyntaxHighlighter extends React.Component<Props> { + static defaultProps: Partial<Props> = { + language: defaultLanguage + }; + + getLanguage = () => { + const { language } = this.props; + if (language) { + return language; + } + return defaultLanguage; + }; + + render() { + const language = this.getLanguage(); + return ( + <ReactSyntaxHighlighter showLineNumbers={false} language={language} style={arduinoLight}> + {this.props.value} + </ReactSyntaxHighlighter> + ); + } +} + +export default SyntaxHighlighter; diff --git a/scm-ui/ui-components/src/Tag.tsx b/scm-ui/ui-components/src/Tag.tsx new file mode 100644 index 0000000000..632cccb6bf --- /dev/null +++ b/scm-ui/ui-components/src/Tag.tsx @@ -0,0 +1,47 @@ +import * as React from "react"; +import classNames from "classnames"; + +type Props = { + className?: string; + color: string; + icon?: string; + label: string; + title?: string; + onClick?: () => void; + onRemove?: () => void; +}; + +class Tag extends React.Component<Props> { + static defaultProps = { + color: "light" + }; + + render() { + const { className, color, icon, label, title, onClick, onRemove } = this.props; + let showIcon = null; + if (icon) { + showIcon = ( + <> + <i className={classNames("fas", `fa-${icon}`)} /> +   + </> + ); + } + let showDelete = null; + if (onRemove) { + showDelete = <a className="tag is-delete" onClick={onRemove} />; + } + + return ( + <> + <span className={classNames("tag", `is-${color}`, className)} title={title} onClick={onClick}> + {showIcon} + {label} + </span> + {showDelete} + </> + ); + } +} + +export default Tag; diff --git a/scm-ui/ui-components/src/Tooltip.tsx b/scm-ui/ui-components/src/Tooltip.tsx new file mode 100644 index 0000000000..393b2213db --- /dev/null +++ b/scm-ui/ui-components/src/Tooltip.tsx @@ -0,0 +1,27 @@ +import React, { ReactNode } from "react"; +import classNames from "classnames"; + +type Props = { + message: string; + className?: string; + location: string; + children: ReactNode; +}; + +class Tooltip extends React.Component<Props> { + static defaultProps = { + location: "right" + }; + + render() { + const { className, message, location, children } = this.props; + const multiline = message.length > 60 ? "has-tooltip-multiline" : ""; + return ( + <span className={classNames("tooltip", "has-tooltip-" + location, multiline, className)} data-tooltip={message}> + {children} + </span> + ); + } +} + +export default Tooltip; diff --git a/scm-ui/ui-components/src/UserAutocomplete.tsx b/scm-ui/ui-components/src/UserAutocomplete.tsx new file mode 100644 index 0000000000..d5a8d0f5e1 --- /dev/null +++ b/scm-ui/ui-components/src/UserAutocomplete.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import AutocompleteProps from "./UserGroupAutocomplete"; +import UserGroupAutocomplete from "./UserGroupAutocomplete"; + +class UserAutocomplete extends React.Component<AutocompleteProps & WithTranslation> { + render() { + const { t } = this.props; + return ( + <UserGroupAutocomplete + label={t("autocomplete.user")} + noOptionsMessage={t("autocomplete.noUserOptions")} + loadingMessage={t("autocomplete.loading")} + placeholder={t("autocomplete.userPlaceholder")} + {...this.props} + /> + ); + } +} + +export default withTranslation("commons")(UserAutocomplete); diff --git a/scm-ui/ui-components/src/UserGroupAutocomplete.tsx b/scm-ui/ui-components/src/UserGroupAutocomplete.tsx new file mode 100644 index 0000000000..f4e445eba9 --- /dev/null +++ b/scm-ui/ui-components/src/UserGroupAutocomplete.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { SelectValue, AutocompleteObject } from "@scm-manager/ui-types"; +import Autocomplete from "./Autocomplete"; +import { apiClient } from "./apiclient"; + +export type AutocompleteProps = { + autocompleteLink?: string; + valueSelected?: (p: SelectValue) => void; + value?: SelectValue; +}; + +type Props = AutocompleteProps & { + label: string; + noOptionsMessage: string; + loadingMessage: string; + placeholder: string; +}; + +export default class UserGroupAutocomplete extends React.Component<Props> { + loadSuggestions = (inputValue: string): Promise<SelectValue[]> => { + const url = this.props.autocompleteLink; + const link = url + "?q="; + return apiClient + .get(link + inputValue) + .then(response => response.json()) + .then((json: AutocompleteObject[]) => { + return json.map(element => { + const label = element.displayName ? `${element.displayName} (${element.id})` : element.id; + return { + value: element, + label + }; + }); + }); + }; + + selectName = (selection: SelectValue) => { + if (this.props.valueSelected) { + this.props.valueSelected(selection); + } + }; + + render() { + return ( + <Autocomplete + loadSuggestions={this.loadSuggestions} + valueSelected={this.selectName} + creatable={true} + {...this.props} + /> + ); + } +} diff --git a/scm-ui/ui-components/src/__resources__/HttpServer.go.ts b/scm-ui/ui-components/src/__resources__/HttpServer.go.ts new file mode 100644 index 0000000000..057caf5781 --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/HttpServer.go.ts @@ -0,0 +1,18 @@ +export default `package main + +import ( + "fmt" + "net/http" +) + +func main() { + http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Welcome to my website!") + }) + + fs := http.FileServer(http.Dir("static/")) + http.Handle("/static/", http.StripPrefix("/static/", fs)) + + http.ListenAndServe(":80", nil) +} +`; diff --git a/scm-ui/ui-components/src/__resources__/HttpServer.java.ts b/scm-ui/ui-components/src/__resources__/HttpServer.java.ts new file mode 100644 index 0000000000..95c7d9a54d --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/HttpServer.java.ts @@ -0,0 +1,32 @@ +export default `package com.example; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +public class Test { + + public static void main(String[] args) throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0); + server.createContext("/test", new MyHandler()); + server.setExecutor(null); // creates a default executor + server.start(); + } + + static class MyHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + String response = "This is the response"; + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + +} +`; diff --git a/scm-ui/ui-components/src/__resources__/HttpServer.js.ts b/scm-ui/ui-components/src/__resources__/HttpServer.js.ts new file mode 100644 index 0000000000..8147420a9e --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/HttpServer.js.ts @@ -0,0 +1,8 @@ +export default `var http = require('http'); + +http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write('Hello World!'); + res.end(); +}).listen(8080); +`; diff --git a/scm-ui/ui-components/src/__resources__/HttpServer.py.ts b/scm-ui/ui-components/src/__resources__/HttpServer.py.ts new file mode 100644 index 0000000000..a02db2bad0 --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/HttpServer.py.ts @@ -0,0 +1,21 @@ +export default `from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer + +PORT_NUMBER = 8080 + +class myHandler(BaseHTTPRequestHandler): + + def do_GET(self): + self.send_response(200) + self.send_header('Content-type','text/html') + self.end_headers() + self.wfile.write("Hello World !") + return + +try: + server = HTTPServer(('', PORT_NUMBER), myHandler) + print 'Started httpserver on port ' , PORT_NUMBER + server.serve_forever() +except KeyboardInterrupt: + print '^C received, shutting down the web server' + server.socket.close() +`; diff --git a/scm-ui/ui-components/src/__resources__/markdown-without-lang.md.ts b/scm-ui/ui-components/src/__resources__/markdown-without-lang.md.ts new file mode 100644 index 0000000000..684db74c66 --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/markdown-without-lang.md.ts @@ -0,0 +1,7 @@ +export default ` +Here is markdown with code, but without Language. + +\`\`\` +Code without Language! +\`\`\` +`; diff --git a/scm-ui/ui-components/src/__resources__/test-page.md.ts b/scm-ui/ui-components/src/__resources__/test-page.md.ts new file mode 100644 index 0000000000..beb1f1b5e7 --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/test-page.md.ts @@ -0,0 +1,172 @@ +export default `# <a name="top"></a>Markdown Test Page + +* [Headings](#Headings) +* [Paragraphs](#Paragraphs) +* [Blockquotes](#Blockquotes) +* [Lists](#Lists) +* [Horizontal rule](#Horizontal) +* [Table](#Table) +* [Code](#Code) +* [Inline elements](#Inline) + +*** + +# <a name="Headings"></a>Headings + +# Heading one + +Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor. Aliquip et adipisicing sit sit fugiat commodo id sunt. Nostrud enim ad commodo incididunt cupidatat in ullamco ullamco Lorem cupidatat velit enim et Lorem. Ut laborum cillum laboris fugiat culpa sint irure do reprehenderit culpa occaecat. Exercitation esse mollit tempor magna aliqua in occaecat aliquip veniam reprehenderit nisi dolor in laboris dolore velit. + +## Heading two + +Aute officia nulla deserunt do deserunt cillum velit magna. Officia veniam culpa anim minim dolore labore pariatur voluptate id ad est duis quis velit dolor pariatur enim. Incididunt enim excepteur do veniam consequat culpa do voluptate dolor fugiat ad adipisicing sit. Labore officia est adipisicing dolore proident eiusmod exercitation deserunt ullamco anim do occaecat velit. Elit dolor consectetur proident sunt aliquip est do tempor quis aliqua culpa aute. Duis in tempor exercitation pariatur et adipisicing mollit irure tempor ut enim esse commodo laboris proident. Do excepteur laborum anim esse aliquip eu sit id Lorem incididunt elit irure ea nulla dolor et. Nulla amet fugiat qui minim deserunt enim eu cupidatat aute officia do velit ea reprehenderit. + +### Heading three + +Voluptate cupidatat cillum elit quis ipsum eu voluptate fugiat consectetur enim. Quis ut voluptate culpa ex anim aute consectetur dolore proident voluptate exercitation eiusmod. Esse in do anim magna minim culpa sint. Adipisicing ipsum consectetur proident ullamco magna sit amet aliqua aute fugiat laborum exercitation duis et. + +#### Heading four + +Commodo fugiat aliqua minim quis pariatur mollit id tempor. Non occaecat minim esse enim aliqua adipisicing nostrud duis consequat eu adipisicing qui. Minim aliquip sit excepteur ipsum consequat laborum pariatur excepteur. Veniam fugiat et amet ad elit anim laborum duis mollit occaecat et et ipsum et reprehenderit. Occaecat aliquip dolore adipisicing sint labore occaecat officia fugiat. Quis adipisicing exercitation exercitation eu amet est laboris sunt nostrud ipsum reprehenderit ullamco. Enim sint ut consectetur id anim aute voluptate exercitation mollit dolore magna magna est Lorem. Ut adipisicing adipisicing aliqua ullamco voluptate labore nisi tempor esse magna incididunt. + +##### Heading five + +Veniam enim esse amet veniam deserunt laboris amet enim consequat. Minim nostrud deserunt cillum consectetur commodo eu enim nostrud ullamco occaecat excepteur. Aliquip et ut est commodo enim dolor amet sint excepteur. Amet ad laboris laborum deserunt sint sunt aliqua commodo ex duis deserunt enim est ex labore ut. Duis incididunt velit adipisicing non incididunt adipisicing adipisicing. Ad irure duis nisi tempor eu dolor fugiat magna et consequat tempor eu ex dolore. Mollit esse nisi qui culpa ut nisi ex proident culpa cupidatat cillum culpa occaecat anim. Ut officia sit ea nisi ea excepteur nostrud ipsum et nulla. + +###### Heading six + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + +[[Top]](#top) + +# <a name="Paragraphs"></a>Paragraphs + +Incididunt ex adipisicing ea ullamco consectetur in voluptate proident fugiat tempor deserunt reprehenderit ullamco id dolore laborum. Do laboris laboris minim incididunt qui consectetur exercitation adipisicing dolore et magna consequat magna anim sunt. Officia fugiat Lorem sunt pariatur incididunt Lorem reprehenderit proident irure. Dolore ipsum aliqua mollit ad officia fugiat sit eu aliquip cupidatat ipsum duis laborum laborum fugiat esse. Voluptate anim ex dolore deserunt ea ex eiusmod irure. Occaecat excepteur aliqua exercitation aliquip dolor esse eu eu. + +Officia dolore laborum aute incididunt commodo nisi velit est est elit et dolore elit exercitation. Enim aliquip magna id ipsum aliquip consectetur ad nulla quis. Incididunt pariatur dolor consectetur cillum enim velit cupidatat laborum quis ex. + +Officia irure in non voluptate adipisicing sit amet tempor duis dolore deserunt enim ut. Reprehenderit incididunt in ad anim et deserunt deserunt Lorem laborum quis. Enim aute anim labore proident laboris voluptate elit excepteur in. Ex labore nulla velit officia ullamco Lorem Lorem id do. Dolore ullamco ipsum magna dolor pariatur voluptate ipsum id occaecat ipsum. Dolore tempor quis duis commodo quis quis enim. + +[[Top]](#top) + +# <a name="Blockquotes"></a>Blockquotes + +Ad nisi laborum aute cupidatat magna deserunt eu id laboris id. Aliquip nulla cupidatat sint ex Lorem mollit laborum dolor amet est ut esse aute. Nostrud ex consequat id incididunt proident ipsum minim duis aliqua ut ex et ad quis. Laborum sint esse cillum anim nulla cillum consectetur aliqua sit. Nisi excepteur cillum labore amet excepteur commodo enim occaecat consequat ipsum proident exercitation duis id in. + +> Ipsum et cupidatat mollit exercitation enim duis sunt irure aliqua reprehenderit mollit. Pariatur Lorem pariatur laboris do culpa do elit irure. Eiusmod amet nulla voluptate velit culpa et aliqua ad reprehenderit sit ut. + +Labore ea magna Lorem consequat aliquip consectetur cillum duis dolore. Et veniam dolor qui incididunt minim amet laboris sit. Dolore ad esse commodo et dolore amet est velit ut nisi ea. Excepteur ea nulla commodo dolore anim dolore adipisicing eiusmod labore id enim esse quis mollit deserunt est. Minim ea culpa voluptate nostrud commodo proident in duis aliquip minim. + +> Qui est sit et reprehenderit aute est esse enim aliqua id aliquip ea anim. Pariatur sint reprehenderit mollit velit voluptate enim consectetur sint enim. Quis exercitation proident elit non id qui culpa dolore esse aliquip consequat. + +Ipsum excepteur cupidatat sunt minim ad eiusmod tempor sit. + +> Deserunt excepteur adipisicing culpa pariatur cillum laboris ullamco nisi fugiat cillum officia. In cupidatat nulla aliquip tempor ad Lorem Lorem quis voluptate officia consectetur pariatur ex in est duis. Mollit id esse est elit exercitation voluptate nostrud nisi laborum magna dolore dolore tempor in est consectetur. + +Adipisicing voluptate ipsum culpa voluptate id aute laboris labore esse fugiat veniam ullamco occaecat do ut. Tempor et esse reprehenderit veniam proident ipsum irure sit ullamco et labore ea excepteur nulla labore ut. Ex aute minim quis tempor in eu id id irure ea nostrud dolor esse. + +[[Top]](#top) + +# <a name="Lists"></a>Lists + +### Ordered List + +1. Longan +2. Lychee +3. Excepteur ad cupidatat do elit laborum amet cillum reprehenderit consequat quis. + Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip aliquip velit pariatur dolore. +4. Marionberry +5. Melon + - Cantaloupe + - Honeydew + - Watermelon +6. Miracle fruit +7. Mulberry + +### Unordered List + +- Olive +- Orange + - Blood orange + - Clementine +- Papaya +- Ut aute ipsum occaecat nisi culpa Lorem id occaecat cupidatat id id magna laboris ad duis. Fugiat cillum dolore veniam nostrud proident sint consectetur eiusmod irure adipisicing. +- Passionfruit + +[[Top]](#top) + +# <a name="Horizontal"></a>Horizontal rule + +In dolore velit aliquip labore mollit minim tempor veniam eu veniam ad in sint aliquip mollit mollit. Ex occaecat non deserunt elit laborum sunt tempor sint consequat culpa culpa qui sit. Irure ad commodo eu voluptate mollit cillum cupidatat veniam proident amet minim reprehenderit. + +*** + +In laboris eiusmod reprehenderit aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi. Dolor amet cillum excepteur consequat sint non sint. + +[[Top]](#top) + +# <a name="Table"></a>Table + +Duis sunt ut pariatur reprehenderit mollit mollit magna dolore in pariatur nulla commodo sit dolor ad fugiat. Laboris amet ea occaecat duis eu enim exercitation deserunt ea laborum occaecat reprehenderit. Et incididunt dolor commodo consequat mollit nisi proident non pariatur in et incididunt id. Eu ut et Lorem ea ex magna minim ipsum ipsum do. + +| Table Heading 1 | Table Heading 2 | Center align | Right align | Table Heading 5 | +| :-------------- | :-------------- | :-------------: | --------------: | :-------------- | +| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 | +| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 | +| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 | +| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 | +| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 | + +Minim id consequat adipisicing cupidatat laborum culpa veniam non consectetur et duis pariatur reprehenderit eu ex consectetur. Sunt nisi qui eiusmod ut cillum laborum Lorem officia aliquip laboris ullamco nostrud laboris non irure laboris. Cillum dolore labore Lorem deserunt mollit voluptate esse incididunt ex dolor. + +[[Top]](#top) + +# <a name="Code"></a>Code + +## Inline code + +Ad amet irure est magna id mollit Lorem in do duis enim. Excepteur velit nisi magna ea pariatur pariatur ullamco fugiat deserunt sint non sint. Duis duis est \`code in text\` velit velit aute culpa ex quis pariatur pariatur laborum aute pariatur duis tempor sunt ad. Irure magna voluptate dolore consectetur consectetur irure esse. Anim magna \`<strong>in culpa qui officia</strong>\` dolor eiusmod esse amet aute cupidatat aliqua do id voluptate cupidatat reprehenderit amet labore deserunt. + +## Highlighted + +Et fugiat ad nisi amet magna labore do cillum fugiat occaecat cillum Lorem proident. In sint dolor ullamco ad do adipisicing amet id excepteur Lorem aliquip sit irure veniam laborum duis cillum. Aliqua occaecat minim cillum deserunt magna sunt laboris do do irure ea nostrud consequat ut voluptate ex. + +\`\`\`go +package main + +import ( + "fmt" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) +} + +func main() { + http.HandleFunc("/", handler) + http.ListenAndServe(":8080", nil) +} +\`\`\` + +Ex amet id ex aliquip id do laborum excepteur exercitation elit sint commodo occaecat nostrud est. Nostrud pariatur esse veniam laborum non sint magna sit laboris minim in id. Aliqua pariatur pariatur excepteur adipisicing irure culpa consequat commodo et ex id ad. + +[[Top]](#top) + +# <a name="Inline"></a>Inline elements + +Sint ea anim ipsum ad commodo cupidatat do **exercitation** incididunt et minim ad labore sunt. Minim deserunt labore laboris velit nulla incididunt ipsum nulla. Ullamco ad laborum ea qui et anim in laboris exercitation tempor sit officia laborum reprehenderit culpa velit quis. **Consequat commodo** reprehenderit duis [irure](#!) esse esse exercitation minim enim Lorem dolore duis irure. Nisi Lorem reprehenderit ea amet excepteur dolor excepteur magna labore proident voluptate ipsum. Reprehenderit ex esse deserunt aliqua ea officia mollit Lorem nulla magna enim. Et ad ipsum labore enim ipsum **cupidatat consequat**. Commodo non ea cupidatat magna deserunt dolore ipsum velit nulla elit veniam nulla eiusmod proident officia. + +![Super wide](http://placekitten.com/1280/800) + +*Proident sit veniam in est proident officia adipisicing* ea tempor cillum non cillum velit deserunt. Voluptate laborum incididunt sit consectetur Lorem irure incididunt voluptate nostrud. Commodo ut eiusmod tempor cupidatat esse enim minim ex anim consequat. Mollit sint culpa qui laboris quis consectetur ad sint esse. Amet anim anim minim ullamco et duis non irure. Sit tempor adipisicing ea laboris \`culpa ex duis sint\` anim aute reprehenderit id eu ea. Aute [excepteur proident](#!) Lorem minim adipisicing nostrud mollit ad ut voluptate do nulla esse occaecat aliqua sint anim. + +![Not so big](http://placekitten.com/480/400) + +Incididunt in culpa cupidatat mollit cillum qui proident sit. In cillum aliquip incididunt voluptate magna amet cupidatat cillum pariatur sint aliqua est _enim **anim** voluptate_. Magna aliquip proident incididunt id duis pariatur eiusmod incididunt commodo culpa dolore sit. Culpa do nostrud elit ad exercitation anim pariatur non minim nisi **adipisicing sunt _officia_**. Do deserunt magna mollit Lorem commodo ipsum do cupidatat mollit enim ut elit veniam ea voluptate. + +[![Manny Pacquiao](https://img.youtube.com/vi/s6bCmZmy9aQ/0.jpg)](https://youtu.be/s6bCmZmy9aQ) + +Reprehenderit non eu quis in ad elit esse qui aute id [incididunt](#!) dolore cillum. Esse laboris consequat dolor anim exercitation tempor aliqua deserunt velit magna laboris. Culpa culpa minim duis amet mollit do quis amet commodo nulla irure. +`; diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap new file mode 100644 index 0000000000..0350b44219 --- /dev/null +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -0,0 +1,2313 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Buttons|AddButton Default 1`] = ` +<div + className="sc-htoDjs bIDNS" +> + <button + className="button is-default" + onClick={[Function]} + type="button" + > + <span + className="icon is-medium" + > + <i + className="fas fa-plus has-text-inherit" + /> + </span> + <span> + + Add + </span> + </button> +</div> +`; + +exports[`Storyshots Buttons|Button Colors 1`] = ` +<div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-primary" + onClick={[Function]} + type="button" + > + primary + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-link" + onClick={[Function]} + type="button" + > + link + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-info" + onClick={[Function]} + type="button" + > + info + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-success" + onClick={[Function]} + type="button" + > + success + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-warning" + onClick={[Function]} + type="button" + > + warning + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-danger" + onClick={[Function]} + type="button" + > + danger + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-white" + onClick={[Function]} + type="button" + > + white + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-light" + onClick={[Function]} + type="button" + > + light + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-dark" + onClick={[Function]} + type="button" + > + dark + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-black" + onClick={[Function]} + type="button" + > + black + + </button> + </div> + <div + className="sc-htoDjs bIDNS" + > + <button + className="button is-text" + onClick={[Function]} + type="button" + > + text + + </button> + </div> +</div> +`; + +exports[`Storyshots Buttons|Button Loading 1`] = ` +<div + className="sc-htoDjs bIDNS" +> + <button + className="button is-primary is-loading" + onClick={[Function]} + type="button" + > + + Loading Button + </button> +</div> +`; + +exports[`Storyshots Buttons|CreateButton Default 1`] = ` +<div + className="sc-htoDjs bIDNS" +> + <div + className="sc-gzVnrw fzShco has-text-centered" + > + <button + className="button is-primary" + onClick={[Function]} + type="button" + > + + Create + </button> + </div> +</div> +`; + +exports[`Storyshots Buttons|DeleteButton Default 1`] = ` +<div + className="sc-htoDjs bIDNS" +> + <button + className="button is-warning" + onClick={[Function]} + type="button" + > + <span + className="icon is-medium" + > + <i + className="fas fa-times has-text-inherit" + /> + </span> + <span> + + Delete + </span> + </button> +</div> +`; + +exports[`Storyshots Buttons|DownloadButton Default 1`] = ` +<div + className="sc-htoDjs bIDNS" +> + <a + className="button is-link" + disabled={false} + href="" + onClick={[Function]} + > + <span + className="icon is-medium" + > + <i + className="fas fa-arrow-circle-down" + /> + </span> + <span> + Download + </span> + </a> +</div> +`; + +exports[`Storyshots Buttons|DownloadButton Disabled 1`] = ` +<div + className="sc-htoDjs bIDNS" +> + <a + className="button is-link" + disabled={true} + href="" + onClick={[Function]} + > + <span + className="icon is-medium" + > + <i + className="fas fa-arrow-circle-down" + /> + </span> + <span> + Download + </span> + </a> +</div> +`; + +exports[`Storyshots Buttons|EditButton Default 1`] = ` +<div + className="sc-htoDjs bIDNS" +> + <button + className="button is-default" + onClick={[Function]} + type="button" + > + + Edit + </button> +</div> +`; + +exports[`Storyshots Buttons|SubmitButton Default 1`] = ` +<div + className="sc-htoDjs bIDNS" +> + <button + className="button is-primary" + onClick={[Function]} + type="submit" + > + + Submit + </button> +</div> +`; + +exports[`Storyshots DateFromNow Default 1`] = ` +<div> + <p> + <time + className="sc-bdVaJa dkavpQ" + title="2009-06-30 18:30:00" + > + over 10 years ago + </time> + </p> + <p> + <time + className="sc-bdVaJa dkavpQ" + title="2019-06-30 18:30:00" + > + 3 months ago + </time> + </p> + <p> + <time + className="sc-bdVaJa dkavpQ" + title="2019-10-12 13:56:40" + > + less than a minute ago + </time> + </p> + <p> + <time + className="sc-bdVaJa dkavpQ" + title="2019-10-11 13:56:40" + > + 1 day ago + </time> + </p> +</div> +`; + +exports[`Storyshots Forms|Checkbox Default 1`] = ` +<div + className="sc-gipzik xsalO" +> + <div + className="field" + > + <div + className="control" + > + <label + className="checkbox" + > + <input + checked={false} + onChange={[Function]} + type="checkbox" + /> + + Not checked + </label> + </div> + </div> + <div + className="field" + > + <div + className="control" + > + <label + className="checkbox" + > + <input + checked={true} + onChange={[Function]} + type="checkbox" + /> + + Checked + </label> + </div> + </div> +</div> +`; + +exports[`Storyshots Forms|Checkbox Disabled 1`] = ` +<div + className="sc-gipzik xsalO" +> + <div + className="field" + > + <div + className="control" + > + <label + className="checkbox" + disabled={true} + > + <input + checked={true} + disabled={true} + onChange={[Function]} + type="checkbox" + /> + + Checked but disabled + </label> + </div> + </div> +</div> +`; + +exports[`Storyshots Forms|Radio Default 1`] = ` +<div + className="sc-csuQGl fFFkRK" +> + <label + className="radio" + > + <input + checked={false} + onChange={[Function]} + type="radio" + /> + + Not checked + </label> + <label + className="radio" + > + <input + checked={true} + onChange={[Function]} + type="radio" + /> + + Checked + </label> +</div> +`; + +exports[`Storyshots Forms|Radio Disabled 1`] = ` +<div + className="sc-csuQGl fFFkRK" +> + <label + className="radio" + disabled={true} + > + <input + checked={true} + disabled={true} + onChange={[Function]} + type="radio" + /> + + Checked but disabled + </label> +</div> +`; + +exports[`Storyshots Loading Default 1`] = ` +<div> + <div + className="sc-bwzfXH fuRxdH is-flex" + > + <img + alt="loading.alt" + className="sc-htpNat bhQHKl" + src="/images/loading.svg" + /> + <p + className="has-text-centered" + /> + </div> +</div> +`; + +exports[`Storyshots Logo Default 1`] = ` +<div + className="sc-bxivhb gXEZub" +> + <img + alt="logo.alt" + src="/images/logo.png" + /> +</div> +`; + +exports[`Storyshots MarkdownView Code without Lang 1`] = ` +<div + className="sc-EHOje lbpDzp" +> + <div + className="sc-ifAKCX frmxmY" + > + <div + className="content" + > + <p> + Here is markdown with code, but without Language. + </p> + <pre + style={ + Object { + "background": "#FFFFFF", + "color": "#434f54", + "display": "block", + "overflowX": "auto", + "padding": "0.5em", + } + } + > + <code> + Code without Language! + </code> + </pre> + </div> + </div> +</div> +`; + +exports[`Storyshots MarkdownView Default 1`] = ` +<div + className="sc-EHOje lbpDzp" +> + <div + className="sc-ifAKCX frmxmY" + > + <div + className="content" + > + <h1> + <a + name="top" + /> + Markdown Test Page + </h1> + <ul> + <li> + <a + href="#Headings" + > + Headings + </a> + </li> + <li> + <a + href="#Paragraphs" + > + Paragraphs + </a> + </li> + <li> + <a + href="#Blockquotes" + > + Blockquotes + </a> + </li> + <li> + <a + href="#Lists" + > + Lists + </a> + </li> + <li> + <a + href="#Horizontal" + > + Horizontal rule + </a> + </li> + <li> + <a + href="#Table" + > + Table + </a> + </li> + <li> + <a + href="#Code" + > + Code + </a> + </li> + <li> + <a + href="#Inline" + > + Inline elements + </a> + </li> + </ul> + <hr /> + <h1> + <a + name="Headings" + /> + Headings + </h1> + <h1> + Heading one + </h1> + <p> + Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor. Aliquip et adipisicing sit sit fugiat commodo id sunt. Nostrud enim ad commodo incididunt cupidatat in ullamco ullamco Lorem cupidatat velit enim et Lorem. Ut laborum cillum laboris fugiat culpa sint irure do reprehenderit culpa occaecat. Exercitation esse mollit tempor magna aliqua in occaecat aliquip veniam reprehenderit nisi dolor in laboris dolore velit. + </p> + <h2> + Heading two + </h2> + <p> + Aute officia nulla deserunt do deserunt cillum velit magna. Officia veniam culpa anim minim dolore labore pariatur voluptate id ad est duis quis velit dolor pariatur enim. Incididunt enim excepteur do veniam consequat culpa do voluptate dolor fugiat ad adipisicing sit. Labore officia est adipisicing dolore proident eiusmod exercitation deserunt ullamco anim do occaecat velit. Elit dolor consectetur proident sunt aliquip est do tempor quis aliqua culpa aute. Duis in tempor exercitation pariatur et adipisicing mollit irure tempor ut enim esse commodo laboris proident. Do excepteur laborum anim esse aliquip eu sit id Lorem incididunt elit irure ea nulla dolor et. Nulla amet fugiat qui minim deserunt enim eu cupidatat aute officia do velit ea reprehenderit. + </p> + <h3> + Heading three + </h3> + <p> + Voluptate cupidatat cillum elit quis ipsum eu voluptate fugiat consectetur enim. Quis ut voluptate culpa ex anim aute consectetur dolore proident voluptate exercitation eiusmod. Esse in do anim magna minim culpa sint. Adipisicing ipsum consectetur proident ullamco magna sit amet aliqua aute fugiat laborum exercitation duis et. + </p> + <h4> + Heading four + </h4> + <p> + Commodo fugiat aliqua minim quis pariatur mollit id tempor. Non occaecat minim esse enim aliqua adipisicing nostrud duis consequat eu adipisicing qui. Minim aliquip sit excepteur ipsum consequat laborum pariatur excepteur. Veniam fugiat et amet ad elit anim laborum duis mollit occaecat et et ipsum et reprehenderit. Occaecat aliquip dolore adipisicing sint labore occaecat officia fugiat. Quis adipisicing exercitation exercitation eu amet est laboris sunt nostrud ipsum reprehenderit ullamco. Enim sint ut consectetur id anim aute voluptate exercitation mollit dolore magna magna est Lorem. Ut adipisicing adipisicing aliqua ullamco voluptate labore nisi tempor esse magna incididunt. + </p> + <h5> + Heading five + </h5> + <p> + Veniam enim esse amet veniam deserunt laboris amet enim consequat. Minim nostrud deserunt cillum consectetur commodo eu enim nostrud ullamco occaecat excepteur. Aliquip et ut est commodo enim dolor amet sint excepteur. Amet ad laboris laborum deserunt sint sunt aliqua commodo ex duis deserunt enim est ex labore ut. Duis incididunt velit adipisicing non incididunt adipisicing adipisicing. Ad irure duis nisi tempor eu dolor fugiat magna et consequat tempor eu ex dolore. Mollit esse nisi qui culpa ut nisi ex proident culpa cupidatat cillum culpa occaecat anim. Ut officia sit ea nisi ea excepteur nostrud ipsum et nulla. + </p> + <h6> + Heading six + </h6> + <p> + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + </p> + <p> + <a + href="#top" + > + [Top] + </a> + </p> + <h1> + <a + name="Paragraphs" + /> + Paragraphs + </h1> + <p> + Incididunt ex adipisicing ea ullamco consectetur in voluptate proident fugiat tempor deserunt reprehenderit ullamco id dolore laborum. Do laboris laboris minim incididunt qui consectetur exercitation adipisicing dolore et magna consequat magna anim sunt. Officia fugiat Lorem sunt pariatur incididunt Lorem reprehenderit proident irure. Dolore ipsum aliqua mollit ad officia fugiat sit eu aliquip cupidatat ipsum duis laborum laborum fugiat esse. Voluptate anim ex dolore deserunt ea ex eiusmod irure. Occaecat excepteur aliqua exercitation aliquip dolor esse eu eu. + </p> + <p> + Officia dolore laborum aute incididunt commodo nisi velit est est elit et dolore elit exercitation. Enim aliquip magna id ipsum aliquip consectetur ad nulla quis. Incididunt pariatur dolor consectetur cillum enim velit cupidatat laborum quis ex. + </p> + <p> + Officia irure in non voluptate adipisicing sit amet tempor duis dolore deserunt enim ut. Reprehenderit incididunt in ad anim et deserunt deserunt Lorem laborum quis. Enim aute anim labore proident laboris voluptate elit excepteur in. Ex labore nulla velit officia ullamco Lorem Lorem id do. Dolore ullamco ipsum magna dolor pariatur voluptate ipsum id occaecat ipsum. Dolore tempor quis duis commodo quis quis enim. + </p> + <p> + <a + href="#top" + > + [Top] + </a> + </p> + <h1> + <a + name="Blockquotes" + /> + Blockquotes + </h1> + <p> + Ad nisi laborum aute cupidatat magna deserunt eu id laboris id. Aliquip nulla cupidatat sint ex Lorem mollit laborum dolor amet est ut esse aute. Nostrud ex consequat id incididunt proident ipsum minim duis aliqua ut ex et ad quis. Laborum sint esse cillum anim nulla cillum consectetur aliqua sit. Nisi excepteur cillum labore amet excepteur commodo enim occaecat consequat ipsum proident exercitation duis id in. + </p> + <blockquote> + <p> + Ipsum et cupidatat mollit exercitation enim duis sunt irure aliqua reprehenderit mollit. Pariatur Lorem pariatur laboris do culpa do elit irure. Eiusmod amet nulla voluptate velit culpa et aliqua ad reprehenderit sit ut. + </p> + </blockquote> + <p> + Labore ea magna Lorem consequat aliquip consectetur cillum duis dolore. Et veniam dolor qui incididunt minim amet laboris sit. Dolore ad esse commodo et dolore amet est velit ut nisi ea. Excepteur ea nulla commodo dolore anim dolore adipisicing eiusmod labore id enim esse quis mollit deserunt est. Minim ea culpa voluptate nostrud commodo proident in duis aliquip minim. + </p> + <blockquote> + <p> + Qui est sit et reprehenderit aute est esse enim aliqua id aliquip ea anim. Pariatur sint reprehenderit mollit velit voluptate enim consectetur sint enim. Quis exercitation proident elit non id qui culpa dolore esse aliquip consequat. + </p> + </blockquote> + <p> + Ipsum excepteur cupidatat sunt minim ad eiusmod tempor sit. + </p> + <blockquote> + <p> + Deserunt excepteur adipisicing culpa pariatur cillum laboris ullamco nisi fugiat cillum officia. In cupidatat nulla aliquip tempor ad Lorem Lorem quis voluptate officia consectetur pariatur ex in est duis. Mollit id esse est elit exercitation voluptate nostrud nisi laborum magna dolore dolore tempor in est consectetur. + </p> + </blockquote> + <p> + Adipisicing voluptate ipsum culpa voluptate id aute laboris labore esse fugiat veniam ullamco occaecat do ut. Tempor et esse reprehenderit veniam proident ipsum irure sit ullamco et labore ea excepteur nulla labore ut. Ex aute minim quis tempor in eu id id irure ea nostrud dolor esse. + </p> + <p> + <a + href="#top" + > + [Top] + </a> + </p> + <h1> + <a + name="Lists" + /> + Lists + </h1> + <h3> + Ordered List + </h3> + <ol> + <li> + Longan + </li> + <li> + Lychee + </li> + <li> + Excepteur ad cupidatat do elit laborum amet cillum reprehenderit consequat quis. +Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip aliquip velit pariatur dolore. + </li> + <li> + Marionberry + </li> + <li> + Melon + <ul> + <li> + Cantaloupe + </li> + <li> + Honeydew + </li> + <li> + Watermelon + </li> + </ul> + </li> + <li> + Miracle fruit + </li> + <li> + Mulberry + </li> + </ol> + <h3> + Unordered List + </h3> + <ul> + <li> + Olive + </li> + <li> + Orange + <ul> + <li> + Blood orange + </li> + <li> + Clementine + </li> + </ul> + </li> + <li> + Papaya + </li> + <li> + Ut aute ipsum occaecat nisi culpa Lorem id occaecat cupidatat id id magna laboris ad duis. Fugiat cillum dolore veniam nostrud proident sint consectetur eiusmod irure adipisicing. + </li> + <li> + Passionfruit + </li> + </ul> + <p> + <a + href="#top" + > + [Top] + </a> + </p> + <h1> + <a + name="Horizontal" + /> + Horizontal rule + </h1> + <p> + In dolore velit aliquip labore mollit minim tempor veniam eu veniam ad in sint aliquip mollit mollit. Ex occaecat non deserunt elit laborum sunt tempor sint consequat culpa culpa qui sit. Irure ad commodo eu voluptate mollit cillum cupidatat veniam proident amet minim reprehenderit. + </p> + <hr /> + <p> + In laboris eiusmod reprehenderit aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi. Dolor amet cillum excepteur consequat sint non sint. + </p> + <p> + <a + href="#top" + > + [Top] + </a> + </p> + <h1> + <a + name="Table" + /> + Table + </h1> + <p> + Duis sunt ut pariatur reprehenderit mollit mollit magna dolore in pariatur nulla commodo sit dolor ad fugiat. Laboris amet ea occaecat duis eu enim exercitation deserunt ea laborum occaecat reprehenderit. Et incididunt dolor commodo consequat mollit nisi proident non pariatur in et incididunt id. Eu ut et Lorem ea ex magna minim ipsum ipsum do. + </p> + <table> + <thead> + <tr> + <th + style={ + Object { + "textAlign": "left", + } + } + > + Table Heading 1 + </th> + <th + style={ + Object { + "textAlign": "left", + } + } + > + Table Heading 2 + </th> + <th + style={ + Object { + "textAlign": "center", + } + } + > + Center align + </th> + <th + style={ + Object { + "textAlign": "right", + } + } + > + Right align + </th> + <th + style={ + Object { + "textAlign": "left", + } + } + > + Table Heading 5 + </th> + </tr> + </thead> + <tbody> + <tr> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 1 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 2 + </td> + <td + style={ + Object { + "textAlign": "center", + } + } + > + Item 3 + </td> + <td + style={ + Object { + "textAlign": "right", + } + } + > + Item 4 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 5 + </td> + </tr> + <tr> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 1 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 2 + </td> + <td + style={ + Object { + "textAlign": "center", + } + } + > + Item 3 + </td> + <td + style={ + Object { + "textAlign": "right", + } + } + > + Item 4 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 5 + </td> + </tr> + <tr> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 1 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 2 + </td> + <td + style={ + Object { + "textAlign": "center", + } + } + > + Item 3 + </td> + <td + style={ + Object { + "textAlign": "right", + } + } + > + Item 4 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 5 + </td> + </tr> + <tr> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 1 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 2 + </td> + <td + style={ + Object { + "textAlign": "center", + } + } + > + Item 3 + </td> + <td + style={ + Object { + "textAlign": "right", + } + } + > + Item 4 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 5 + </td> + </tr> + <tr> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 1 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 2 + </td> + <td + style={ + Object { + "textAlign": "center", + } + } + > + Item 3 + </td> + <td + style={ + Object { + "textAlign": "right", + } + } + > + Item 4 + </td> + <td + style={ + Object { + "textAlign": "left", + } + } + > + Item 5 + </td> + </tr> + </tbody> + </table> + <p> + Minim id consequat adipisicing cupidatat laborum culpa veniam non consectetur et duis pariatur reprehenderit eu ex consectetur. Sunt nisi qui eiusmod ut cillum laborum Lorem officia aliquip laboris ullamco nostrud laboris non irure laboris. Cillum dolore labore Lorem deserunt mollit voluptate esse incididunt ex dolor. + </p> + <p> + <a + href="#top" + > + [Top] + </a> + </p> + <h1> + <a + name="Code" + /> + Code + </h1> + <h2> + Inline code + </h2> + <p> + Ad amet irure est magna id mollit Lorem in do duis enim. Excepteur velit nisi magna ea pariatur pariatur ullamco fugiat deserunt sint non sint. Duis duis est + <code> + code in text + </code> + velit velit aute culpa ex quis pariatur pariatur laborum aute pariatur duis tempor sunt ad. Irure magna voluptate dolore consectetur consectetur irure esse. Anim magna + <code> + <strong>in culpa qui officia</strong> + </code> + dolor eiusmod esse amet aute cupidatat aliqua do id voluptate cupidatat reprehenderit amet labore deserunt. + </p> + <h2> + Highlighted + </h2> + <p> + Et fugiat ad nisi amet magna labore do cillum fugiat occaecat cillum Lorem proident. In sint dolor ullamco ad do adipisicing amet id excepteur Lorem aliquip sit irure veniam laborum duis cillum. Aliqua occaecat minim cillum deserunt magna sunt laboris do do irure ea nostrud consequat ut voluptate ex. + </p> + <pre + style={ + Object { + "background": "#FFFFFF", + "color": "#434f54", + "display": "block", + "overflowX": "auto", + "padding": "0.5em", + } + } + > + <code> + package main + +import ( + "fmt" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) +} + +func main() { + http.HandleFunc("/", handler) + http.ListenAndServe(":8080", nil) +} + </code> + </pre> + <p> + Ex amet id ex aliquip id do laborum excepteur exercitation elit sint commodo occaecat nostrud est. Nostrud pariatur esse veniam laborum non sint magna sit laboris minim in id. Aliqua pariatur pariatur excepteur adipisicing irure culpa consequat commodo et ex id ad. + </p> + <p> + <a + href="#top" + > + [Top] + </a> + </p> + <h1> + <a + name="Inline" + /> + Inline elements + </h1> + <p> + Sint ea anim ipsum ad commodo cupidatat do + <strong> + exercitation + </strong> + incididunt et minim ad labore sunt. Minim deserunt labore laboris velit nulla incididunt ipsum nulla. Ullamco ad laborum ea qui et anim in laboris exercitation tempor sit officia laborum reprehenderit culpa velit quis. + <strong> + Consequat commodo + </strong> + reprehenderit duis + <a + href="#!" + > + irure + </a> + esse esse exercitation minim enim Lorem dolore duis irure. Nisi Lorem reprehenderit ea amet excepteur dolor excepteur magna labore proident voluptate ipsum. Reprehenderit ex esse deserunt aliqua ea officia mollit Lorem nulla magna enim. Et ad ipsum labore enim ipsum + <strong> + cupidatat consequat + </strong> + . Commodo non ea cupidatat magna deserunt dolore ipsum velit nulla elit veniam nulla eiusmod proident officia. + </p> + <p> + <img + alt="Super wide" + src="http://placekitten.com/1280/800" + /> + </p> + <p> + <em> + Proident sit veniam in est proident officia adipisicing + </em> + ea tempor cillum non cillum velit deserunt. Voluptate laborum incididunt sit consectetur Lorem irure incididunt voluptate nostrud. Commodo ut eiusmod tempor cupidatat esse enim minim ex anim consequat. Mollit sint culpa qui laboris quis consectetur ad sint esse. Amet anim anim minim ullamco et duis non irure. Sit tempor adipisicing ea laboris + <code> + culpa ex duis sint + </code> + anim aute reprehenderit id eu ea. Aute + <a + href="#!" + > + excepteur proident + </a> + Lorem minim adipisicing nostrud mollit ad ut voluptate do nulla esse occaecat aliqua sint anim. + </p> + <p> + <img + alt="Not so big" + src="http://placekitten.com/480/400" + /> + </p> + <p> + Incididunt in culpa cupidatat mollit cillum qui proident sit. In cillum aliquip incididunt voluptate magna amet cupidatat cillum pariatur sint aliqua est + <em> + enim + <strong> + anim + </strong> + voluptate + </em> + . Magna aliquip proident incididunt id duis pariatur eiusmod incididunt commodo culpa dolore sit. Culpa do nostrud elit ad exercitation anim pariatur non minim nisi + <strong> + adipisicing sunt + <em> + officia + </em> + </strong> + . Do deserunt magna mollit Lorem commodo ipsum do cupidatat mollit enim ut elit veniam ea voluptate. + </p> + <p> + <a + href="https://youtu.be/s6bCmZmy9aQ" + > + <img + alt="Manny Pacquiao" + src="https://img.youtube.com/vi/s6bCmZmy9aQ/0.jpg" + /> + </a> + </p> + <p> + Reprehenderit non eu quis in ad elit esse qui aute id + <a + href="#!" + > + incididunt + </a> + dolore cillum. Esse laboris consequat dolor anim exercitation tempor aliqua deserunt velit magna laboris. Culpa culpa minim duis amet mollit do quis amet commodo nulla irure. + </p> + </div> + </div> +</div> +`; + +exports[`Storyshots SyntaxHighlighter Go 1`] = ` +<div + className="sc-bZQynM hAJIOS" +> + <pre + style={ + Object { + "background": "#FFFFFF", + "color": "#434f54", + "display": "block", + "overflowX": "auto", + "padding": "0.5em", + } + } + > + <code> + <span + style={ + Object { + "color": "#00979D", + } + } + > + package + </span> + main + + + <span + style={ + Object { + "color": "#00979D", + } + } + > + import + </span> + ( + + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "fmt" + </span> + + + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "net/http" + </span> + +) + + + <span + style={ + Object { + "color": "#728E00", + } + } + > + <span + style={ + Object { + "color": "#00979D", + } + } + > + func + </span> + + <span + style={ + Object { + "color": "#880000", + "fontWeight": "bold", + } + } + > + main + </span> + <span + className="hljs-params" + style={Object {}} + > + () + </span> + </span> + { + http.HandleFunc( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "/" + </span> + , + <span + style={ + Object { + "color": "#728E00", + } + } + > + <span + style={ + Object { + "color": "#00979D", + } + } + > + func + </span> + + <span + className="hljs-params" + style={Object {}} + > + (w http.ResponseWriter, r *http.Request) + </span> + </span> + { + fmt.Fprintf(w, + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "Welcome to my website!" + </span> + ) + }) + + fs := http.FileServer(http.Dir( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "static/" + </span> + )) + http.Handle( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "/static/" + </span> + , http.StripPrefix( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "/static/" + </span> + , fs)) + + http.ListenAndServe( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + ":80" + </span> + , + <span + style={ + Object { + "color": "#D35400", + } + } + > + nil + </span> + ) +} + + </code> + </pre> +</div> +`; + +exports[`Storyshots SyntaxHighlighter Java 1`] = ` +<div + className="sc-bZQynM hAJIOS" +> + <pre + style={ + Object { + "background": "#FFFFFF", + "color": "#434f54", + "display": "block", + "overflowX": "auto", + "padding": "0.5em", + } + } + > + <code> + <span + style={ + Object { + "color": "#00979D", + } + } + > + package + </span> + com.example; + + + <span + style={ + Object { + "color": "#00979D", + } + } + > + import + </span> + java.io.IOException; + + <span + style={ + Object { + "color": "#00979D", + } + } + > + import + </span> + java.io.OutputStream; + + <span + style={ + Object { + "color": "#00979D", + } + } + > + import + </span> + java.net.InetSocketAddress; + + + <span + style={ + Object { + "color": "#00979D", + } + } + > + import + </span> + com.sun.net.httpserver.HttpExchange; + + <span + style={ + Object { + "color": "#00979D", + } + } + > + import + </span> + com.sun.net.httpserver.HttpHandler; + + <span + style={ + Object { + "color": "#00979D", + } + } + > + import + </span> + com.sun.net.httpserver.HttpServer; + + + <span + style={ + Object { + "color": "#00979D", + } + } + > + public + </span> + + <span + className="hljs-class" + style={Object {}} + > + <span + style={ + Object { + "color": "#00979D", + } + } + > + class + </span> + + <span + style={ + Object { + "color": "#880000", + "fontWeight": "bold", + } + } + > + Test + </span> + + </span> + { + + + <span + style={ + Object { + "color": "#728E00", + } + } + > + <span + style={ + Object { + "color": "#00979D", + } + } + > + public + </span> + + <span + style={ + Object { + "color": "#00979D", + } + } + > + static + </span> + + <span + style={ + Object { + "color": "#00979D", + } + } + > + void + </span> + + <span + style={ + Object { + "color": "#880000", + "fontWeight": "bold", + } + } + > + main + </span> + <span + className="hljs-params" + style={Object {}} + > + (String[] args) + </span> + + <span + style={ + Object { + "color": "#00979D", + } + } + > + throws + </span> + Exception + </span> + { + HttpServer server = HttpServer.create( + <span + style={ + Object { + "color": "#00979D", + } + } + > + new + </span> + InetSocketAddress( + <span + style={ + Object { + "color": "#8A7B52", + } + } + > + 8000 + </span> + ), + <span + style={ + Object { + "color": "#8A7B52", + } + } + > + 0 + </span> + ); + server.createContext( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "/test" + </span> + , + <span + style={ + Object { + "color": "#00979D", + } + } + > + new + </span> + MyHandler()); + server.setExecutor( + <span + style={ + Object { + "color": "#00979D", + } + } + > + null + </span> + ); + <span + style={ + Object { + "color": "rgba(149,165,166,.8)", + } + } + > + // creates a default executor + </span> + + server.start(); + } + + + <span + style={ + Object { + "color": "#00979D", + } + } + > + static + </span> + + <span + className="hljs-class" + style={Object {}} + > + <span + style={ + Object { + "color": "#00979D", + } + } + > + class + </span> + + <span + style={ + Object { + "color": "#880000", + "fontWeight": "bold", + } + } + > + MyHandler + </span> + + <span + style={ + Object { + "color": "#00979D", + } + } + > + implements + </span> + + <span + style={ + Object { + "color": "#880000", + "fontWeight": "bold", + } + } + > + HttpHandler + </span> + + </span> + { + + <span + style={ + Object { + "color": "#434f54", + } + } + > + @Override + </span> + + + <span + style={ + Object { + "color": "#728E00", + } + } + > + <span + style={ + Object { + "color": "#00979D", + } + } + > + public + </span> + + <span + style={ + Object { + "color": "#00979D", + } + } + > + void + </span> + + <span + style={ + Object { + "color": "#880000", + "fontWeight": "bold", + } + } + > + handle + </span> + <span + className="hljs-params" + style={Object {}} + > + (HttpExchange t) + </span> + + <span + style={ + Object { + "color": "#00979D", + } + } + > + throws + </span> + IOException + </span> + { + String response = + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "This is the response" + </span> + ; + t.sendResponseHeaders( + <span + style={ + Object { + "color": "#8A7B52", + } + } + > + 200 + </span> + , response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + +} + + </code> + </pre> +</div> +`; + +exports[`Storyshots SyntaxHighlighter Javascript 1`] = ` +<div + className="sc-bZQynM hAJIOS" +> + <pre + style={ + Object { + "background": "#FFFFFF", + "color": "#434f54", + "display": "block", + "overflowX": "auto", + "padding": "0.5em", + } + } + > + <code> + <span + style={ + Object { + "color": "#00979D", + } + } + > + var + </span> + http = + <span + style={ + Object { + "color": "#D35400", + } + } + > + require + </span> + ( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + 'http' + </span> + ); + +http.createServer( + <span + style={ + Object { + "color": "#728E00", + } + } + > + <span + style={ + Object { + "color": "#00979D", + } + } + > + function + </span> + ( + <span + className="hljs-params" + style={Object {}} + > + req, res + </span> + ) + </span> + { + res.writeHead( + <span + style={ + Object { + "color": "#8A7B52", + } + } + > + 200 + </span> + , { + <span + style={ + Object { + "color": "#005C5F", + } + } + > + 'Content-Type' + </span> + : + <span + style={ + Object { + "color": "#005C5F", + } + } + > + 'text/plain' + </span> + }); + res.write( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + 'Hello World!' + </span> + ); + res.end(); +}).listen( + <span + style={ + Object { + "color": "#8A7B52", + } + } + > + 8080 + </span> + ); + + </code> + </pre> +</div> +`; + +exports[`Storyshots SyntaxHighlighter Python 1`] = ` +<div + className="sc-bZQynM hAJIOS" +> + <pre + style={ + Object { + "background": "#FFFFFF", + "color": "#434f54", + "display": "block", + "overflowX": "auto", + "padding": "0.5em", + } + } + > + <code> + <span + style={ + Object { + "color": "#00979D", + } + } + > + from + </span> + BaseHTTPServer + <span + style={ + Object { + "color": "#00979D", + } + } + > + import + </span> + BaseHTTPRequestHandler,HTTPServer + +PORT_NUMBER = + <span + style={ + Object { + "color": "#8A7B52", + } + } + > + 8080 + </span> + + + + <span + className="hljs-class" + style={Object {}} + > + <span + style={ + Object { + "color": "#00979D", + } + } + > + class + </span> + + <span + style={ + Object { + "color": "#880000", + "fontWeight": "bold", + } + } + > + myHandler + </span> + <span + className="hljs-params" + style={Object {}} + > + (BaseHTTPRequestHandler) + </span> + : + </span> + + + + <span + style={ + Object { + "color": "#728E00", + } + } + > + <span + style={ + Object { + "color": "#00979D", + } + } + > + def + </span> + + <span + style={ + Object { + "color": "#880000", + "fontWeight": "bold", + } + } + > + do_GET + </span> + <span + className="hljs-params" + style={Object {}} + > + (self) + </span> + : + </span> + + self.send_response( + <span + style={ + Object { + "color": "#8A7B52", + } + } + > + 200 + </span> + ) + self.send_header( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + 'Content-type' + </span> + , + <span + style={ + Object { + "color": "#005C5F", + } + } + > + 'text/html' + </span> + ) + self.end_headers() + self.wfile.write( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + "Hello World !" + </span> + ) + + <span + style={ + Object { + "color": "#00979D", + } + } + > + return + </span> + + + + <span + style={ + Object { + "color": "#00979D", + } + } + > + try + </span> + : + server = HTTPServer(( + <span + style={ + Object { + "color": "#005C5F", + } + } + > + '' + </span> + , PORT_NUMBER), myHandler) + + <span + style={ + Object { + "color": "#00979D", + } + } + > + print + </span> + + <span + style={ + Object { + "color": "#005C5F", + } + } + > + 'Started httpserver on port ' + </span> + , PORT_NUMBER + server.serve_forever() + + <span + style={ + Object { + "color": "#00979D", + } + } + > + except + </span> + KeyboardInterrupt: + + <span + style={ + Object { + "color": "#00979D", + } + } + > + print + </span> + + <span + style={ + Object { + "color": "#005C5F", + } + } + > + '^C received, shutting down the web server' + </span> + + server.socket.close() + + </code> + </pre> +</div> +`; diff --git a/scm-ui/ui-components/src/apiclient.test.ts b/scm-ui/ui-components/src/apiclient.test.ts new file mode 100644 index 0000000000..871d8089b7 --- /dev/null +++ b/scm-ui/ui-components/src/apiclient.test.ts @@ -0,0 +1,91 @@ +import { apiClient, createUrl, extractXsrfTokenFromCookie } from "./apiclient"; +import fetchMock from "fetch-mock"; +import { BackendError } from "./errors"; + +describe("create url", () => { + it("should not change absolute urls", () => { + expect(createUrl("https://www.scm-manager.org")).toBe("https://www.scm-manager.org"); + }); + + it("should add prefix for api", () => { + expect(createUrl("/users")).toBe("/api/v2/users"); + expect(createUrl("users")).toBe("/api/v2/users"); + }); +}); + +describe("error handling tests", () => { + const earthNotFoundError = { + transactionId: "42t", + errorCode: "42e", + message: "earth not found", + context: [ + { + type: "planet", + id: "earth" + } + ] + }; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should create a normal error, if the content type is not scmm-error", done => { + fetchMock.getOnce("/api/v2/error", { + status: 404 + }); + + apiClient.get("/error").catch((err: Error) => { + expect(err.name).toEqual("Error"); + expect(err.message).toContain("404"); + done(); + }); + }); + + it("should create an backend error, if the content type is scmm-error", done => { + fetchMock.getOnce("/api/v2/error", { + status: 404, + headers: { + "Content-Type": "application/vnd.scmm-error+json;v=2" + }, + body: earthNotFoundError + }); + + apiClient.get("/error").catch((err: BackendError) => { + expect(err).toBeInstanceOf(BackendError); + + expect(err.message).toEqual("earth not found"); + expect(err.statusCode).toBe(404); + + expect(err.transactionId).toEqual("42t"); + expect(err.errorCode).toEqual("42e"); + expect(err.context).toEqual([ + { + type: "planet", + id: "earth" + } + ]); + done(); + }); + }); +}); + +describe("extract xsrf token", () => { + it("should return undefined if no cookie exists", () => { + const token = extractXsrfTokenFromCookie(undefined); + expect(token).toBeUndefined(); + }); + + it("should return undefined without X-Bearer-Token exists", () => { + const token = extractXsrfTokenFromCookie("a=b; c=d; e=f"); + expect(token).toBeUndefined(); + }); + + it("should return xsrf token", () => { + const cookie = + "a=b; X-Bearer-Token=eyJhbGciOiJIUzI1NiJ9.eyJ4c3JmIjoiYjE0NDRmNWEtOWI5Mi00ZDA0LWFkMzMtMTAxYjY3MWQ1YTc0Iiwic3ViIjoic2NtYWRtaW4iLCJqdGkiOiI2RFJpQVphNWwxIiwiaWF0IjoxNTc0MDcyNDQ4LCJleHAiOjE1NzQwNzYwNDgsInNjbS1tYW5hZ2VyLnJlZnJlc2hFeHBpcmF0aW9uIjoxNTc0MTE1NjQ4OTU5LCJzY20tbWFuYWdlci5wYXJlbnRUb2tlbklkIjoiNkRSaUFaYTVsMSJ9.VUJtKeWUn3xtHCEbG51r7ceXZ8CF3cmN8J-eb9EDY_U; c=d"; + const token = extractXsrfTokenFromCookie(cookie); + expect(token).toBe("b1444f5a-9b92-4d04-ad33-101b671d5a74"); + }); +}); diff --git a/scm-ui/ui-components/src/apiclient.ts b/scm-ui/ui-components/src/apiclient.ts new file mode 100644 index 0000000000..473bf3b925 --- /dev/null +++ b/scm-ui/ui-components/src/apiclient.ts @@ -0,0 +1,176 @@ +import { contextPath } from "./urls"; +import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError } from "./errors"; +import { BackendErrorContent } from "./errors"; + +const extractXsrfTokenFromJwt = (jwt: string) => { + const parts = jwt.split("."); + if (parts.length === 3) { + return JSON.parse(atob(parts[1])).xsrf; + } +}; + +// @VisibleForTesting +export const extractXsrfTokenFromCookie = (cookieString?: string) => { + if (cookieString) { + const cookies = cookieString.split(";"); + for (const c of cookies) { + const parts = c.trim().split("="); + if (parts[0] === "X-Bearer-Token") { + return extractXsrfTokenFromJwt(parts[1]); + } + } + } +}; + +const extractXsrfToken = () => { + return extractXsrfTokenFromCookie(document.cookie); +}; + +const applyFetchOptions: (p: RequestInit) => RequestInit = o => { + if (!o.headers) { + o.headers = {}; + } + + // @ts-ignore We are sure that here we only get headers of type Record<string, string> + const headers: Record<string, string> = o.headers; + headers["Cache"] = "no-cache"; + // identify the request as ajax request + headers["X-Requested-With"] = "XMLHttpRequest"; + // identify the web interface + headers["X-SCM-Client"] = "WUI"; + + const xsrf = extractXsrfToken(); + if (xsrf) { + headers["X-XSRF-Token"] = xsrf; + } + + o.credentials = "same-origin"; + o.headers = headers; + return o; +}; + +function handleFailure(response: Response) { + if (!response.ok) { + if (isBackendError(response)) { + return response.json().then((content: BackendErrorContent) => { + throw createBackendError(content, response.status); + }); + } else { + if (response.status === 401) { + throw new UnauthorizedError("Unauthorized", 401); + } else if (response.status === 403) { + throw new ForbiddenError("Forbidden", 403); + } + + throw new Error("server returned status code " + response.status); + } + } + return response; +} + +export function createUrl(url: string) { + if (url.includes("://")) { + return url; + } + let urlWithStartingSlash = url; + if (url.indexOf("/") !== 0) { + urlWithStartingSlash = "/" + urlWithStartingSlash; + } + return `${contextPath}/api/v2${urlWithStartingSlash}`; +} + +class ApiClient { + get(url: string): Promise<Response> { + return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure); + } + + post(url: string, payload?: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) { + return this.httpRequestWithJSONBody("POST", url, contentType, additionalHeaders, payload); + } + + postText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) { + return this.httpRequestWithTextBody("POST", url, additionalHeaders, payload); + } + + putText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) { + return this.httpRequestWithTextBody("PUT", url, additionalHeaders, payload); + } + + postBinary(url: string, fileAppender: (p: FormData) => void, additionalHeaders: Record<string, string> = {}) { + const formData = new FormData(); + fileAppender(formData); + + const options: RequestInit = { + method: "POST", + body: formData, + headers: additionalHeaders + }; + return this.httpRequestWithBinaryBody(options, url); + } + + put(url: string, payload: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) { + return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload); + } + + head(url: string) { + let options: RequestInit = { + method: "HEAD" + }; + options = applyFetchOptions(options); + return fetch(createUrl(url), options).then(handleFailure); + } + + delete(url: string): Promise<Response> { + let options: RequestInit = { + method: "DELETE" + }; + options = applyFetchOptions(options); + return fetch(createUrl(url), options).then(handleFailure); + } + + httpRequestWithJSONBody( + method: string, + url: string, + contentType: string, + additionalHeaders: Record<string, string>, + payload?: any + ): Promise<Response> { + const options: RequestInit = { + method: method, + headers: additionalHeaders + }; + if (payload) { + options.body = JSON.stringify(payload); + } + return this.httpRequestWithBinaryBody(options, url, contentType); + } + + httpRequestWithTextBody( + method: string, + url: string, + additionalHeaders: Record<string, string> = {}, + payload: string + ) { + const options: RequestInit = { + method: method, + headers: additionalHeaders + }; + options.body = payload; + return this.httpRequestWithBinaryBody(options, url, "text/plain"); + } + + httpRequestWithBinaryBody(options: RequestInit, url: string, contentType?: string) { + options = applyFetchOptions(options); + if (contentType) { + if (!options.headers) { + options.headers = {}; + } + // @ts-ignore We are sure that here we only get headers of type Record<string, string> + options.headers["Content-Type"] = contentType; + } + + return fetch(createUrl(url), options).then(handleFailure); + } +} + +export const apiClient = new ApiClient(); diff --git a/scm-ui/ui-components/src/avatar/Avatar.ts b/scm-ui/ui-components/src/avatar/Avatar.ts new file mode 100644 index 0000000000..f3021b50d5 --- /dev/null +++ b/scm-ui/ui-components/src/avatar/Avatar.ts @@ -0,0 +1,6 @@ +export type Person = { + name: string; + mail?: string; +}; + +export const EXTENSION_POINT = "avatar.factory"; diff --git a/scm-ui/ui-components/src/avatar/AvatarImage.tsx b/scm-ui/ui-components/src/avatar/AvatarImage.tsx new file mode 100644 index 0000000000..48808b582f --- /dev/null +++ b/scm-ui/ui-components/src/avatar/AvatarImage.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { binder } from "@scm-manager/ui-extensions"; +import { Image } from ".."; +import { Person } from "./Avatar"; +import { EXTENSION_POINT } from "./Avatar"; + +type Props = { + person: Person; +}; + +class AvatarImage extends React.Component<Props> { + render() { + const { person } = this.props; + + const avatarFactory = binder.getExtension(EXTENSION_POINT); + if (avatarFactory) { + const avatar = avatarFactory(person); + + return <Image className="has-rounded-border" src={avatar} alt={person.name} />; + } + + return null; + } +} + +export default AvatarImage; diff --git a/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx b/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx new file mode 100644 index 0000000000..09ed7391cf --- /dev/null +++ b/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx @@ -0,0 +1,18 @@ +import React, { Component, ReactNode } from "react"; +import { binder } from "@scm-manager/ui-extensions"; +import { EXTENSION_POINT } from "./Avatar"; + +type Props = { + children: ReactNode; +}; + +class AvatarWrapper extends Component<Props> { + render() { + if (binder.hasExtension(EXTENSION_POINT)) { + return <>{this.props.children}</>; + } + return null; + } +} + +export default AvatarWrapper; diff --git a/scm-ui/ui-components/src/avatar/index.ts b/scm-ui/ui-components/src/avatar/index.ts new file mode 100644 index 0000000000..5c2ef79c95 --- /dev/null +++ b/scm-ui/ui-components/src/avatar/index.ts @@ -0,0 +1,2 @@ +export { default as AvatarWrapper } from "./AvatarWrapper"; +export { default as AvatarImage } from "./AvatarImage"; diff --git a/scm-ui/ui-components/src/buttons/AddButton.tsx b/scm-ui/ui-components/src/buttons/AddButton.tsx new file mode 100644 index 0000000000..ddb4115b67 --- /dev/null +++ b/scm-ui/ui-components/src/buttons/AddButton.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import Button, { ButtonProps } from "./Button"; + +class AddButton extends React.Component<ButtonProps> { + render() { + return <Button color="default" icon="plus" {...this.props} />; + } +} + +export default AddButton; diff --git a/scm-ui/ui-components/src/buttons/Button.tsx b/scm-ui/ui-components/src/buttons/Button.tsx new file mode 100644 index 0000000000..3507ba7a06 --- /dev/null +++ b/scm-ui/ui-components/src/buttons/Button.tsx @@ -0,0 +1,91 @@ +import React, { MouseEvent, ReactNode } from "react"; +import classNames from "classnames"; +import { withRouter, RouteComponentProps } from "react-router-dom"; +import Icon from "../Icon"; + +export type ButtonProps = { + label?: string; + loading?: boolean; + disabled?: boolean; + action?: (event: MouseEvent) => void; + link?: string; + className?: string; + icon?: string; + fullWidth?: boolean; + reducedMobile?: boolean; + children?: ReactNode; +}; + +type Props = ButtonProps & + RouteComponentProps & { + title?: string; + type?: "button" | "submit" | "reset"; + color?: string; + }; + +class Button extends React.Component<Props> { + static defaultProps: Partial<Props> = { + type: "button", + color: "default" + }; + + onClick = (event: React.MouseEvent) => { + const { action, link, history } = this.props; + if (action) { + action(event); + } else if (link) { + history.push(link); + } + }; + + render() { + const { + label, + title, + loading, + disabled, + type, + color, + className, + icon, + fullWidth, + reducedMobile, + children + } = this.props; + const loadingClass = loading ? "is-loading" : ""; + const fullWidthClass = fullWidth ? "is-fullwidth" : ""; + const reducedMobileClass = reducedMobile ? "is-reduced-mobile" : ""; + if (icon) { + return ( + <button + type={type} + title={title} + disabled={disabled} + onClick={this.onClick} + className={classNames("button", "is-" + color, loadingClass, fullWidthClass, reducedMobileClass, className)} + > + <span className="icon is-medium"> + <Icon name={icon} color="inherit" /> + </span> + <span> + {label} {children} + </span> + </button> + ); + } + + return ( + <button + type={type} + title={title} + disabled={disabled} + onClick={this.onClick} + className={classNames("button", "is-" + color, loadingClass, fullWidthClass, className)} + > + {label} {children} + </button> + ); + } +} + +export default withRouter(Button); diff --git a/scm-ui/ui-components/src/buttons/ButtonAddons.tsx b/scm-ui/ui-components/src/buttons/ButtonAddons.tsx new file mode 100644 index 0000000000..d5047e23b6 --- /dev/null +++ b/scm-ui/ui-components/src/buttons/ButtonAddons.tsx @@ -0,0 +1,35 @@ +import React, { ReactNode } from "react"; +import classNames from "classnames"; +import styled from "styled-components"; + +const Flex = styled.div` + &.field:not(:last-child) { + margin-bottom: 0; + } +`; + +type Props = { + className?: string; + children: ReactNode; +}; + +class ButtonAddons extends React.Component<Props> { + render() { + const { className, children } = this.props; + + const childWrapper: ReactNode[] = []; + React.Children.forEach(children, child => { + if (child) { + childWrapper.push( + <p className="control" key={childWrapper.length}> + {child} + </p> + ); + } + }); + + return <Flex className={classNames("field", "has-addons", className)}>{childWrapper}</Flex>; + } +} + +export default ButtonAddons; diff --git a/scm-ui/ui-components/src/buttons/ButtonGroup.tsx b/scm-ui/ui-components/src/buttons/ButtonGroup.tsx new file mode 100644 index 0000000000..bcd5ef9e02 --- /dev/null +++ b/scm-ui/ui-components/src/buttons/ButtonGroup.tsx @@ -0,0 +1,28 @@ +import React, { ReactNode } from "react"; +import classNames from "classnames"; + +type Props = { + className?: string; + children: ReactNode; +}; + +class ButtonGroup extends React.Component<Props> { + render() { + const { className, children } = this.props; + + const childWrapper: ReactNode[] = []; + React.Children.forEach(children, child => { + if (child) { + childWrapper.push( + <div className="control" key={childWrapper.length}> + {child} + </div> + ); + } + }); + + return <div className={classNames("field", "is-grouped", className)}>{childWrapper}</div>; + } +} + +export default ButtonGroup; diff --git a/scm-ui/ui-components/src/buttons/CreateButton.tsx b/scm-ui/ui-components/src/buttons/CreateButton.tsx new file mode 100644 index 0000000000..ba0093863b --- /dev/null +++ b/scm-ui/ui-components/src/buttons/CreateButton.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import styled from "styled-components"; +import Button, { ButtonProps } from "./Button"; + +const Wrapper = styled.div` + margin-top: 2em; + padding: 1em 1em; + border: 2px solid #e9f7fd; +`; + +export default class CreateButton extends React.Component<ButtonProps> { + render() { + return ( + <Wrapper className="has-text-centered"> + <Button color="primary" {...this.props} /> + </Wrapper> + ); + } +} diff --git a/scm-ui/ui-components/src/buttons/DeleteButton.tsx b/scm-ui/ui-components/src/buttons/DeleteButton.tsx new file mode 100644 index 0000000000..deed624020 --- /dev/null +++ b/scm-ui/ui-components/src/buttons/DeleteButton.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import Button, { ButtonProps } from "./Button"; + +class DeleteButton extends React.Component<ButtonProps> { + render() { + return <Button color="warning" icon="times" {...this.props} />; + } +} + +export default DeleteButton; diff --git a/scm-ui/ui-components/src/buttons/DownloadButton.tsx b/scm-ui/ui-components/src/buttons/DownloadButton.tsx new file mode 100644 index 0000000000..0e63d34073 --- /dev/null +++ b/scm-ui/ui-components/src/buttons/DownloadButton.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +type Props = { + displayName: string; + url: string; + disabled: boolean; + onClick?: () => void; +}; + +class DownloadButton extends React.Component<Props> { + render() { + const { displayName, url, disabled, onClick } = this.props; + const onClickOrDefault = !!onClick ? onClick : () => {}; + return ( + <> + {/* + we have to ignore the next line, + because jsx a does not the custom disabled attribute + but bulma does. + // @ts-ignore */} + <a className="button is-link" href={url} disabled={disabled} onClick={onClickOrDefault}> + <span className="icon is-medium"> + <i className="fas fa-arrow-circle-down" /> + </span> + <span>{displayName}</span> + </a> + </> + ); + } +} + +export default DownloadButton; diff --git a/scm-ui/ui-components/src/buttons/EditButton.tsx b/scm-ui/ui-components/src/buttons/EditButton.tsx new file mode 100644 index 0000000000..9a89799f95 --- /dev/null +++ b/scm-ui/ui-components/src/buttons/EditButton.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import Button, { ButtonProps } from "./Button"; + +class EditButton extends React.Component<ButtonProps> { + render() { + return <Button color="default" {...this.props} />; + } +} + +export default EditButton; diff --git a/scm-ui/ui-components/src/buttons/RemoveEntryOfTableButton.tsx b/scm-ui/ui-components/src/buttons/RemoveEntryOfTableButton.tsx new file mode 100644 index 0000000000..a9c844f338 --- /dev/null +++ b/scm-ui/ui-components/src/buttons/RemoveEntryOfTableButton.tsx @@ -0,0 +1,32 @@ +import React, { MouseEvent } from "react"; +import { DeleteButton } from "."; +import classNames from "classnames"; + +type Props = { + entryname: string; + removeEntry: (p: string) => void; + disabled: boolean; + label: string; +}; + +type State = {}; + +class RemoveEntryOfTableButton extends React.Component<Props, State> { + render() { + const { label, entryname, removeEntry, disabled } = this.props; + return ( + <div className={classNames("is-pulled-right")}> + <DeleteButton + label={label} + action={(event: MouseEvent) => { + event.preventDefault(); + removeEntry(entryname); + }} + disabled={disabled} + /> + </div> + ); + } +} + +export default RemoveEntryOfTableButton; diff --git a/scm-ui/ui-components/src/buttons/SubmitButton.tsx b/scm-ui/ui-components/src/buttons/SubmitButton.tsx new file mode 100644 index 0000000000..78ddc0c579 --- /dev/null +++ b/scm-ui/ui-components/src/buttons/SubmitButton.tsx @@ -0,0 +1,33 @@ +import React, { MouseEvent } from "react"; +import Button, { ButtonProps } from "./Button"; + +type SubmitButtonProps = ButtonProps & { + scrollToTop: boolean; +}; + +class SubmitButton extends React.Component<SubmitButtonProps> { + static defaultProps = { + scrollToTop: true + }; + + render() { + const { action, scrollToTop } = this.props; + return ( + <Button + type="submit" + color="primary" + {...this.props} + action={(event: MouseEvent) => { + if (action) { + action(event); + } + if (scrollToTop) { + window.scrollTo(0, 0); + } + }} + /> + ); + } +} + +export default SubmitButton; diff --git a/scm-ui/ui-components/src/buttons/index.stories.tsx b/scm-ui/ui-components/src/buttons/index.stories.tsx new file mode 100644 index 0000000000..79d5be448c --- /dev/null +++ b/scm-ui/ui-components/src/buttons/index.stories.tsx @@ -0,0 +1,59 @@ +import React, { ReactNode } from "react"; +import Button from "./Button"; +import { storiesOf } from "@storybook/react"; +import styled from "styled-components"; +import { MemoryRouter } from "react-router-dom"; +import AddButton from "./AddButton"; +import CreateButton from "./CreateButton"; +import DeleteButton from "./DeleteButton"; +import DownloadButton from "./DownloadButton"; +import EditButton from "./EditButton"; +import SubmitButton from "./SubmitButton"; +import { ReactElement } from "react"; + +const colors = ["primary", "link", "info", "success", "warning", "danger", "white", "light", "dark", "black", "text"]; + +const Spacing = styled.div` + padding: 1em; +`; + +type StoryFn = () => ReactNode; + +const RoutingDecorator = (story: StoryFn) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>; + +const SpacingDecorator = (story: StoryFn) => <Spacing>{story()}</Spacing>; + +storiesOf("Buttons|Button", module) + .addDecorator(RoutingDecorator) + .add("Colors", () => ( + <div> + {colors.map(color => ( + <Spacing key={color}> + <Button color={color} label={color} /> + </Spacing> + ))} + </div> + )) + .add("Loading", () => ( + <Spacing> + <Button color={"primary"} loading={true}> + Loading Button + </Button> + </Spacing> + )); + +const buttonStory = (name: string, storyFn: () => ReactElement) => { + return storiesOf("Buttons|" + name, module) + .addDecorator(RoutingDecorator) + .addDecorator(SpacingDecorator) + .add("Default", storyFn); +}; +buttonStory("AddButton", () => <AddButton>Add</AddButton>); +buttonStory("CreateButton", () => <CreateButton>Create</CreateButton>); +buttonStory("DeleteButton", () => <DeleteButton>Delete</DeleteButton>); +buttonStory("DownloadButton", () => <DownloadButton displayName="Download" disabled={false} url="" />).add( + "Disabled", + () => <DownloadButton displayName="Download" disabled={true} url=""></DownloadButton> +); +buttonStory("EditButton", () => <EditButton>Edit</EditButton>); +buttonStory("SubmitButton", () => <SubmitButton>Submit</SubmitButton>); diff --git a/scm-ui/ui-components/src/buttons/index.ts b/scm-ui/ui-components/src/buttons/index.ts new file mode 100644 index 0000000000..c5d536133f --- /dev/null +++ b/scm-ui/ui-components/src/buttons/index.ts @@ -0,0 +1,12 @@ +// @create-index + +export { default as AddButton } from "./AddButton"; +export { default as Button } from "./Button"; +export { default as CreateButton } from "./CreateButton"; +export { default as DeleteButton } from "./DeleteButton"; +export { default as EditButton } from "./EditButton"; +export { default as SubmitButton } from "./SubmitButton"; +export { default as DownloadButton } from "./DownloadButton"; +export { default as ButtonGroup } from "./ButtonGroup"; +export { default as ButtonAddons } from "./ButtonAddons"; +export { default as RemoveEntryOfTableButton } from "./RemoveEntryOfTableButton"; diff --git a/scm-ui/ui-components/src/config/Configuration.tsx b/scm-ui/ui-components/src/config/Configuration.tsx new file mode 100644 index 0000000000..967feabb2b --- /dev/null +++ b/scm-ui/ui-components/src/config/Configuration.tsx @@ -0,0 +1,190 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Links, Link } from "@scm-manager/ui-types"; +import { apiClient, SubmitButton, Loading, ErrorNotification } from "../"; +import { FormEvent } from "react"; + +type RenderProps = { + readOnly: boolean; + initialConfiguration: ConfigurationType; + onConfigurationChange: (p1: ConfigurationType, p2: boolean) => void; +}; + +type Props = WithTranslation & { + link: string; + render: (props: RenderProps) => any; // ??? +}; + +type ConfigurationType = { + _links: Links; +} & object; + +type State = { + error?: Error; + fetching: boolean; + modifying: boolean; + contentType?: string | null; + configChanged: boolean; + + configuration?: ConfigurationType; + modifiedConfiguration?: ConfigurationType; + valid: boolean; +}; + +/** + * GlobalConfiguration uses the render prop pattern to encapsulate the logic for + * synchronizing the configuration with the backend. + */ +class Configuration extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + fetching: true, + modifying: false, + configChanged: false, + valid: false + }; + } + + componentDidMount() { + const { link } = this.props; + + apiClient + .get(link) + .then(this.captureContentType) + .then(response => response.json()) + .then(this.loadConfig) + .catch(this.handleError); + } + + captureContentType = (response: Response) => { + const contentType = response.headers.get("Content-Type"); + this.setState({ + contentType + }); + return response; + }; + + getContentType = (): string => { + const { contentType } = this.state; + return contentType ? contentType : "application/json"; + }; + + handleError = (error: Error) => { + this.setState({ + error, + fetching: false, + modifying: false + }); + }; + + loadConfig = (configuration: ConfigurationType) => { + this.setState({ + configuration, + fetching: false, + error: undefined + }); + }; + + getModificationUrl = (): string | undefined => { + const { configuration } = this.state; + if (configuration) { + const links = configuration._links; + if (links && links.update) { + const link = links.update as Link; + return link.href; + } + } + }; + + isReadOnly = (): boolean => { + const modificationUrl = this.getModificationUrl(); + return !modificationUrl; + }; + + configurationChanged = (configuration: ConfigurationType, valid: boolean) => { + this.setState({ + modifiedConfiguration: configuration, + valid + }); + }; + + modifyConfiguration = (event: FormEvent) => { + event.preventDefault(); + + this.setState({ + modifying: true + }); + + const { modifiedConfiguration } = this.state; + + const modificationUrl = this.getModificationUrl(); + if (modificationUrl) { + apiClient + .put(modificationUrl, modifiedConfiguration, this.getContentType()) + .then(() => + this.setState({ + modifying: false, + configChanged: true, + valid: false + }) + ) + .catch(this.handleError); + } else { + this.setState({ + error: new Error("no modification link available") + }); + } + }; + + renderConfigChangedNotification = () => { + if (this.state.configChanged) { + return ( + <div className="notification is-primary"> + <button + className="delete" + onClick={() => + this.setState({ + configChanged: false + }) + } + /> + {this.props.t("config.form.submit-success-notification")} + </div> + ); + } + return null; + }; + + render() { + const { t } = this.props; + const { fetching, error, configuration, modifying, valid } = this.state; + + if (error) { + return <ErrorNotification error={error} />; + } else if (fetching || !configuration) { + return <Loading />; + } else { + const readOnly = this.isReadOnly(); + + const renderProps: RenderProps = { + readOnly, + initialConfiguration: configuration, + onConfigurationChange: this.configurationChanged + }; + + return ( + <> + {this.renderConfigChangedNotification()} + <form onSubmit={this.modifyConfiguration}> + {this.props.render(renderProps)} + <hr /> + <SubmitButton label={t("config.form.submit")} disabled={!valid || readOnly} loading={modifying} /> + </form> + </> + ); + } + } +} + +export default withTranslation("config")(Configuration); diff --git a/scm-ui/ui-components/src/config/ConfigurationBinder.tsx b/scm-ui/ui-components/src/config/ConfigurationBinder.tsx new file mode 100644 index 0000000000..f2e3669a5e --- /dev/null +++ b/scm-ui/ui-components/src/config/ConfigurationBinder.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import { binder } from "@scm-manager/ui-extensions"; +import { NavLink } from "../navigation"; +import { Route } from "react-router-dom"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Repository, Links, Link } from "@scm-manager/ui-types"; + +type GlobalRouteProps = { + url: string; + links: Links; +}; + +type RepositoryRouteProps = { + url: string; + repository: Repository; +}; + +type RepositoryNavProps = WithTranslation & { url: string }; + +class ConfigurationBinder { + i18nNamespace = "plugins"; + + navLink(to: string, labelI18nKey: string, t: any) { + return <NavLink to={to} label={t(labelI18nKey)} />; + } + + route(path: string, Component: any) { + return <Route path={path} render={() => Component} exact />; + } + + bindGlobal(to: string, labelI18nKey: string, linkName: string, ConfigurationComponent: any) { + // create predicate based on the link name of the index resource + // if the linkname is not available, the navigation link and the route are not bound to the extension points + const configPredicate = (props: any) => { + return props.links && props.links[linkName]; + }; + + // create NavigationLink with translated label + const ConfigNavLink = withTranslation(this.i18nNamespace)(({ t }) => { + return this.navLink("/admin/settings" + to, labelI18nKey, t); + }); + + // bind navigation link to extension point + binder.bind("admin.setting", ConfigNavLink, configPredicate); + + // route for global configuration, passes the link from the index resource to component + const ConfigRoute = ({ url, links, ...additionalProps }: GlobalRouteProps) => { + const link = links[linkName]; + if (link) { + return this.route( + url + "/settings" + to, + <ConfigurationComponent link={(link as Link).href} {...additionalProps} /> + ); + } + }; + + // bind config route to extension point + binder.bind("admin.route", ConfigRoute, configPredicate); + } + + bindRepository(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) { + // create predicate based on the link name of the current repository route + // if the linkname is not available, the navigation link and the route are not bound to the extension points + const repoPredicate = (props: any) => { + return props.repository && props.repository._links && props.repository._links[linkName]; + }; + + // create NavigationLink with translated label + const RepoNavLink = withTranslation(this.i18nNamespace)(({ t, url }: RepositoryNavProps) => { + return this.navLink(url + to, labelI18nKey, t); + }); + + // bind navigation link to extension point + binder.bind("repository.navigation", RepoNavLink, repoPredicate); + + // route for global configuration, passes the current repository to component + const RepoRoute = ({ url, repository, ...additionalProps }: RepositoryRouteProps) => { + const link = repository._links[linkName]; + if (link) { + return this.route( + url + to, + <RepositoryComponent repository={repository} link={(link as Link).href} {...additionalProps} /> + ); + } + }; + + // bind config route to extension point + binder.bind("repository.route", RepoRoute, repoPredicate); + } + + bindRepositorySetting(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) { + // create predicate based on the link name of the current repository route + // if the linkname is not available, the navigation link and the route are not bound to the extension points + const repoPredicate = (props: any) => { + return props.repository && props.repository._links && props.repository._links[linkName]; + }; + + // create NavigationLink with translated label + const RepoNavLink = withTranslation(this.i18nNamespace)(({ t, url }: RepositoryNavProps) => { + return this.navLink(url + "/settings" + to, labelI18nKey, t); + }); + + // bind navigation link to extension point + binder.bind("repository.setting", RepoNavLink, repoPredicate); + + // route for global configuration, passes the current repository to component + const RepoRoute = ({ url, repository, ...additionalProps }: RepositoryRouteProps) => { + const link = repository._links[linkName]; + if (link) { + return this.route( + url + "/settings" + to, + <RepositoryComponent repository={repository} link={(link as Link).href} {...additionalProps} /> + ); + } + }; + + // bind config route to extension point + binder.bind("repository.route", RepoRoute, repoPredicate); + } +} + +export default new ConfigurationBinder(); diff --git a/scm-ui/ui-components/src/config/index.ts b/scm-ui/ui-components/src/config/index.ts new file mode 100644 index 0000000000..8a00fcb87c --- /dev/null +++ b/scm-ui/ui-components/src/config/index.ts @@ -0,0 +1,2 @@ +export { default as ConfigurationBinder } from "./ConfigurationBinder"; +export { default as Configuration } from "./Configuration"; diff --git a/scm-ui/ui-components/src/errors.test.ts b/scm-ui/ui-components/src/errors.test.ts new file mode 100644 index 0000000000..cb06a0bc52 --- /dev/null +++ b/scm-ui/ui-components/src/errors.test.ts @@ -0,0 +1,35 @@ +import { BackendError, UnauthorizedError, createBackendError, NotFoundError } from "./errors"; + +describe("test createBackendError", () => { + const earthNotFoundError = { + transactionId: "42t", + errorCode: "42e", + message: "earth not found", + context: [ + { + type: "planet", + id: "earth" + } + ], + violations: [] + }; + + it("should return a default backend error", () => { + const err = createBackendError(earthNotFoundError, 500); + expect(err).toBeInstanceOf(BackendError); + expect(err.name).toBe("BackendError"); + }); + + // 403 is no backend error + xit("should return an unauthorized error for status code 403", () => { + const err = createBackendError(earthNotFoundError, 403); + expect(err).toBeInstanceOf(UnauthorizedError); + expect(err.name).toBe("UnauthorizedError"); + }); + + it("should return an not found error for status code 404", () => { + const err = createBackendError(earthNotFoundError, 404); + expect(err).toBeInstanceOf(NotFoundError); + expect(err.name).toBe("NotFoundError"); + }); +}); diff --git a/scm-ui/ui-components/src/errors.ts b/scm-ui/ui-components/src/errors.ts new file mode 100644 index 0000000000..5171fe07b4 --- /dev/null +++ b/scm-ui/ui-components/src/errors.ts @@ -0,0 +1,80 @@ +type Context = { + type: string; + id: string; +}[]; +type Violation = { + path: string; + message: string; +}; + +export type BackendErrorContent = { + transactionId: string; + errorCode: string; + message: string; + url?: string; + context: Context; + violations: Violation[]; +}; + +export class BackendError extends Error { + transactionId: string; + errorCode: string; + url: string | null | undefined; + context: Context = []; + statusCode: number; + violations: Violation[]; + + constructor(content: BackendErrorContent, name: string, statusCode: number) { + super(content.message); + this.name = name; + this.transactionId = content.transactionId; + this.errorCode = content.errorCode; + this.url = content.url; + this.context = content.context; + this.statusCode = statusCode; + this.violations = content.violations; + } +} + +export class UnauthorizedError extends Error { + statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} + +export class ForbiddenError extends Error { + statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} + +export class NotFoundError extends BackendError { + constructor(content: BackendErrorContent, statusCode: number) { + super(content, "NotFoundError", statusCode); + } +} + +export class ConflictError extends BackendError { + constructor(content: BackendErrorContent, statusCode: number) { + super(content, "ConflictError", statusCode); + } +} + +export function createBackendError(content: BackendErrorContent, statusCode: number) { + switch (statusCode) { + case 404: + return new NotFoundError(content, statusCode); + case 409: + return new ConflictError(content, statusCode); + default: + return new BackendError(content, "BackendError", statusCode); + } +} + +export function isBackendError(response: Response) { + return response.headers.get("Content-Type") === "application/vnd.scmm-error+json;v=2"; +} diff --git a/scm-ui/ui-components/src/forms/AddEntryToTableField.tsx b/scm-ui/ui-components/src/forms/AddEntryToTableField.tsx new file mode 100644 index 0000000000..d1e1b77cb6 --- /dev/null +++ b/scm-ui/ui-components/src/forms/AddEntryToTableField.tsx @@ -0,0 +1,82 @@ +import React, { MouseEvent } from "react"; + +import { AddButton } from "../buttons"; +import InputField from "./InputField"; + +type Props = { + addEntry: (p: string) => void; + disabled: boolean; + buttonLabel: string; + fieldLabel: string; + errorMessage: string; + helpText?: string; + validateEntry?: (p: string) => boolean; +}; + +type State = { + entryToAdd: string; +}; + +class AddEntryToTableField extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + entryToAdd: "" + }; + } + + isValid = () => { + const { validateEntry } = this.props; + if (!this.state.entryToAdd || this.state.entryToAdd === "" || !validateEntry) { + return true; + } else { + return validateEntry(this.state.entryToAdd); + } + }; + + render() { + const { disabled, buttonLabel, fieldLabel, errorMessage, helpText } = this.props; + return ( + <> + <InputField + label={fieldLabel} + errorMessage={errorMessage} + onChange={this.handleAddEntryChange} + validationError={!this.isValid()} + value={this.state.entryToAdd} + onReturnPressed={this.appendEntry} + disabled={disabled} + helpText={helpText} + /> + <AddButton + label={buttonLabel} + action={this.addButtonClicked} + disabled={disabled || this.state.entryToAdd === "" || !this.isValid()} + /> + </> + ); + } + + addButtonClicked = (event: MouseEvent) => { + event.preventDefault(); + this.appendEntry(); + }; + + appendEntry = () => { + const { entryToAdd } = this.state; + this.props.addEntry(entryToAdd); + this.setState({ + ...this.state, + entryToAdd: "" + }); + }; + + handleAddEntryChange = (entryname: string) => { + this.setState({ + ...this.state, + entryToAdd: entryname + }); + }; +} + +export default AddEntryToTableField; diff --git a/scm-ui/ui-components/src/forms/AutocompleteAddEntryToTableField.tsx b/scm-ui/ui-components/src/forms/AutocompleteAddEntryToTableField.tsx new file mode 100644 index 0000000000..e09b0d1ef2 --- /dev/null +++ b/scm-ui/ui-components/src/forms/AutocompleteAddEntryToTableField.tsx @@ -0,0 +1,90 @@ +import React, { MouseEvent } from "react"; + +import { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; +import Autocomplete from "../Autocomplete"; +import AddButton from "../buttons/AddButton"; + +type Props = { + addEntry: (p: SelectValue) => void; + disabled: boolean; + buttonLabel: string; + fieldLabel: string; + helpText?: string; + loadSuggestions: (p: string) => Promise<SelectValue[]>; + placeholder?: string; + loadingMessage?: string; + noOptionsMessage?: string; +}; + +type State = { + selectedValue?: SelectValue; +}; + +class AutocompleteAddEntryToTableField extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + selectedValue: undefined + }; + } + render() { + const { + disabled, + buttonLabel, + fieldLabel, + helpText, + loadSuggestions, + placeholder, + loadingMessage, + noOptionsMessage + } = this.props; + + const { selectedValue } = this.state; + return ( + <div className="field"> + <Autocomplete + label={fieldLabel} + loadSuggestions={loadSuggestions} + valueSelected={this.handleAddEntryChange} + helpText={helpText} + value={selectedValue} + placeholder={placeholder} + loadingMessage={loadingMessage} + noOptionsMessage={noOptionsMessage} + creatable={true} + /> + + <AddButton label={buttonLabel} action={this.addButtonClicked} disabled={disabled} /> + </div> + ); + } + + addButtonClicked = (event: MouseEvent) => { + event.preventDefault(); + this.appendEntry(); + }; + + appendEntry = () => { + const { selectedValue } = this.state; + if (!selectedValue) { + return; + } + this.setState( + // @ts-ignore null is needed to clear the selection; undefined does not work + { + ...this.state, + selectedValue: null + }, + () => this.props.addEntry(selectedValue) + ); + }; + + handleAddEntryChange = (selection: SelectValue) => { + this.setState({ + ...this.state, + selectedValue: selection + }); + }; +} + +export default AutocompleteAddEntryToTableField; diff --git a/scm-ui/ui-components/src/forms/Checkbox.stories.tsx b/scm-ui/ui-components/src/forms/Checkbox.stories.tsx new file mode 100644 index 0000000000..a3ad57295d --- /dev/null +++ b/scm-ui/ui-components/src/forms/Checkbox.stories.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { storiesOf } from "@storybook/react"; +import Checkbox from "./Checkbox"; +import styled from "styled-components"; + +const Spacing = styled.div` + padding: 2em; +`; + +storiesOf("Forms|Checkbox", module) + .add("Default", () => ( + <Spacing> + <Checkbox label="Not checked" checked={false} /> + <Checkbox label="Checked" checked={true} /> + </Spacing> + )) + .add("Disabled", () => ( + <Spacing> + <Checkbox label="Checked but disabled" checked={true} disabled={true} /> + </Spacing> + )); + diff --git a/scm-ui/ui-components/src/forms/Checkbox.tsx b/scm-ui/ui-components/src/forms/Checkbox.tsx new file mode 100644 index 0000000000..987e4e8209 --- /dev/null +++ b/scm-ui/ui-components/src/forms/Checkbox.tsx @@ -0,0 +1,61 @@ +import React, { ChangeEvent } from "react"; +import { Help } from "../index"; +import LabelWithHelpIcon from "./LabelWithHelpIcon"; + +type Props = { + label?: string; + onChange?: (value: boolean, name?: string) => void; + checked: boolean; + name?: string; + title?: string; + disabled?: boolean; + helpText?: string; +}; + +export default class Checkbox extends React.Component<Props> { + onCheckboxChange = (event: ChangeEvent<HTMLInputElement>) => { + if (this.props.onChange) { + this.props.onChange(event.target.checked, this.props.name); + } + }; + + renderHelp = () => { + const { title, helpText } = this.props; + if (helpText && !title) { + return <Help message={helpText} />; + } + }; + + renderLabelWithHelp = () => { + const { title, helpText } = this.props; + if (title) { + return <LabelWithHelpIcon label={title} helpText={helpText} />; + } + } + + render() { + const { label, checked, disabled } = this.props; + return ( + <div className="field"> + {this.renderLabelWithHelp()} + <div className="control"> + {/* + we have to ignore the next line, + because jsx label does not the custom disabled attribute + but bulma does. + // @ts-ignore */} + <label className="checkbox" disabled={disabled}> + <input + type="checkbox" + checked={checked} + onChange={this.onCheckboxChange} + disabled={disabled} + />{" "} + {label} + {this.renderHelp()} + </label> + </div> + </div> + ); + } +} diff --git a/scm-ui/ui-components/src/forms/DropDown.tsx b/scm-ui/ui-components/src/forms/DropDown.tsx new file mode 100644 index 0000000000..b65c48b49d --- /dev/null +++ b/scm-ui/ui-components/src/forms/DropDown.tsx @@ -0,0 +1,37 @@ +import React, { ChangeEvent } from "react"; +import classNames from "classnames"; + +type Props = { + options: string[]; + optionValues?: string[]; + optionSelected: (p: string) => void; + preselectedOption?: string; + className: string; + disabled?: boolean; +}; + +class DropDown extends React.Component<Props> { + render() { + const { options, optionValues, preselectedOption, className, disabled } = this.props; + return ( + <div className={classNames(className, "select", disabled ? "disabled" : "")}> + <select value={preselectedOption ? preselectedOption : ""} onChange={this.change} disabled={disabled}> + <option key="" /> + {options.map((option, index) => { + return ( + <option key={option} value={optionValues && optionValues[index] ? optionValues[index] : option}> + {option} + </option> + ); + })} + </select> + </div> + ); + } + + change = (event: ChangeEvent<HTMLSelectElement>) => { + this.props.optionSelected(event.target.value); + }; +} + +export default DropDown; diff --git a/scm-ui/ui-components/src/forms/FilterInput.tsx b/scm-ui/ui-components/src/forms/FilterInput.tsx new file mode 100644 index 0000000000..f789fe1ee0 --- /dev/null +++ b/scm-ui/ui-components/src/forms/FilterInput.tsx @@ -0,0 +1,58 @@ +import React, { ChangeEvent, FormEvent } from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import styled from "styled-components"; + +type Props = WithTranslation & { + filter: (p: string) => void; + value?: string; +}; + +type State = { + value: string; +}; + +const FixedHeightInput = styled.input` + height: 2.5rem; +`; + +class FilterInput extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + value: this.props.value ? this.props.value : "" + }; + } + + handleChange = (event: ChangeEvent<HTMLInputElement>) => { + this.setState({ + value: event.target.value + }); + }; + + handleSubmit = (event: FormEvent) => { + this.props.filter(this.state.value); + event.preventDefault(); + }; + + render() { + const { t } = this.props; + return ( + <form className="input-field" onSubmit={this.handleSubmit}> + <div className="control has-icons-left"> + <FixedHeightInput + className="input" + type="search" + placeholder={t("filterEntries")} + value={this.state.value} + onChange={this.handleChange} + /> + <span className="icon is-small is-left"> + <i className="fas fa-search" /> + </span> + </div> + </form> + ); + } +} + +export default withTranslation("commons")(FilterInput); diff --git a/scm-ui/ui-components/src/forms/InputField.tsx b/scm-ui/ui-components/src/forms/InputField.tsx new file mode 100644 index 0000000000..a0fd1c631c --- /dev/null +++ b/scm-ui/ui-components/src/forms/InputField.tsx @@ -0,0 +1,76 @@ +import React, { ChangeEvent, KeyboardEvent } from "react"; +import classNames from "classnames"; +import LabelWithHelpIcon from "./LabelWithHelpIcon"; + +type Props = { + label?: string; + name?: string; + placeholder?: string; + value?: string; + type?: string; + autofocus?: boolean; + onChange: (value: string, name?: string) => void; + onReturnPressed?: () => void; + validationError?: boolean; + errorMessage?: string; + disabled?: boolean; + helpText?: string; +}; + +class InputField extends React.Component<Props> { + static defaultProps = { + type: "text", + placeholder: "" + }; + + field: HTMLInputElement | null | undefined; + + componentDidMount() { + if (this.props.autofocus && this.field) { + this.field.focus(); + } + } + + handleInput = (event: ChangeEvent<HTMLInputElement>) => { + this.props.onChange(event.target.value, this.props.name); + }; + + handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => { + const onReturnPressed = this.props.onReturnPressed; + if (!onReturnPressed) { + return; + } + if (event.key === "Enter") { + event.preventDefault(); + onReturnPressed(); + } + }; + + render() { + const { type, placeholder, value, validationError, errorMessage, disabled, label, helpText } = this.props; + const errorView = validationError ? "is-danger" : ""; + const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : ""; + return ( + <div className="field"> + <LabelWithHelpIcon label={label} helpText={helpText} /> + <div className="control"> + <input + ref={input => { + this.field = input; + }} + className={classNames("input", errorView)} + type={type} + placeholder={placeholder} + value={value} + onChange={this.handleInput} + onKeyPress={this.handleKeyPress} + disabled={disabled} + /> + </div> + {helper} + </div> + ); + } +} + +export default InputField; diff --git a/scm-ui/ui-components/src/forms/LabelWithHelpIcon.tsx b/scm-ui/ui-components/src/forms/LabelWithHelpIcon.tsx new file mode 100644 index 0000000000..4cc8fbbea7 --- /dev/null +++ b/scm-ui/ui-components/src/forms/LabelWithHelpIcon.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import Help from "../Help"; + +type Props = { + label?: string; + helpText?: string; +}; + +class LabelWithHelpIcon extends React.Component<Props> { + renderHelp() { + const { helpText } = this.props; + if (helpText) { + return <Help message={helpText} />; + } + } + + render() { + const { label } = this.props; + + if (label) { + const help = this.renderHelp(); + return ( + <label className="label"> + {label} {help} + </label> + ); + } + + return ""; + } +} + +export default LabelWithHelpIcon; diff --git a/scm-ui/ui-components/src/forms/MemberNameTagGroup.tsx b/scm-ui/ui-components/src/forms/MemberNameTagGroup.tsx new file mode 100644 index 0000000000..753129bc01 --- /dev/null +++ b/scm-ui/ui-components/src/forms/MemberNameTagGroup.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { DisplayedUser } from "@scm-manager/ui-types"; +import TagGroup from "./TagGroup"; + +type Props = WithTranslation & { + members: string[]; + memberListChanged: (p: string[]) => void; + label?: string; + helpText?: string; +}; + +class MemberNameTagGroup extends React.Component<Props> { + render() { + const { members, label, helpText, t } = this.props; + const membersExtended = members.map(id => { + return { + id, + displayName: id, + mail: "" + }; + }); + return ( + <TagGroup + items={membersExtended} + label={label ? label : t("group.members")} + helpText={helpText ? helpText : t("groupForm.help.memberHelpText")} + onRemove={this.removeEntry} + /> + ); + } + + removeEntry = (membersExtended: DisplayedUser[]) => { + const members = membersExtended.map(function(item) { + return item["id"]; + }); + this.props.memberListChanged(members); + }; +} + +export default withTranslation("groups")(MemberNameTagGroup); diff --git a/scm-ui/ui-components/src/forms/PasswordConfirmation.tsx b/scm-ui/ui-components/src/forms/PasswordConfirmation.tsx new file mode 100644 index 0000000000..a90ef7faaa --- /dev/null +++ b/scm-ui/ui-components/src/forms/PasswordConfirmation.tsx @@ -0,0 +1,107 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import InputField from "./InputField"; + +type State = { + password: string; + confirmedPassword: string; + passwordValid: boolean; + passwordConfirmationFailed: boolean; +}; +type Props = WithTranslation & { + passwordChanged: (p1: string, p2: boolean) => void; + passwordValidator?: (p: string) => boolean; +}; + +class PasswordConfirmation extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + password: "", + confirmedPassword: "", + passwordValid: true, + passwordConfirmationFailed: false + }; + } + + componentDidMount() { + this.setState({ + password: "", + confirmedPassword: "", + passwordValid: true, + passwordConfirmationFailed: false + }); + } + + render() { + const { t } = this.props; + return ( + <div className="columns is-multiline"> + <div className="column is-half"> + <InputField + label={t("password.newPassword")} + type="password" + onChange={this.handlePasswordChange} + value={this.state.password ? this.state.password : ""} + validationError={!this.state.passwordValid} + errorMessage={t("password.passwordInvalid")} + /> + </div> + <div className="column is-half"> + <InputField + label={t("password.confirmPassword")} + type="password" + onChange={this.handlePasswordValidationChange} + value={this.state ? this.state.confirmedPassword : ""} + validationError={this.state.passwordConfirmationFailed} + errorMessage={t("password.passwordConfirmFailed")} + /> + </div> + </div> + ); + } + + validatePassword = (password: string) => { + const { passwordValidator } = this.props; + if (passwordValidator) { + return passwordValidator(password); + } + + return password.length >= 6 && password.length < 32; + }; + + handlePasswordValidationChange = (confirmedPassword: string) => { + const passwordConfirmed = this.state.password === confirmedPassword; + + this.setState( + { + confirmedPassword, + passwordConfirmationFailed: !passwordConfirmed + }, + this.propagateChange + ); + }; + + handlePasswordChange = (password: string) => { + const passwordConfirmationFailed = password !== this.state.confirmedPassword; + + this.setState( + { + passwordValid: this.validatePassword(password), + passwordConfirmationFailed, + password: password + }, + this.propagateChange + ); + }; + + isValid = () => { + return this.state.passwordValid && !this.state.passwordConfirmationFailed; + }; + + propagateChange = () => { + this.props.passwordChanged(this.state.password, this.isValid()); + }; +} + +export default withTranslation("commons")(PasswordConfirmation); diff --git a/scm-ui/ui-components/src/forms/Radio.stories.tsx b/scm-ui/ui-components/src/forms/Radio.stories.tsx new file mode 100644 index 0000000000..d2a0dd2f0e --- /dev/null +++ b/scm-ui/ui-components/src/forms/Radio.stories.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { storiesOf } from "@storybook/react"; +import Radio from "./Radio"; +import styled from "styled-components"; + +const Spacing = styled.div` + padding: 2em; +`; + +storiesOf("Forms|Radio", module) + .add("Default", () => ( + <Spacing> + <Radio label="Not checked" checked={false} /> + <Radio label="Checked" checked={true} /> + </Spacing> + )) + .add("Disabled", () => ( + <Spacing> + <Radio label="Checked but disabled" checked={true} disabled={true} /> + </Spacing> + )); diff --git a/scm-ui/ui-components/src/forms/Radio.tsx b/scm-ui/ui-components/src/forms/Radio.tsx new file mode 100644 index 0000000000..429fc91077 --- /dev/null +++ b/scm-ui/ui-components/src/forms/Radio.tsx @@ -0,0 +1,53 @@ +import React, { ChangeEvent } from "react"; +import { Help } from "../index"; + +type Props = { + label?: string; + name?: string; + value?: string; + checked: boolean; + onChange?: (value: boolean, name?: string) => void; + disabled?: boolean; + helpText?: string; +}; + +class Radio extends React.Component<Props> { + renderHelp = () => { + const helpText = this.props.helpText; + if (helpText) { + return <Help message={helpText} />; + } + }; + + onValueChange = (event: ChangeEvent<HTMLInputElement>) => { + if (this.props.onChange) { + this.props.onChange(event.target.checked, this.props.name); + } + }; + + render() { + return ( + <> + {/* + we have to ignore the next line, + because jsx label does not the custom disabled attribute + but bulma does. + // @ts-ignore */} + <label className="radio" disabled={this.props.disabled}> + <input + type="radio" + name={this.props.name} + value={this.props.value} + checked={this.props.checked} + onChange={this.onValueChange} + disabled={this.props.disabled} + />{" "} + {this.props.label} + {this.renderHelp()} + </label> + </> + ); + } +} + +export default Radio; diff --git a/scm-ui/ui-components/src/forms/Select.tsx b/scm-ui/ui-components/src/forms/Select.tsx new file mode 100644 index 0000000000..02791609f9 --- /dev/null +++ b/scm-ui/ui-components/src/forms/Select.tsx @@ -0,0 +1,66 @@ +import React, { ChangeEvent } from "react"; +import classNames from "classnames"; +import LabelWithHelpIcon from "./LabelWithHelpIcon"; + +export type SelectItem = { + value: string; + label: string; +}; + +type Props = { + name?: string; + label?: string; + options: SelectItem[]; + value?: string; + onChange: (value: string, name?: string) => void; + loading?: boolean; + helpText?: string; + disabled?: boolean; +}; + +class Select extends React.Component<Props> { + field: HTMLSelectElement | null | undefined; + + componentDidMount() { + // trigger change after render, if value is null to set it to the first value + // of the given options. + if (!this.props.value && this.field && this.field.value) { + this.props.onChange(this.field.value); + } + } + + handleInput = (event: ChangeEvent<HTMLSelectElement>) => { + this.props.onChange(event.target.value, this.props.name); + }; + + render() { + const { options, value, label, helpText, loading, disabled } = this.props; + const loadingClass = loading ? "is-loading" : ""; + + return ( + <div className="field"> + <LabelWithHelpIcon label={label} helpText={helpText} /> + <div className={classNames("control select", loadingClass)}> + <select + ref={input => { + this.field = input; + }} + value={value} + onChange={this.handleInput} + disabled={disabled} + > + {options.map(opt => { + return ( + <option value={opt.value} key={"KEY_" + opt.value}> + {opt.label} + </option> + ); + })} + </select> + </div> + </div> + ); + } +} + +export default Select; diff --git a/scm-ui/ui-components/src/forms/TagGroup.tsx b/scm-ui/ui-components/src/forms/TagGroup.tsx new file mode 100644 index 0000000000..9e21c25251 --- /dev/null +++ b/scm-ui/ui-components/src/forms/TagGroup.tsx @@ -0,0 +1,49 @@ +import * as React from "react"; +import { DisplayedUser } from "@scm-manager/ui-types"; +import { Help, Tag } from "../index"; + +type Props = { + items: DisplayedUser[]; + label: string; + helpText?: string; + onRemove: (p: DisplayedUser[]) => void; +}; + +export default class TagGroup extends React.Component<Props> { + render() { + const { items, label, helpText } = this.props; + let help = null; + if (helpText) { + help = <Help className="is-relative" message={helpText} />; + } + return ( + <div className="field is-grouped is-grouped-multiline"> + {label && items ? ( + <div className="control"> + <strong> + {label} + {help} + {items.length > 0 ? ":" : ""} + </strong> + </div> + ) : ( + "" + )} + {items.map((item, key) => { + return ( + <div className="control" key={key}> + <div className="tags has-addons"> + <Tag color="info is-outlined" label={item.displayName} onRemove={() => this.removeEntry(item)} /> + </div> + </div> + ); + })} + </div> + ); + } + + removeEntry = (item: DisplayedUser) => { + const newItems = this.props.items.filter(name => name !== item); + this.props.onRemove(newItems); + }; +} diff --git a/scm-ui/ui-components/src/forms/Textarea.tsx b/scm-ui/ui-components/src/forms/Textarea.tsx new file mode 100644 index 0000000000..b16c33b708 --- /dev/null +++ b/scm-ui/ui-components/src/forms/Textarea.tsx @@ -0,0 +1,51 @@ +import React, { ChangeEvent } from "react"; +import LabelWithHelpIcon from "./LabelWithHelpIcon"; + +type Props = { + name?: string; + label?: string; + placeholder?: string; + value?: string; + autofocus?: boolean; + onChange: (value: string, name?: string) => void; + helpText?: string; + disabled?: boolean; +}; + +class Textarea extends React.Component<Props> { + field: HTMLTextAreaElement | null | undefined; + + componentDidMount() { + if (this.props.autofocus && this.field) { + this.field.focus(); + } + } + + handleInput = (event: ChangeEvent<HTMLTextAreaElement>) => { + this.props.onChange(event.target.value, this.props.name); + }; + + render() { + const { placeholder, value, label, helpText, disabled } = this.props; + + return ( + <div className="field"> + <LabelWithHelpIcon label={label} helpText={helpText} /> + <div className="control"> + <textarea + className="textarea" + ref={input => { + this.field = input; + }} + placeholder={placeholder} + onChange={this.handleInput} + value={value} + disabled={!!disabled} + /> + </div> + </div> + ); + } +} + +export default Textarea; diff --git a/scm-ui/ui-components/src/forms/index.ts b/scm-ui/ui-components/src/forms/index.ts new file mode 100644 index 0000000000..7050f53f86 --- /dev/null +++ b/scm-ui/ui-components/src/forms/index.ts @@ -0,0 +1,15 @@ +// @create-index + +export { default as AddEntryToTableField } from "./AddEntryToTableField"; +export { default as AutocompleteAddEntryToTableField } from "./AutocompleteAddEntryToTableField"; +export { default as TagGroup } from "./TagGroup"; +export { default as MemberNameTagGroup } from "./MemberNameTagGroup"; +export { default as Checkbox } from "./Checkbox"; +export { default as Radio } from "./Radio"; +export { default as FilterInput } from "./FilterInput"; +export { default as InputField } from "./InputField"; +export { default as Select } from "./Select"; +export { default as Textarea } from "./Textarea"; +export { default as PasswordConfirmation } from "./PasswordConfirmation"; +export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon"; +export { default as DropDown } from "./DropDown"; diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts new file mode 100644 index 0000000000..f5c7f62418 --- /dev/null +++ b/scm-ui/ui-components/src/index.ts @@ -0,0 +1,77 @@ +// @create-index + +import * as validation from "./validation"; +import * as urls from "./urls"; +import * as repositories from "./repositories"; + +// not sure if it is required +import { + File, + FileChangeType, + Hunk, + Change, + BaseContext, + AnnotationFactory, + AnnotationFactoryContext, + DiffEventHandler, + DiffEventContext +} from "./repos"; + +export { validation, urls, repositories }; + +export { default as DateFromNow } from "./DateFromNow"; +export { default as ErrorNotification } from "./ErrorNotification"; +export { default as ErrorPage } from "./ErrorPage"; +export { default as Icon } from "./Icon"; +export { default as Image } from "./Image"; +export { default as Loading } from "./Loading"; +export { default as Logo } from "./Logo"; +export { default as MailLink } from "./MailLink"; +export { default as Notification } from "./Notification"; +export { default as Paginator } from "./Paginator"; +export { default as LinkPaginator } from "./LinkPaginator"; +export { default as StatePaginator } from "./StatePaginator"; + +export { default as FileSize } from "./FileSize"; +export { default as ProtectedRoute } from "./ProtectedRoute"; +export { default as Help } from "./Help"; +export { default as HelpIcon } from "./HelpIcon"; +export { default as Tag } from "./Tag"; +export { default as Tooltip } from "./Tooltip"; +// TODO do we need this? getPageFromMatch is already exported by urls +export { getPageFromMatch } from "./urls"; +export { default as Autocomplete } from "./Autocomplete"; +export { default as GroupAutocomplete } from "./GroupAutocomplete"; +export { default as UserAutocomplete } from "./UserAutocomplete"; +export { default as BranchSelector } from "./BranchSelector"; +export { default as Breadcrumb } from "./Breadcrumb"; +export { default as MarkdownView } from "./MarkdownView"; +export { default as SyntaxHighlighter } from "./SyntaxHighlighter"; +export { default as ErrorBoundary } from "./ErrorBoundary"; +export { default as OverviewPageActions } from "./OverviewPageActions"; +export { default as CardColumnGroup } from "./CardColumnGroup"; +export { default as CardColumn } from "./CardColumn"; + +export { apiClient } from "./apiclient"; +export * from "./errors"; + +export * from "./avatar"; +export * from "./buttons"; +export * from "./config"; +export * from "./forms"; +export * from "./layout"; +export * from "./modals"; +export * from "./navigation"; +export * from "./repos"; + +export { + File, + FileChangeType, + Hunk, + Change, + BaseContext, + AnnotationFactory, + AnnotationFactoryContext, + DiffEventHandler, + DiffEventContext +}; diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx new file mode 100644 index 0000000000..6c56061a71 --- /dev/null +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { Me } from "@scm-manager/ui-types"; +import { Link } from "react-router-dom"; + +type Props = { + me?: Me; +}; + +class Footer extends React.Component<Props> { + render() { + const { me } = this.props; + if (!me) { + return ""; + } + return ( + <footer className="footer"> + <div className="container is-centered"> + <p className="has-text-centered"> + <Link to={"/me"}>{me.displayName}</Link> + </p> + </div> + </footer> + ); + } +} + +export default Footer; diff --git a/scm-ui/ui-components/src/layout/Header.tsx b/scm-ui/ui-components/src/layout/Header.tsx new file mode 100644 index 0000000000..ea3eb01382 --- /dev/null +++ b/scm-ui/ui-components/src/layout/Header.tsx @@ -0,0 +1,30 @@ +import React, { ReactNode } from "react"; +import Logo from "./../Logo"; + +type Props = { + children?: ReactNode; +}; + +class Header extends React.Component<Props> { + render() { + const { children } = this.props; + return ( + <section className="hero is-dark is-small"> + <div className="hero-body"> + <div className="container"> + <div className="columns is-vcentered"> + <div className="column"> + <Logo /> + </div> + </div> + </div> + </div> + <div className="hero-foot"> + <div className="container">{children}</div> + </div> + </section> + ); + } +} + +export default Header; diff --git a/scm-ui/ui-components/src/layout/Level.tsx b/scm-ui/ui-components/src/layout/Level.tsx new file mode 100644 index 0000000000..4d21761abd --- /dev/null +++ b/scm-ui/ui-components/src/layout/Level.tsx @@ -0,0 +1,20 @@ +import React, { ReactNode } from "react"; +import classNames from "classnames"; + +type Props = { + className?: string; + left?: ReactNode; + right?: ReactNode; +}; + +export default class Level extends React.Component<Props> { + render() { + const { className, left, right } = this.props; + return ( + <div className={classNames("level", className)}> + <div className="level-left">{left}</div> + <div className="level-right">{right}</div> + </div> + ); + } +} diff --git a/scm-ui/ui-components/src/layout/Page.tsx b/scm-ui/ui-components/src/layout/Page.tsx new file mode 100644 index 0000000000..472acc0410 --- /dev/null +++ b/scm-ui/ui-components/src/layout/Page.tsx @@ -0,0 +1,107 @@ +import React, { ReactNode } from "react"; +import classNames from "classnames"; +import styled from "styled-components"; +import Loading from "./../Loading"; +import ErrorNotification from "./../ErrorNotification"; +import Title from "./Title"; +import Subtitle from "./Subtitle"; +import PageActions from "./PageActions"; +import ErrorBoundary from "../ErrorBoundary"; + +type Props = { + title?: string; + subtitle?: string; + loading?: boolean; + error?: Error; + showContentOnError?: boolean; + children: ReactNode; +}; + +const PageActionContainer = styled.div` + justify-content: flex-end; + align-items: center; + + // every child except first + > * ~ * { + margin-left: 1.25rem; + } +`; + +export default class Page extends React.Component<Props> { + render() { + const { error } = this.props; + return ( + <section className="section"> + <div className="container"> + {this.renderPageHeader()} + <ErrorBoundary> + <ErrorNotification error={error} /> + {this.renderContent()} + </ErrorBoundary> + </div> + </section> + ); + } + + isPageAction(node: any) { + return ( + node.displayName === PageActions.displayName || (node.type && node.type.displayName === PageActions.displayName) + ); + } + + renderPageHeader() { + const { error, title, subtitle, children } = this.props; + + let pageActions = null; + let pageActionsExists = false; + React.Children.forEach(children, child => { + if (child && !error) { + if (this.isPageAction(child)) { + pageActions = ( + <PageActionContainer + className={classNames("column", "is-three-fifths", "is-mobile-action-spacing", "is-flex")} + > + {child} + </PageActionContainer> + ); + pageActionsExists = true; + } + } + }); + const underline = pageActionsExists ? <hr className="header-with-actions" /> : null; + + return ( + <> + <div className="columns"> + <div className="column"> + <Title title={title} /> + <Subtitle subtitle={subtitle} /> + </div> + {pageActions} + </div> + {underline} + </> + ); + } + + renderContent() { + const { loading, children, showContentOnError, error } = this.props; + + if (error && !showContentOnError) { + return null; + } + if (loading) { + return <Loading />; + } + + const content: ReactNode[] = []; + React.Children.forEach(children, child => { + if (child) { + if (!this.isPageAction(child)) { + content.push(child); + } + } + }); + return content; + } +} diff --git a/scm-ui/ui-components/src/layout/PageActions.tsx b/scm-ui/ui-components/src/layout/PageActions.tsx new file mode 100644 index 0000000000..9e7e037e6b --- /dev/null +++ b/scm-ui/ui-components/src/layout/PageActions.tsx @@ -0,0 +1,27 @@ +import React, { ReactNode } from "react"; +import Loading from "./../Loading"; + +type Props = { + loading?: boolean; + error?: Error; + children: ReactNode; +}; + +export default class PageActions extends React.Component<Props> { + static displayName = "PageActions"; + + render() { + return <>{this.renderContent()}</>; + } + + renderContent() { + const { loading, children, error } = this.props; + if (error) { + return null; + } + if (loading) { + return <Loading />; + } + return children; + } +} diff --git a/scm-ui/ui-components/src/layout/Subtitle.tsx b/scm-ui/ui-components/src/layout/Subtitle.tsx new file mode 100644 index 0000000000..38c75ba4fb --- /dev/null +++ b/scm-ui/ui-components/src/layout/Subtitle.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +type Props = { + subtitle?: string; +}; + +class Subtitle extends React.Component<Props> { + render() { + const { subtitle } = this.props; + if (subtitle) { + return <h2 className="subtitle">{subtitle}</h2>; + } + return null; + } +} + +export default Subtitle; diff --git a/scm-ui/ui-components/src/layout/Title.tsx b/scm-ui/ui-components/src/layout/Title.tsx new file mode 100644 index 0000000000..f512753051 --- /dev/null +++ b/scm-ui/ui-components/src/layout/Title.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +type Props = { + title?: string; +}; + +class Title extends React.Component<Props> { + render() { + const { title } = this.props; + if (title) { + return <h1 className="title">{title}</h1>; + } + return null; + } +} + +export default Title; diff --git a/scm-ui/ui-components/src/layout/index.ts b/scm-ui/ui-components/src/layout/index.ts new file mode 100644 index 0000000000..b78bea4beb --- /dev/null +++ b/scm-ui/ui-components/src/layout/index.ts @@ -0,0 +1,9 @@ +// @create-index + +export { default as Footer } from "./Footer"; +export { default as Header } from "./Header"; +export { default as Level } from "./Level"; +export { default as Page } from "./Page"; +export { default as PageActions } from "./PageActions"; +export { default as Subtitle } from "./Subtitle"; +export { default as Title } from "./Title"; diff --git a/scm-ui/ui-components/src/modals/ConfirmAlert.tsx b/scm-ui/ui-components/src/modals/ConfirmAlert.tsx new file mode 100644 index 0000000000..5a08dac7ec --- /dev/null +++ b/scm-ui/ui-components/src/modals/ConfirmAlert.tsx @@ -0,0 +1,61 @@ +import * as React from "react"; +import ReactDOM from "react-dom"; +import Modal from "./Modal"; +import classNames from "classnames"; + +type Button = { + className?: string; + label: string; + onClick: () => void | null; +}; + +type Props = { + title: string; + message: string; + buttons: Button[]; +}; + +class ConfirmAlert extends React.Component<Props> { + handleClickButton = (button: Button) => { + if (button.onClick) { + button.onClick(); + } + this.close(); + }; + + close = () => { + const container = document.getElementById("modalRoot"); + if (container) { + ReactDOM.unmountComponentAtNode(container); + } + }; + + render() { + const { title, message, buttons } = this.props; + + const body = <>{message}</>; + + const footer = ( + <div className="field is-grouped"> + {buttons.map((button, i) => ( + <p className="control"> + <a className={classNames("button", "is-info", button.className)} key={i} onClick={() => this.handleClickButton(button)}> + {button.label} + </a> + </p> + ))} + </div> + ); + + return <Modal title={title} closeFunction={() => this.close()} body={body} active={true} footer={footer} />; + } +} + +export function confirmAlert(properties: Props) { + const root = document.getElementById("modalRoot"); + if (root) { + ReactDOM.render(<ConfirmAlert {...properties} />, root); + } +} + +export default ConfirmAlert; diff --git a/scm-ui/ui-components/src/modals/Modal.tsx b/scm-ui/ui-components/src/modals/Modal.tsx new file mode 100644 index 0000000000..52a73773ba --- /dev/null +++ b/scm-ui/ui-components/src/modals/Modal.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import classNames from "classnames"; + +type Props = { + title: string; + closeFunction: () => void; + body: any; + footer?: any; + active: boolean; + className?: string; + headColor: string; +}; + +class Modal extends React.Component<Props> { + static defaultProps = { + headColor: "light" + }; + + render() { + const { title, closeFunction, body, footer, active, className, headColor } = this.props; + + const isActive = active ? "is-active" : null; + + let showFooter = null; + if (footer) { + showFooter = <footer className="modal-card-foot">{footer}</footer>; + } + + return ( + <div className={classNames("modal", className, isActive)}> + <div className="modal-background" /> + <div className="modal-card"> + <header className={classNames("modal-card-head", `has-background-${headColor}`)}> + <p className="modal-card-title is-marginless">{title}</p> + <button className="delete" aria-label="close" onClick={closeFunction} /> + </header> + <section className="modal-card-body">{body}</section> + {showFooter} + </div> + </div> + ); + } +} + +export default Modal; diff --git a/scm-ui/ui-components/src/modals/index.ts b/scm-ui/ui-components/src/modals/index.ts new file mode 100644 index 0000000000..aabf65dbba --- /dev/null +++ b/scm-ui/ui-components/src/modals/index.ts @@ -0,0 +1,4 @@ +// @create-index + +export { default as ConfirmAlert, confirmAlert } from "./ConfirmAlert"; +export { default as Modal } from "./Modal"; diff --git a/scm-ui/ui-components/src/navigation/NavAction.tsx b/scm-ui/ui-components/src/navigation/NavAction.tsx new file mode 100644 index 0000000000..ec7aa3a757 --- /dev/null +++ b/scm-ui/ui-components/src/navigation/NavAction.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +type Props = { + icon?: string; + label: string; + action: () => void; +}; + +class NavAction extends React.Component<Props> { + render() { + const { label, icon, action } = this.props; + + let showIcon = null; + if (icon) { + showIcon = ( + <> + <i className={icon}></i>{" "} + </> + ); + } + + return ( + <li> + <a onClick={action} href="javascript:void(0);"> + {showIcon} + {label} + </a> + </li> + ); + } +} + +export default NavAction; diff --git a/scm-ui/ui-components/src/navigation/NavLink.tsx b/scm-ui/ui-components/src/navigation/NavLink.tsx new file mode 100644 index 0000000000..f87351d496 --- /dev/null +++ b/scm-ui/ui-components/src/navigation/NavLink.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import classNames from "classnames"; +import { Link, Route } from "react-router-dom"; + +// TODO mostly copy of PrimaryNavigationLink + +type Props = { + to: string; + icon?: string; + label: string; + activeOnlyWhenExact?: boolean; + activeWhenMatch?: (route: any) => boolean; +}; + +class NavLink extends React.Component<Props> { + static defaultProps = { + activeOnlyWhenExact: true + }; + + isActive(route: any) { + const { activeWhenMatch } = this.props; + return route.match || (activeWhenMatch && activeWhenMatch(route)); + } + + renderLink = (route: any) => { + const { to, icon, label } = this.props; + + let showIcon = null; + if (icon) { + showIcon = ( + <> + <i className={classNames(icon, "fa-fw")} />{" "} + </> + ); + } + + return ( + <li> + <Link className={this.isActive(route) ? "is-active" : ""} to={to}> + {showIcon} + {label} + </Link> + </li> + ); + }; + + render() { + const { to, activeOnlyWhenExact } = this.props; + + return <Route path={to} exact={activeOnlyWhenExact} children={this.renderLink} />; + } +} + +export default NavLink; diff --git a/scm-ui/ui-components/src/navigation/Navigation.tsx b/scm-ui/ui-components/src/navigation/Navigation.tsx new file mode 100644 index 0000000000..fc12f9de02 --- /dev/null +++ b/scm-ui/ui-components/src/navigation/Navigation.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from "react"; + +type Props = { + children?: ReactNode; +}; + +class Navigation extends React.Component<Props> { + render() { + return <aside className="menu">{this.props.children}</aside>; + } +} + +export default Navigation; diff --git a/scm-ui/ui-components/src/navigation/PrimaryNavigation.tsx b/scm-ui/ui-components/src/navigation/PrimaryNavigation.tsx new file mode 100644 index 0000000000..42e2a6a52d --- /dev/null +++ b/scm-ui/ui-components/src/navigation/PrimaryNavigation.tsx @@ -0,0 +1,90 @@ +import React, { ReactNode } from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import PrimaryNavigationLink from "./PrimaryNavigationLink"; +import { Links } from "@scm-manager/ui-types"; +import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; + +type Props = WithTranslation & { + links: Links; +}; + +type Appender = (to: string, match: string, label: string, linkName: string) => void; + +class PrimaryNavigation extends React.Component<Props> { + createNavigationAppender = (navigationItems: ReactNode[]): Appender => { + const { t, links } = this.props; + + return (to: string, match: string, label: string, linkName: string) => { + const link = links[linkName]; + if (link) { + const navigationItem = <PrimaryNavigationLink to={to} match={match} label={t(label)} key={linkName} />; + navigationItems.push(navigationItem); + } + }; + }; + + appendLogout = (navigationItems: ReactNode[], append: Appender) => { + const { t, links } = this.props; + + const props = { + links, + label: t("primary-navigation.logout") + }; + + if (binder.hasExtension("primary-navigation.logout", props)) { + navigationItems.push( + <ExtensionPoint key="primary-navigation.logout" name="primary-navigation.logout" props={props} /> + ); + } else { + append("/logout", "/logout", "primary-navigation.logout", "logout"); + } + }; + + createNavigationItems = () => { + const navigationItems: ReactNode[] = []; + const { t, links } = this.props; + + const props = { + links, + label: t("primary-navigation.first-menu") + }; + + const append = this.createNavigationAppender(navigationItems); + if (binder.hasExtension("primary-navigation.first-menu", props)) { + navigationItems.push( + <ExtensionPoint key="primary-navigation.first-menu" name="primary-navigation.first-menu" props={props} /> + ); + } + append("/repos/", "/(repo|repos)", "primary-navigation.repositories", "repositories"); + append("/users/", "/(user|users)", "primary-navigation.users", "users"); + append("/groups/", "/(group|groups)", "primary-navigation.groups", "groups"); + append("/admin", "/admin", "primary-navigation.admin", "config"); + + navigationItems.push( + <ExtensionPoint + key="primary-navigation" + name="primary-navigation" + renderAll={true} + props={{ + links: this.props.links + }} + /> + ); + + this.appendLogout(navigationItems, append); + + return navigationItems; + }; + + render() { + const navigationItems = this.createNavigationItems(); + + return ( + <nav className="tabs is-boxed"> + <ul>{navigationItems}</ul> + </nav> + ); + } +} + +export default withTranslation("commons")(PrimaryNavigation); diff --git a/scm-ui/ui-components/src/navigation/PrimaryNavigationLink.tsx b/scm-ui/ui-components/src/navigation/PrimaryNavigationLink.tsx new file mode 100644 index 0000000000..de538f30d2 --- /dev/null +++ b/scm-ui/ui-components/src/navigation/PrimaryNavigationLink.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { Route, Link } from "react-router-dom"; + +type Props = { + to: string; + label: string; + match?: string; + activeOnlyWhenExact?: boolean; +}; + +class PrimaryNavigationLink extends React.Component<Props> { + renderLink = (route: any) => { + const { to, label } = this.props; + return ( + <li className={route.match ? "is-active" : ""}> + <Link to={to}>{label}</Link> + </li> + ); + }; + + render() { + const { to, match, activeOnlyWhenExact } = this.props; + const path = match ? match : to; + return <Route path={path} exact={activeOnlyWhenExact} children={this.renderLink} />; + } +} + +export default PrimaryNavigationLink; diff --git a/scm-ui/ui-components/src/navigation/Section.tsx b/scm-ui/ui-components/src/navigation/Section.tsx new file mode 100644 index 0000000000..b6f0542506 --- /dev/null +++ b/scm-ui/ui-components/src/navigation/Section.tsx @@ -0,0 +1,20 @@ +import React, { ReactNode } from "react"; + +type Props = { + label: string; + children?: ReactNode; +}; + +class Section extends React.Component<Props> { + render() { + const { label, children } = this.props; + return ( + <div> + <p className="menu-label">{label}</p> + <ul className="menu-list">{children}</ul> + </div> + ); + } +} + +export default Section; diff --git a/scm-ui/ui-components/src/navigation/SubNavigation.tsx b/scm-ui/ui-components/src/navigation/SubNavigation.tsx new file mode 100644 index 0000000000..258658561a --- /dev/null +++ b/scm-ui/ui-components/src/navigation/SubNavigation.tsx @@ -0,0 +1,59 @@ +import React, { ReactNode } from "react"; +import { Link, Route } from "react-router-dom"; +import classNames from "classnames"; + +type Props = { + to: string; + icon?: string; + label: string; + activeOnlyWhenExact?: boolean; + activeWhenMatch?: (route: any) => boolean; + children?: ReactNode; +}; + +class SubNavigation extends React.Component<Props> { + static defaultProps = { + activeOnlyWhenExact: false + }; + + isActive(route: any) { + const { activeWhenMatch } = this.props; + return route.match || (activeWhenMatch && activeWhenMatch(route)); + } + + renderLink = (route: any) => { + const { to, icon, label } = this.props; + + let defaultIcon = "fas fa-cog"; + if (icon) { + defaultIcon = icon; + } + + let children = null; + if (this.isActive(route)) { + children = <ul className="sub-menu">{this.props.children}</ul>; + } + + return ( + <li> + <Link className={this.isActive(route) ? "is-active" : ""} to={to}> + <i className={classNames(defaultIcon, "fa-fw")} /> {label} + </Link> + {children} + </li> + ); + }; + + render() { + const { to, activeOnlyWhenExact } = this.props; + + // removes last part of url + const parents = to.split("/"); + parents.splice(-1, 1); + const parent = parents.join("/"); + + return <Route path={parent} exact={activeOnlyWhenExact} children={this.renderLink} />; + } +} + +export default SubNavigation; diff --git a/scm-ui/ui-components/src/navigation/index.ts b/scm-ui/ui-components/src/navigation/index.ts new file mode 100644 index 0000000000..6c83ffc86d --- /dev/null +++ b/scm-ui/ui-components/src/navigation/index.ts @@ -0,0 +1,9 @@ +// @create-index + +export { default as NavAction } from "./NavAction"; +export { default as NavLink } from "./NavLink"; +export { default as Navigation } from "./Navigation"; +export { default as SubNavigation } from "./SubNavigation"; +export { default as PrimaryNavigation } from "./PrimaryNavigation"; +export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink"; +export { default as Section } from "./Section"; diff --git a/scm-ui/ui-components/src/repos/Diff.tsx b/scm-ui/ui-components/src/repos/Diff.tsx new file mode 100644 index 0000000000..d7123903a3 --- /dev/null +++ b/scm-ui/ui-components/src/repos/Diff.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import DiffFile from "./DiffFile"; +import { DiffObjectProps, File } from "./DiffTypes"; +import Notification from "../Notification"; +import { WithTranslation, withTranslation } from "react-i18next"; + +type Props = WithTranslation & + DiffObjectProps & { + diff: File[]; + defaultCollapse?: boolean; + }; + +class Diff extends React.Component<Props> { + static defaultProps: Partial<Props> = { + sideBySide: false + }; + + render() { + const { diff, t, ...fileProps } = this.props; + return ( + <> + {diff.length === 0 ? ( + <Notification type="info">{t("diff.noDiffFound")}</Notification> + ) : ( + diff.map((file, index) => <DiffFile key={index} file={file} {...fileProps} {...this.props} />) + )} + </> + ); + } +} + +export default withTranslation("repos")(Diff); diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx new file mode 100644 index 0000000000..0c51a30982 --- /dev/null +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -0,0 +1,274 @@ +import React from "react"; +import { withTranslation, WithTranslation } from "react-i18next"; +import classNames from "classnames"; +import styled from "styled-components"; +// @ts-ignore +import { Change, Diff as DiffComponent, getChangeKey, Hunk } from "react-diff-view"; +import { Button, ButtonGroup } from "../buttons"; +import Tag from "../Tag"; +import Icon from "../Icon"; +import { File, Hunk as HunkType, DiffObjectProps } from "./DiffTypes"; + +type Props = DiffObjectProps & + WithTranslation & { + file: File; + defaultCollapse?: boolean; + }; + +type Collapsible = { + collapsed?: boolean; +}; + +type State = Collapsible & { + sideBySide: boolean; +}; + +const DiffFilePanel = styled.div` + /* remove bottom border for collapsed panels */ + ${(props: Collapsible) => (props.collapsed ? "border-bottom: none;" : "")}; +`; + +const FlexWrapLevel = styled.div` + /* breaks into a second row + when buttons and title become too long */ + flex-wrap: wrap; +`; + +const FullWidthTitleHeader = styled.div` + max-width: 100%; +`; + +const TitleWrapper = styled.span` + margin-left: 0.25rem; +`; + +const ButtonWrapper = styled.div` + /* align child to right */ + margin-left: auto; +`; + +const HunkDivider = styled.hr` + margin: 0.5rem 0; +`; + +const ChangeTypeTag = styled(Tag)` + margin-left: 0.75rem; +`; + +const ModifiedDiffComponent = styled(DiffComponent)` + /* column sizing */ + > colgroup .diff-gutter-col { + width: 3.25rem; + } + /* prevent following content from moving down */ + > .diff-gutter:empty:hover::after { + font-size: 0.7rem; + } + /* smaller font size for code */ + & .diff-line { + font-size: 0.75rem; + } + /* comment padding for sidebyside view */ + &.split .diff-widget-content .is-indented-line { + padding-left: 3.25rem; + } + /* comment padding for combined view */ + &.unified .diff-widget-content .is-indented-line { + padding-left: 6.5rem; + } +`; + +class DiffFile extends React.Component<Props, State> { + static defaultProps: Partial<Props> = { + defaultCollapse: false + }; + + constructor(props: Props) { + super(props); + this.state = { + collapsed: !!this.props.defaultCollapse, + sideBySide: false + }; + } + + // collapse diff by clicking collapseDiffs button + componentDidUpdate(prevProps: Props) { + const { defaultCollapse } = this.props; + if (prevProps.defaultCollapse !== defaultCollapse) { + this.setState({ + collapsed: defaultCollapse + }); + } + } + + toggleCollapse = () => { + const { file } = this.props; + if (file && !file.isBinary) { + this.setState(state => ({ + collapsed: !state.collapsed + })); + } + }; + + toggleSideBySide = () => { + this.setState(state => ({ + sideBySide: !state.sideBySide + })); + }; + + setCollapse = (collapsed: boolean) => { + this.setState({ + collapsed + }); + }; + + createHunkHeader = (hunk: HunkType, i: number) => { + if (i > 0) { + return <HunkDivider />; + } + return null; + }; + + collectHunkAnnotations = (hunk: HunkType) => { + const { annotationFactory, file } = this.props; + if (annotationFactory) { + return annotationFactory({ + hunk, + file + }); + } + }; + + handleClickEvent = (change: Change, hunk: HunkType) => { + const { file, onClick } = this.props; + const context = { + changeId: getChangeKey(change), + change, + hunk, + file + }; + if (onClick) { + onClick(context); + } + }; + + createCustomEvents = (hunk: HunkType) => { + const { onClick } = this.props; + if (onClick) { + return { + gutter: { + onClick: (change: Change) => { + this.handleClickEvent(change, hunk); + } + } + }; + } + }; + + renderHunk = (hunk: HunkType, i: number) => { + return ( + <Hunk + key={hunk.content} + hunk={hunk} + header={this.createHunkHeader(hunk, i)} + widgets={this.collectHunkAnnotations(hunk)} + customEvents={this.createCustomEvents(hunk)} + /> + ); + }; + + renderFileTitle = (file: File) => { + if (file.oldPath !== file.newPath && (file.type === "copy" || file.type === "rename")) { + return ( + <> + {file.oldPath} <Icon name="arrow-right" color="inherit" /> {file.newPath} + </> + ); + } else if (file.type === "delete") { + return file.oldPath; + } + return file.newPath; + }; + + hoverFileTitle = (file: File): string => { + if (file.oldPath !== file.newPath && (file.type === "copy" || file.type === "rename")) { + return `${file.oldPath} > ${file.newPath}`; + } else if (file.type === "delete") { + return file.oldPath; + } + return file.newPath; + }; + + renderChangeTag = (file: File) => { + const { t } = this.props; + if (!file.type) { + return; + } + const key = "diff.changes." + file.type; + let value = t(key); + if (key === value) { + value = file.type; + } + const color = + value === "added" ? "success is-outlined" : value === "deleted" ? "danger is-outlined" : "info is-outlined"; + + return <ChangeTypeTag className={classNames("is-rounded", "has-text-weight-normal")} color={color} label={value} />; + }; + + render() { + const { file, fileControlFactory, fileAnnotationFactory, t } = this.props; + const { collapsed, sideBySide } = this.state; + const viewType = sideBySide ? "split" : "unified"; + + let body = null; + let icon = "angle-right"; + if (!collapsed) { + const fileAnnotations = fileAnnotationFactory ? fileAnnotationFactory(file) : null; + icon = "angle-down"; + body = ( + <div className="panel-block is-paddingless"> + {fileAnnotations} + <ModifiedDiffComponent className={viewType} viewType={viewType}> + {file.hunks.map(this.renderHunk)} + </ModifiedDiffComponent> + </div> + ); + } + const collapseIcon = file && !file.isBinary ? <Icon name={icon} color="inherit" /> : null; + + const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null; + return ( + <DiffFilePanel className={classNames("panel", "is-size-6")} collapsed={(file && file.isBinary) || collapsed}> + <div className="panel-heading"> + <FlexWrapLevel className="level"> + <FullWidthTitleHeader + className={classNames("level-left", "is-flex", "has-cursor-pointer")} + onClick={this.toggleCollapse} + title={this.hoverFileTitle(file)} + > + {collapseIcon} + <TitleWrapper className={classNames("is-ellipsis-overflow", "is-size-6")}> + {this.renderFileTitle(file)} + </TitleWrapper> + {this.renderChangeTag(file)} + </FullWidthTitleHeader> + <ButtonWrapper className={classNames("level-right", "is-flex")}> + <ButtonGroup> + <Button + action={this.toggleSideBySide} + icon={sideBySide ? "align-left" : "columns"} + label={t(sideBySide ? "diff.combined" : "diff.sideBySide")} + reducedMobile={true} + /> + {fileControls} + </ButtonGroup> + </ButtonWrapper> + </FlexWrapLevel> + </div> + {body} + </DiffFilePanel> + ); + } +} + +export default withTranslation("repos")(DiffFile); diff --git a/scm-ui/ui-components/src/repos/DiffTypes.ts b/scm-ui/ui-components/src/repos/DiffTypes.ts new file mode 100644 index 0000000000..58dd5aef5b --- /dev/null +++ b/scm-ui/ui-components/src/repos/DiffTypes.ts @@ -0,0 +1,74 @@ +import { ReactNode } from "react"; + +// We place the types here and not in @scm-manager/ui-types, +// because they represent not a real scm-manager related type. +// This types represents only the required types for the Diff related components, +// such as every other component does with its Props. + +export type FileChangeType = "add" | "modify" | "delete" | "copy" | "rename"; + +export type File = { + hunks: Hunk[]; + newEndingNewLine: boolean; + newMode?: string; + newPath: string; + newRevision?: string; + oldEndingNewLine: boolean; + oldMode?: string; + oldPath: string; + oldRevision?: string; + type: FileChangeType; + // TODO does this property exists? + isBinary?: boolean; +}; + +export type Hunk = { + changes: Change[]; + content: string; +}; + +export type ChangeType = "insert" | "delete" | "normal"; + +export type Change = { + content: string; + isNormal?: boolean; + isInsert?: boolean; + isDelete?: boolean; + lineNumber?: number; + newLineNumber?: number; + oldLineNumber?: number; + type: ChangeType; +}; + +export type BaseContext = { + hunk: Hunk; + file: File; +}; + +export type AnnotationFactoryContext = BaseContext; + +export type FileAnnotationFactory = (file: File) => ReactNode[]; + +// key = change id, value = react component +export type AnnotationFactory = ( + context: AnnotationFactoryContext +) => { + [key: string]: any; +}; + +export type DiffEventContext = BaseContext & { + changeId: string; + change: Change; +}; + +export type DiffEventHandler = (context: DiffEventContext) => void; + +export type FileControlFactory = (file: File, setCollapseState: (p: boolean) => void) => ReactNode | null | undefined; + +export type DiffObjectProps = { + sideBySide: boolean; + onClick?: DiffEventHandler; + fileControlFactory?: FileControlFactory; + fileAnnotationFactory?: FileAnnotationFactory; + annotationFactory?: AnnotationFactory; +}; diff --git a/scm-ui/ui-components/src/repos/LoadingDiff.tsx b/scm-ui/ui-components/src/repos/LoadingDiff.tsx new file mode 100644 index 0000000000..45b62f9b35 --- /dev/null +++ b/scm-ui/ui-components/src/repos/LoadingDiff.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { apiClient } from "../apiclient"; +import ErrorNotification from "../ErrorNotification"; +// @ts-ignore +import parser from "gitdiff-parser"; + +import Loading from "../Loading"; +import Diff from "./Diff"; +import { DiffObjectProps, File } from "./DiffTypes"; + +type Props = DiffObjectProps & { + url: string; + defaultCollapse?: boolean; +}; + +type State = { + diff?: File[]; + loading: boolean; + error?: Error; +}; + +class LoadingDiff extends React.Component<Props, State> { + static defaultProps = { + sideBySide: false + }; + + constructor(props: Props) { + super(props); + this.state = { + loading: true + }; + } + + componentDidMount() { + this.fetchDiff(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.url !== this.props.url) { + this.fetchDiff(); + } + } + + fetchDiff = () => { + const { url } = this.props; + this.setState({loading: true}); + apiClient + .get(url) + .then(response => response.text()) + .then(parser.parse) + // $FlowFixMe + .then((diff: any) => { + this.setState({ + loading: false, + diff: diff + }); + }) + .catch((error: Error) => { + this.setState({ + loading: false, + error + }); + }); + }; + + render() { + const { diff, loading, error } = this.state; + if (error) { + return <ErrorNotification error={error} />; + } else if (loading) { + return <Loading />; + } else if (!diff) { + return null; + } else { + return <Diff diff={diff} {...this.props} />; + } + } +} + +export default LoadingDiff; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx new file mode 100644 index 0000000000..ed2b63ea2d --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { Changeset } from "@scm-manager/ui-types"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { WithTranslation, withTranslation } from "react-i18next"; + +type Props = WithTranslation & { + changeset: Changeset; +}; + +class ChangesetAuthor extends React.Component<Props> { + render() { + const { changeset } = this.props; + if (!changeset.author) { + return null; + } + + const { name, mail } = changeset.author; + if (mail) { + return this.withExtensionPoint(this.renderWithMail(name, mail)); + } + return this.withExtensionPoint(<>{name}</>); + } + + renderWithMail(name: string, mail: string) { + const { t } = this.props; + return ( + <a href={"mailto:" + mail} title={t("changeset.author.mailto") + " " + mail}> + {name} + </a> + ); + } + + withExtensionPoint(child: any) { + const { t } = this.props; + return ( + <> + {t("changeset.author.prefix")} {child} + <ExtensionPoint + name="changesets.author.suffix" + props={{ + changeset: this.props.changeset + }} + renderAll={true} + /> + </> + ); + } +} + +export default withTranslation("repos")(ChangesetAuthor); diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetButtonGroup.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetButtonGroup.tsx new file mode 100644 index 0000000000..712aabb35e --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetButtonGroup.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { Changeset, Repository } from "@scm-manager/ui-types"; +import { ButtonAddons, Button } from "../../buttons"; +import { createChangesetLink, createSourcesLink } from "./changesets"; +import { WithTranslation, withTranslation } from "react-i18next"; + +type Props = WithTranslation & { + repository: Repository; + changeset: Changeset; +}; + +class ChangesetButtonGroup extends React.Component<Props> { + render() { + const { repository, changeset, t } = this.props; + const changesetLink = createChangesetLink(repository, changeset); + const sourcesLink = createSourcesLink(repository, changeset); + return ( + <ButtonAddons className="is-marginless"> + <Button link={changesetLink} icon="exchange-alt" label={t("changeset.buttons.details")} reducedMobile={true} /> + <Button link={sourcesLink} icon="code" label={t("changeset.buttons.sources")} reducedMobile={true} /> + </ButtonAddons> + ); + } +} + +export default withTranslation("repos")(ChangesetButtonGroup); diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx new file mode 100644 index 0000000000..78f4e90cb3 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { Changeset, Link } from "@scm-manager/ui-types"; +import LoadingDiff from "../LoadingDiff"; +import Notification from "../../Notification"; +import { WithTranslation, withTranslation } from "react-i18next"; + +type Props = WithTranslation & { + changeset: Changeset; + defaultCollapse?: boolean; +}; + +class ChangesetDiff extends React.Component<Props> { + isDiffSupported(changeset: Changeset) { + return !!changeset._links.diff; + } + + createUrl(changeset: Changeset) { + if (changeset._links.diff) { + const link = changeset._links.diff as Link; + return link.href + "?format=GIT"; + } + throw new Error("diff link is missing"); + } + + render() { + const { changeset, defaultCollapse, t } = this.props; + if (!this.isDiffSupported(changeset)) { + return <Notification type="danger">{t("changeset.diffNotSupported")}</Notification>; + } else { + const url = this.createUrl(changeset); + return <LoadingDiff url={url} defaultCollapse={defaultCollapse} />; + } + } +} + +export default withTranslation("repos")(ChangesetDiff); diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetId.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetId.tsx new file mode 100644 index 0000000000..ed10ac35bd --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetId.tsx @@ -0,0 +1,40 @@ +import { Link } from "react-router-dom"; +import React from "react"; +import { Changeset, Repository } from "@scm-manager/ui-types"; +import { createChangesetLink } from "./changesets"; + +type Props = { + repository: Repository; + changeset: Changeset; + link: boolean; +}; + +export default class ChangesetId extends React.Component<Props> { + static defaultProps = { + link: true + }; + + shortId = (changeset: Changeset) => { + return changeset.id.substr(0, 7); + }; + + renderLink = () => { + const { repository, changeset } = this.props; + const link = createChangesetLink(repository, changeset); + + return <Link to={link}>{this.shortId(changeset)}</Link>; + }; + + renderText = () => { + const { changeset } = this.props; + return this.shortId(changeset); + }; + + render() { + const { link } = this.props; + if (link) { + return this.renderLink(); + } + return this.renderText(); + } +} diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetList.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetList.tsx new file mode 100644 index 0000000000..108eb44db3 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetList.tsx @@ -0,0 +1,21 @@ +import ChangesetRow from "./ChangesetRow"; +import React from "react"; + +import { Changeset, Repository } from "@scm-manager/ui-types"; + +type Props = { + repository: Repository; + changesets: Changeset[]; +}; + +class ChangesetList extends React.Component<Props> { + render() { + const { repository, changesets } = this.props; + const content = changesets.map(changeset => { + return <ChangesetRow key={changeset.id} repository={repository} changeset={changeset} />; + }); + return <>{content}</>; + } +} + +export default ChangesetList; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx new file mode 100644 index 0000000000..365250370c --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import { Trans, WithTranslation, withTranslation } from "react-i18next"; +import classNames from "classnames"; +import styled from "styled-components"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { Changeset, Repository } from "@scm-manager/ui-types"; +import DateFromNow from "../../DateFromNow"; +import { AvatarWrapper, AvatarImage } from "../../avatar"; +import { parseDescription } from "./changesets"; +import ChangesetId from "./ChangesetId"; +import ChangesetAuthor from "./ChangesetAuthor"; +import ChangesetTags from "./ChangesetTags"; +import ChangesetButtonGroup from "./ChangesetButtonGroup"; + +type Props = WithTranslation & { + repository: Repository; + changeset: Changeset; +}; + +const Wrapper = styled.div` + // & references parent rule + // have a look at https://cssinjs.org/jss-plugin-nested?v=v10.0.0-alpha.9 + & + & { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid rgba(219, 219, 219, 0.5); + } +`; + +const AvatarFigure = styled.figure` + margin-top: 0.5rem; + margin-right: 0.5rem; +`; + +const FixedSizedAvatar = styled.div` + width: 35px; + height: 35px; +`; + +const Metadata = styled.div` + margin-left: 0; + width: 100%; +`; + +const AuthorWrapper = styled.p` + margin-top: 0.5rem; +`; + +const VCenteredColumn = styled.div` + align-self: center; +`; + +const VCenteredChildColumn = styled.div` + align-items: center; + justify-content: flex-end; +`; + +class ChangesetRow extends React.Component<Props> { + createChangesetId = (changeset: Changeset) => { + const { repository } = this.props; + return <ChangesetId changeset={changeset} repository={repository} />; + }; + + render() { + const { repository, changeset } = this.props; + const description = parseDescription(changeset.description); + const changesetId = this.createChangesetId(changeset); + const dateFromNow = <DateFromNow date={changeset.date} />; + + return ( + <Wrapper> + <div className="columns is-gapless is-mobile"> + <div className="column is-three-fifths"> + <div className="columns is-gapless"> + <div className="column is-four-fifths"> + <div className="media"> + <AvatarWrapper> + <AvatarFigure className="media-left"> + <FixedSizedAvatar className="image"> + <AvatarImage person={changeset.author} /> + </FixedSizedAvatar> + </AvatarFigure> + </AvatarWrapper> + <Metadata className="media-right"> + <h4 className="has-text-weight-bold is-ellipsis-overflow"> + <ExtensionPoint + name="changeset.description" + props={{ + changeset, + value: description.title + }} + renderAll={false} + > + {description.title} + </ExtensionPoint> + </h4> + <p className="is-hidden-touch"> + <Trans i18nKey="repos:changeset.summary" components={[changesetId, dateFromNow]} /> + </p> + <p className="is-hidden-desktop"> + <Trans i18nKey="repos:changeset.shortSummary" components={[changesetId, dateFromNow]} /> + </p> + <AuthorWrapper className="is-size-7"> + <ChangesetAuthor changeset={changeset} /> + </AuthorWrapper> + </Metadata> + </div> + </div> + <VCenteredColumn className="column"> + <ChangesetTags changeset={changeset} /> + </VCenteredColumn> + </div> + </div> + <VCenteredChildColumn className={classNames("column", "is-flex")}> + <ChangesetButtonGroup repository={repository} changeset={changeset} /> + <ExtensionPoint + name="changeset.right" + props={{ + repository, + changeset + }} + renderAll={true} + /> + </VCenteredChildColumn> + </div> + </Wrapper> + ); + } +} + +export default withTranslation("repos")(ChangesetRow); diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetTag.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetTag.tsx new file mode 100644 index 0000000000..6d5d43aef2 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetTag.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Tag } from "@scm-manager/ui-types"; +import ChangesetTagBase from "./ChangesetTagBase"; + +type Props = { + tag: Tag; +}; + +class ChangesetTag extends React.Component<Props> { + render() { + const { tag } = this.props; + return <ChangesetTagBase icon="tag" label={tag.name} />; + } +} + +export default ChangesetTag; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetTagBase.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetTagBase.tsx new file mode 100644 index 0000000000..95abbfb306 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetTagBase.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import Tag from "../../Tag"; + +type Props = { + icon: string; + label: string; +}; + +class ChangesetTagBase extends React.Component<Props> { + render() { + const { icon, label } = this.props; + return <Tag color="info" icon={icon} label={label} />; + } +} + +export default ChangesetTagBase; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetTags.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetTags.tsx new file mode 100644 index 0000000000..bc3e5cd144 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetTags.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Changeset } from "@scm-manager/ui-types"; +import ChangesetTag from "./ChangesetTag"; +import ChangesetTagsCollapsed from "./ChangesetTagsCollapsed"; + +type Props = { + changeset: Changeset; +}; + +class ChangesetTags extends React.Component<Props> { + getTags = () => { + const { changeset } = this.props; + return changeset._embedded.tags || []; + }; + + render() { + const tags = this.getTags(); + + if (tags.length === 1) { + return <ChangesetTag tag={tags[0]} />; + } else if (tags.length > 1) { + return <ChangesetTagsCollapsed tags={tags} />; + } else { + return null; + } + } +} + +export default ChangesetTags; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetTagsCollapsed.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetTagsCollapsed.tsx new file mode 100644 index 0000000000..30549d6fc7 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetTagsCollapsed.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Tag } from "@scm-manager/ui-types"; +import Tooltip from "../../Tooltip"; +import ChangesetTagBase from "./ChangesetTagBase"; + +type Props = WithTranslation & { + tags: Tag[]; +}; + +class ChangesetTagsCollapsed extends React.Component<Props> { + render() { + const { tags, t } = this.props; + const message = tags.map(tag => tag.name).join(", "); + return ( + <Tooltip location="top" message={message}> + <ChangesetTagBase icon="tags" label={tags.length + " " + t("changeset.tags")} /> + </Tooltip> + ); + } +} + +export default withTranslation("repos")(ChangesetTagsCollapsed); diff --git a/scm-ui/ui-components/src/repos/changesets/changesets.test.ts b/scm-ui/ui-components/src/repos/changesets/changesets.test.ts new file mode 100644 index 0000000000..e15cfdea44 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/changesets.test.ts @@ -0,0 +1,20 @@ +import { parseDescription } from "./changesets"; + +describe("parseDescription tests", () => { + it("should return a description with title and message", () => { + const desc = parseDescription("Hello\nTrillian"); + expect(desc.title).toBe("Hello"); + expect(desc.message).toBe("Trillian"); + }); + + it("should return a description with title and without message", () => { + const desc = parseDescription("Hello Trillian"); + expect(desc.title).toBe("Hello Trillian"); + }); + + it("should return an empty description for undefined", () => { + const desc = parseDescription(); + expect(desc.title).toBe(""); + expect(desc.message).toBe(""); + }); +}); diff --git a/scm-ui/ui-components/src/repos/changesets/changesets.ts b/scm-ui/ui-components/src/repos/changesets/changesets.ts new file mode 100644 index 0000000000..1ffc2b5db6 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/changesets.ts @@ -0,0 +1,34 @@ +import { Changeset, Repository } from "@scm-manager/ui-types"; + +export type Description = { + title: string; + message: string; +}; + +export function createChangesetLink(repository: Repository, changeset: Changeset) { + return `/repo/${repository.namespace}/${repository.name}/changeset/${changeset.id}`; +} + +export function createSourcesLink(repository: Repository, changeset: Changeset) { + return `/repo/${repository.namespace}/${repository.name}/sources/${changeset.id}`; +} + +export function parseDescription(description?: string): Description { + const desc = description ? description : ""; + const lineBreak = desc.indexOf("\n"); + + let title; + let message = ""; + + if (lineBreak > 0) { + title = desc.substring(0, lineBreak); + message = desc.substring(lineBreak + 1); + } else { + title = desc; + } + + return { + title, + message + }; +} diff --git a/scm-ui/ui-components/src/repos/changesets/index.ts b/scm-ui/ui-components/src/repos/changesets/index.ts new file mode 100644 index 0000000000..4bd2e1ddd7 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/index.ts @@ -0,0 +1,12 @@ +import * as changesets from "./changesets"; +export { changesets }; + +export { default as ChangesetAuthor } from "./ChangesetAuthor"; +export { default as ChangesetButtonGroup } from "./ChangesetButtonGroup"; +export { default as ChangesetDiff } from "./ChangesetDiff"; +export { default as ChangesetId } from "./ChangesetId"; +export { default as ChangesetList } from "./ChangesetList"; +export { default as ChangesetRow } from "./ChangesetRow"; +export { default as ChangesetTag } from "./ChangesetTag"; +export { default as ChangesetTags } from "./ChangesetTags"; +export { default as ChangesetTagsCollapsed } from "./ChangesetTagsCollapsed"; diff --git a/scm-ui/ui-components/src/repos/diffs.test.ts b/scm-ui/ui-components/src/repos/diffs.test.ts new file mode 100644 index 0000000000..5819b1fa04 --- /dev/null +++ b/scm-ui/ui-components/src/repos/diffs.test.ts @@ -0,0 +1,67 @@ +import { File, FileChangeType, Hunk } from "./DiffTypes"; +import { getPath, createHunkIdentifier, createHunkIdentifierFromContext } from "./diffs"; + +describe("tests for diff util functions", () => { + const file = (type: FileChangeType, oldPath: string, newPath: string): File => { + return { + hunks: [], + type: type, + oldPath, + newPath, + newEndingNewLine: true, + oldEndingNewLine: true + }; + }; + + const add = (path: string) => { + return file("add", "/dev/null", path); + }; + + const rm = (path: string) => { + return file("delete", path, "/dev/null"); + }; + + const modify = (path: string) => { + return file("modify", path, path); + }; + + const createHunk = (content: string): Hunk => { + return { + content, + changes: [] + }; + }; + + describe("getPath tests", () => { + it("should pick the new path, for type add", () => { + const file = add("/etc/passwd"); + const path = getPath(file); + expect(path).toBe("/etc/passwd"); + }); + + it("should pick the old path, for type delete", () => { + const file = rm("/etc/passwd"); + const path = getPath(file); + expect(path).toBe("/etc/passwd"); + }); + }); + + describe("createHunkIdentifier tests", () => { + it("should create identifier", () => { + const file = modify("/etc/passwd"); + const hunk = createHunk("@@ -1,18 +1,15 @@"); + const identifier = createHunkIdentifier(file, hunk); + expect(identifier).toBe("modify_/etc/passwd_@@ -1,18 +1,15 @@"); + }); + }); + + describe("createHunkIdentifierFromContext tests", () => { + it("should create identifier", () => { + const identifier = createHunkIdentifierFromContext({ + file: rm("/etc/passwd"), + hunk: createHunk("@@ -1,42 +1,39 @@") + }); + expect(identifier).toBe("delete_/etc/passwd_@@ -1,42 +1,39 @@"); + }); + }); +}); diff --git a/scm-ui/ui-components/src/repos/diffs.ts b/scm-ui/ui-components/src/repos/diffs.ts new file mode 100644 index 0000000000..8ceeb0f604 --- /dev/null +++ b/scm-ui/ui-components/src/repos/diffs.ts @@ -0,0 +1,17 @@ +import { BaseContext, File, Hunk } from "./DiffTypes"; + +export function getPath(file: File) { + if (file.type === "delete") { + return file.oldPath; + } + return file.newPath; +} + +export function createHunkIdentifier(file: File, hunk: Hunk) { + const path = getPath(file); + return `${file.type}_${path}_${hunk.content}`; +} + +export function createHunkIdentifierFromContext(ctx: BaseContext) { + return createHunkIdentifier(ctx.file, ctx.hunk); +} diff --git a/scm-ui/ui-components/src/repos/index.ts b/scm-ui/ui-components/src/repos/index.ts new file mode 100644 index 0000000000..bc3aec1949 --- /dev/null +++ b/scm-ui/ui-components/src/repos/index.ts @@ -0,0 +1,35 @@ +import * as diffs from "./diffs"; + +import { + File, + FileChangeType, + Hunk, + Change, + ChangeType, + BaseContext, + AnnotationFactory, + AnnotationFactoryContext, + DiffEventHandler, + DiffEventContext +} from "./DiffTypes"; + +export { diffs }; + +export * from "./changesets"; + +export { default as Diff } from "./Diff"; +export { default as DiffFile } from "./DiffFile"; +export { default as LoadingDiff } from "./LoadingDiff"; + +export { + File, + FileChangeType, + Hunk, + Change, + ChangeType, + BaseContext, + AnnotationFactory, + AnnotationFactoryContext, + DiffEventHandler, + DiffEventContext +}; diff --git a/scm-ui/ui-components/src/repositories.test.ts b/scm-ui/ui-components/src/repositories.test.ts new file mode 100644 index 0000000000..d21ba5b8e7 --- /dev/null +++ b/scm-ui/ui-components/src/repositories.test.ts @@ -0,0 +1,98 @@ +import { Repository } from "@scm-manager/ui-types"; +import { getProtocolLinkByType } from "./repositories"; + +describe("getProtocolLinkByType tests", () => { + it("should return the http protocol link", () => { + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: { + protocol: [ + { + name: "http", + href: "http://scm.scm-manager.org/repo/scm/core" + } + ] + } + }; + + const link = getProtocolLinkByType(repository, "http"); + expect(link).toBe("http://scm.scm-manager.org/repo/scm/core"); + }); + + it("should return the http protocol link from multiple protocols", () => { + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: { + protocol: [ + { + name: "http", + href: "http://scm.scm-manager.org/repo/scm/core" + }, + { + name: "ssh", + href: "git@scm.scm-manager.org:scm/core" + } + ] + } + }; + + const link = getProtocolLinkByType(repository, "http"); + expect(link).toBe("http://scm.scm-manager.org/repo/scm/core"); + }); + + it("should return the http protocol, even if the protocol is a single link", () => { + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: { + protocol: { + name: "http", + href: "http://scm.scm-manager.org/repo/scm/core" + } + } + }; + + const link = getProtocolLinkByType(repository, "http"); + expect(link).toBe("http://scm.scm-manager.org/repo/scm/core"); + }); + + it("should return null, if such a protocol does not exists", () => { + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: { + protocol: [ + { + name: "http", + href: "http://scm.scm-manager.org/repo/scm/core" + }, + { + name: "ssh", + href: "git@scm.scm-manager.org:scm/core" + } + ] + } + }; + + const link = getProtocolLinkByType(repository, "awesome"); + expect(link).toBeNull(); + }); + + it("should return null, if no protocols are available", () => { + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: {} + }; + + const link = getProtocolLinkByType(repository, "http"); + expect(link).toBeNull(); + }); +}); diff --git a/scm-ui/ui-components/src/repositories.ts b/scm-ui/ui-components/src/repositories.ts new file mode 100644 index 0000000000..431743233a --- /dev/null +++ b/scm-ui/ui-components/src/repositories.ts @@ -0,0 +1,18 @@ +import { Repository } from "@scm-manager/ui-types"; + +// util methods for repositories + +export function getProtocolLinkByType(repository: Repository, type: string) { + let protocols = repository._links.protocol; + if (protocols) { + if (!Array.isArray(protocols)) { + protocols = [protocols]; + } + for (const proto of protocols) { + if (proto.name === type) { + return proto.href; + } + } + } + return null; +} diff --git a/scm-ui/ui-components/src/storyshots.test.ts b/scm-ui/ui-components/src/storyshots.test.ts new file mode 100644 index 0000000000..ee4e34653e --- /dev/null +++ b/scm-ui/ui-components/src/storyshots.test.ts @@ -0,0 +1,6 @@ +import path from "path"; +import initStoryshots from "@storybook/addon-storyshots"; + +initStoryshots({ + configPath: path.resolve(__dirname, "..", ".storybook") +}); diff --git a/scm-ui/ui-components/src/urls.test.ts b/scm-ui/ui-components/src/urls.test.ts new file mode 100644 index 0000000000..180237a1a2 --- /dev/null +++ b/scm-ui/ui-components/src/urls.test.ts @@ -0,0 +1,67 @@ +import { concat, getPageFromMatch, getQueryStringFromLocation, withEndingSlash } from "./urls"; + +describe("tests for withEndingSlash", () => { + it("should append missing slash", () => { + expect(withEndingSlash("abc")).toBe("abc/"); + }); + + it("should not append a second slash", () => { + expect(withEndingSlash("abc/")).toBe("abc/"); + }); +}); + +describe("concat tests", () => { + it("should concat the parts to a single url", () => { + expect(concat("a")).toBe("a"); + expect(concat("a", "b")).toBe("a/b"); + expect(concat("a", "b", "c")).toBe("a/b/c"); + }); +}); + +describe("tests for getPageFromMatch", () => { + function createMatch(page: string) { + return { + params: { + page + } + }; + } + + it("should return 1 for NaN", () => { + const match = createMatch("any"); + expect(getPageFromMatch(match)).toBe(1); + }); + + it("should return 1 for 0", () => { + const match = createMatch("0"); + expect(getPageFromMatch(match)).toBe(1); + }); + + it("should return the given number", () => { + const match = createMatch("42"); + expect(getPageFromMatch(match)).toBe(42); + }); +}); + +describe("tests for getQueryStringFromLocation", () => { + function createLocation(search: string) { + return { + search + }; + } + + it("should return the query string", () => { + const location = createLocation("?q=abc"); + expect(getQueryStringFromLocation(location)).toBe("abc"); + }); + + it("should return query string from multiple parameters", () => { + const location = createLocation("?x=a&y=b&q=abc&z=c"); + expect(getQueryStringFromLocation(location)).toBe("abc"); + }); + + it("should return undefined if q is not available", () => { + const location = createLocation("?x=a&y=b&z=c"); + expect(getQueryStringFromLocation(location)).toBeUndefined(); + }); +}); diff --git a/scm-ui/ui-components/src/urls.ts b/scm-ui/ui-components/src/urls.ts new file mode 100644 index 0000000000..3c524bce37 --- /dev/null +++ b/scm-ui/ui-components/src/urls.ts @@ -0,0 +1,35 @@ +import queryString from "query-string"; + +//@ts-ignore +export const contextPath = window.ctxPath || ""; + +export function withContextPath(path: string) { + return contextPath + path; +} + +export function withEndingSlash(url: string) { + if (url.endsWith("/")) { + return url; + } + return url + "/"; +} + +export function concat(base: string, ...parts: string[]) { + let url = base; + for (const p of parts) { + url = withEndingSlash(url) + p; + } + return url; +} + +export function getPageFromMatch(match: any) { + let page = parseInt(match.params.page, 10); + if (isNaN(page) || !page) { + page = 1; + } + return page; +} + +export function getQueryStringFromLocation(location: any) { + return location.search ? queryString.parse(location.search).q : undefined; +} diff --git a/scm-ui/ui-components/src/validation.test.ts b/scm-ui/ui-components/src/validation.test.ts new file mode 100644 index 0000000000..eb729214d1 --- /dev/null +++ b/scm-ui/ui-components/src/validation.test.ts @@ -0,0 +1,116 @@ +import * as validator from "./validation"; + +describe("test name validation", () => { + // invalid names taken from ValidationUtilTest.java + const invalidNames = [ + "@test", + " test 123", + " test 123 ", + "test 123 ", + "test/123", + "test%123", + "test:123", + "t ", + " t", + " t ", + "", + " invalid_name", + "another%one", + "!!!", + "!_!" + ]; + for (const name of invalidNames) { + it(`should return false for '${name}'`, () => { + expect(validator.isNameValid(name)).toBe(false); + }); + } + + // valid names taken from ValidationUtilTest.java + const validNames = [ + "test", + "test.git", + "Test123.git", + "Test123-git", + "Test_user-123.git", + "test@scm-manager.de", + "test123", + "tt", + "t", + "valid_name", + "another1", + "stillValid", + "this.one_as-well", + "and@this" + ]; + for (const name of validNames) { + it(`should return true for '${name}'`, () => { + expect(validator.isNameValid(name)).toBe(true); + }); + } +}); + +describe("test mail validation", () => { + // invalid taken from ValidationUtilTest.java + const invalid = [ + "ostfalia.de", + "@ostfalia.de", + "s.sdorra@", + "s.sdorra@ostfalia", + "s.sdorra@ ostfalia.de", + "s.sdorra@[ostfalia.de" + ]; + for (const mail of invalid) { + it(`should return false for '${mail}'`, () => { + expect(validator.isMailValid(mail)).toBe(false); + }); + } + + // valid taken from ValidationUtilTest.java + const valid = [ + "s.sdorra@ostfalia.de", + "sdorra@ostfalia.de", + "s.sdorra@hbk-bs.de", + "s.sdorra@gmail.com", + "s.sdorra@t.co", + "s.sdorra@ucla.college", + "s.sdorra@example.xn--p1ai", + "s.sdorra@scm.solutions", + "s'sdorra@scm.solutions", + '"S Sdorra"@scm.solutions' + ]; + for (const mail of valid) { + it(`should return true for '${mail}'`, () => { + expect(validator.isMailValid(mail)).toBe(true); + }); + } +}); + +describe("test number validation", () => { + const invalid = ["1a", "35gu", "dj6", "45,5", "test"]; + for (const number of invalid) { + it(`should return false for '${number}'`, () => { + expect(validator.isNumberValid(number)).toBe(false); + }); + } + const valid = ["1", "35", "2", "235", "34.4"]; + for (const number of valid) { + it(`should return true for '${number}'`, () => { + expect(validator.isNumberValid(number)).toBe(true); + }); + } +}); + +describe("test path validation", () => { + const invalid = ["//", "some//path", "end//"]; + for (const path of invalid) { + it(`should return false for '${path}'`, () => { + expect(validator.isPathValid(path)).toBe(false); + }); + } + const valid = ["", "/", "dir", "some/path", "end/"]; + for (const path of valid) { + it(`should return true for '${path}'`, () => { + expect(validator.isPathValid(path)).toBe(true); + }); + } +}); diff --git a/scm-ui/ui-components/src/validation.ts b/scm-ui/ui-components/src/validation.ts new file mode 100644 index 0000000000..2c6bf2b668 --- /dev/null +++ b/scm-ui/ui-components/src/validation.ts @@ -0,0 +1,21 @@ +const nameRegex = /^[A-Za-z0-9\.\-_][A-Za-z0-9\.\-_@]*$/; + +export const isNameValid = (name: string) => { + return nameRegex.test(name); +}; + +const mailRegex = /^[ -~]+@[A-Za-z0-9][\w\-.]*\.[A-Za-z0-9][A-Za-z0-9-]+$/; + +export const isMailValid = (mail: string) => { + return mailRegex.test(mail); +}; + +export const isNumberValid = (number: any) => { + return !isNaN(number); +}; + +const pathRegex = /^((?!\/{2,}).)*$/; + +export const isPathValid = (path: string) => { + return pathRegex.test(path); +}; diff --git a/scm-ui/ui-components/tsconfig.json b/scm-ui/ui-components/tsconfig.json new file mode 100644 index 0000000000..7e3ee63a2d --- /dev/null +++ b/scm-ui/ui-components/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@scm-manager/tsconfig" +} diff --git a/scm-ui/ui-extensions/README.md b/scm-ui/ui-extensions/README.md new file mode 100644 index 0000000000..63d623dbb6 --- /dev/null +++ b/scm-ui/ui-extensions/README.md @@ -0,0 +1,106 @@ +# ui-extensions + +UI-Extensions contains the building blocks for the [SCM-Manager](https://scm-manager.org) ui extension system. + +## Extensions and ExtensionPoints + +Extension points are spots in the ui, where the ui could be extended or modified. +An extension point requires a unique name and is represented as [React](https://reactjs.org/) component. + +Example: + +```xml +<div> + <h2>Repository</h2> + <ExtensionPoint name="repo.details" /> +</div> +``` + +We can register an extension, in the form of a [React](https://reactjs.org/) component, to the "repo.details" extension point, by using the binder: + +```javascript +import { binder } from "@scm-manager/ui-extensions"; + +const Rtfm = () => { + return <strong>Read the f*** manual</strong>; +}; + +binder.bind("repo.details", Rtfm); +``` + +The ExtensionPoint will now find and render the Rtfm component. + +### Render multiple extensions + +An extension point can render multiple extensions at one. This can be done with the renderAll parameter: + + +```javascript +<div> + <h2>Repository</h2> + <ExtensionPoint name="repo.details" renderAll={true} /> +</div> +``` + +Now we can bind multiple components to the same extension point. + +```javascript +const Rtfm = () => { + return <strong>Read the f*** manual</strong>; +}; + +const RealyRtfm = () => { + return <h1>Read the f*** manual</h1>; +}; + +binder.bind("repo.details", Rtfm); +binder.bind("repo.details", RealyRtfm); +``` + +### Passing props to extensions + +An extension point author can pass React properties to the extensions. This can be done with the `props` property: + +```javascript +<div> + <ExtensionPoint name="repo.title" props={{name: "myrepo"}} /> +</div> +``` + +The extension becomes now the defined react properties as input: + +```javascript +const Title = (props) => { + return <h1>Repository {props.name}</h1>; +}; + +binder.bind("repo.title", Title); +``` + +### Defaults + +An ExtensionPoint is able to render a default, if no extension is bound to the ExtensionPoint. +The default can be passed as React children: + +```javascript +<ExtensionPoint name="repo.title"> + <h1>Default Title</h1> +</ExtensionPoint> +``` + +### Conditional rendering + +An extension can specify a predicate function to the binder. +This function becomes the props of the ExtensionPoint as input and only if the predicate returns true the extension will be rendered: + +```javascript +const GitAvatar = () => { + return <img src="/git/avatar.png" alt="git avatar" />; +}; + +binder.bind("repo.avatar", GitAvatar, (props) => props.type === "git"); +``` + +```javascript +<ExtensionPoint name="repo.avatar" props={type: "git"} /> +``` diff --git a/scm-ui/ui-extensions/package.json b/scm-ui/ui-extensions/package.json new file mode 100644 index 0000000000..28e1d99905 --- /dev/null +++ b/scm-ui/ui-extensions/package.json @@ -0,0 +1,33 @@ +{ + "name": "@scm-manager/ui-extensions", + "version": "2.0.0-SNAPSHOT", + "main": "src/index.ts", + "license": "BSD-3-Clause", + "private": false, + "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", + "scripts": { + "typecheck": "tsc", + "test": "jest" + }, + "dependencies": { + "react": "^16.10.2" + }, + "devDependencies": { + "@types/enzyme": "^3.10.3", + "@types/jest": "^24.0.19", + "@types/react": "^16.9.9", + "typescript": "^3.6.4" + }, + "babel": { + "presets": [ + "@scm-manager/babel-preset" + ] + }, + "jest": { + "preset": "@scm-manager/jest-preset" + }, + "prettier": "@scm-manager/prettier-config", + "publishConfig": { + "access": "public" + } +} diff --git a/scm-ui/ui-extensions/src/ExtensionPoint.test.tsx b/scm-ui/ui-extensions/src/ExtensionPoint.test.tsx new file mode 100644 index 0000000000..c40362070f --- /dev/null +++ b/scm-ui/ui-extensions/src/ExtensionPoint.test.tsx @@ -0,0 +1,146 @@ +import React from "react"; +import ExtensionPoint from "./ExtensionPoint"; +import { shallow, mount } from "enzyme"; +import "@scm-manager/ui-tests/enzyme"; +import binder from "./binder"; + +jest.mock("./binder"); + +const mockedBinder = binder as jest.Mocked<typeof binder>; + +describe("ExtensionPoint test", () => { + beforeEach(() => { + mockedBinder.hasExtension.mockReset(); + mockedBinder.getExtension.mockReset(); + mockedBinder.getExtensions.mockReset(); + }); + + it("should render nothing, if no extension was bound", () => { + mockedBinder.hasExtension.mockReturnValue(true); + mockedBinder.getExtensions.mockReturnValue([]); + const rendered = shallow(<ExtensionPoint name="something.special" />); + expect(rendered.text()).toBe(""); + }); + + it("should render the given component", () => { + const label = () => { + return <label>Extension One</label>; + }; + mockedBinder.hasExtension.mockReturnValue(true); + mockedBinder.getExtension.mockReturnValue(label); + + const rendered = mount(<ExtensionPoint name="something.special" />); + expect(rendered.text()).toBe("Extension One"); + }); + + // We use this wrapper since Enzyme cannot handle React Fragments (see https://github.com/airbnb/enzyme/issues/1213) + class ExtensionPointEnzymeFix extends ExtensionPoint { + render() { + return <div>{super.render()}</div>; + } + } + it("should render the given components", () => { + const labelOne = () => { + return <label>Extension One</label>; + }; + const labelTwo = () => { + return <label>Extension Two</label>; + }; + + mockedBinder.hasExtension.mockReturnValue(true); + mockedBinder.getExtensions.mockReturnValue([labelOne, labelTwo]); + + const rendered = mount(<ExtensionPointEnzymeFix name="something.special" renderAll={true} />); + const text = rendered.text(); + expect(text).toContain("Extension One"); + expect(text).toContain("Extension Two"); + }); + + it("should render the given component, with the given props", () => { + type Props = { + value: string; + }; + + const label = (props: Props) => { + return <label>{props.value}</label>; + }; + + mockedBinder.hasExtension.mockReturnValue(true); + mockedBinder.getExtension.mockReturnValue(label); + + const rendered = mount( + <ExtensionPoint + name="something.special" + props={{ + value: "Awesome" + }} + /> + ); + const text = rendered.text(); + expect(text).toContain("Awesome"); + }); + + it("should render children, if no extension is bound", () => { + const rendered = mount( + <ExtensionPoint name="something.special"> + <p>Cool stuff</p> + </ExtensionPoint> + ); + const text = rendered.text(); + expect(text).toContain("Cool stuff"); + }); + + it("should not render children, if an extension was bound", () => { + const label = () => { + return <label>Bound Extension</label>; + }; + + mockedBinder.hasExtension.mockReturnValue(true); + mockedBinder.getExtension.mockReturnValue(label); + + const rendered = mount( + <ExtensionPoint name="something.special"> + <p>Cool stuff</p> + </ExtensionPoint> + ); + const text = rendered.text(); + expect(text).toContain("Bound Extension"); + }); + + it("should pass the context of the parent component", () => { + const UserContext = React.createContext({ + name: "anonymous" + }); + + type HelloProps = { + name: string; + }; + + const Hello = (props: HelloProps) => { + return <label>Hello {props.name}</label>; + }; + + const HelloUser = () => { + return <UserContext.Consumer>{({ name }) => <Hello name={name} />}</UserContext.Consumer>; + }; + + mockedBinder.hasExtension.mockReturnValue(true); + mockedBinder.getExtension.mockReturnValue(HelloUser); + + const App = () => { + return ( + <UserContext.Provider + value={{ + name: "Trillian" + }} + > + <ExtensionPoint name="hello" /> + </UserContext.Provider> + ); + }; + + const rendered = mount(<App />); + const text = rendered.text(); + expect(text).toBe("Hello Trillian"); + }); +}); diff --git a/scm-ui/ui-extensions/src/ExtensionPoint.tsx b/scm-ui/ui-extensions/src/ExtensionPoint.tsx new file mode 100644 index 0000000000..9ae68a1224 --- /dev/null +++ b/scm-ui/ui-extensions/src/ExtensionPoint.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import binder from "./binder"; + +type Props = { + name: string; + renderAll?: boolean; + props?: object; + children?: React.ReactNode; +}; + +/** + * ExtensionPoint renders components which are bound to an extension point. + */ +class ExtensionPoint extends React.Component<Props> { + renderAll(name: string, props?: object) { + const extensions = binder.getExtensions(name, props); + return ( + <> + {extensions.map((Component, index) => { + return <Component key={index} {...props} />; + })} + </> + ); + } + + renderSingle(name: string, props?: object) { + const Component = binder.getExtension(name, props); + if (!Component) { + return null; + } + return <Component {...props} />; + } + + renderDefault() { + const { children } = this.props; + if (children) { + return <>{children}</>; + } + return null; + } + + render() { + const { name, renderAll, props } = this.props; + if (!binder.hasExtension(name, props)) { + return this.renderDefault(); + } else if (renderAll) { + return this.renderAll(name, props); + } + return this.renderSingle(name, props); + } +} + +export default ExtensionPoint; diff --git a/scm-ui/ui-extensions/src/binder.test.ts b/scm-ui/ui-extensions/src/binder.test.ts new file mode 100644 index 0000000000..2bcd302a36 --- /dev/null +++ b/scm-ui/ui-extensions/src/binder.test.ts @@ -0,0 +1,57 @@ +import { Binder } from "./binder"; + +describe("binder tests", () => { + let binder: Binder; + + beforeEach(() => { + binder = new Binder(); + }); + + it("should return an empty array for non existing extension points", () => { + const extensions = binder.getExtensions("hitchhiker"); + expect(extensions).toEqual([]); + }); + + it("should return the binded extensions", () => { + binder.bind("hitchhicker.trillian", "heartOfGold"); + binder.bind("hitchhicker.trillian", "earth"); + + const extensions = binder.getExtensions("hitchhicker.trillian"); + expect(extensions).toEqual(["heartOfGold", "earth"]); + }); + + it("should return the first bound extension", () => { + binder.bind("hitchhicker.trillian", "heartOfGold"); + binder.bind("hitchhicker.trillian", "earth"); + + expect(binder.getExtension("hitchhicker.trillian")).toBe("heartOfGold"); + }); + + it("should return null if no extension was bound", () => { + expect(binder.getExtension("hitchhicker.trillian")).toBe(null); + }); + + it("should return true, if an extension is bound", () => { + binder.bind("hitchhicker.trillian", "heartOfGold"); + expect(binder.hasExtension("hitchhicker.trillian")).toBe(true); + }); + + it("should return false, if no extension is bound", () => { + expect(binder.hasExtension("hitchhicker.trillian")).toBe(false); + }); + + type Props = { + category: string; + }; + + it("should return only extensions which predicates matches", () => { + binder.bind("hitchhicker.trillian", "heartOfGold", (props: Props) => props.category === "a"); + binder.bind("hitchhicker.trillian", "earth", (props: Props) => props.category === "b"); + binder.bind("hitchhicker.trillian", "earth2", (props: Props) => props.category === "a"); + + const extensions = binder.getExtensions("hitchhicker.trillian", { + category: "b" + }); + expect(extensions).toEqual(["earth"]); + }); +}); diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts new file mode 100644 index 0000000000..a359973a50 --- /dev/null +++ b/scm-ui/ui-extensions/src/binder.ts @@ -0,0 +1,78 @@ +type Predicate = (props: any) => boolean; + +type ExtensionRegistration = { + predicate: Predicate; + extension: any; +}; + +/** + * Binder is responsible for binding plugin extensions to their corresponding extension points. + * The Binder class is mainly exported for testing, plugins should only use the default export. + */ +export class Binder { + extensionPoints: { + [key: string]: Array<ExtensionRegistration>; + }; + + constructor() { + this.extensionPoints = {}; + } + + /** + * Binds an extension to the extension point. + * + * @param extensionPoint name of extension point + * @param extension provided extension + * @param predicate to decide if the extension gets rendered for the given props + */ + bind(extensionPoint: string, extension: any, predicate?: Predicate) { + if (!this.extensionPoints[extensionPoint]) { + this.extensionPoints[extensionPoint] = []; + } + const registration = { + predicate: predicate ? predicate : () => true, + extension + }; + this.extensionPoints[extensionPoint].push(registration); + } + + /** + * Returns the first extension or null for the given extension point and its props. + * + * @param extensionPoint name of extension point + * @param props of the extension point + */ + getExtension(extensionPoint: string, props?: object) { + const extensions = this.getExtensions(extensionPoint, props); + if (extensions.length > 0) { + return extensions[0]; + } + return null; + } + + /** + * Returns all registered extensions for the given extension point and its props. + * + * @param extensionPoint name of extension point + * @param props of the extension point + */ + getExtensions(extensionPoint: string, props?: object): Array<any> { + let registrations = this.extensionPoints[extensionPoint] || []; + if (props) { + registrations = registrations.filter(reg => reg.predicate(props || {})); + } + return registrations.map(reg => reg.extension); + } + + /** + * Returns true if at least one extension is bound to the extension point and its props. + */ + hasExtension(extensionPoint: string, props?: object): boolean { + return this.getExtensions(extensionPoint, props).length > 0; + } +} + +// singleton binder +const binder = new Binder(); + +export default binder; diff --git a/scm-ui/ui-extensions/src/index.ts b/scm-ui/ui-extensions/src/index.ts new file mode 100644 index 0000000000..73267e29a3 --- /dev/null +++ b/scm-ui/ui-extensions/src/index.ts @@ -0,0 +1,2 @@ +export { default as binder } from "./binder"; +export { default as ExtensionPoint } from "./ExtensionPoint"; diff --git a/scm-ui/ui-extensions/src/testing/Label.tsx b/scm-ui/ui-extensions/src/testing/Label.tsx new file mode 100644 index 0000000000..78e2c5c877 --- /dev/null +++ b/scm-ui/ui-extensions/src/testing/Label.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +type Props = { + value: string; +}; + +class Label extends React.Component<Props> { + render() { + return <label>{this.props.value}</label>; + } +} + +export default Label; diff --git a/scm-ui/ui-extensions/tsconfig.json b/scm-ui/ui-extensions/tsconfig.json new file mode 100644 index 0000000000..7e3ee63a2d --- /dev/null +++ b/scm-ui/ui-extensions/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@scm-manager/tsconfig" +} diff --git a/scm-ui/ui-plugins/package.json b/scm-ui/ui-plugins/package.json new file mode 100644 index 0000000000..2e1fdb119f --- /dev/null +++ b/scm-ui/ui-plugins/package.json @@ -0,0 +1,39 @@ +{ + "name": "@scm-manager/ui-plugins", + "version": "2.0.0-SNAPSHOT", + "license": "BSD-3-Clause", + "dependencies": { + "@scm-manager/babel-preset": "^2.0.0-SNAPSHOT", + "@scm-manager/eslint-config": "^2.0.0-SNAPSHOT", + "@scm-manager/jest-preset": "^2.0.0-SNAPSHOT", + "@scm-manager/prettier-config": "^2.0.0-SNAPSHOT", + "@scm-manager/tsconfig": "^2.0.0-SNAPSHOT", + "@scm-manager/ui-components": "^2.0.0-SNAPSHOT", + "@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT", + "@scm-manager/ui-scripts": "^2.0.0-SNAPSHOT", + "@scm-manager/ui-tests": "^2.0.0-SNAPSHOT", + "@scm-manager/ui-types": "^2.0.0-SNAPSHOT", + "@types/classnames": "^2.2.9", + "@types/enzyme": "^3.10.3", + "@types/fetch-mock": "^7.3.1", + "@types/i18next": "^13.0.0", + "@types/jest": "^24.0.19", + "@types/query-string": "5", + "@types/react": "^16.9.9", + "@types/react-redux": "5.0.7", + "@types/react-router-dom": "^5.1.0", + "@types/styled-components": "^4.1.19", + "classnames": "^2.2.6", + "jest": "^24.9.0", + "query-string": "^5.0.1", + "react": "^16.10.2", + "react-i18next": "^10.13.1", + "react-redux": "^5.0.7", + "react-router-dom": "^5.1.2", + "redux": "^4.0.0", + "styled-components": "^4.4.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/scm-ui/ui-polyfill/package.json b/scm-ui/ui-polyfill/package.json new file mode 100644 index 0000000000..81922e37c8 --- /dev/null +++ b/scm-ui/ui-polyfill/package.json @@ -0,0 +1,14 @@ +{ + "name": "@scm-manager/ui-polyfill", + "version": "2.0.0-SNAPSHOT", + "description": "Polyfills for SCM-Manager UI", + "main": "src/index.js", + "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", + "license": "BSD-3-Clause", + "private": true, + "prettier": "@scm-manager/prettier-config", + "dependencies": { + "@babel/polyfill": "^7.6.0", + "whatwg-fetch": "^3.0.0" + } +} diff --git a/scm-ui/ui-polyfill/src/index.js b/scm-ui/ui-polyfill/src/index.js new file mode 100644 index 0000000000..ad6c931796 --- /dev/null +++ b/scm-ui/ui-polyfill/src/index.js @@ -0,0 +1,2 @@ +import "@babel/polyfill"; +import "whatwg-fetch"; diff --git a/scm-ui/ui-scripts/bin/ui-scripts.js b/scm-ui/ui-scripts/bin/ui-scripts.js new file mode 100755 index 0000000000..41549ac27a --- /dev/null +++ b/scm-ui/ui-scripts/bin/ui-scripts.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node +const { spawnSync } = require("child_process"); + +const commands = ["plugin", "plugin-watch", "publish"]; + +const args = process.argv.slice(2); + +const commandIndex = args.findIndex(arg => { + return commands.includes(arg); +}); + +const command = commandIndex === -1 ? args[0] : args[commandIndex]; +const nodeArgs = commandIndex > 0 ? args.slice(0, commandIndex) : []; + +if (commands.includes(command)) { + const result = spawnSync( + "node", + nodeArgs + .concat(require.resolve("../src/commands/" + command)) + .concat(args.slice(commandIndex + 1)), + { stdio: "inherit" } + ); + if (result.signal) { + if (result.signal === "SIGKILL") { + console.log( + "The build failed because the process exited too early. " + + "This probably means the system ran out of memory or someone called " + + "`kill -9` on the process." + ); + } else if (result.signal === "SIGTERM") { + console.log( + "The build failed because the process exited too early. " + + "Someone might have called `kill` or `killall`, or the system could " + + "be shutting down." + ); + } + process.exit(1); + } + process.exit(result.status); +} else { + console.log("Unknown script \"" + command + "\"."); + console.log("Perhaps you need to update react-scripts?"); + console.log( + "See: https://facebook.github.io/create-react-app/docs/updating-to-new-releases" + ); +} diff --git a/scm-ui/ui-scripts/package.json b/scm-ui/ui-scripts/package.json new file mode 100644 index 0000000000..7ef5d2819c --- /dev/null +++ b/scm-ui/ui-scripts/package.json @@ -0,0 +1,38 @@ +{ + "name": "@scm-manager/ui-scripts", + "version": "2.0.0-SNAPSHOT", + "description": "Build scripts for SCM-Manager", + "main": "src/index.js", + "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", + "license": "BSD-3-Clause", + "private": false, + "bin": { + "ui-scripts": "./bin/ui-scripts.js" + }, + "dependencies": { + "babel-loader": "^8.0.6", + "cache-loader": "^4.1.0", + "css-loader": "^3.2.0", + "file-loader": "^4.2.0", + "mini-css-extract-plugin": "^0.8.0", + "mustache": "^3.1.0", + "node-sass": "^4.12.0", + "optimize-css-assets-webpack-plugin": "^5.0.3", + "sass-loader": "^8.0.0", + "script-loader": "^0.7.2", + "style-loader": "^1.0.0", + "thread-loader": "^2.1.3", + "webpack": "^4.41.1", + "webpack-cli": "^3.3.9", + "webpack-dev-server": "^3.8.2" + }, + "eslintConfig": { + "extends": "@scm-manager/eslint-config", + "rules": { + "no-console": "off" + } + }, + "publishConfig": { + "access": "public" + } +} diff --git a/scm-ui/ui-scripts/src/commands/plugin-watch.js b/scm-ui/ui-scripts/src/commands/plugin-watch.js new file mode 100644 index 0000000000..158610f954 --- /dev/null +++ b/scm-ui/ui-scripts/src/commands/plugin-watch.js @@ -0,0 +1,11 @@ +const webpack = require("webpack"); +const createPluginConfig = require("../createPluginConfig"); + +const config = createPluginConfig("development"); +const compiler = webpack(config); + +compiler.watch({}, (err, stats) => { + console.log(stats.toString({ + colors: true + })); +}); diff --git a/scm-ui/ui-scripts/src/commands/plugin.js b/scm-ui/ui-scripts/src/commands/plugin.js new file mode 100644 index 0000000000..64b691ef5e --- /dev/null +++ b/scm-ui/ui-scripts/src/commands/plugin.js @@ -0,0 +1,13 @@ +const webpack = require("webpack"); +const createPluginConfig = require("../createPluginConfig"); + +const config = createPluginConfig("production"); + +webpack(config, (err, stats) => { + console.log(stats.toString({ + colors: true + })); + if (err || stats.hasErrors()) { + process.exit(1); + } +}); diff --git a/scm-ui/ui-scripts/src/commands/publish.js b/scm-ui/ui-scripts/src/commands/publish.js new file mode 100644 index 0000000000..d3cdc60724 --- /dev/null +++ b/scm-ui/ui-scripts/src/commands/publish.js @@ -0,0 +1,26 @@ +const lerna = require("../lerna"); +const versions = require("../versions"); + +const args = process.argv.slice(2); + +if (args.length < 1) { + console.log("usage ui-scripts publish version"); + process.exit(1); +} + +const version = args[0]; +const index = version.indexOf("-SNAPSHOT"); +if (index > 0) { + const snapshotVersion = version.substring(0, index) + "-" + versions.createSnapshotVersion(); + console.log("publish snapshot release " + snapshotVersion); + lerna.version(snapshotVersion); + lerna.publish(); + lerna.version(version); +} else { + // ?? not sure + lerna.publish(); +} + + + + diff --git a/scm-ui/ui-scripts/src/createPluginConfig.js b/scm-ui/ui-scripts/src/createPluginConfig.js new file mode 100644 index 0000000000..3e210fa022 --- /dev/null +++ b/scm-ui/ui-scripts/src/createPluginConfig.js @@ -0,0 +1,76 @@ +const path = require("path"); +const fs = require("fs"); + +const root = process.cwd(); + +const packageJsonPath = path.join(root, "package.json"); +const packageJSON = JSON.parse(fs.readFileSync(packageJsonPath, { encoding: "UTF-8" })); + +let name = packageJSON.name; +const orgaIndex = name.indexOf("/"); +if (orgaIndex > 0) { + name = name.substring(orgaIndex + 1); +} + +module.exports = function(mode) { + return { + context: root, + entry: { + [name]: [path.resolve(__dirname, "webpack-public-path.js"), packageJSON.main || "src/main/js/index.js"] + }, + mode, + devtool: "source-map", + target: "web", + node: { + fs: "empty", + net: "empty", + tls: "empty" + }, + externals: [ + "react", + "react-dom", + "react-i18next", + "react-router-dom", + "styled-components", + "@scm-manager/ui-types", + "@scm-manager/ui-extensions", + "@scm-manager/ui-components", + "classnames", + "query-string", + "redux", + "react-redux" + ], + module: { + rules: [ + { + test: /\.(js|ts|jsx|tsx)$/i, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: ["@scm-manager/babel-preset"] + } + } + }, + { + test: /\.(css|scss|sass)$/i, + use: ["style-loader", "css-loader", "sass-loader"] + }, + { + test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/, + use: ["file-loader"] + } + ] + }, + resolve: { + extensions: [".ts", ".tsx", ".js", ".jsx", ".css", ".scss", ".json"] + }, + output: { + path: path.join(root, "target", name + "-" + packageJSON.version, "webapp", "assets"), + filename: "[name].bundle.js", + chunkFilename: name + ".[name].chunk.js", + library: name, + libraryTarget: "amd" + } + }; +}; diff --git a/scm-ui/ui-scripts/src/lerna.js b/scm-ui/ui-scripts/src/lerna.js new file mode 100644 index 0000000000..b5404fbd62 --- /dev/null +++ b/scm-ui/ui-scripts/src/lerna.js @@ -0,0 +1,33 @@ +const { spawnSync } = require("child_process"); + +const yarn = args => { + const result = spawnSync("yarn", args, { stdio: "inherit" }); + if (result.error) { + console.log("could not start yarn command:", result.error); + process.exit(2); + } else if (result.status !== 0) { + console.log("yarn process ends with status code:", result.status); + process.exit(3); + } +}; + +const version = version => { + yarn([ + "run", + "lerna", + "--no-git-tag-version", + "--no-push", + "version", + "--yes", + version + ]); +}; + +const publish = () => { + yarn(["run", "lerna", "publish", "from-package", "--yes"]); +}; + +module.exports = { + version, + publish +}; diff --git a/scm-ui/ui-scripts/src/middleware/ContextPathMiddleware.js b/scm-ui/ui-scripts/src/middleware/ContextPathMiddleware.js new file mode 100644 index 0000000000..c24a4c0b25 --- /dev/null +++ b/scm-ui/ui-scripts/src/middleware/ContextPathMiddleware.js @@ -0,0 +1,11 @@ +function crateContextPathMiddleware(contextPath) { + return function(req, resp, next) { + const url = req.url; + if (url.indexOf(contextPath) === 0) { + req.url = url.substr(contextPath.length); + } + next(); + } +} + +module.exports = crateContextPathMiddleware; diff --git a/scm-ui/ui-scripts/src/middleware/IndexMiddleware.js b/scm-ui/ui-scripts/src/middleware/IndexMiddleware.js new file mode 100644 index 0000000000..52eaef8cff --- /dev/null +++ b/scm-ui/ui-scripts/src/middleware/IndexMiddleware.js @@ -0,0 +1,20 @@ +const mustache = require("mustache"); +const fs = require("fs"); +// disable escaping +mustache.escape = function(text) { + return text; +}; + +function createIndexMiddleware(file, params) { + const template = fs.readFileSync(file, { encoding: "UTF-8" }); + return function(req, resp, next) { + if (req.url === "/index.html") { + const content = mustache.render(template, params); + resp.send(content); + } else { + next(); + } + } +} + +module.exports = createIndexMiddleware; diff --git a/scm-ui/ui-scripts/src/versions.js b/scm-ui/ui-scripts/src/versions.js new file mode 100644 index 0000000000..632f4123da --- /dev/null +++ b/scm-ui/ui-scripts/src/versions.js @@ -0,0 +1,16 @@ +const createSnapshotVersion = () => { + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth().toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + const seconds = date.getSeconds().toString().padStart(2, "0"); + + return `${year}${month}${day}-${hours}${minutes}${seconds}`; +}; + +module.exports = { + createSnapshotVersion +}; diff --git a/scm-ui/ui-scripts/src/webpack-public-path.js b/scm-ui/ui-scripts/src/webpack-public-path.js new file mode 100644 index 0000000000..2375a9c104 --- /dev/null +++ b/scm-ui/ui-scripts/src/webpack-public-path.js @@ -0,0 +1,5 @@ +/* global __webpack_public_path__ */ +// setup webpack public path: +// https://stackoverflow.com/questions/39879680/example-of-setting-webpack-public-path-at-runtime +// https://github.com/coryhouse/react-slingshot/pull/207/files +__webpack_public_path__ = window.ctxPath + "/assets/"; diff --git a/scm-ui/ui-scripts/src/webpack.config.js b/scm-ui/ui-scripts/src/webpack.config.js new file mode 100644 index 0000000000..2dd5b4c470 --- /dev/null +++ b/scm-ui/ui-scripts/src/webpack.config.js @@ -0,0 +1,155 @@ +const path = require("path"); +const createIndexMiddleware = require("./middleware/IndexMiddleware"); +const createContextPathMiddleware = require("./middleware/ContextPathMiddleware"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); + +const root = path.resolve(process.cwd(), "scm-ui"); + +module.exports = [ + { + context: root, + entry: { + webapp: [path.resolve(__dirname, "webpack-public-path.js"), "./ui-webapp/src/index.tsx"] + }, + devtool: "cheap-module-eval-source-map", + target: "web", + node: { + fs: "empty", + net: "empty", + tls: "empty" + }, + module: { + rules: [ + { + parser: { + system: false, + systemjs: false + } + }, + { + test: /\.(js|ts|jsx|tsx)$/i, + exclude: /node_modules/, + use: [ + { + loader: "cache-loader" + }, + { + loader: "thread-loader" + }, + { + loader: "babel-loader", + options: { + cacheDirectory: true, + presets: ["@scm-manager/babel-preset"] + } + } + ] + }, + { + test: /\.(css|scss|sass)$/i, + use: [ + // Creates `style` nodes from JS strings + "style-loader", + // Translates CSS into CommonJS + "css-loader", + // Compiles Sass to CSS + "sass-loader" + ] + }, + { + test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/, + use: ["file-loader"] + } + ] + }, + resolve: { + extensions: [".ts", ".tsx", ".js", ".jsx", ".css", ".scss", ".json"] + }, + output: { + path: path.join(root, "target", "assets"), + filename: "[name].bundle.js" + }, + devServer: { + contentBase: path.join(root, "ui-webapp", "public"), + compress: false, + historyApiFallback: true, + overlay: true, + port: 3000, + before: function(app) { + app.use(createContextPathMiddleware("/scm")); + }, + after: function(app) { + const templatePath = path.join(root, "ui-webapp", "public", "index.mustache"); + const renderParams = { + contextPath: "/scm" + }; + app.use(createIndexMiddleware(templatePath, renderParams)); + }, + publicPath: "/assets/" + }, + optimization: { + runtimeChunk: "single", + splitChunks: { + chunks: "all", + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + priority: -10 + // chunks: chunk => chunk.name !== "polyfill" + }, + default: { + minChunks: 2, + priority: -20, + reuseExistingChunk: true + } + } + } + } + }, + { + context: root, + entry: "./ui-styles/src/scm.scss", + module: { + rules: [ + { + test: /\.(css|scss|sass)$/i, + use: [ + { + loader: MiniCssExtractPlugin.loader + }, + "css-loader", + "sass-loader" + ] + }, + { + test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/, + use: ["file-loader"] + } + ] + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: "ui-styles.css", + ignoreOrder: false + }) + ], + optimization: { + minimizer: [new OptimizeCSSAssetsPlugin({})] + }, + output: { + path: path.join(root, "target", "assets"), + filename: "ui-styles.bundle.js" + } + }, + { + context: path.resolve(root), + entry: { + polyfills: "./ui-polyfill/src/index.js" + }, + output: { + path: path.resolve(root, "target", "assets"), + filename: "[name].bundle.js" + } + } +]; diff --git a/scm-ui/ui-styles/package.json b/scm-ui/ui-styles/package.json new file mode 100644 index 0000000000..16fdb5b307 --- /dev/null +++ b/scm-ui/ui-styles/package.json @@ -0,0 +1,27 @@ +{ + "name": "@scm-manager/ui-styles", + "version": "2.0.0-SNAPSHOT", + "description": "Styles for SCM-Manager", + "main": "src/scm.scss", + "license": "BSD-3-Clause", + "private": true, + "scripts": { + "serve": "webpack-dev-server" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^5.11.2", + "bulma": "^0.7.5", + "bulma-popover": "^1.0.0", + "bulma-tooltip": "^3.0.0", + "react-diff-view": "^1.8.1" + }, + "devDependencies": { + "css-loader": "^3.2.0", + "node-sass": "^4.12.0", + "sass-loader": "^8.0.0", + "style-loader": "^1.0.0", + "webpack": "^4.41.0", + "webpack-dev-server": "^3.8.2" + }, + "prettier": "@scm-manager/prettier-config" +} diff --git a/scm-ui/ui-styles/public/index.html b/scm-ui/ui-styles/public/index.html new file mode 100644 index 0000000000..504398bfa1 --- /dev/null +++ b/scm-ui/ui-styles/public/index.html @@ -0,0 +1,979 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>SCM-Manager Styleguide + + + + + + + + + +
+
+
+
+
+

Styleguide SCM-Manager 2

+
Important:
+ This overview is focused on visual, not structural or functional aspects. It may contain class names not yet integrated in SCM-Manager and can only display the present state. There may be changes to some elements in the near future.
+
+
+
+

Colors

+
+
Overview for all main colors with examples for lighter nuances.
+
    +
  • Colors with 75%-25% opacity will only be used for elements like graphs (where there is a need for extra color shades) and relevant interaction states (like disabled buttons).
  • +
  • As soon as there is a use case where the separation of the colors for "info" and "link" seems necessary, it will be possible to change the current color assignment.
  • +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Class Color (100%) Color (75%) Color (50%) Color (25%)
.has-background-dark
+ .has-background-dark-75
+ .has-background-dark-50
+ .has-background-dark-25
 
+
+ #363636
 
+
+ #686868
 
+
+ #9a9a9a
 
+
+ #cdcdcd
.has-background-info
+ .has-background-info-75
+ .has-background-info-50
+ .has-background-info-25
 
+
+ #33b2e8
 
+
+ #66c5ee
 
+
+ #99d8f3
 
+
+ #ccecf9
.has-background-link
+ .has-background-link-75
+ .has-background-link-50
+ .has-background-link-25
 
+
+ #33b2e8
 
+
+ #66c5ee
 
+
+ #99d8f3
 
+
+ #ccecf9
.has-background-primary
+ .has-background-primary-75
+ .has-background-primary-50
+ .has-background-primary-25
 
+
+ #00d1df
 
+
+ #40dde7
 
+
+ #7fe8ef
 
+
+ #bff3f7
.has-background-success
+ .has-background-success-75
+ .has-background-success-50
+ .has-background-success-25
 
+
+ #00c79b
 
+
+ #40d5b4
 
+
+ #7fe3cd
 
+
+ #bff1e6
.has-background-warning
+ .has-background-warning-75
+ .has-background-warning-50
+ .has-background-warning-25
 
+
+ #ffdd57
 
+
+ #ffe681
 
+
+ #ffeeab
 
+
+ #fff6d5
.has-background-danger
+ .has-background-danger-75
+ .has-background-danger-50
+ .has-background-danger-25
 
+
+ #ff3860
 
+
+ #ff6a88
 
+
+ #ff9baf
 
+
+ #ffcdd7
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Class Text
.has-text-dark
Beispieltext
.has-text-info
Beispieltext
.has-text-link
Beispieltext
.has-text-primary
Beispieltext
.has-text-success
Beispieltext
.has-text-warning Attention: Has color #FFB600 because of readability issues with the original color #ffdd57.Beispieltext
.has-text-danger
Beispieltext
+
+
+

Hero & Main Navigation (Tab-based)

+
+
May contain mobile hamburger menu in the future?
+
+
+
+
+
+
+
SCM-Manager
+
+
+
+
+ +
+
+
+
+

Sidebar Navigation

+
+
+
+
+
+
    +
  • Plugins are always listed after "Information", "Commits" and "Sources".
  • +
  • "Settings" is always located at the bottom.
  • +
  • The order of the navigation elements should be consistent for all subpages.
  • +
  • Icons for plugins and submenus are still missing.
  • +
+
+
+
+
+
+ +
+
+
+
+
+
+

Buttons

+
+
+
+

Standard Buttons

+
Standard Buttons are using the full base colors.
+
+
+
+
+ +
+
+
+
+
+
+

Disabled buttons

+
Disabled buttons are using 25% of the base colors.
+
+
+
+
+ +
+
+
+
+
+
+

Hovered buttons

+
The hover color of each button is 10% darker than the base color.
+
+
+
+
+ +
+
+
+
+
+
+

Active buttons

+
The active color of each button is 20% darker than the base color.
+
+
+
+
+
+ +
+
+
+
+
+
+
+

Button groups and addons

+
+
    +
  • If buttons are part of the same functional group they can be displayed as "addon buttons".
  • +
  • Avoid buttons sticking together without being addon buttons.
  • +
  • If you don't want buttons to be addons, please place enough space between them.
  • +
  • In cases where just the first button is visible and others might be added later in the process, or buttons aren't part of the same functional group, "grouped buttons" can be used.
  • +
  • If the contents of buttons have a direct connection (like "Create" and "Delete") they should be placed next to each other.
  • +
+
+
Button addon
+
+ +
+
Button group
+
+ +
+
+

Pagination

+
Disabled pagination
+
+
Disabled navigation buttons are either grey or light blue.
+ +
+
+
Active pagination
+
+
Active navigation buttons are either white or blue (100% tone of info/link color).
+ The buttons containing the page numbers should have the same width, even when reaching triple digits. For this a width of 80px is necessary.
+ +
+
+

Create

+
"Create" buttons at the top of pages should always be positioned on the right side.
+ Create buttons at the end of a page should be full-width and complemented with a border.
+
+ +
+
+ +
+
+
+

Submit

+
When a primary action like a "submit" button for a form is used, it should be placed on the bottom right of the page.
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+
+
+

Add and delete

+
Icons are added before the text to mark these buttons as special elements.
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+
+
+

Button texts

+
Button letterings should always be precise. Example: Use "create user" instead of "create" for button lettering.
+
+
+
+
+
+

+ +

+

+ +

+

+ +

+
+
+
+
+
+
+
+
+

Miscellaneous elements

+
+ +

Modals

+
Even if the content of the modals differs, they all should use the same basic classes .modal-content and .modal-card to ensure a constant layout. Buttons inside the modals should use the standard button formats.
+
+
+ + +
+
+

Standard tables

+
Titles inside the tables should be in bold letters.
+ + + + + + + + + + + + + + + + + + + + + + + +
NameG1
DescriptionAwesome
Typexml
Creation Date5 days ago
Last Modified15 minutes ago
+
+
+

Group and user tables

+
Special tables can be used for group- or user-lists. Colors at the left side could display roles, for example green = normal user, yellow = admin.
+ +
+
+

Forms

+
If possible, forms should be divided into two columns to get a better grasp of the content. It is recommended to divide unrelated content with horizontal lines.
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+

Radio buttons

+
Radio buttons should have enough space between them and the labels.
+
+

Add new Permission

+
+
+ + +
+
+
+
+
+
+

Select

+
Select elements should always contain placeholder texts.
+
+
+
+
+
+ +
+
+
+
+
+
+

Headlines

+
Subheadlines should only be used if there is reasonable content for them.
+
+
+

Users

+

Create, read, update and delete users

+
+
+ +
+
+

Tags

+
Tags can use background in all full base colors.
+   tip OPEN Warning +

+ +
+

Statistics

+
Already done but still in review? Add later?
+ +
+
+

Notifications

+
Notification are using the full base colors.
+
+
+
+ + Primary/Success
+ Primar lorem ipsum dolor sit amet, consectetur + adipiscing elit lorem ipsum dolor. Pellentesque risus mi, tempus quis placerat ut, porta nec nulla. Vestibulum rhoncus ac ex sit amet fringilla. Nullam gravida purus diam, et dictum felis venenatis efficitur.
+
+
+
+ + Info/Link
+ Primar lorem ipsum dolor sit amet, consectetur + adipiscing elit lorem ipsum dolor. Pellentesque risus mi, tempus quis placerat ut, porta nec nulla. Vestibulum rhoncus ac ex sit amet fringilla. Nullam gravida purus diam, et dictum felis venenatis efficitur.
+
+
+
+ + Success
+ Primar lorem ipsum dolor sit amet, consectetur + adipiscing elit lorem ipsum dolor. Pellentesque risus mi, tempus quis placerat ut, porta nec nulla. Vestibulum rhoncus ac ex sit amet fringilla. Nullam gravida purus diam, et dictum felis venenatis efficitur.
+
+
+
+
+
+ + Warning
+ Primar lorem ipsum dolor sit amet, consectetur + adipiscing elit lorem ipsum dolor. Pellentesque risus mi, tempus quis placerat ut, porta nec nulla. Vestibulum rhoncus ac ex sit amet fringilla. Nullam gravida purus diam, et dictum felis venenatis efficitur.
+
+
+
+ + Danger
+ Primar lorem ipsum dolor sit amet, consectetur + adipiscing elit lorem ipsum dolor. Pellentesque risus mi, tempus quis placerat ut, porta nec nulla. Vestibulum rhoncus ac ex sit amet fringilla. Nullam gravida purus diam, et dictum felis venenatis efficitur.
+
+
+
+
+
+
+
+

Examples of use

+
+ +

Display of users (e.g. changesets)

+
Use tags and different font-weights to visually structure information.
+

+
+

Sebastian Sdorra

+
+

Sebastian Sdorra s.sdorra@gmail.com

+

Changeset e7468dd was committed 2 months ago

+
+
 
+
+


+

+
+
+

Complex screens (e.g. pull requests)

+
+ Screens with a big bunch of information should be carefully structured to help the user with orientation. +
    +
  • Make sure areas with different functionalities are clearly separated (use horizontal lines, boxes and/or bigger gaps between them).
  • +
  • Buttons with different actions should have different colors.
  • +
  • The "reject pull request" and "Merge pull request" buttons are good examples for a meaningful usage of addon buttons.
  • +
  • Information belonging together should be placed next to each other (for example the user who did the last edit and the date of the last edit).
  • +
  • Make sure important states like "open" are properly marked. For example, with tags.
  • +
+
+
+
+
+
+
+

#1 Merge-Conflicts

+
+   Edit pull request
+
+
+
feature/e   master
+
+
OPEN
+
+
+
Hier gibt es Merge Konflikte
+
+
+
+
msuewer a month ago
+ +
+
+ +
+

+

+

+

+
+
+
+ +
+
+
+
+

msuewer a month ago
+ Testkommentar
+

+
+
+
+ +
+
+
+
+
+

msuewer a month ago
+ das c ist falsch!
+

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+

+ +

+
+
+
+
+
+
+
+
+
+

Group Member List

+
Is there a new version of it which is still in review? Add later?
+ + + + + + +
Members +
+ +
+
+
+
+
+ + + + + diff --git a/scm-ui/ui-styles/public/js/polyfills.js b/scm-ui/ui-styles/public/js/polyfills.js new file mode 100644 index 0000000000..a3c9c7cab7 --- /dev/null +++ b/scm-ui/ui-styles/public/js/polyfills.js @@ -0,0 +1,535 @@ +!function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var c="function"==typeof require&&require;if(!u&&c)return c(o,!0);if(i)return i(o,!0);var a=new Error("Cannot find module '"+o+"'");throw a.code="MODULE_NOT_FOUND",a}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(n){var r=t[o][1][n];return s(r||n)},f,f.exports,e,t,n,r)}return n[o].exports}for(var i="function"==typeof require&&require,o=0;o2?arguments[2]:void 0,s=Math.min((void 0===f?u:i(f,u))-a,u-c),l=1;for(a0;)a in r?r[c]=r[a]:delete r[c],c+=l,a+=l;return r}},{110:110,114:114,115:115}],17:[function(t,n,r){"use strict";var e=t(115),i=t(110),o=t(114);n.exports=function fill(t){for(var n=e(this),r=o(n.length),u=arguments.length,c=i(u>1?arguments[1]:void 0,r),a=u>2?arguments[2]:void 0,f=void 0===a?r:i(a,r);f>c;)n[c++]=t;return n}},{110:110,114:114,115:115}],18:[function(t,n,r){var e=t(113),i=t(114),o=t(110);n.exports=function(t){return function(n,r,u){var c,a=e(n),f=i(a.length),s=o(u,f);if(t&&r!=r){for(;f>s;)if((c=a[s++])!=c)return!0}else for(;f>s;s++)if((t||s in a)&&a[s]===r)return t||s||0;return!t&&-1}}},{110:110,113:113,114:114}],19:[function(t,n,r){var e=t(31),i=t(52),o=t(115),u=t(114),c=t(22);n.exports=function(t,n){var r=1==t,a=2==t,f=3==t,s=4==t,l=6==t,h=5==t||l,p=n||c;return function(n,c,v){for(var y,d,g=o(n),m=i(g),x=e(c,v,3),b=u(m.length),w=0,S=r?p(n,b):a?p(n,0):void 0;b>w;w++)if((h||w in m)&&(y=m[w],d=x(y,w,g),t))if(r)S[w]=d;else if(d)switch(t){case 3:return!0;case 5:return y;case 6:return w;case 2:S.push(y)}else if(s)return!1;return l?-1:f||s?s:S}}},{114:114,115:115,22:22,31:31,52:52}],20:[function(t,n,r){var e=t(11),i=t(115),o=t(52),u=t(114);n.exports=function(t,n,r,c,a){e(n);var f=i(t),s=o(f),l=u(f.length),h=a?l-1:0,p=a?-1:1;if(r<2)for(;;){if(h in s){c=s[h],h+=p;break}if(h+=p,a?h<0:l<=h)throw TypeError("Reduce of empty array with no initial value")}for(;a?h>=0:l>h;h+=p)h in s&&(c=n(c,s[h],h,f));return c}},{11:11,114:114,115:115,52:52}],21:[function(t,n,r){var e=t(56),i=t(54),o=t(125)("species");n.exports=function(t){var n;return i(t)&&(n=t.constructor,"function"!=typeof n||n!==Array&&!i(n.prototype)||(n=void 0),e(n)&&null===(n=n[o])&&(n=void 0)),void 0===n?Array:n}},{125:125,54:54,56:56}],22:[function(t,n,r){var e=t(21);n.exports=function(t,n){return new(e(t))(n)}},{21:21}],23:[function(t,n,r){"use strict";var e=t(11),i=t(56),o=t(51),u=[].slice,c={},a=function(t,n,r){if(!(n in c)){for(var e=[],i=0;i1?arguments[1]:void 0,3);r=r?r.n:this._f;)for(e(r.v,r.k,this);r&&r.r;)r=r.p},has:function has(t){return!!d(v(this,n),t)}}),h&&e(s.prototype,"size",{get:function(){return v(this,n)[y]}}),s},def:function(t,n,r){var e,i,o=d(t,n);return o?o.v=r:(t._l=o={i:i=p(n,!0),k:n,v:r,p:e=t._l,n:void 0,r:!1},t._f||(t._f=o),e&&(e.n=o),t[y]++,"F"!==i&&(t._i[i]=o)),t},getEntry:d,setStrong:function(t,n,r){f(t,n,function(t,r){this._t=v(t,n),this._k=r,this._l=void 0},function(){for(var t=this,n=t._k,r=t._l;r&&r.r;)r=r.p;return t._t&&(t._l=r=r?r.n:t._t._f)?"keys"==n?s(0,r.k):"values"==n?s(0,r.v):s(0,[r.k,r.v]):(t._t=void 0,s(1))},r?"entries":"values",!r,!0),l(n)}}},{122:122,14:14,31:31,35:35,44:44,60:60,62:62,69:69,73:73,74:74,92:92,96:96}],27:[function(t,n,r){"use strict";var e=t(92),i=t(69).getWeak,o=t(15),u=t(56),c=t(14),a=t(44),f=t(19),s=t(46),l=t(122),h=f(5),p=f(6),v=0,y=function(t){return t._l||(t._l=new d)},d=function(){this.a=[]},g=function(t,n){return h(t.a,function(t){return t[0]===n})};d.prototype={get:function(t){var n=g(this,t);if(n)return n[1]},has:function(t){return!!g(this,t)},set:function(t,n){var r=g(this,t);r?r[1]=n:this.a.push([t,n])},delete:function(t){var n=p(this.a,function(n){return n[0]===t});return~n&&this.a.splice(n,1),!!~n}},n.exports={getConstructor:function(t,n,r,o){var f=t(function(t,e){c(t,f,n,"_i"),t._t=n,t._i=v++,t._l=void 0,void 0!=e&&a(e,r,t[o],t)});return e(f.prototype,{delete:function(t){if(!u(t))return!1;var r=i(t);return!0===r?y(l(this,n)).delete(t):r&&s(r,this._i)&&delete r[this._i]},has:function has(t){if(!u(t))return!1;var r=i(t);return!0===r?y(l(this,n)).has(t):r&&s(r,this._i)}}),f},def:function(t,n,r){var e=i(o(n),!0);return!0===e?y(t).set(n,r):e[t._i]=r,t},ufstore:y}},{122:122,14:14,15:15,19:19,44:44,46:46,56:56,69:69,92:92}],28:[function(t,n,r){"use strict";var e=t(45),i=t(39),o=t(93),u=t(92),c=t(69),a=t(44),f=t(14),s=t(56),l=t(41),h=t(61),p=t(97),v=t(50);n.exports=function(t,n,r,y,d,g){var m=e[t],x=m,b=d?"set":"add",w=x&&x.prototype,S={},_=function(t){var n=w[t];o(w,t,"delete"==t?function(t){return!(g&&!s(t))&&n.call(this,0===t?0:t)}:"has"==t?function has(t){return!(g&&!s(t))&&n.call(this,0===t?0:t)}:"get"==t?function get(t){return g&&!s(t)?void 0:n.call(this,0===t?0:t)}:"add"==t?function add(t){return n.call(this,0===t?0:t),this}:function set(t,r){return n.call(this,0===t?0:t,r),this})};if("function"==typeof x&&(g||w.forEach&&!l(function(){(new x).entries().next()}))){var E=new x,F=E[b](g?{}:-0,1)!=E,O=l(function(){E.has(1)}),P=h(function(t){new x(t)}),I=!g&&l(function(){for(var t=new x,n=5;n--;)t[b](n,n);return!t.has(-0)});P||(x=n(function(n,r){f(n,x,t);var e=v(new m,n,x);return void 0!=r&&a(r,d,e[b],e),e}),x.prototype=w,w.constructor=x),(O||I)&&(_("delete"),_("has"),d&&_("get")),(I||F)&&_(b),g&&w.clear&&delete w.clear}else x=y.getConstructor(n,t,d,b),u(x.prototype,r),c.NEED=!0;return p(x,t),S[t]=x,i(i.G+i.W+i.F*(x!=m),S),g||y.setStrong(x,t,d),x}},{14:14,39:39,41:41,44:44,45:45,50:50,56:56,61:61,69:69,92:92,93:93,97:97}],29:[function(t,n,r){var e=n.exports={version:"2.5.7"};"number"==typeof __e&&(__e=e)},{}],30:[function(t,n,r){"use strict";var e=t(74),i=t(91);n.exports=function(t,n,r){n in t?e.f(t,n,i(0,r)):t[n]=r}},{74:74,91:91}],31:[function(t,n,r){var e=t(11);n.exports=function(t,n,r){if(e(t),void 0===n)return t;switch(r){case 1:return function(r){return t.call(n,r)};case 2:return function(r,e){return t.call(n,r,e)};case 3:return function(r,e,i){return t.call(n,r,e,i)}}return function(){return t.apply(n,arguments)}}},{11:11}],32:[function(t,n,r){"use strict";var e=t(41),i=Date.prototype.getTime,o=Date.prototype.toISOString,u=function(t){return t>9?t:"0"+t};n.exports=e(function(){return"0385-07-25T07:06:39.999Z"!=o.call(new Date(-5e13-1))})||!e(function(){o.call(new Date(NaN))})?function toISOString(){if(!isFinite(i.call(this)))throw RangeError("Invalid time value");var t=this,n=t.getUTCFullYear(),r=t.getUTCMilliseconds(),e=n<0?"-":n>9999?"+":"";return e+("00000"+Math.abs(n)).slice(e?-6:-4)+"-"+u(t.getUTCMonth()+1)+"-"+u(t.getUTCDate())+"T"+u(t.getUTCHours())+":"+u(t.getUTCMinutes())+":"+u(t.getUTCSeconds())+"."+(r>99?r:"0"+u(r))+"Z"}:o},{41:41}],33:[function(t,n,r){"use strict";var e=t(15),i=t(116);n.exports=function(t){if("string"!==t&&"number"!==t&&"default"!==t)throw TypeError("Incorrect hint");return i(e(this),"number"!=t)}},{116:116,15:15}],34:[function(t,n,r){n.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},{}],35:[function(t,n,r){n.exports=!t(41)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},{41:41}],36:[function(t,n,r){var e=t(56),i=t(45).document,o=e(i)&&e(i.createElement);n.exports=function(t){return o?i.createElement(t):{}}},{45:45,56:56}],37:[function(t,n,r){n.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},{}],38:[function(t,n,r){var e=t(82),i=t(79),o=t(83);n.exports=function(t){var n=e(t),r=i.f;if(r)for(var u,c=r(t),a=o.f,f=0;c.length>f;)a.call(t,u=c[f++])&&n.push(u);return n}},{79:79,82:82,83:83}],39:[function(t,n,r){var e=t(45),i=t(29),o=t(47),u=t(93),c=t(31),a=function(t,n,r){var f,s,l,h,p=t&a.F,v=t&a.G,y=t&a.S,d=t&a.P,g=t&a.B,m=v?e:y?e[n]||(e[n]={}):(e[n]||{}).prototype,x=v?i:i[n]||(i[n]={}),b=x.prototype||(x.prototype={});v&&(r=n);for(f in r)s=!p&&m&&void 0!==m[f],l=(s?m:r)[f],h=g&&s?c(l,e):d&&"function"==typeof l?c(Function.call,l):l,m&&u(m,f,l,t&a.U),x[f]!=l&&o(x,f,h),d&&b[f]!=l&&(b[f]=l)};e.core=i,a.F=1,a.G=2,a.S=4,a.P=8,a.B=16,a.W=32,a.U=64,a.R=128,n.exports=a},{29:29,31:31,45:45,47:47,93:93}],40:[function(t,n,r){var e=t(125)("match");n.exports=function(t){var n=/./;try{"/./"[t](n)}catch(r){try{return n[e]=!1,!"/./"[t](n)}catch(t){}}return!0}},{125:125}],41:[function(t,n,r){n.exports=function(t){try{return!!t()}catch(t){return!0}}},{}],42:[function(t,n,r){"use strict";var e=t(47),i=t(93),o=t(41),u=t(34),c=t(125);n.exports=function(t,n,r){var a=c(t),f=r(u,a,""[t]),s=f[0],l=f[1];o(function(){var n={};return n[a]=function(){return 7},7!=""[t](n)})&&(i(String.prototype,t,s),e(RegExp.prototype,a,2==n?function(t,n){return l.call(t,this,n)}:function(t){return l.call(t,this)}))}},{125:125,34:34,41:41,47:47,93:93}],43:[function(t,n,r){"use strict";var e=t(15);n.exports=function(){var t=e(this),n="";return t.global&&(n+="g"),t.ignoreCase&&(n+="i"),t.multiline&&(n+="m"),t.unicode&&(n+="u"),t.sticky&&(n+="y"),n}},{15:15}],44:[function(t,n,r){var e=t(31),i=t(58),o=t(53),u=t(15),c=t(114),a=t(126),f={},s={},r=n.exports=function(t,n,r,l,h){var p,v,y,d,g=h?function(){return t}:a(t),m=e(r,l,n?2:1),x=0;if("function"!=typeof g)throw TypeError(t+" is not iterable!");if(o(g)){for(p=c(t.length);p>x;x++)if((d=n?m(u(v=t[x])[0],v[1]):m(t[x]))===f||d===s)return d}else for(y=g.call(t);!(v=y.next()).done;)if((d=i(y,m,v.value,n))===f||d===s)return d};r.BREAK=f,r.RETURN=s},{114:114,126:126,15:15,31:31,53:53,58:58}],45:[function(t,n,r){var e=n.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=e)},{}],46:[function(t,n,r){var e={}.hasOwnProperty;n.exports=function(t,n){return e.call(t,n)}},{}],47:[function(t,n,r){var e=t(74),i=t(91);n.exports=t(35)?function(t,n,r){return e.f(t,n,i(1,r))}:function(t,n,r){return t[n]=r,t}},{35:35,74:74,91:91}],48:[function(t,n,r){var e=t(45).document;n.exports=e&&e.documentElement},{45:45}],49:[function(t,n,r){n.exports=!t(35)&&!t(41)(function(){return 7!=Object.defineProperty(t(36)("div"),"a",{get:function(){return 7}}).a})},{35:35,36:36,41:41}],50:[function(t,n,r){var e=t(56),i=t(95).set;n.exports=function(t,n,r){var o,u=n.constructor;return u!==r&&"function"==typeof u&&(o=u.prototype)!==r.prototype&&e(o)&&i&&i(t,o),t}},{56:56,95:95}],51:[function(t,n,r){n.exports=function(t,n,r){var e=void 0===r;switch(n.length){case 0:return e?t():t.call(r);case 1:return e?t(n[0]):t.call(r,n[0]);case 2:return e?t(n[0],n[1]):t.call(r,n[0],n[1]);case 3:return e?t(n[0],n[1],n[2]):t.call(r,n[0],n[1],n[2]);case 4:return e?t(n[0],n[1],n[2],n[3]):t.call(r,n[0],n[1],n[2],n[3])}return t.apply(r,n)}},{}],52:[function(t,n,r){var e=t(25);n.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==e(t)?t.split(""):Object(t)}},{25:25}],53:[function(t,n,r){var e=t(63),i=t(125)("iterator"),o=Array.prototype;n.exports=function(t){return void 0!==t&&(e.Array===t||o[i]===t)}},{125:125,63:63}],54:[function(t,n,r){var e=t(25);n.exports=Array.isArray||function isArray(t){return"Array"==e(t)}},{25:25}],55:[function(t,n,r){var e=t(56),i=Math.floor;n.exports=function isInteger(t){return!e(t)&&isFinite(t)&&i(t)===t}},{56:56}],56:[function(t,n,r){n.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},{}],57:[function(t,n,r){var e=t(56),i=t(25),o=t(125)("match");n.exports=function(t){var n;return e(t)&&(void 0!==(n=t[o])?!!n:"RegExp"==i(t))}},{125:125,25:25,56:56}],58:[function(t,n,r){var e=t(15);n.exports=function(t,n,r,i){try{return i?n(e(r)[0],r[1]):n(r)}catch(n){var o=t.return;throw void 0!==o&&e(o.call(t)),n}}},{15:15}],59:[function(t,n,r){"use strict";var e=t(73),i=t(91),o=t(97),u={};t(47)(u,t(125)("iterator"),function(){return this}),n.exports=function(t,n,r){t.prototype=e(u,{next:i(1,r)}),o(t,n+" Iterator")}},{125:125,47:47,73:73,91:91,97:97}],60:[function(t,n,r){"use strict";var e=t(64),i=t(39),o=t(93),u=t(47),c=t(63),a=t(59),f=t(97),s=t(80),l=t(125)("iterator"),h=!([].keys&&"next"in[].keys()),p=function(){return this};n.exports=function(t,n,r,v,y,d,g){a(r,n,v);var m,x,b,w=function(t){if(!h&&t in F)return F[t];switch(t){case"keys":return function keys(){return new r(this,t)};case"values":return function values(){return new r(this,t)}}return function entries(){return new r(this,t)}},S=n+" Iterator",_="values"==y,E=!1,F=t.prototype,O=F[l]||F["@@iterator"]||y&&F[y],P=O||w(y),I=y?_?w("entries"):P:void 0,A="Array"==n?F.entries||O:O;if(A&&(b=s(A.call(new t)))!==Object.prototype&&b.next&&(f(b,S,!0),e||"function"==typeof b[l]||u(b,l,p)),_&&O&&"values"!==O.name&&(E=!0,P=function values(){return O.call(this)}),e&&!g||!h&&!E&&F[l]||u(F,l,P),c[n]=P,c[S]=p,y)if(m={values:_?P:w("values"),keys:d?P:w("keys"),entries:I},g)for(x in m)x in F||o(F,x,m[x]);else i(i.P+i.F*(h||E),n,m);return m}},{125:125,39:39,47:47,59:59,63:63,64:64,80:80,93:93,97:97}],61:[function(t,n,r){var e=t(125)("iterator"),i=!1;try{var o=[7][e]();o.return=function(){i=!0},Array.from(o,function(){throw 2})}catch(t){}n.exports=function(t,n){if(!n&&!i)return!1;var r=!1;try{var o=[7],u=o[e]();u.next=function(){return{done:r=!0}},o[e]=function(){return u},t(o)}catch(t){}return r}},{125:125}],62:[function(t,n,r){n.exports=function(t,n){return{value:n,done:!!t}}},{}],63:[function(t,n,r){n.exports={}},{}],64:[function(t,n,r){n.exports=!1},{}],65:[function(t,n,r){var e=Math.expm1;n.exports=!e||e(10)>22025.465794806718||e(10)<22025.465794806718||-2e-17!=e(-2e-17)?function expm1(t){return 0==(t=+t)?t:t>-1e-6&&t<1e-6?t+t*t/2:Math.exp(t)-1}:e},{}],66:[function(t,n,r){var e=t(68),i=Math.pow,o=i(2,-52),u=i(2,-23),c=i(2,127)*(2-u),a=i(2,-126),f=function(t){return t+1/o-1/o};n.exports=Math.fround||function fround(t){var n,r,i=Math.abs(t),s=e(t);return ic||r!=r?s*(1/0):s*r)}},{68:68}],67:[function(t,n,r){n.exports=Math.log1p||function log1p(t){return(t=+t)>-1e-8&&t<1e-8?t-t*t/2:Math.log(1+t)}},{}],68:[function(t,n,r){n.exports=Math.sign||function sign(t){return 0==(t=+t)||t!=t?t:t<0?-1:1}},{}],69:[function(t,n,r){var e=t(120)("meta"),i=t(56),o=t(46),u=t(74).f,c=0,a=Object.isExtensible||function(){return!0},f=!t(41)(function(){return a(Object.preventExtensions({}))}),s=function(t){u(t,e,{value:{i:"O"+ ++c,w:{}}})},l=function(t,n){if(!i(t))return"symbol"==typeof t?t:("string"==typeof t?"S":"P")+t;if(!o(t,e)){if(!a(t))return"F";if(!n)return"E";s(t)}return t[e].i},h=function(t,n){if(!o(t,e)){if(!a(t))return!0;if(!n)return!1;s(t)}return t[e].w},p=function(t){return f&&v.NEED&&a(t)&&!o(t,e)&&s(t),t},v=n.exports={KEY:e,NEED:!1,fastKey:l,getWeak:h,onFreeze:p}},{120:120,41:41,46:46,56:56,74:74}],70:[function(t,n,r){var e=t(45),i=t(109).set,o=e.MutationObserver||e.WebKitMutationObserver,u=e.process,c=e.Promise,a="process"==t(25)(u);n.exports=function(){var t,n,r,f=function(){var e,i;for(a&&(e=u.domain)&&e.exit();t;){i=t.fn,t=t.next;try{i()}catch(e){throw t?r():n=void 0,e}}n=void 0,e&&e.enter()};if(a)r=function(){u.nextTick(f)};else if(!o||e.navigator&&e.navigator.standalone)if(c&&c.resolve){var s=c.resolve(void 0);r=function(){s.then(f)}}else r=function(){i.call(e,f)};else{var l=!0,h=document.createTextNode("");new o(f).observe(h,{characterData:!0}),r=function(){h.data=l=!l}}return function(e){var i={fn:e,next:void 0};n&&(n.next=i),t||(t=i,r()),n=i}}},{109:109,25:25,45:45}],71:[function(t,n,r){"use strict";function PromiseCapability(t){var n,r;this.promise=new t(function(t,e){if(void 0!==n||void 0!==r)throw TypeError("Bad Promise constructor");n=t,r=e}),this.resolve=e(n),this.reject=e(r)}var e=t(11);n.exports.f=function(t){return new PromiseCapability(t)}},{11:11}],72:[function(t,n,r){"use strict";var e=t(82),i=t(79),o=t(83),u=t(115),c=t(52),a=Object.assign;n.exports=!a||t(41)(function(){var t={},n={},r=Symbol(),e="abcdefghijklmnopqrst";return t[r]=7,e.split("").forEach(function(t){n[t]=t}),7!=a({},t)[r]||Object.keys(a({},n)).join("")!=e})?function assign(t,n){for(var r=u(t),a=arguments.length,f=1,s=i.f,l=o.f;a>f;)for(var h,p=c(arguments[f++]),v=s?e(p).concat(s(p)):e(p),y=v.length,d=0;y>d;)l.call(p,h=v[d++])&&(r[h]=p[h]);return r}:a},{115:115,41:41,52:52,79:79,82:82,83:83}],73:[function(t,n,r){var e=t(15),i=t(75),o=t(37),u=t(98)("IE_PROTO"),c=function(){},a=function(){var n,r=t(36)("iframe"),e=o.length;for(r.style.display="none",t(48).appendChild(r),r.src="javascript:",n=r.contentWindow.document,n.open(),n.write(" + + + +
+
+ + + + + + + + {{#liveReloadURL}} + + {{/liveReloadURL}} + + diff --git a/scm-ui/ui-webapp/public/locales/de/admin.json b/scm-ui/ui-webapp/public/locales/de/admin.json new file mode 100644 index 0000000000..af34c5b759 --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/de/admin.json @@ -0,0 +1,104 @@ +{ + "admin": { + "menu": { + "navigationLabel": "Administrations Navigation", + "informationNavLink": "Informationen", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell" + }, + "info": { + "currentAppVersion": "Aktuelle Software-Versionsnummer", + "communityTitle": "Community Support", + "communityIconAlt": "Community Support Icon", + "communityInfo": "Das SCM-Manager Support-Team steht für allgemeine Fragen, die Meldung von Fehlern sowie Anfragen für Features gerne für Sie über die offiziellen Kanäle bereit.", + "communityButton": "Unser Team kontaktieren", + "enterpriseTitle": "Enterprise Support", + "enterpriseIconAlt": "Enterprise Support Icon", + "enterpriseInfo": "Sie benötigen Unterstützung bei der Integration von SCM-Manager in Ihre Prozesse, bei der Anpassung des Tools auf Ihre Anforderungen oder einfach ein Service Level Agreement (SLA)?", + "enterprisePartner": "Treten Sie mit unserem Entwicklungs-Partner Cloudogu in Kontakt! Das Team freut sich auf den Austausch über Ihre individuellen Anforderungen und erstellt Ihnen gerne ein maßgeschneidertes Angebot.", + "enterpriseLink": "https://cloudogu.com/de/scm-manager-enterprise/", + "enterpriseButton": "Enterprise Support anfragen" + } + }, + "plugins": { + "title": "Plugins", + "installedSubtitle": "Installierte Plugins", + "availableSubtitle": "Verfügbare Plugins", + "menu": { + "pluginsNavLink": "Plugins", + "installedNavLink": "Installiert", + "availableNavLink": "Verfügbar" + }, + "executePending": "Änderungen ausführen", + "outdatedPlugins": "{{count}} veraltetes Plugin", + "outdatedPlugins_plural": "{{count}} veraltete Plugins", + "updateAll": "Alle Plugins aktualisieren", + "cancelPending": "Änderungen abbrechen", + "noPlugins": "Keine Plugins gefunden.", + "modal": { + "title": { + "install": "{{name}} Plugin installieren", + "update": "{{name}} Plugin aktualisieren", + "uninstall": "{{name}} Plugin deinstallieren" + }, + "restart": "Neustarten, um Plugin-Änderungen wirksam zu machen", + "install": "Installieren", + "update": "Aktualisieren", + "uninstall": "Deinstallieren", + "installQueue": "Werden installiert:", + "updateQueue": "Werden aktualisiert:", + "uninstallQueue": "Werden deinstalliert:", + "installAndRestart": "Installieren und Neustarten", + "updateAndRestart": "Aktualisieren und Neustarten", + "uninstallAndRestart": "Deinstallieren and Neustarten", + "executeAndRestart": "Ausführen und Neustarten", + "updateAll": "Alle Plugins aktualisieren", + "abort": "Abbrechen", + "author": "Autor", + "version": "Version", + "currentVersion": "Installierte Version", + "newVersion": "Neue Version", + "dependencyNotification": "Mit diesem Plugin werden folgende Abhängigkeiten mit installiert, wenn sie noch nicht vorhanden sind!", + "dependencies": "Abhängigkeiten", + "successNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:", + "reload": "jetzt neu laden", + "restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.", + "executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet.", + "cancelPending": "Die folgenden Plugin-Änderungen werden abgebrochen und zurückgesetzt.", + "updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam." + } + }, + "repositoryRole": { + "navLink": "Berechtigungsrollen", + "title": "Berechtigungsrollen", + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Berechtigungsrollen Fehler", + "createSubtitle": "Berechtigungsrolle erstellen", + "editSubtitle": "Berechtigungsrolle bearbeiten", + "overview": { + "title": "Übersicht aller verfügbaren Berechtigungsrollen", + "noPermissionRoles": "Keine Berechtigungsrollen gefunden.", + "createButton": "Berechtigungsrolle erstellen" + }, + "editButton": "Bearbeiten", + "name": "Name", + "type": "Typ", + "verbs": "Berechtigungen", + "system": "System", + "form": { + "name": "Name", + "permissions": "Berechtigungen", + "submit": "Speichern" + }, + "delete": { + "button": "Löschen", + "subtitle": "Berechtigungsrolle löschen", + "confirmAlert": { + "title": "Berechtigungsrolle löschen?", + "message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.", + "submit": "Ja", + "cancel": "Nein" + } + } + } +} diff --git a/scm-ui/ui-webapp/public/locales/de/commons.json b/scm-ui/ui-webapp/public/locales/de/commons.json new file mode 100644 index 0000000000..8c575cc4e9 --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/de/commons.json @@ -0,0 +1,90 @@ +{ + "login": { + "title": "Anmeldung", + "subtitle": "Bitte anmelden, um fortzufahren.", + "logo-alt": "SCM-Manager", + "username-placeholder": "Benutzername", + "password-placeholder": "Passwort", + "submit": "Anmelden", + "plugin": "Plugin", + "feature": "Feature", + "tip": "Tipp", + "loading": "Lade Daten ..." + }, + "logout": { + "error": { + "title": "Abmeldung fehlgeschlagen", + "subtitle": "Während der Abmeldung ist ein Fehler aufgetreten." + } + }, + "app": { + "error": { + "title": "Fehler", + "subtitle": "Ein unbekannter Fehler ist aufgetreten." + } + }, + "breadcrumb": { + "home": "Hauptseite" + }, + "errorNotification": { + "prefix": "Fehler", + "loginLink": "Erneute Anmeldung", + "timeout": "Die Session ist abgelaufen.", + "wrongLoginCredentials": "Ungültige Anmeldedaten", + "forbidden": "Sie haben nicht die Berechtigung, diesen Datensatz zu sehen" + }, + "loading": { + "alt": "Lade ..." + }, + "logo": { + "alt": "SCM-Manager" + }, + "primary-navigation": { + "repositories": "Repositories", + "users": "Benutzer", + "logout": "Abmelden", + "groups": "Gruppen", + "admin": "Administration" + }, + "filterEntries": "Einträge filtern", + "autocomplete": { + "group": "Gruppe", + "user": "Benutzer", + "noGroupOptions": "Kein Gruppenname als Vorschlag verfügbar", + "groupPlaceholder": "Gruppe eingeben", + "noUserOptions": "Kein Benutzername als Vorschlag verfügbar", + "userPlaceholder": "Benutzer eingeben", + "loading": "suche..." + }, + "paginator": { + "next": "Weiter", + "previous": "Zurück" + }, + "profile": { + "navigationLabel": "Profil Navigation", + "informationNavLink": "Information", + "changePasswordNavLink": "Passwort ändern", + "settingsNavLink": "Einstellungen", + "username": "Benutzername", + "displayName": "Anzeigename", + "mail": "E-Mail", + "groups": "Gruppen", + "information": "Informationen", + "change-password": "Passwort ändern", + "error-title": "Fehler", + "error-subtitle": "Das Profil kann nicht angezeigt werden.", + "error": "Fehler", + "error-message": "'me' ist nicht definiert" + }, + "password": { + "label": "Passwort", + "newPassword": "Neues Passwort", + "currentPassword": "Aktuelles Passwort", + "currentPasswordHelpText": "Dieses Passwort wird momentan bereits verwendet.", + "confirmPassword": "Passwort wiederholen", + "passwordInvalid": "Das Passwort muss zwischen 6 und 32 Zeichen lang sein!", + "passwordConfirmFailed": "Passwörter müssen identisch sein!", + "submit": "Speichern", + "changedSuccessfully": "Passwort erfolgreich geändert!" + } +} diff --git a/scm-ui/ui-webapp/public/locales/de/config.json b/scm-ui/ui-webapp/public/locales/de/config.json new file mode 100644 index 0000000000..4cbfff2afc --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/de/config.json @@ -0,0 +1,78 @@ +{ + "config": { + "navigationLabel": "Administrations Navigation", + "title": "Globale Einstellungen", + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Einstellungen Fehler", + "form": { + "submit": "Speichern", + "submit-success-notification": "Einstellungen wurden erfolgreich geändert!", + "no-read-permission-notification": "Hinweis: Es fehlen Berechtigungen zum Lesen der Einstellungen!", + "no-write-permission-notification": "Hinweis: Es fehlen Berechtigungen zum Bearbeiten der Einstellungen!" + } + }, + "proxy-settings": { + "name": "Proxy Einstellungen", + "proxy-password": "Proxy Passwort", + "proxy-port": "Proxy Port", + "proxy-server": "Proxy Server", + "proxy-user": "Proxy Benutzer", + "enable-proxy": "Proxy aktivieren", + "proxy-excludes": "Proxy Excludes", + "remove-proxy-exclude-button": "Proxy Exclude löschen", + "add-proxy-exclude-error": "Der Proxy Exclude ist ungültig", + "add-proxy-exclude-textfield": "Neue Proxy Excludes hinzufügen", + "add-proxy-exclude-button": "Proxy Exclude hinzufügen" + }, + "base-url-settings": { + "name": "Base URL Einstellungen", + "base-url": "Base URL", + "force-base-url": "Base URL erzwingen" + }, + "login-attempt": { + "name": "Anmeldeversuche", + "login-attempt-limit": "Limit für Anmeldeversuche", + "login-attempt-limit-timeout": "Timeout bei fehlgeschlagenen Anmeldeversuchen" + }, + "general-settings": { + "realm-description": "Realm Beschreibung", + "disable-grouping-grid": "Gruppen deaktivieren", + "date-format": "Datumsformat", + "anonymous-access-enabled": "Anonyme Zugriffe erlauben", + "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", + "plugin-url": "Plugin Center URL", + "enabled-xsrf-protection": "XSRF Protection aktivieren", + "namespace-strategy": "Namespace Strategie", + "login-info-url": "Login Info URL" + }, + "validation": { + "date-format-invalid": "Das Datumsformat ist ungültig", + "login-attempt-limit-timeout-invalid": "Dies ist keine Zahl", + "login-attempt-limit-invalid": "Dies ist keine Zahl", + "plugin-url-invalid": "Dies ist keine gültige URL" + }, + "help": { + "realmDescriptionHelpText": "Beschreibung des Authentication Realm.", + "dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.", + "pluginUrlHelpText": "Die URL der Plugin Center API. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur", + "enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.", + "disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.", + "allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf freigegebene Repositories.", + "skipFailedAuthenticatorsHelpText": "Die Kette der Authentifikatoren wird nicht beendet, wenn ein Authentifikator einen Benutzer findet, ihn aber nicht erfolgreich authentifizieren kann.", + "adminGroupsHelpText": "Namen von Gruppen mit Admin-Berechtigungen.", + "adminUsersHelpText": "Namen von Benutzern mit Admin-Berechtigungen.", + "forceBaseUrlHelpText": "Zugriffe, die von einer anderen URL kommen, werden auf die Base URL weiter geleitet.", + "baseUrlHelpText": "Die URL der Applikation mit Kontextpfad, z.B. http://localhost:8080/scm", + "loginAttemptLimitHelpText": "Maximale Anzahl von Anmeldeversuchen. Durch Verwendung von -1 wird die Begrenzung der Anmeldeversuche deaktiviert.", + "loginAttemptLimitTimeoutHelpText": "Timeout in Sekunden für Benutzer, die vorübergehend wegen zu vieler fehlgeschlagener Anmeldeversuche, deaktiviert wurden.", + "enableProxyHelpText": "Proxy aktivieren", + "proxyPortHelpText": "Der Proxy Port", + "proxyPasswordHelpText": "Das Passwort für die Proxy Server Anmeldung.", + "proxyServerHelpText": "Der Proxy Server", + "proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.", + "proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", + "enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.", + "nameSpaceStrategyHelpText": "Strategie für Namespaces.", + "loginInfoUrlHelpText": "URL zu der Login Information (Plugin und Feature Tipps auf der Login Seite). Um die Login Information zu deaktivieren, kann das Feld leer gelassen werden." + } +} diff --git a/scm-ui/ui-webapp/public/locales/de/groups.json b/scm-ui/ui-webapp/public/locales/de/groups.json new file mode 100644 index 0000000000..04b368c56b --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/de/groups.json @@ -0,0 +1,72 @@ +{ + "group": { + "name": "Name", + "description": "Beschreibung", + "creationDate": "Erstellt", + "lastModified": "Zuletzt bearbeitet", + "type": "Typ", + "external": "Extern", + "internal": "Intern", + "members": "Mitglieder" + }, + "groups": { + "title": "Gruppen", + "subtitle": "Verwaltung der Gruppen", + "noGroups": "Keine Gruppen gefunden." + }, + "singleGroup": { + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Gruppen Fehler", + "menu": { + "navigationLabel": "Gruppen Navigation", + "informationNavLink": "Informationen", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell", + "setPermissionsNavLink": "Berechtigungen" + } + }, + "add-group": { + "title": "Gruppe erstellen", + "subtitle": "Erstellen einer neuen Gruppe" + }, + "create-group-button": { + "label": "Gruppe erstellen" + }, + "edit-group-button": { + "label": "Bearbeiten" + }, + "add-member-button": { + "label": "Mitglied hinzufügen" + }, + "add-member-textfield": { + "error": "Ungültiger Name für Mitglied" + }, + "add-member-autocomplete": { + "placeholder": "Mitglied hinzufügen", + "loading": "Suche...", + "no-options": "Kein Vorschlag für Benutzername verfügbar" + }, + "groupForm": { + "subtitle": "Gruppe bearbeiten", + "externalSubtitle": "Externe Gruppe bearbeiten", + "submit": "Speichern", + "nameError": "Name ist ungültig", + "descriptionError": "Beschreibung ist ungültig", + "help": { + "nameHelpText": "Eindeutiger Name der Gruppe", + "descriptionHelpText": "Eine kurze Beschreibung der Gruppe", + "memberHelpText": "Benutzername des Mitglieds der Gruppe", + "externalHelpText": "Mitglieder dieser Gruppe werden von einem externen System wie z.B.: einem LDAP-Server verwaltet" + } + }, + "deleteGroup": { + "subtitle": "Gruppe löschen", + "button": "Löschen", + "confirmAlert": { + "title": "Gruppe löschen", + "message": "Soll die Gruppe wirklich gelöscht werden?", + "submit": "Ja", + "cancel": "Nein" + } + } +} diff --git a/scm-ui/ui-webapp/public/locales/de/permissions.json b/scm-ui/ui-webapp/public/locales/de/permissions.json new file mode 100644 index 0000000000..57c061743a --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/de/permissions.json @@ -0,0 +1,6 @@ +{ + "setPermissions": { + "button": "Berechtigungen speichern", + "setPermissionsSuccessful": "Berechtigungen erfolgreich gespeichert" + } +} diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json new file mode 100644 index 0000000000..4dfba695ab --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -0,0 +1,187 @@ +{ + "repository": { + "namespace": "Namespace", + "name": "Name", + "type": "Typ", + "contact": "Kontakt", + "description": "Beschreibung", + "creationDate": "Erstellt", + "lastModified": "Zuletzt bearbeitet" + }, + "validation": { + "namespace-invalid": "Der Namespace des Repository ist ungültig", + "name-invalid": "Der Name des Repository ist ungültig", + "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein", + "branch": { + "nameInvalid": "Der Name des Branches ist ungültig" + } + }, + "help": { + "namespaceHelpText": "Der Namespace des Repository. Dieser wird Teil der URL des Repository sein.", + "nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.", + "typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).", + "contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.", + "descriptionHelpText": "Eine kurze Beschreibung des Repository." + }, + "repositoryRoot": { + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Repository Fehler", + "menu": { + "navigationLabel": "Repository Navigation", + "informationNavLink": "Informationen", + "branchesNavLink": "Branches", + "historyNavLink": "Commits", + "sourcesNavLink": "Sources", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell", + "permissionsNavLink": "Berechtigungen" + } + }, + "overview": { + "title": "Repositories", + "subtitle": "Übersicht aller verfügbaren Repositories", + "noRepositories": "Keine Repositories gefunden.", + "createButton": "Repository erstellen" + }, + "create": { + "title": "Repository erstellen", + "subtitle": "Erstellen eines neuen Repository" + }, + "branches": { + "overview": { + "title": "Übersicht aller verfügbaren Branches", + "noBranches": "Keine Branches gefunden.", + "createButton": "Branch erstellen" + }, + "table": { + "branches": "Branches" + }, + "create": { + "title": "Branch erstellen", + "source": "Quellbranch", + "name": "Name", + "submit": "Branch erstellen" + } + }, + "branch": { + "name": "Name:", + "commits": "Commits", + "sources": "Sources", + "defaultTag": "Default" + }, + "changesets": { + "errorTitle": "Fehler", + "errorSubtitle": "Changesets konnten nicht abgerufen werden", + "noChangesets": "Keine Changesets in diesem Branch gefunden.", + "branchSelectorLabel": "Branches", + "collapseDiffs": "Auf-/Zuklappen" + }, + "changeset": { + "description": "Beschreibung", + "summary": "Changeset <0/> wurde <1/> committet", + "shortSummary": "Committet <0/> <1/>", + "tags": "Tags", + "diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt", + "author": { + "prefix": "Verfasst von", + "mailto": "Mail senden an" + }, + "buttons": { + "details": "Details", + "sources": "Sources" + } + }, + "repositoryForm": { + "subtitle": "Repository bearbeiten", + "submit": "Speichern" + }, + "sources": { + "file-tree": { + "name": "Name", + "length": "Größe", + "lastModified": "Zuletzt bearbeitet", + "description": "Beschreibung", + "branch": "Branch" + }, + "content": { + "historyButton": "History", + "sourcesButton": "Sources", + "downloadButton": "Download", + "path": "Pfad", + "branch": "Branch", + "lastModified": "Zuletzt bearbeitet", + "description": "Beschreibung", + "size": "Größe" + }, + "noSources": "Keine Sources in diesem Branch gefunden.", + "extension" : { + "notBound": "Keine Erweiterung angebunden." + } + }, + "permission": { + "title": "Berechtigungen bearbeiten", + "user": "Benutzer", + "group": "Gruppe", + "error-title": "Fehler", + "error-subtitle": "Unbekannter Fehler bei Berechtigung", + "name": "Benutzer oder Gruppe", + "role": "Rolle", + "custom": "CUSTOM", + "permissions": "Berechtigung", + "group-permission": "Gruppenberechtigung", + "user-permission": "Benutzerberechtigung", + "edit-permission": { + "delete-button": "Löschen", + "save-button": "Änderungen speichern" + }, + "advanced-button": { + "label": "Erweitert" + }, + "delete-permission-button": { + "label": "Löschen", + "confirm-alert": { + "title": "Berechtigung löschen", + "message": "Soll die Berechtigung wirklich gelöscht werden?", + "submit": "Ja", + "cancel": "Nein" + } + }, + "add-permission": { + "add-permission-heading": "Neue Berechtigung hinzufügen", + "submit-button": "Speichern", + "name-input-invalid": "Die Berechtigung darf nicht leer sein! Falls sie nicht leer ist, ist der Name ungültig oder die Berechtigung besteht bereits!" + }, + "help": { + "groupPermissionHelpText": "Zeigt ob es sich bei der Berechtigung um eine Gruppenberechtigung handelt. Wenn hier kein Haken gesetzt ist, handelt es sich um eine Benutzerberechtigung.", + "nameHelpText": "Verwaltung von Berechtigungen für Benutzer und Gruppen", + "roleHelpText": "READ = read; WRITE = read und write; OWNER = read, write und auch die Möglichkeit Einstellungen und Berechtigungen zu verwalten. Wenn hier nichts angezeigt wird, den Erweitert-Button benutzen, um Details zu sehen.", + "permissionsHelpText": "Hier können individuelle Berechtigungen unabhängig von vordefinierten Rollen vergeben werden." + }, + "advanced": { + "dialog": { + "title": "Erweiterte Berechtigungen", + "submit": "Speichern", + "abort": "Abbrechen" + } + } + }, + "deleteRepo": { + "subtitle": "Repository löschen", + "button": "Löschen", + "confirmAlert": { + "title": "Repository löschen", + "message": "Soll das Repository wirklich gelöscht werden?", + "submit": "Ja", + "cancel": "Nein" + } + }, + "diff": { + "sideBySide": "Zweispaltig", + "combined": "Kombiniert", + "noDiffFound": "Kein Diff zwischen den ausgewählten Branches gefunden." + }, + "fileUpload": { + "clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.", + "dragAndDrop": "Sie können Ihre Datei auch direkt in die Dropzone ziehen." + } +} diff --git a/scm-ui/ui-webapp/public/locales/de/users.json b/scm-ui/ui-webapp/public/locales/de/users.json new file mode 100644 index 0000000000..25b6858c8b --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/de/users.json @@ -0,0 +1,65 @@ +{ + "user": { + "name": "Benutzername", + "displayName": "Anzeigename", + "mail": "E-Mail", + "password": "Passwort", + "active": "Aktiv", + "inactive": "Inaktiv", + "type": "Typ", + "creationDate": "Erstellt", + "lastModified": "Zuletzt bearbeitet" + }, + "validation": { + "mail-invalid": "Diese E-Mail ist ungültig", + "name-invalid": "Dieser Name ist ungültig", + "displayname-invalid": "Dieser Anzeigename ist ungültig" + }, + "help": { + "usernameHelpText": "Einzigartiger Name des Benutzers", + "displayNameHelpText": "Anzeigename des Benutzers", + "mailHelpText": "E-Mail Adresse des Benutzers", + "adminHelpText": "Ein Administrator kann Repositories, Gruppen und Benutzer erstellen, bearbeiten und löschen.", + "activeHelpText": "Aktivierung oder Deaktivierung eines Benutzers" + }, + "users": { + "title": "Benutzer", + "subtitle": "Verwaltung der Benutzer", + "noUsers": "Keine Benutzer gefunden.", + "createButton": "Benutzer erstellen" + }, + "singleUser": { + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Benutzer Fehler", + "menu": { + "navigationLabel": "Benutzer Navigation", + "informationNavLink": "Informationen", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell", + "setPasswordNavLink": "Passwort", + "setPermissionsNavLink": "Berechtigungen" + } + }, + "createUser": { + "title": "Benutzer erstellen", + "subtitle": "Erstellen eines neuen Benutzers" + }, + "deleteUser": { + "subtitle": "Benutzer löschen", + "button": "Löschen", + "confirmAlert": { + "title": "Benutzer löschen", + "message": "Soll der Benutzer wirklich gelöscht werden?", + "submit": "Ja", + "cancel": "Nein" + } + }, + "singleUserPassword": { + "button": "Passwort setzen", + "setPasswordSuccessful": "Das Passwort wurde erfolgreich gespeichert." + }, + "userForm": { + "subtitle": "Benutzer bearbeiten", + "button": "Speichern" + } +} diff --git a/scm-ui/ui-webapp/public/locales/en/admin.json b/scm-ui/ui-webapp/public/locales/en/admin.json new file mode 100644 index 0000000000..c315d1d142 --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/en/admin.json @@ -0,0 +1,104 @@ +{ + "admin": { + "menu": { + "navigationLabel": "Administration Navigation", + "informationNavLink": "Information", + "settingsNavLink": "Settings", + "generalNavLink": "General" + }, + "info": { + "currentAppVersion": "Current Application Version", + "communityTitle": "Community Support", + "communityIconAlt": "Community Support Icon", + "communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.", + "communityButton": "Contact our team", + "enterpriseTitle": "Enterprise Support", + "enterpriseIconAlt": "Enterprise Support Icon", + "enterpriseInfo": "You require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)?", + "enterprisePartner": "Contact our development partner Cloudogu! Their team is looking forward to discussing your individual requirements with you and will be more than happy to give you a quote.", + "enterpriseLink": "https://cloudogu.com/en/scm-manager-enterprise/", + "enterpriseButton": "Request Enterprise Support" + } + }, + "plugins": { + "title": "Plugins", + "installedSubtitle": "Installed Plugins", + "availableSubtitle": "Available Plugins", + "menu": { + "pluginsNavLink": "Plugins", + "installedNavLink": "Installed", + "availableNavLink": "Available" + }, + "executePending": "Execute changes", + "outdatedPlugins": "{{count}} outdated plugin", + "outdatedPlugins_plural": "{{count}} outdated plugins", + "updateAll": "Update all plugins", + "cancelPending": "Cancel changes", + "noPlugins": "No plugins found.", + "modal": { + "title": { + "install": "Install {{name}} Plugin", + "update": "Update {{name}} Plugin", + "uninstall": "Uninstall {{name}} Plugin" + }, + "restart": "Restart to make plugin changes effective", + "install": "Install", + "update": "Update", + "uninstall": "Uninstall", + "installQueue": "Will be installed:", + "updateQueue": "Will be updated:", + "uninstallQueue": "Will be uninstalled:", + "installAndRestart": "Install and Restart", + "updateAndRestart": "Update and Restart", + "uninstallAndRestart": "Uninstall and Restart", + "executeAndRestart": "Execute and Restart", + "updateAll": "Update all plugins", + "abort": "Abort", + "author": "Author", + "version": "Version", + "currentVersion": "Installed version", + "newVersion": "New version", + "dependencyNotification": "With this plugin, the following dependencies will be installed if they are not available yet!", + "dependencies": "Dependencies", + "successNotification": "Successful installed plugin. You have to reload the page, to see ui changes:", + "reload": "reload now", + "restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.", + "executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted.", + "cancelPending": "The following plugin changes will be canceled.", + "updateAllInfo": "The following plugin changes will be executed. You need to restart the scm-manager to make these changes effective." + } + }, + "repositoryRole": { + "navLink": "Permission Roles", + "title": "Permission Roles", + "errorTitle": "Error", + "errorSubtitle": "Unknown Permission Role Error", + "createSubtitle": "Create Permission Role", + "editSubtitle": "Edit Permission Role", + "overview": { + "title": "Overview of all permission roles", + "noPermissionRoles": "No permission roles found.", + "createButton": "Create Permission Role" + }, + "editButton": "Edit", + "name": "Name", + "type": "Type", + "verbs": "Permissions", + "system": "System", + "form": { + "name": "Name", + "permissions": "Permissions", + "submit": "Save" + }, + "delete": { + "button": "Delete", + "subtitle": "Delete permission role", + "confirmAlert": { + "title": "Delete permission role", + "message": "Do you really want to delete this permission role? All users will lose their corresponding permissions.", + "submit": "Yes", + "cancel": "No" + } + } + } +} diff --git a/scm-ui/ui-webapp/public/locales/en/commons.json b/scm-ui/ui-webapp/public/locales/en/commons.json new file mode 100644 index 0000000000..f7bab46d22 --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/en/commons.json @@ -0,0 +1,91 @@ +{ + "login": { + "title": "Login", + "subtitle": "Please login to proceed", + "logo-alt": "SCM-Manager", + "username-placeholder": "Your Username", + "password-placeholder": "Your Password", + "submit": "Login", + "plugin": "Plugin", + "feature": "Feature", + "tip": "Tip", + "loading": "Loading ...", + "error": "Error" + }, + "logout": { + "error": { + "title": "Logout failed", + "subtitle": "Something went wrong during logout" + } + }, + "app": { + "error": { + "title": "Error", + "subtitle": "Unknown error occurred" + } + }, + "breadcrumb": { + "home": "Main page" + }, + "errorNotification": { + "prefix": "Error", + "loginLink": "You can login here again.", + "timeout": "The session has expired", + "wrongLoginCredentials": "Invalid credentials", + "forbidden": "You don't have permission to view this entity" + }, + "loading": { + "alt": "Loading ..." + }, + "logo": { + "alt": "SCM-Manager" + }, + "primary-navigation": { + "repositories": "Repositories", + "users": "Users", + "logout": "Logout", + "groups": "Groups", + "admin": "Administration" + }, + "filterEntries": "filter entries", + "autocomplete": { + "group": "Group", + "user": "User", + "noGroupOptions": "No group suggestion available", + "groupPlaceholder": "Enter group", + "noUserOptions": "No user suggestion available", + "userPlaceholder": "Enter user", + "loading": "Loading..." + }, + "paginator": { + "next": "Next", + "previous": "Previous" + }, + "profile": { + "navigationLabel": "Profile Navigation", + "informationNavLink": "Information", + "changePasswordNavLink": "Change password", + "settingsNavLink": "Settings", + "username": "Username", + "displayName": "Display Name", + "mail": "E-Mail", + "groups": "Groups", + "information": "Information", + "change-password": "Change Password", + "error-title": "Error", + "error-subtitle": "Cannot display profile", + "error": "Error", + "error-message": "'me' is undefined" + }, + "password": { + "label": "Password", + "newPassword": "New password", + "currentPassword": "Current password", + "currentPasswordHelpText": "The password currently in use", + "confirmPassword": "Confirm password", + "passwordInvalid": "Password has to be between 6 and 32 characters", + "passwordConfirmFailed": "Passwords have to be identical", + "submit": "Submit", + "changedSuccessfully": "Password changed successfully" + } +} diff --git a/scm-ui/ui-webapp/public/locales/en/config.json b/scm-ui/ui-webapp/public/locales/en/config.json new file mode 100644 index 0000000000..a3720cd67c --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/en/config.json @@ -0,0 +1,78 @@ +{ + "config": { + "navigationLabel": "Administration Navigation", + "title": "Global Configuration", + "errorTitle": "Error", + "errorSubtitle": "Unknown Config Error", + "form": { + "submit": "Submit", + "submit-success-notification": "Configuration changed successfully!", + "no-read-permission-notification": "Please note: You do not have the permission to see the config!", + "no-write-permission-notification": "Please note: You do not have the permission to edit the config!" + } + }, + "proxy-settings": { + "name": "Proxy Settings", + "proxy-password": "Proxy Password", + "proxy-port": "Proxy Port", + "proxy-server": "Proxy Server", + "proxy-user": "Proxy User", + "enable-proxy": "Enable Proxy", + "proxy-excludes": "Proxy Excludes", + "remove-proxy-exclude-button": "Remove Proxy Exclude", + "add-proxy-exclude-error": "The proxy exclude you want to add is not valid", + "add-proxy-exclude-textfield": "Add proxy exclude you want to add to proxy excludes here", + "add-proxy-exclude-button": "Add Proxy Exclude" + }, + "base-url-settings": { + "name": "Base URL Settings", + "base-url": "Base URL", + "force-base-url": "Force Base URL" + }, + "login-attempt": { + "name": "Login Attempt", + "login-attempt-limit": "Login Attempt Limit", + "login-attempt-limit-timeout": "Login Attempt Limit Timeout" + }, + "general-settings": { + "realm-description": "Realm Description", + "disable-grouping-grid": "Disable Grouping Grid", + "date-format": "Date Format", + "anonymous-access-enabled": "Anonymous Access Enabled", + "skip-failed-authenticators": "Skip Failed Authenticators", + "plugin-url": "Plugin Center URL", + "enabled-xsrf-protection": "Enabled XSRF Protection", + "namespace-strategy": "Namespace Strategy", + "login-info-url": "Login Info URL" + }, + "validation": { + "date-format-invalid": "The date format is not valid", + "login-attempt-limit-timeout-invalid": "This is not a number", + "login-attempt-limit-invalid": "This is not a number", + "plugin-url-invalid": "This is not a valid url" + }, + "help": { + "realmDescriptionHelpText": "Enter authentication realm description.", + "dateFormatHelpText": "Moments date format. Please have a look at the MomentJS documentation.", + "pluginUrlHelpText": "The url of the Plugin Center API. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture", + "enableForwardingHelpText": "Enable mod_proxy port forwarding.", + "disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.", + "allowAnonymousAccessHelpText": "Anonymous users have access on granted repositories.", + "skipFailedAuthenticatorsHelpText": "Do not stop the authentication chain, if an authenticator finds the user but fails to authenticate the user.", + "adminGroupsHelpText": "Names of groups with admin permissions.", + "adminUsersHelpText": "Names of users with admin permissions.", + "forceBaseUrlHelpText": "Redirects to the base url if the request comes from a other url.", + "baseUrlHelpText": "The url of the application (with context path), i.e. http://localhost:8080/scm", + "loginAttemptLimitHelpText": "Maximum allowed login attempts. Use -1 to disable the login attempt limit.", + "loginAttemptLimitTimeoutHelpText": "Timeout in seconds for users which are temporary disabled, because of too many failed login attempts.", + "enableProxyHelpText": "Enable Proxy", + "proxyPortHelpText": "The proxy port", + "proxyPasswordHelpText": "The password for the proxy server authentication.", + "proxyServerHelpText": "The proxy server", + "proxyUserHelpText": "The username for the proxy server authentication.", + "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", + "enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", + "nameSpaceStrategyHelpText": "The namespace strategy.", + "loginInfoUrlHelpText": "URL to login information (plugin and feature tips at login page). If this is omitted, no login information will be displayed." + } +} diff --git a/scm-ui/ui-webapp/public/locales/en/groups.json b/scm-ui/ui-webapp/public/locales/en/groups.json new file mode 100644 index 0000000000..0153f30d30 --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/en/groups.json @@ -0,0 +1,72 @@ +{ + "group": { + "name": "Name", + "description": "Description", + "creationDate": "Creation Date", + "lastModified": "Last Modified", + "type": "Type", + "external": "External", + "internal": "Internal", + "members": "Members" + }, + "groups": { + "title": "Groups", + "subtitle": "Create, read, update and delete groups", + "noGroups": "No groups found." + }, + "singleGroup": { + "errorTitle": "Error", + "errorSubtitle": "Unknown group error", + "menu": { + "navigationLabel": "Group Navigation", + "informationNavLink": "Information", + "settingsNavLink": "Settings", + "generalNavLink": "General", + "setPermissionsNavLink": "Permissions" + } + }, + "add-group": { + "title": "Create Group", + "subtitle": "Create a new group" + }, + "create-group-button": { + "label": "Create Group" + }, + "edit-group-button": { + "label": "Edit" + }, + "add-member-button": { + "label": "Add Member" + }, + "add-member-textfield": { + "error": "Invalid member name" + }, + "add-member-autocomplete": { + "placeholder": "Add Member", + "loading": "Loading...", + "no-options": "No suggestion available" + }, + "groupForm": { + "subtitle": "Edit Group", + "externalSubtitle": "Edit external group", + "submit": "Submit", + "nameError": "Group name is invalid", + "descriptionError": "Description is invalid", + "help": { + "nameHelpText": "Unique name of the group", + "descriptionHelpText": "A short description of the group", + "memberHelpText": "Usernames of the group members", + "externalHelpText": "Members are managed by an external system such as LDAP" + } + }, + "deleteGroup": { + "subtitle": "Delete Group", + "button": "Delete", + "confirmAlert": { + "title": "Delete Group", + "message": "Do you really want to delete the group?", + "submit": "Yes", + "cancel": "No" + } + } +} diff --git a/scm-ui/ui-webapp/public/locales/en/permissions.json b/scm-ui/ui-webapp/public/locales/en/permissions.json new file mode 100644 index 0000000000..9c3663e77b --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/en/permissions.json @@ -0,0 +1,6 @@ +{ + "setPermissions": { + "button": "Set permissions", + "setPermissionsSuccessful": "Permissions set successfully" + } +} diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json new file mode 100644 index 0000000000..e2ccf8326e --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -0,0 +1,194 @@ +{ + "repository": { + "namespace": "Namespace", + "name": "Name", + "type": "Type", + "contact": "Contact", + "description": "Description", + "creationDate": "Creation Date", + "lastModified": "Last Modified" + }, + "validation": { + "namespace-invalid": "The repository namespace is invalid", + "name-invalid": "The repository name is invalid", + "contact-invalid": "Contact must be a valid mail address", + "branch": { + "nameInvalid": "The branch name is invalid" + } + }, + "help": { + "namespaceHelpText": "The namespace of the repository. This name will be part of the repository url.", + "nameHelpText": "The name of the repository. This name will be part of the repository url.", + "typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).", + "contactHelpText": "Email address of the person who is responsible for this repository.", + "descriptionHelpText": "A short description of the repository." + }, + "repositoryRoot": { + "errorTitle": "Error", + "errorSubtitle": "Unknown repository error", + "menu": { + "navigationLabel": "Repository Navigation", + "informationNavLink": "Information", + "branchesNavLink": "Branches", + "historyNavLink": "Commits", + "sourcesNavLink": "Sources", + "settingsNavLink": "Settings", + "generalNavLink": "General", + "permissionsNavLink": "Permissions" + } + }, + "overview": { + "title": "Repositories", + "subtitle": "Overview of available repositories", + "noRepositories": "No repositories found.", + "createButton": "Create Repository" + }, + "create": { + "title": "Create Repository", + "subtitle": "Create a new repository" + }, + "branches": { + "overview": { + "title": "Overview of all branches", + "noBranches": "No branches found.", + "createButton": "Create Branch" + }, + "table": { + "branches": "Branches" + }, + "create": { + "title": "Create Branch", + "source": "Source Branch", + "name": "Name", + "submit": "Create Branch" + } + }, + "branch": { + "name": "Name:", + "commits": "Commits", + "sources": "Sources", + "defaultTag": "Default" + }, + "changesets": { + "errorTitle": "Error", + "errorSubtitle": "Could not fetch changesets", + "noChangesets": "No changesets found for this branch.", + "branchSelectorLabel": "Branches", + "collapseDiffs": "Collapse" + }, + "changeset": { + "description": "Description", + "summary": "Changeset <0/> was committed <1/>", + "shortSummary": "Committed <0/> <1/>", + "tags": "Tags", + "diffNotSupported": "Diff of changesets is not supported by the type of repository", + "author": { + "prefix": "Authored by", + "mailto": "Send mail to" + }, + "buttons": { + "details": "Details", + "sources": "Sources" + } + }, + "repositoryForm": { + "subtitle": "Edit Repository", + "submit": "Save" + }, + "sources": { + "file-tree": { + "name": "Name", + "length": "Length", + "lastModified": "Last modified", + "description": "Description", + "branch": "Branch" + }, + "content": { + "historyButton": "History", + "sourcesButton": "Sources", + "downloadButton": "Download", + "path": "Path", + "branch": "Branch", + "lastModified": "Last modified", + "description": "Description", + "size": "Size" + }, + "noSources": "No sources found for this branch.", + "extension" : { + "notBound": "No extension bound." + } + }, + "permission": { + "title": "Edit Permissions", + "user": "User", + "group": "Group", + "error-title": "Error", + "error-subtitle": "Unknown permissions error", + "name": "User or group", + "role": "Role", + "custom": "CUSTOM", + "permissions": "Permissions", + "group-permission": "Group Permission", + "user-permission": "User Permission", + "edit-permission": { + "delete-button": "Delete", + "save-button": "Save Changes" + }, + "advanced-button": { + "label": "Advanced" + }, + "delete-permission-button": { + "label": "Delete", + "confirm-alert": { + "title": "Delete permission", + "message": "Do you really want to delete the permission?", + "submit": "Yes", + "cancel": "No" + } + }, + "add-permission": { + "add-permission-heading": "Add new Permission", + "submit-button": "Submit", + "name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!" + }, + "help": { + "groupPermissionHelpText": "States if a permission is a group permission. If this is not checked, it is a user permission.", + "nameHelpText": "Manage permissions for a specific user or group.", + "roleHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions. If nothing is selected here, use the 'Advanced' Button to see detailed permissions.", + "permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles." + }, + "advanced": { + "dialog": { + "title": "Advanced Permissions", + "submit": "Submit", + "abort": "Abort" + } + } + }, + "deleteRepo": { + "subtitle": "Delete Repository", + "button": "Delete", + "confirmAlert": { + "title": "Delete repository", + "message": "Do you really want to delete the repository?", + "submit": "Yes", + "cancel": "No" + } + }, + "diff": { + "changes": { + "add": "added", + "delete": "deleted", + "modify": "modified", + "rename": "renamed", + "copy": "copied" + }, + "sideBySide": "side-by-side", + "combined": "combined", + "noDiffFound": "No Diff between the selected branches found." + }, + "fileUpload": { + "clickHere": "Click here to select your file", + "dragAndDrop": "Drag 'n' drop some files here" + } +} diff --git a/scm-ui/ui-webapp/public/locales/en/users.json b/scm-ui/ui-webapp/public/locales/en/users.json new file mode 100644 index 0000000000..b492923a3f --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/en/users.json @@ -0,0 +1,65 @@ +{ + "user": { + "name": "Username", + "displayName": "Display Name", + "mail": "E-Mail", + "password": "Password", + "active": "Active", + "inactive": "Inactive", + "type": "Type", + "creationDate": "Creation Date", + "lastModified": "Last Modified" + }, + "validation": { + "mail-invalid": "This email is invalid", + "name-invalid": "This name is invalid", + "displayname-invalid": "This displayname is invalid" + }, + "help": { + "usernameHelpText": "Unique name of the user.", + "displayNameHelpText": "Display name of the user.", + "mailHelpText": "Email address of the user.", + "adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.", + "activeHelpText": "Activate or deactivate the user." + }, + "users": { + "title": "Users", + "subtitle": "Create, read, update and delete users", + "noUsers": "No users found.", + "createButton": "Create User" + }, + "singleUser": { + "errorTitle": "Error", + "errorSubtitle": "Unknown user error", + "menu": { + "navigationLabel": "User Navigation", + "informationNavLink": "Information", + "settingsNavLink": "Settings", + "generalNavLink": "General", + "setPasswordNavLink": "Password", + "setPermissionsNavLink": "Permissions" + } + }, + "createUser": { + "title": "Create User", + "subtitle": "Create a new user" + }, + "deleteUser": { + "subtitle": "Delete User", + "button": "Delete", + "confirmAlert": { + "title": "Delete user", + "message": "Do you really want to delete the user?", + "submit": "Yes", + "cancel": "No" + } + }, + "singleUserPassword": { + "button": "Set password", + "setPasswordSuccessful": "Password successfully set" + }, + "userForm": { + "subtitle": "Edit User", + "button": "Submit" + } +} diff --git a/scm-ui/ui-webapp/public/locales/es/admin.json b/scm-ui/ui-webapp/public/locales/es/admin.json new file mode 100644 index 0000000000..155313ed99 --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/es/admin.json @@ -0,0 +1,99 @@ +{ + "admin": { + "menu": { + "navigationLabel": "Menú de administración", + "informationNavLink": "Información", + "settingsNavLink": "Ajustes", + "generalNavLink": "General" + }, + "info": { + "currentAppVersion": "Versión actual de la aplicación", + "communityTitle": "Soporte de la comunidad", + "communityIconAlt": "Icono del soporte de la comunidad", + "communityInfo": "Contacte con el equipo de soporte de SCM-Manager para questiones acerca de SCM-Manager, para informar de errores o pedir nuevas funcionalidades use los canales oficiales.", + "communityButton": "Contactar con nuestro equipo", + "enterpriseTitle": "Soporte empresarial", + "enterpriseIconAlt": "Icono del soporte para empresas", + "enterpriseInfo": "¿Necesita ayuda para la integración de SMC-Manager en sus procesos, con la personalización de la herramienta o simplemente un acuerdo de nivel de servicio (SLA)?", + "enterprisePartner": "Póngase en contacto con nuestro socio de desarrollo Cloudogu! Su equipo está esperando para tratar sus requisitos con usted y estará encantado de darle un presupuesto.", + "enterpriseLink": "https://cloudogu.com/en/scm-manager-enterprise/", + "enterpriseButton": "Pedir soporte empresarial" + } + }, + "plugins": { + "title": "Complementos", + "installedSubtitle": "Complementos instalados", + "availableSubtitle": "Complementos disponibles", + "menu": { + "pluginsNavLink": "Complementos", + "installedNavLink": "Instalados", + "availableNavLink": "Disponibles" + }, + "executePending": "Ejecutar los complementos pendientes", + "updateAll": "Actualizar todos los complementos", + "cancelPending": "Cancelar los complementos pendientes", + "noPlugins": "No se han encontrado complementos.", + "modal": { + "title": { + "install": "Instalar complemento {{name}}", + "update": "Actualizar complemento {{name}}", + "uninstall": "Desinstalar complemento {{name}}" + }, + "restart": "Reiniciar para activar", + "install": "Instalar", + "update": "Actualizar", + "uninstall": "Desinstalar", + "installQueue": "Será instalado:", + "updateQueue": "Será actualizado:", + "uninstallQueue": "Será desinstalado:", + "installAndRestart": "Instalar y reiniciar", + "updateAndRestart": "Actualizar y reiniciar", + "uninstallAndRestart": "Desinstalar y reiniciar", + "executeAndRestart": "Ejecutar y reiniciar", + "abort": "Cancelar", + "author": "Autor", + "version": "Versión", + "currentVersion": "Versión instalada", + "newVersion": "Nueva versión", + "dependencyNotification": "Con este complemento las siguientes dependencias serán instaladas si no lo han sido ya", + "dependencies": "Dependencias", + "successNotification": "Complemento instalado correctamente. Necesita recargar la página para ver los cambios:", + "reload": "recargar ahora", + "restartNotification": "Usted debería reiniciar scm-manager sólo si actualmente no hay nadie trabajando con el.", + "executePending": "Se ejecutarán los siguientes cambios en el complemento y luego se reiniciará el contexto scm-manager." + } + }, + "repositoryRole": { + "navLink": "Roles y permisos", + "title": "Roles y permisos", + "errorTitle": "Error", + "errorSubtitle": "Error desconocido", + "createSubtitle": "Crear nuevo rol", + "editSubtitle": "Editar rol", + "overview": { + "title": "Visión general de todos los roles", + "noPermissionRoles": "No se han encontrado roles.", + "createButton": "Crear rol" + }, + "editButton": "Editar", + "name": "Nombre", + "type": "Tipo", + "verbs": "Permisos", + "system": "Sistema", + "form": { + "name": "Nombre", + "permissions": "Permisos", + "submit": "Guardar" + }, + "delete": { + "button": "Borrar", + "subtitle": "Eliminar el rol", + "confirmAlert": { + "title": "Eliminar el rol", + "message": "¿Realmente desea borrar el rol? Todos los usuarios de este rol perderń sus permisos.", + "submit": "Sí", + "cancel": "No" + } + } + } +} diff --git a/scm-ui/ui-webapp/public/locales/es/commons.json b/scm-ui/ui-webapp/public/locales/es/commons.json new file mode 100644 index 0000000000..06db500845 --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/es/commons.json @@ -0,0 +1,93 @@ +{ + "login": { + "title": "Iniciar sesión", + "subtitle": "Por favor inicie sesión para continuar", + "logo-alt": "SCM-Manager", + "username-placeholder": "Su nombre de usuario", + "password-placeholder": "Su contraseña", + "submit": "Iniciar sesión", + "plugin": "Plugin", + "feature": "Feature", + "tip": "Tip", + "loading": "Cargando ...", + "error": "Error" + }, + "logout": { + "error": { + "title": "Cierre de sesión fallido", + "subtitle": "Ha ocurrido un error al cerrar la sesión" + } + }, + "app": { + "error": { + "title": "Error", + "subtitle": "Ha ocurrido un error desconocido" + } + }, + "breadcrumb": { + "home": "Página principal" + }, + "errorNotification": { + "prefix": "Error", + "loginLink": "Aquí puede iniciar la sesión de nuevo.", + "timeout": "La sesión ha caducado", + "wrongLoginCredentials": "Credenciales incorrectas", + "forbidden": "Usted no tiene permiso para ver esta sección" + }, + "loading": { + "alt": "Cargando ..." + }, + "logo": { + "alt": "SCM-Manager" + }, + "primary-navigation": { + "repositories": "Repositorios", + "users": "Usuarios", + "logout": "Cerrar sesión", + "groups": "Grupos", + "admin": "Administración" + }, + "filterEntries": "Filtrar entradas", + "autocomplete": { + "group": "Grupo", + "user": "Usuario", + "noGroupOptions": "No hay sugerencias disponibles", + "groupPlaceholder": "Nombre del grupo", + "noUserOptions": "No hay sugerencias disponibles", + "userPlaceholder": "Nombre de usuario", + "loading": "Cargando..." + }, + "paginator": { + "next": "Siguiente", + "previous": "Anterior" + }, + "profile": { + "navigationLabel": "Menú de sección", + "informationNavLink": "Información", + "changePasswordNavLink": "Cambiar contraseña", + "settingsNavLink": "Ajustes", + "username": "Nombre de usuario", + "displayName": "Nombre a mostrar", + "mail": "Correo electrónico", + "groups": "Grupos", + "information": "Información", + "change-password": "Cambiar contraseña", + "error-title": "Error", + "error-subtitle": "No se puede mostrar la sección", + "error": "Error", + "error-message": "'me' no está definido" + }, + "password": { + "label": "Contraseña", + "newPassword": "Nueva contraseña", + "passwordHelpText": "Contraseña del usuario en texto plano", + "passwordConfirmHelpText": "Repita la contraseña para confirmar", + "currentPassword": "Contraseña actual", + "currentPasswordHelpText": "La contraseña ya está en uso", + "confirmPassword": "Confirme la contraseña", + "passwordInvalid": "La contraseña debe tener entre 6 y 32 caracteres", + "passwordConfirmFailed": "Las contraseñas deben ser identicas", + "submit": "Guardar", + "changedSuccessfully": "Contraseña cambiada correctamente" + } +} diff --git a/scm-ui/ui-webapp/public/locales/es/config.json b/scm-ui/ui-webapp/public/locales/es/config.json new file mode 100644 index 0000000000..8357e72546 --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/es/config.json @@ -0,0 +1,78 @@ +{ + "config": { + "navigationLabel": "Menú de administración", + "title": "Configuración global", + "errorTitle": "Error", + "errorSubtitle": "Error de configuración desconocido", + "form": { + "submit": "Enviar", + "submit-success-notification": "¡Configuración cambiada correctamente!", + "no-read-permission-notification": "Por favor, tenga en cuenta: ¡No tiene permiso para ver la configuración!", + "no-write-permission-notification": "Por favor, tenga en cuenta: ¡No tiene permiso para editar la configuración!" + } + }, + "proxy-settings": { + "name": "Ajustes del proxy", + "proxy-password": "Contraseña del proxy", + "proxy-port": "Puerto del proxy", + "proxy-server": "Servidor proxy", + "proxy-user": "Usuario del proxy", + "enable-proxy": "Habilitar proxy", + "proxy-excludes": "Excepciones del proxy", + "remove-proxy-exclude-button": "Eliminar las excepciones del proxy", + "add-proxy-exclude-error": "La excepción que desea añadir al proxy es incorrecta", + "add-proxy-exclude-textfield": "Añada aquí las excepciones que desee incluir al proxy", + "add-proxy-exclude-button": "Añadir excepción al proxy" + }, + "base-url-settings": { + "name": "Ajustes de la URL base", + "base-url": "URL base", + "force-base-url": "Forzar la URL base" + }, + "login-attempt": { + "name": "Intento de inicio de sesión", + "login-attempt-limit": "Límite de intentos de inicio de sesión", + "login-attempt-limit-timeout": "Tiempo de espera para el intento de inicio de sesión" + }, + "general-settings": { + "realm-description": "Descripción del dominio", + "disable-grouping-grid": "Deshabilitar grupos", + "date-format": "Formato de la fecha", + "anonymous-access-enabled": "Acceso anónimo habilitado", + "skip-failed-authenticators": "Omitir autenticadores fallidos", + "plugin-url": "URL del almacén de complementos", + "enabled-xsrf-protection": "Protección XSRF habilitada", + "namespace-strategy": "Estrategia para el espacio de nombres", + "login-info-url": "URL de información de inicio de sesión" + }, + "validation": { + "date-format-invalid": "El formato de la fecha es incorrecto", + "login-attempt-limit-timeout-invalid": "El valor no es un número", + "login-attempt-limit-invalid": "El valor no es un número", + "plugin-url-invalid": "La URL es incorrecta" + }, + "help": { + "realmDescriptionHelpText": "Descripción del dominio de autenticación.", + "dateFormatHelpText": "Formato de la fecha. Por favor, heche un vistazo a la documentación de MomentJS.", + "pluginUrlHelpText": "La URL de la API del almacén de complementos. Explicación de los marcadores: version = Versión de SCM-Manager; os = Sistema operativo; arch = Arquitectura", + "enableForwardingHelpText": "Habilitar el redireccionamiento de puertos para mod_proxy.", + "disableGroupingGridHelpText": "Deshabilitar los grupos de repositorios. Se requiere una recarga completa de la página después de un cambio en este valor.", + "allowAnonymousAccessHelpText": "Los usuarios anónimos tienen acceso a repositorios otorgados.", + "skipFailedAuthenticatorsHelpText": "No detenga la cadena de autenticación si un autenticador encuentra al usuario pero no puede autenticarlo.", + "adminGroupsHelpText": "Nombres de los grupos con permisos de administrador.", + "adminUsersHelpText": "Nombres de los usuarios con permisos de administrador.", + "forceBaseUrlHelpText": "Redirige a la URL base si la solicitud proviene de otra URL.", + "baseUrlHelpText": "La URL de la aplicación (con la ruta del contexto), por ejemplo: http://localhost:8080/scm", + "loginAttemptLimitHelpText": "Máximo número permitido de intentos de inicio de sesión. Use -1 para deshabilitar este límite.", + "loginAttemptLimitTimeoutHelpText": "Tiempo de espera en segundos para los usuarios que están deshabilitados temporalmente debido a demasiado intentos fallidos de inicio de sesión.", + "enableProxyHelpText": "Habilitar proxy", + "proxyPortHelpText": "El puerto del proxy", + "proxyPasswordHelpText": "La contraseña para la autenticación del servidor proxy.", + "proxyServerHelpText": "El servidor proxy", + "proxyUserHelpText": "El nombre de usuario para la autenticación del servidor proxy.", + "proxyExcludesHelpText": "Patrones globales para hostnames que deben excluirse de la configuración del proxy.", + "enableXsrfProtectionHelpText": "Habilitar la protección de cookies XSRF. Nota: Esta funcionalidad todavía es experimental.", + "nameSpaceStrategyHelpText": "La estrategia para el espacio de nombres.", + "loginInfoUrlHelpText": "URL para la información en el inicio de sesión (consejos sobre complementos y funcionalidades en la página de inicio de sesión). Si esto se omite, no se mostrará información de inicio de sesión." + } +} diff --git a/scm-ui/ui-webapp/public/locales/es/groups.json b/scm-ui/ui-webapp/public/locales/es/groups.json new file mode 100644 index 0000000000..0ce3e7765a --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/es/groups.json @@ -0,0 +1,72 @@ +{ + "group": { + "name": "Nombre", + "description": "Descripción", + "creationDate": "Fecha de creación", + "lastModified": "Última modificación", + "type": "Tipo", + "external": "Externo", + "internal": "Interno", + "members": "Miembros" + }, + "groups": { + "title": "Grupos", + "subtitle": "Crear, leer, actualizar y borrar grupos", + "noGroups": "No se han encontrado grupos." + }, + "singleGroup": { + "errorTitle": "Error", + "errorSubtitle": "Error de grupo desconocido", + "menu": { + "navigationLabel": "Menú de grupo", + "informationNavLink": "Información", + "settingsNavLink": "Ajustes", + "generalNavLink": "General", + "setPermissionsNavLink": "Permisos" + } + }, + "add-group": { + "title": "Crear grupo", + "subtitle": "Crear un nuevo grupo" + }, + "create-group-button": { + "label": "Crear grupo" + }, + "edit-group-button": { + "label": "Editar" + }, + "add-member-button": { + "label": "Añadir miembro" + }, + "add-member-textfield": { + "error": "El nombre del miembro es incorrecto" + }, + "add-member-autocomplete": { + "placeholder": "Añadir miembro", + "loading": "Cargando...", + "no-options": "No hay sugerencias disponibles" + }, + "groupForm": { + "subtitle": "Editar grupo", + "externalSubtitle": "Editar grupo externo", + "submit": "Guardar", + "nameError": "El nombre del grupo es incorrecto", + "descriptionError": "La descripción es incorrecta", + "help": { + "nameHelpText": "Nombre único del grupo", + "descriptionHelpText": "Descripción breve del grupo", + "memberHelpText": "Nombres de usuario de los miembros del grupo", + "externalHelpText": "Los miembros son gestionados por un sistema externo como por ejemplo LDAP" + } + }, + "deleteGroup": { + "subtitle": "Borrar grupo", + "button": "Borrar", + "confirmAlert": { + "title": "Borrar grupo", + "message": "¿Realmente desea borrar el grupo?", + "submit": "Sí", + "cancel": "No" + } + } +} diff --git a/scm-ui/ui-webapp/public/locales/es/permissions.json b/scm-ui/ui-webapp/public/locales/es/permissions.json new file mode 100644 index 0000000000..d3da1103ea --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/es/permissions.json @@ -0,0 +1,6 @@ +{ + "setPermissions": { + "button": "Guardar", + "setPermissionsSuccessful": "Permisos guardados correctamente" + } +} diff --git a/scm-ui/ui-webapp/public/locales/es/repos.json b/scm-ui/ui-webapp/public/locales/es/repos.json new file mode 100644 index 0000000000..c3bf57c630 --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/es/repos.json @@ -0,0 +1,194 @@ +{ + "repository": { + "namespace": "Espacio de nombres", + "name": "Nombre", + "type": "Tipo", + "contact": "Contacto", + "description": "Descripción", + "creationDate": "Fecha de creación", + "lastModified": "Última modificación" + }, + "validation": { + "namespace-invalid": "El espacio de nombres del repositorio es incorrecto", + "name-invalid": "El nombre del repositorio es incorrecto", + "contact-invalid": "El contacto debe ser una dirección de correo electrónico válida", + "branch": { + "nameInvalid": "El nombre de la rama es incorrecto" + } + }, + "help": { + "namespaceHelpText": "El espacio de nombres del repositorio. Este nombre formará parte de la URL del repositorio.", + "nameHelpText": "El nombre del repositorio. Este nombre formará parte de la URL del repositorio.", + "typeHelpText": "El tipo del repositorio (Mercurial, Git or Subversion).", + "contactHelpText": "Dirección del correo electrónico de la persona responsable del repositorio.", + "descriptionHelpText": "Breve descripción del repositorio." + }, + "repositoryRoot": { + "errorTitle": "Error", + "errorSubtitle": "Error de repositorio desconocido", + "menu": { + "navigationLabel": "Menú de repositorio", + "informationNavLink": "Información", + "branchesNavLink": "Ramas", + "historyNavLink": "Commits", + "sourcesNavLink": "Fuentes", + "settingsNavLink": "Ajustes", + "generalNavLink": "General", + "permissionsNavLink": "Permisos" + } + }, + "overview": { + "title": "Repositorios", + "subtitle": "Visión general de los repositorios disponibles", + "noRepositories": "No se han encontrado repositorios.", + "createButton": "Crear repositorio" + }, + "create": { + "title": "Crear repositorio", + "subtitle": "Crear un nuevo repositorio" + }, + "branches": { + "overview": { + "title": "Vivisón general de todas las ramas", + "noBranches": "No se han encontrado ramas.", + "createButton": "Crear rama" + }, + "table": { + "branches": "Ramas" + }, + "create": { + "title": "Crear rama", + "source": "Rama padre", + "name": "Nombre", + "submit": "Crear rama" + } + }, + "branch": { + "name": "Nombre:", + "commits": "Commits", + "sources": "Fuentes", + "defaultTag": "Por defecto" + }, + "changesets": { + "errorTitle": "Error", + "errorSubtitle": "No se han podido recuperar los changesets", + "noChangesets": "No se han encontrado changesets para esta rama branch.", + "branchSelectorLabel": "Ramas", + "collapseDiffs": "Colapso" + }, + "changeset": { + "description": "Descripción", + "summary": "El changeset <0/> fue entregado <1/>", + "shortSummary": "Entregado <0/> <1/>", + "tags": "Etiquetas", + "diffNotSupported": "La comparación de changesets no es soportada por el tipo de repositorio", + "author": { + "prefix": "Creado por", + "mailto": "Enviar correo electrónico a" + }, + "buttons": { + "details": "Detalles", + "sources": "Fuentes" + } + }, + "repositoryForm": { + "subtitle": "Editar repositorio", + "submit": "Guardar" + }, + "sources": { + "file-tree": { + "name": "Nombre", + "length": "Longitud", + "lastModified": "Última modificación", + "description": "Descripción", + "branch": "Rama" + }, + "content": { + "historyButton": "Historia", + "sourcesButton": "Fuentes", + "downloadButton": "Descargar", + "path": "Ruta", + "branch": "Rama", + "lastModified": "Última modificación", + "description": "Discripción", + "size": "tamaño" + }, + "noSources": "No se han encontrado fuentes para esta rama.", + "extension" : { + "notBound": "Sin extensión conectada." + } + }, + "permission": { + "title": "Editar permisos", + "user": "Usuario", + "group": "Grupo", + "error-title": "Error", + "error-subtitle": "Error de permisos desconocido", + "name": "Usuario o grupo", + "role": "Rol", + "custom": "Personalizar", + "permissions": "Permisos", + "group-permission": "Permiso de grupo", + "user-permission": "Permiso de usuario", + "edit-permission": { + "delete-button": "Borrar", + "save-button": "Guardar cambios" + }, + "advanced-button": { + "label": "Avanzado" + }, + "delete-permission-button": { + "label": "Borrar", + "confirm-alert": { + "title": "Borrar permiso", + "message": "¿Realmente desea borrar el permiso?", + "submit": "Sí", + "cancel": "No" + } + }, + "add-permission": { + "add-permission-heading": "Añadir nuevo permiso", + "submit-button": "Guardar", + "name-input-invalid": "¡No se permiten permisos vacíos! ¡Si el permiso no está vacío, su nombre es inválido o ya existe!" + }, + "help": { + "groupPermissionHelpText": "Establece si un permiso es de grupo. Si no está marcado es un permiso de usuario.", + "nameHelpText": "Gestionar los permisos de un usuario o grupo.", + "roleHelpText": "READ = leer; WRITE = leer and escribir; OWNER = leer, escribir y también la capacidad de gestionar las propiedades y permisos. Si no hay nada seleccionado use el botón 'Avanzado' para ver los permisos en detalle.", + "permissionsHelpText": "Use esto para especificar su propio conjunto de permisos independientemente de los roles predefinidos." + }, + "advanced": { + "dialog": { + "title": "Permisos avanzados", + "submit": "Guardar", + "abort": "Cancelar" + } + } + }, + "deleteRepo": { + "subtitle": "Borrar repositorio", + "button": "Borrar", + "confirmAlert": { + "title": "Borrar repositorio", + "message": "¿Realmente desea borrar el repositorio?", + "submit": "sí", + "cancel": "No" + } + }, + "diff": { + "changes": { + "add": "añadido", + "delete": "borrado", + "modify": "modificado", + "rename": "renombrado", + "copy": "copiado" + }, + "sideBySide": "dos columnas", + "combined": "combinado", + "noDiffFound": "No se encontraron diferencias entre las ramas seleccionadas." + }, + "fileUpload": { + "clickHere": "Haga click aquí para seleccionar su fichero", + "dragAndDrop": "Arrastre y suelte los ficheros aquí" + } +} diff --git a/scm-ui/ui-webapp/public/locales/es/users.json b/scm-ui/ui-webapp/public/locales/es/users.json new file mode 100644 index 0000000000..d5ea97b95a --- /dev/null +++ b/scm-ui/ui-webapp/public/locales/es/users.json @@ -0,0 +1,65 @@ +{ + "user": { + "name": "Nombre de usuario", + "displayName": "Nombre a mostrar", + "mail": "Correo electrónico", + "password": "Contraseña", + "active": "Activo", + "inactive": "Inactivo", + "type": "Tipo", + "creationDate": "Fecha de creación", + "lastModified": "Última modificación" + }, + "validation": { + "mail-invalid": "El correo electrónico es incorrecto", + "name-invalid": "El nombre es incorrecto", + "displayname-invalid": "El nombre a mostrar es incorrecto" + }, + "help": { + "usernameHelpText": "Nombre único del usuario.", + "displayNameHelpText": "Nombre de usuario a mostrar.", + "mailHelpText": "Dirección de correo electrónico del usuario.", + "adminHelpText": "Un administrador es capaz de crear, modificar y borrar repositorios, grupos y usuarios.", + "activeHelpText": "Activar o desactivar el usuario." + }, + "users": { + "title": "Usuarios", + "subtitle": "Crear, leer, actualizar y borrar usuarios", + "noUsers": "No se han encontrado usuarios.", + "createButton": "Crear usuario" + }, + "singleUser": { + "errorTitle": "Error", + "errorSubtitle": "Error de usuario desconocido", + "menu": { + "navigationLabel": "Menú de usuario", + "informationNavLink": "Información", + "settingsNavLink": "Ajustes", + "generalNavLink": "General", + "setPasswordNavLink": "Contraseña", + "setPermissionsNavLink": "Permisos" + } + }, + "createUser": { + "title": "Crear usuario", + "subtitle": "Crear un nuevo usuario" + }, + "deleteUser": { + "subtitle": "Borrar usuario", + "button": "Borrar", + "confirmAlert": { + "title": "Borrar usuario", + "message": "¿Realmente desea borrar el usuario?", + "submit": "Sí", + "cancel": "No" + } + }, + "singleUserPassword": { + "button": "Guardar", + "setPasswordSuccessful": "Contraseña guardada correctamente" + }, + "userForm": { + "subtitle": "Editar usuario", + "button": "Guardar" + } +} diff --git a/scm-ui/ui-webapp/public/manifest.json b/scm-ui/ui-webapp/public/manifest.json new file mode 100644 index 0000000000..8b1dd8f4c3 --- /dev/null +++ b/scm-ui/ui-webapp/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "SCM-Manager", + "name": "SCM-Manager", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/scm-ui/ui-webapp/src/admin/components/form/BaseUrlSettings.tsx b/scm-ui/ui-webapp/src/admin/components/form/BaseUrlSettings.tsx new file mode 100644 index 0000000000..debf78b507 --- /dev/null +++ b/scm-ui/ui-webapp/src/admin/components/form/BaseUrlSettings.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Checkbox, InputField, Subtitle } from "@scm-manager/ui-components"; + +type Props = WithTranslation & { + baseUrl: string; + forceBaseUrl: boolean; + onChange: (p1: boolean, p2: any, p3: string) => void; + hasUpdatePermission: boolean; +}; + +class BaseUrlSettings extends React.Component { + render() { + const { t, baseUrl, forceBaseUrl, hasUpdatePermission } = this.props; + + return ( +
+ +
+
+ + +
+
+
+ ); + } + + handleBaseUrlChange = (value: string) => { + this.props.onChange(true, value, "baseUrl"); + }; + handleForceBaseUrlChange = (value: boolean) => { + this.props.onChange(true, value, "forceBaseUrl"); + }; +} + +export default withTranslation("config")(BaseUrlSettings); diff --git a/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx b/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx new file mode 100644 index 0000000000..f7f8f67659 --- /dev/null +++ b/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx @@ -0,0 +1,190 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { NamespaceStrategies, Config } from "@scm-manager/ui-types"; +import { SubmitButton, Notification } from "@scm-manager/ui-components"; +import ProxySettings from "./ProxySettings"; +import GeneralSettings from "./GeneralSettings"; +import BaseUrlSettings from "./BaseUrlSettings"; +import LoginAttempt from "./LoginAttempt"; + +type Props = WithTranslation & { + submitForm: (p: Config) => void; + config?: Config; + loading?: boolean; + configReadPermission: boolean; + configUpdatePermission: boolean; + namespaceStrategies?: NamespaceStrategies; +}; + +type State = { + config: Config; + showNotification: boolean; + error: { + loginAttemptLimitTimeout: boolean; + loginAttemptLimit: boolean; + }; + changed: boolean; +}; + +class ConfigForm extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + config: { + proxyPassword: null, + proxyPort: 0, + proxyServer: "", + proxyUser: null, + enableProxy: false, + realmDescription: "", + disableGroupingGrid: false, + dateFormat: "", + anonymousAccessEnabled: false, + baseUrl: "", + forceBaseUrl: false, + loginAttemptLimit: 0, + proxyExcludes: [], + skipFailedAuthenticators: false, + pluginUrl: "", + loginAttemptLimitTimeout: 0, + enabledXsrfProtection: true, + namespaceStrategy: "", + loginInfoUrl: "", + _links: {} + }, + showNotification: false, + error: { + loginAttemptLimitTimeout: false, + loginAttemptLimit: false + }, + changed: false + }; + } + + componentDidMount() { + const { config, configUpdatePermission } = this.props; + if (config) { + this.setState({ + ...this.state, + config: { + ...config + } + }); + } + if (!configUpdatePermission) { + this.setState({ + ...this.state, + showNotification: true + }); + } + } + + submit = (event: Event) => { + event.preventDefault(); + this.setState({ + changed: false + }); + this.props.submitForm(this.state.config); + }; + + render() { + const { loading, t, namespaceStrategies, configReadPermission, configUpdatePermission } = this.props; + const config = this.state.config; + + let noPermissionNotification = null; + + if (!configReadPermission) { + return ; + } + + if (this.state.showNotification) { + noPermissionNotification = ( + this.onClose()} + /> + ); + } + + return ( +
+ {noPermissionNotification} + this.onChange(isValid, changedValue, name)} + hasUpdatePermission={configUpdatePermission} + /> +
+ this.onChange(isValid, changedValue, name)} + hasUpdatePermission={configUpdatePermission} + /> +
+ this.onChange(isValid, changedValue, name)} + hasUpdatePermission={configUpdatePermission} + /> +
+ this.onChange(isValid, changedValue, name)} + hasUpdatePermission={configUpdatePermission} + /> +
+ + + ); + } + + onChange = (isValid: boolean, changedValue: any, name: string) => { + this.setState({ + ...this.state, + config: { + ...this.state.config, + [name]: changedValue + }, + error: { + ...this.state.error, + [name]: !isValid + }, + changed: true + }); + }; + + hasError = () => { + return this.state.error.loginAttemptLimit || this.state.error.loginAttemptLimitTimeout; + }; + + onClose = () => { + this.setState({ + ...this.state, + showNotification: false + }); + }; +} + +export default withTranslation("config")(ConfigForm); diff --git a/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx b/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx new file mode 100644 index 0000000000..7cfaa99b96 --- /dev/null +++ b/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx @@ -0,0 +1,125 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { Checkbox, InputField } from "@scm-manager/ui-components"; +import { NamespaceStrategies } from "@scm-manager/ui-types"; +import NamespaceStrategySelect from "./NamespaceStrategySelect"; + +type Props = WithTranslation & { + realmDescription: string; + loginInfoUrl: string; + disableGroupingGrid: boolean; + dateFormat: string; + anonymousAccessEnabled: boolean; + skipFailedAuthenticators: boolean; + pluginUrl: string; + enabledXsrfProtection: boolean; + namespaceStrategy: string; + namespaceStrategies?: NamespaceStrategies; + onChange: (p1: boolean, p2: any, p3: string) => void; + hasUpdatePermission: boolean; +}; + +class GeneralSettings extends React.Component { + render() { + const { + t, + realmDescription, + loginInfoUrl, + pluginUrl, + enabledXsrfProtection, + anonymousAccessEnabled, + namespaceStrategy, + hasUpdatePermission, + namespaceStrategies + } = this.props; + + return ( +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ ); + } + + handleLoginInfoUrlChange = (value: string) => { + this.props.onChange(true, value, "loginInfoUrl"); + }; + handleRealmDescriptionChange = (value: string) => { + this.props.onChange(true, value, "realmDescription"); + }; + handleEnabledXsrfProtectionChange = (value: boolean) => { + this.props.onChange(true, value, "enabledXsrfProtection"); + }; + handleEnableAnonymousAccess = (value: boolean) => { + this.props.onChange(true, value, "anonymousAccessEnabled"); + }; + handleNamespaceStrategyChange = (value: string) => { + this.props.onChange(true, value, "namespaceStrategy"); + }; + handlePluginCenterUrlChange = (value: string) => { + this.props.onChange(true, value, "pluginUrl"); + }; +} + +export default withTranslation("config")(GeneralSettings); diff --git a/scm-ui/ui-webapp/src/admin/components/form/LoginAttempt.tsx b/scm-ui/ui-webapp/src/admin/components/form/LoginAttempt.tsx new file mode 100644 index 0000000000..6ea1834a09 --- /dev/null +++ b/scm-ui/ui-webapp/src/admin/components/form/LoginAttempt.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { InputField, Subtitle, validation as validator } from "@scm-manager/ui-components"; + +type Props = WithTranslation & { + loginAttemptLimit: number; + loginAttemptLimitTimeout: number; + onChange: (p1: boolean, p2: any, p3: string) => void; + hasUpdatePermission: boolean; +}; + +type State = { + loginAttemptLimitError: boolean; + loginAttemptLimitTimeoutError: boolean; +}; + +class LoginAttempt extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + loginAttemptLimitError: false, + loginAttemptLimitTimeoutError: false + }; + } + render() { + const { t, loginAttemptLimit, loginAttemptLimitTimeout, hasUpdatePermission } = this.props; + + return ( +
+ +
+
+ +
+
+ +
+
+
+ ); + } + + //TODO: set Error in ConfigForm to disable Submit Button! + handleLoginAttemptLimitChange = (value: string) => { + this.setState({ + ...this.state, + loginAttemptLimitError: !validator.isNumberValid(value) + }); + this.props.onChange(validator.isNumberValid(value), value, "loginAttemptLimit"); + }; + + handleLoginAttemptLimitTimeoutChange = (value: string) => { + this.setState({ + ...this.state, + loginAttemptLimitTimeoutError: !validator.isNumberValid(value) + }); + this.props.onChange(validator.isNumberValid(value), value, "loginAttemptLimitTimeout"); + }; +} + +export default withTranslation("config")(LoginAttempt); diff --git a/scm-ui/ui-webapp/src/admin/components/form/NamespaceStrategySelect.tsx b/scm-ui/ui-webapp/src/admin/components/form/NamespaceStrategySelect.tsx new file mode 100644 index 0000000000..b6c8da6cb5 --- /dev/null +++ b/scm-ui/ui-webapp/src/admin/components/form/NamespaceStrategySelect.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { withTranslation, WithTranslation } from "react-i18next"; +import { NamespaceStrategies } from "@scm-manager/ui-types"; +import { Select } from "@scm-manager/ui-components"; + +type Props = WithTranslation & { + namespaceStrategies: NamespaceStrategies; + label: string; + value?: string; + disabled?: boolean; + helpText?: string; + onChange: (value: string, name?: string) => void; +}; + +class NamespaceStrategySelect extends React.Component { + createNamespaceOptions = () => { + const { namespaceStrategies, t } = this.props; + let available = []; + if (namespaceStrategies && namespaceStrategies.available) { + available = namespaceStrategies.available; + } + + return available.map(ns => { + const key = "namespaceStrategies." + ns; + let label = t(key); + if (label === key) { + label = ns; + } + return { + value: ns, + label: label + }; + }); + }; + + findSelected = () => { + const { namespaceStrategies, value } = this.props; + if (!namespaceStrategies || !namespaceStrategies.available || namespaceStrategies.available.indexOf(value) < 0) { + return namespaceStrategies.current; + } + return value; + }; + + render() { + const { label, helpText, disabled, onChange } = this.props; + const nsOptions = this.createNamespaceOptions(); + return ( +
' - }); - iconEl = Ext.get(bodyEl.dom.firstChild); - var contentEl = bodyEl.dom.childNodes[1]; - msgEl = Ext.get(contentEl.firstChild); - textboxEl = Ext.get(contentEl.childNodes[2].firstChild); - textboxEl.enableDisplayMode(); - textboxEl.addKeyListener([10,13], function(){ - if(dlg.isVisible() && opt && opt.buttons){ - if(opt.buttons.ok){ - handleButton("ok"); - }else if(opt.buttons.yes){ - handleButton("yes"); - } - } - }); - textareaEl = Ext.get(contentEl.childNodes[2].childNodes[1]); - textareaEl.enableDisplayMode(); - progressBar = new Ext.ProgressBar({ - renderTo:bodyEl - }); - bodyEl.createChild({cls:'x-clear'}); - } - return dlg; - }, - - /** - * Updates the message box body text - * @param {String} text (optional) Replaces the message box element's innerHTML with the specified string (defaults to - * the XHTML-compliant non-breaking space character '&#160;') - * @return {Ext.MessageBox} this - */ - updateText : function(text){ - if(!dlg.isVisible() && !opt.width){ - dlg.setSize(this.maxWidth, 100); // resize first so content is never clipped from previous shows - } - // Append a space here for sizing. In IE, for some reason, it wraps text incorrectly without one in some cases - msgEl.update(text ? text + ' ' : ' '); - - var iw = iconCls != '' ? (iconEl.getWidth() + iconEl.getMargins('lr')) : 0, - mw = msgEl.getWidth() + msgEl.getMargins('lr'), - fw = dlg.getFrameWidth('lr'), - bw = dlg.body.getFrameWidth('lr'), - w; - - w = Math.max(Math.min(opt.width || iw+mw+fw+bw, opt.maxWidth || this.maxWidth), - Math.max(opt.minWidth || this.minWidth, bwidth || 0)); - - if(opt.prompt === true){ - activeTextEl.setWidth(w-iw-fw-bw); - } - if(opt.progress === true || opt.wait === true){ - progressBar.setSize(w-iw-fw-bw); - } - if(Ext.isIE && w == bwidth){ - w += 4; //Add offset when the content width is smaller than the buttons. - } - msgEl.update(text || ' '); - dlg.setSize(w, 'auto').center(); - return this; - }, - - /** - * Updates a progress-style message box's text and progress bar. Only relevant on message boxes - * initiated via {@link Ext.MessageBox#progress} or {@link Ext.MessageBox#wait}, - * or by calling {@link Ext.MessageBox#show} with progress: true. - * @param {Number} value Any number between 0 and 1 (e.g., .5, defaults to 0) - * @param {String} progressText The progress text to display inside the progress bar (defaults to '') - * @param {String} msg The message box's body text is replaced with the specified string (defaults to undefined - * so that any existing body text will not get overwritten by default unless a new value is passed in) - * @return {Ext.MessageBox} this - */ - updateProgress : function(value, progressText, msg){ - progressBar.updateProgress(value, progressText); - if(msg){ - this.updateText(msg); - } - return this; - }, - - /** - * Returns true if the message box is currently displayed - * @return {Boolean} True if the message box is visible, else false - */ - isVisible : function(){ - return dlg && dlg.isVisible(); - }, - - /** - * Hides the message box if it is displayed - * @return {Ext.MessageBox} this - */ - hide : function(){ - var proxy = dlg ? dlg.activeGhost : null; - if(this.isVisible() || proxy){ - dlg.hide(); - handleHide(); - if (proxy){ - // unghost is a private function, but i saw no better solution - // to fix the locking problem when dragging while it closes - dlg.unghost(false, false); - } - } - return this; - }, - - /** - * Displays a new message box, or reinitializes an existing message box, based on the config options - * passed in. All display functions (e.g. prompt, alert, etc.) on MessageBox call this function internally, - * although those calls are basic shortcuts and do not support all of the config options allowed here. - * @param {Object} config The following config options are supported:
    - *
  • animEl : String/Element
    An id or Element from which the message box should animate as it - * opens and closes (defaults to undefined)
  • - *
  • buttons : Object/Boolean
    A button config object (e.g., Ext.MessageBox.OKCANCEL or {ok:'Foo', - * cancel:'Bar'}), or false to not show any buttons (defaults to false)
  • - *
  • closable : Boolean
    False to hide the top-right close button (defaults to true). Note that - * progress and wait dialogs will ignore this property and always hide the close button as they can only - * be closed programmatically.
  • - *
  • cls : String
    A custom CSS class to apply to the message box's container element
  • - *
  • defaultTextHeight : Number
    The default height in pixels of the message box's multiline textarea - * if displayed (defaults to 75)
  • - *
  • fn : Function
    A callback function which is called when the dialog is dismissed either - * by clicking on the configured buttons, or on the dialog close button, or by pressing - * the return button to enter input. - *

    Progress and wait dialogs will ignore this option since they do not respond to user - * actions and can only be closed programmatically, so any required function should be called - * by the same code after it closes the dialog. Parameters passed:

      - *
    • buttonId : String
      The ID of the button pressed, one of:
        - *
      • ok
      • - *
      • yes
      • - *
      • no
      • - *
      • cancel
      • - *
    • - *
    • text : String
      Value of the input field if either prompt - * or multiline is true
    • - *
    • opt : Object
      The config object passed to show.
    • - *

  • - *
  • scope : Object
    The scope of the callback function
  • - *
  • icon : String
    A CSS class that provides a background image to be used as the body icon for the - * dialog (e.g. Ext.MessageBox.WARNING or 'custom-class') (defaults to '')
  • - *
  • iconCls : String
    The standard {@link Ext.Window#iconCls} to - * add an optional header icon (defaults to '')
  • - *
  • maxWidth : Number
    The maximum width in pixels of the message box (defaults to 600)
  • - *
  • minWidth : Number
    The minimum width in pixels of the message box (defaults to 100)
  • - *
  • modal : Boolean
    False to allow user interaction with the page while the message box is - * displayed (defaults to true)
  • - *
  • msg : String
    A string that will replace the existing message box body text (defaults to the - * XHTML-compliant non-breaking space character '&#160;')
  • - *
  • multiline : Boolean
    - * True to prompt the user to enter multi-line text (defaults to false)
  • - *
  • progress : Boolean
    True to display a progress bar (defaults to false)
  • - *
  • progressText : String
    The text to display inside the progress bar if progress = true (defaults to '')
  • - *
  • prompt : Boolean
    True to prompt the user to enter single-line text (defaults to false)
  • - *
  • proxyDrag : Boolean
    True to display a lightweight proxy while dragging (defaults to false)
  • - *
  • title : String
    The title text
  • - *
  • value : String
    The string value to set into the active textbox element if displayed
  • - *
  • wait : Boolean
    True to display a progress bar (defaults to false)
  • - *
  • waitConfig : Object
    A {@link Ext.ProgressBar#waitConfig} object (applies only if wait = true)
  • - *
  • width : Number
    The width of the dialog in pixels
  • - *
- * Example usage: - *

-Ext.Msg.show({
-   title: 'Address',
-   msg: 'Please enter your address:',
-   width: 300,
-   buttons: Ext.MessageBox.OKCANCEL,
-   multiline: true,
-   fn: saveAddress,
-   animEl: 'addAddressBtn',
-   icon: Ext.MessageBox.INFO
-});
-
- * @return {Ext.MessageBox} this - */ - show : function(options){ - if(this.isVisible()){ - this.hide(); - } - opt = options; - var d = this.getDialog(opt.title || " "); - - d.setTitle(opt.title || " "); - var allowClose = (opt.closable !== false && opt.progress !== true && opt.wait !== true); - d.tools.close.setDisplayed(allowClose); - activeTextEl = textboxEl; - opt.prompt = opt.prompt || (opt.multiline ? true : false); - if(opt.prompt){ - if(opt.multiline){ - textboxEl.hide(); - textareaEl.show(); - textareaEl.setHeight(Ext.isNumber(opt.multiline) ? opt.multiline : this.defaultTextHeight); - activeTextEl = textareaEl; - }else{ - textboxEl.show(); - textareaEl.hide(); - } - }else{ - textboxEl.hide(); - textareaEl.hide(); - } - activeTextEl.dom.value = opt.value || ""; - if(opt.prompt){ - d.focusEl = activeTextEl; - }else{ - var bs = opt.buttons; - var db = null; - if(bs && bs.ok){ - db = buttons["ok"]; - }else if(bs && bs.yes){ - db = buttons["yes"]; - } - if (db){ - d.focusEl = db; - } - } - if(Ext.isDefined(opt.iconCls)){ - d.setIconClass(opt.iconCls); - } - this.setIcon(Ext.isDefined(opt.icon) ? opt.icon : bufferIcon); - bwidth = updateButtons(opt.buttons); - progressBar.setVisible(opt.progress === true || opt.wait === true); - this.updateProgress(0, opt.progressText); - this.updateText(opt.msg); - if(opt.cls){ - d.el.addClass(opt.cls); - } - d.proxyDrag = opt.proxyDrag === true; - d.modal = opt.modal !== false; - d.mask = opt.modal !== false ? mask : false; - if(!d.isVisible()){ - // force it to the end of the z-index stack so it gets a cursor in FF - document.body.appendChild(dlg.el.dom); - d.setAnimateTarget(opt.animEl); - //workaround for window internally enabling keymap in afterShow - d.on('show', function(){ - if(allowClose === true){ - d.keyMap.enable(); - }else{ - d.keyMap.disable(); - } - }, this, {single:true}); - d.show(opt.animEl); - } - if(opt.wait === true){ - progressBar.wait(opt.waitConfig); - } - return this; - }, - - /** - * Adds the specified icon to the dialog. By default, the class 'ext-mb-icon' is applied for default - * styling, and the class passed in is expected to supply the background image url. Pass in empty string ('') - * to clear any existing icon. This method must be called before the MessageBox is shown. - * The following built-in icon classes are supported, but you can also pass in a custom class name: - *
-Ext.MessageBox.INFO
-Ext.MessageBox.WARNING
-Ext.MessageBox.QUESTION
-Ext.MessageBox.ERROR
-         *
- * @param {String} icon A CSS classname specifying the icon's background image url, or empty string to clear the icon - * @return {Ext.MessageBox} this - */ - setIcon : function(icon){ - if(!dlg){ - bufferIcon = icon; - return; - } - bufferIcon = undefined; - if(icon && icon != ''){ - iconEl.removeClass('x-hidden'); - iconEl.replaceClass(iconCls, icon); - bodyEl.addClass('x-dlg-icon'); - iconCls = icon; - }else{ - iconEl.replaceClass(iconCls, 'x-hidden'); - bodyEl.removeClass('x-dlg-icon'); - iconCls = ''; - } - return this; - }, - - /** - * Displays a message box with a progress bar. This message box has no buttons and is not closeable by - * the user. You are responsible for updating the progress bar as needed via {@link Ext.MessageBox#updateProgress} - * and closing the message box when the process is complete. - * @param {String} title The title bar text - * @param {String} msg The message box body text - * @param {String} progressText (optional) The text to display inside the progress bar (defaults to '') - * @return {Ext.MessageBox} this - */ - progress : function(title, msg, progressText){ - this.show({ - title : title, - msg : msg, - buttons: false, - progress:true, - closable:false, - minWidth: this.minProgressWidth, - progressText: progressText - }); - return this; - }, - - /** - * Displays a message box with an infinitely auto-updating progress bar. This can be used to block user - * interaction while waiting for a long-running process to complete that does not have defined intervals. - * You are responsible for closing the message box when the process is complete. - * @param {String} msg The message box body text - * @param {String} title (optional) The title bar text - * @param {Object} config (optional) A {@link Ext.ProgressBar#waitConfig} object - * @return {Ext.MessageBox} this - */ - wait : function(msg, title, config){ - this.show({ - title : title, - msg : msg, - buttons: false, - closable:false, - wait:true, - modal:true, - minWidth: this.minProgressWidth, - waitConfig: config - }); - return this; - }, - - /** - * Displays a standard read-only message box with an OK button (comparable to the basic JavaScript alert prompt). - * If a callback function is passed it will be called after the user clicks the button, and the - * id of the button that was clicked will be passed as the only parameter to the callback - * (could also be the top-right close button). - * @param {String} title The title bar text - * @param {String} msg The message box body text - * @param {Function} fn (optional) The callback function invoked after the message box is closed - * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser wnidow. - * @return {Ext.MessageBox} this - */ - alert : function(title, msg, fn, scope){ - this.show({ - title : title, - msg : msg, - buttons: this.OK, - fn: fn, - scope : scope, - minWidth: this.minWidth - }); - return this; - }, - - /** - * Displays a confirmation message box with Yes and No buttons (comparable to JavaScript's confirm). - * If a callback function is passed it will be called after the user clicks either button, - * and the id of the button that was clicked will be passed as the only parameter to the callback - * (could also be the top-right close button). - * @param {String} title The title bar text - * @param {String} msg The message box body text - * @param {Function} fn (optional) The callback function invoked after the message box is closed - * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser wnidow. - * @return {Ext.MessageBox} this - */ - confirm : function(title, msg, fn, scope){ - this.show({ - title : title, - msg : msg, - buttons: this.YESNO, - fn: fn, - scope : scope, - icon: this.QUESTION, - minWidth: this.minWidth - }); - return this; - }, - - /** - * Displays a message box with OK and Cancel buttons prompting the user to enter some text (comparable to JavaScript's prompt). - * The prompt can be a single-line or multi-line textbox. If a callback function is passed it will be called after the user - * clicks either button, and the id of the button that was clicked (could also be the top-right - * close button) and the text that was entered will be passed as the two parameters to the callback. - * @param {String} title The title bar text - * @param {String} msg The message box body text - * @param {Function} fn (optional) The callback function invoked after the message box is closed - * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser wnidow. - * @param {Boolean/Number} multiline (optional) True to create a multiline textbox using the defaultTextHeight - * property, or the height in pixels to create the textbox (defaults to false / single-line) - * @param {String} value (optional) Default value of the text input element (defaults to '') - * @return {Ext.MessageBox} this - */ - prompt : function(title, msg, fn, scope, multiline, value){ - this.show({ - title : title, - msg : msg, - buttons: this.OKCANCEL, - fn: fn, - minWidth: this.minPromptWidth, - scope : scope, - prompt:true, - multiline: multiline, - value: value - }); - return this; - }, - - /** - * Button config that displays a single OK button - * @type Object - */ - OK : {ok:true}, - /** - * Button config that displays a single Cancel button - * @type Object - */ - CANCEL : {cancel:true}, - /** - * Button config that displays OK and Cancel buttons - * @type Object - */ - OKCANCEL : {ok:true, cancel:true}, - /** - * Button config that displays Yes and No buttons - * @type Object - */ - YESNO : {yes:true, no:true}, - /** - * Button config that displays Yes, No and Cancel buttons - * @type Object - */ - YESNOCANCEL : {yes:true, no:true, cancel:true}, - /** - * The CSS class that provides the INFO icon image - * @type String - */ - INFO : 'ext-mb-info', - /** - * The CSS class that provides the WARNING icon image - * @type String - */ - WARNING : 'ext-mb-warning', - /** - * The CSS class that provides the QUESTION icon image - * @type String - */ - QUESTION : 'ext-mb-question', - /** - * The CSS class that provides the ERROR icon image - * @type String - */ - ERROR : 'ext-mb-error', - - /** - * The default height in pixels of the message box's multiline textarea if displayed (defaults to 75) - * @type Number - */ - defaultTextHeight : 75, - /** - * The maximum width in pixels of the message box (defaults to 600) - * @type Number - */ - maxWidth : 600, - /** - * The minimum width in pixels of the message box (defaults to 100) - * @type Number - */ - minWidth : 100, - /** - * The minimum width in pixels of the message box if it is a progress-style dialog. This is useful - * for setting a different minimum width than text-only dialogs may need (defaults to 250). - * @type Number - */ - minProgressWidth : 250, - /** - * The minimum width in pixels of the message box if it is a prompt dialog. This is useful - * for setting a different minimum width than text-only dialogs may need (defaults to 250). - * @type Number - */ - minPromptWidth: 250, - /** - * An object containing the default button text strings that can be overriden for localized language support. - * Supported properties are: ok, cancel, yes and no. Generally you should include a locale-specific - * resource file for handling language support across the framework. - * Customize the default text like so: Ext.MessageBox.buttonText.yes = "oui"; //french - * @type Object - */ - buttonText : { - ok : "OK", - cancel : "Cancel", - yes : "Yes", - no : "No" - } - }; -}(); - -/** - * Shorthand for {@link Ext.MessageBox} - */ -Ext.Msg = Ext.MessageBox;/** - * @class Ext.dd.PanelProxy - * A custom drag proxy implementation specific to {@link Ext.Panel}s. This class is primarily used internally - * for the Panel's drag drop implementation, and should never need to be created directly. - * @constructor - * @param panel The {@link Ext.Panel} to proxy for - * @param config Configuration options - */ -Ext.dd.PanelProxy = Ext.extend(Object, { - - constructor : function(panel, config){ - this.panel = panel; - this.id = this.panel.id +'-ddproxy'; - Ext.apply(this, config); - }, - - /** - * @cfg {Boolean} insertProxy True to insert a placeholder proxy element while dragging the panel, - * false to drag with no proxy (defaults to true). - */ - insertProxy : true, - - // private overrides - setStatus : Ext.emptyFn, - reset : Ext.emptyFn, - update : Ext.emptyFn, - stop : Ext.emptyFn, - sync: Ext.emptyFn, - - /** - * Gets the proxy's element - * @return {Element} The proxy's element - */ - getEl : function(){ - return this.ghost; - }, - - /** - * Gets the proxy's ghost element - * @return {Element} The proxy's ghost element - */ - getGhost : function(){ - return this.ghost; - }, - - /** - * Gets the proxy's element - * @return {Element} The proxy's element - */ - getProxy : function(){ - return this.proxy; - }, - - /** - * Hides the proxy - */ - hide : function(){ - if(this.ghost){ - if(this.proxy){ - this.proxy.remove(); - delete this.proxy; - } - this.panel.el.dom.style.display = ''; - this.ghost.remove(); - delete this.ghost; - } - }, - - /** - * Shows the proxy - */ - show : function(){ - if(!this.ghost){ - this.ghost = this.panel.createGhost(this.panel.initialConfig.cls, undefined, Ext.getBody()); - this.ghost.setXY(this.panel.el.getXY()); - if(this.insertProxy){ - this.proxy = this.panel.el.insertSibling({cls:'x-panel-dd-spacer'}); - this.proxy.setSize(this.panel.getSize()); - } - this.panel.el.dom.style.display = 'none'; - } - }, - - // private - repair : function(xy, callback, scope){ - this.hide(); - if(typeof callback == "function"){ - callback.call(scope || this); - } - }, - - /** - * Moves the proxy to a different position in the DOM. This is typically called while dragging the Panel - * to keep the proxy sync'd to the Panel's location. - * @param {HTMLElement} parentNode The proxy's parent DOM node - * @param {HTMLElement} before (optional) The sibling node before which the proxy should be inserted (defaults - * to the parent's last child if not specified) - */ - moveProxy : function(parentNode, before){ - if(this.proxy){ - parentNode.insertBefore(this.proxy.dom, before); - } - } -}); - -// private - DD implementation for Panels -Ext.Panel.DD = Ext.extend(Ext.dd.DragSource, { - - constructor : function(panel, cfg){ - this.panel = panel; - this.dragData = {panel: panel}; - this.proxy = new Ext.dd.PanelProxy(panel, cfg); - Ext.Panel.DD.superclass.constructor.call(this, panel.el, cfg); - var h = panel.header, - el = panel.body; - if(h){ - this.setHandleElId(h.id); - el = panel.header; - } - el.setStyle('cursor', 'move'); - this.scroll = false; - }, - - showFrame: Ext.emptyFn, - startDrag: Ext.emptyFn, - b4StartDrag: function(x, y) { - this.proxy.show(); - }, - b4MouseDown: function(e) { - var x = e.getPageX(), - y = e.getPageY(); - this.autoOffset(x, y); - }, - onInitDrag : function(x, y){ - this.onStartDrag(x, y); - return true; - }, - createFrame : Ext.emptyFn, - getDragEl : function(e){ - return this.proxy.ghost.dom; - }, - endDrag : function(e){ - this.proxy.hide(); - this.panel.saveState(); - }, - - autoOffset : function(x, y) { - x -= this.startPageX; - y -= this.startPageY; - this.setDelta(x, y); - } -});/** - * @class Ext.state.Provider - * Abstract base class for state provider implementations. This class provides methods - * for encoding and decoding typed variables including dates and defines the - * Provider interface. - */ -Ext.state.Provider = Ext.extend(Ext.util.Observable, { - - constructor : function(){ - /** - * @event statechange - * Fires when a state change occurs. - * @param {Provider} this This state provider - * @param {String} key The state key which was changed - * @param {String} value The encoded value for the state - */ - this.addEvents("statechange"); - this.state = {}; - Ext.state.Provider.superclass.constructor.call(this); - }, - - /** - * Returns the current value for a key - * @param {String} name The key name - * @param {Mixed} defaultValue A default value to return if the key's value is not found - * @return {Mixed} The state data - */ - get : function(name, defaultValue){ - return typeof this.state[name] == "undefined" ? - defaultValue : this.state[name]; - }, - - /** - * Clears a value from the state - * @param {String} name The key name - */ - clear : function(name){ - delete this.state[name]; - this.fireEvent("statechange", this, name, null); - }, - - /** - * Sets the value for a key - * @param {String} name The key name - * @param {Mixed} value The value to set - */ - set : function(name, value){ - this.state[name] = value; - this.fireEvent("statechange", this, name, value); - }, - - /** - * Decodes a string previously encoded with {@link #encodeValue}. - * @param {String} value The value to decode - * @return {Mixed} The decoded value - */ - decodeValue : function(cookie){ - /** - * a -> Array - * n -> Number - * d -> Date - * b -> Boolean - * s -> String - * o -> Object - * -> Empty (null) - */ - var re = /^(a|n|d|b|s|o|e)\:(.*)$/, - matches = re.exec(unescape(cookie)), - all, - type, - v, - kv; - if(!matches || !matches[1]){ - return; // non state cookie - } - type = matches[1]; - v = matches[2]; - switch(type){ - case 'e': - return null; - case 'n': - return parseFloat(v); - case 'd': - return new Date(Date.parse(v)); - case 'b': - return (v == '1'); - case 'a': - all = []; - if(v != ''){ - Ext.each(v.split('^'), function(val){ - all.push(this.decodeValue(val)); - }, this); - } - return all; - case 'o': - all = {}; - if(v != ''){ - Ext.each(v.split('^'), function(val){ - kv = val.split('='); - all[kv[0]] = this.decodeValue(kv[1]); - }, this); - } - return all; - default: - return v; - } - }, - - /** - * Encodes a value including type information. Decode with {@link #decodeValue}. - * @param {Mixed} value The value to encode - * @return {String} The encoded value - */ - encodeValue : function(v){ - var enc, - flat = '', - i = 0, - len, - key; - if(v == null){ - return 'e:1'; - }else if(typeof v == 'number'){ - enc = 'n:' + v; - }else if(typeof v == 'boolean'){ - enc = 'b:' + (v ? '1' : '0'); - }else if(Ext.isDate(v)){ - enc = 'd:' + v.toGMTString(); - }else if(Ext.isArray(v)){ - for(len = v.length; i < len; i++){ - flat += this.encodeValue(v[i]); - if(i != len - 1){ - flat += '^'; - } - } - enc = 'a:' + flat; - }else if(typeof v == 'object'){ - for(key in v){ - if(typeof v[key] != 'function' && v[key] !== undefined){ - flat += key + '=' + this.encodeValue(v[key]) + '^'; - } - } - enc = 'o:' + flat.substring(0, flat.length-1); - }else{ - enc = 's:' + v; - } - return escape(enc); - } -}); -/** - * @class Ext.state.Manager - * This is the global state manager. By default all components that are "state aware" check this class - * for state information if you don't pass them a custom state provider. In order for this class - * to be useful, it must be initialized with a provider when your application initializes. Example usage: -

-// in your initialization function
-init : function(){
-   Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
-   var win = new Window(...);
-   win.restoreState();
-}
- 
- * @singleton - */ -Ext.state.Manager = function(){ - var provider = new Ext.state.Provider(); - - return { - /** - * Configures the default state provider for your application - * @param {Provider} stateProvider The state provider to set - */ - setProvider : function(stateProvider){ - provider = stateProvider; - }, - - /** - * Returns the current value for a key - * @param {String} name The key name - * @param {Mixed} defaultValue The default value to return if the key lookup does not match - * @return {Mixed} The state data - */ - get : function(key, defaultValue){ - return provider.get(key, defaultValue); - }, - - /** - * Sets the value for a key - * @param {String} name The key name - * @param {Mixed} value The state data - */ - set : function(key, value){ - provider.set(key, value); - }, - - /** - * Clears a value from the state - * @param {String} name The key name - */ - clear : function(key){ - provider.clear(key); - }, - - /** - * Gets the currently configured state provider - * @return {Provider} The state provider - */ - getProvider : function(){ - return provider; - } - }; -}(); -/** - * @class Ext.state.CookieProvider - * @extends Ext.state.Provider - * The default Provider implementation which saves state via cookies. - *
Usage: -

-   var cp = new Ext.state.CookieProvider({
-       path: "/cgi-bin/",
-       expires: new Date(new Date().getTime()+(1000*60*60*24*30)), //30 days
-       domain: "extjs.com"
-   });
-   Ext.state.Manager.setProvider(cp);
- 
- * @cfg {String} path The path for which the cookie is active (defaults to root '/' which makes it active for all pages in the site) - * @cfg {Date} expires The cookie expiration date (defaults to 7 days from now) - * @cfg {String} domain The domain to save the cookie for. Note that you cannot specify a different domain than - * your page is on, but you can specify a sub-domain, or simply the domain itself like 'extjs.com' to include - * all sub-domains if you need to access cookies across different sub-domains (defaults to null which uses the same - * domain the page is running on including the 'www' like 'www.extjs.com') - * @cfg {Boolean} secure True if the site is using SSL (defaults to false) - * @constructor - * Create a new CookieProvider - * @param {Object} config The configuration object - */ -Ext.state.CookieProvider = Ext.extend(Ext.state.Provider, { - - constructor : function(config){ - Ext.state.CookieProvider.superclass.constructor.call(this); - this.path = "/"; - this.expires = new Date(new Date().getTime()+(1000*60*60*24*7)); //7 days - this.domain = null; - this.secure = false; - Ext.apply(this, config); - this.state = this.readCookies(); - }, - - // private - set : function(name, value){ - if(typeof value == "undefined" || value === null){ - this.clear(name); - return; - } - this.setCookie(name, value); - Ext.state.CookieProvider.superclass.set.call(this, name, value); - }, - - // private - clear : function(name){ - this.clearCookie(name); - Ext.state.CookieProvider.superclass.clear.call(this, name); - }, - - // private - readCookies : function(){ - var cookies = {}, - c = document.cookie + ";", - re = /\s?(.*?)=(.*?);/g, - matches, - name, - value; - while((matches = re.exec(c)) != null){ - name = matches[1]; - value = matches[2]; - if(name && name.substring(0,3) == "ys-"){ - cookies[name.substr(3)] = this.decodeValue(value); - } - } - return cookies; - }, - - // private - setCookie : function(name, value){ - document.cookie = "ys-"+ name + "=" + this.encodeValue(value) + - ((this.expires == null) ? "" : ("; expires=" + this.expires.toGMTString())) + - ((this.path == null) ? "" : ("; path=" + this.path)) + - ((this.domain == null) ? "" : ("; domain=" + this.domain)) + - ((this.secure == true) ? "; secure" : ""); - }, - - // private - clearCookie : function(name){ - document.cookie = "ys-" + name + "=null; expires=Thu, 01-Jan-70 00:00:01 GMT" + - ((this.path == null) ? "" : ("; path=" + this.path)) + - ((this.domain == null) ? "" : ("; domain=" + this.domain)) + - ((this.secure == true) ? "; secure" : ""); - } -});/** - * @class Ext.DataView - * @extends Ext.BoxComponent - * A mechanism for displaying data using custom layout templates and formatting. DataView uses an {@link Ext.XTemplate} - * as its internal templating mechanism, and is bound to an {@link Ext.data.Store} - * so that as the data in the store changes the view is automatically updated to reflect the changes. The view also - * provides built-in behavior for many common events that can occur for its contained items including click, doubleclick, - * mouseover, mouseout, etc. as well as a built-in selection model. In order to use these features, an {@link #itemSelector} - * config must be provided for the DataView to determine what nodes it will be working with. - * - *

The example below binds a DataView to a {@link Ext.data.Store} and renders it into an {@link Ext.Panel}.

- *

-var store = new Ext.data.JsonStore({
-    url: 'get-images.php',
-    root: 'images',
-    fields: [
-        'name', 'url',
-        {name:'size', type: 'float'},
-        {name:'lastmod', type:'date', dateFormat:'timestamp'}
-    ]
-});
-store.load();
-
-var tpl = new Ext.XTemplate(
-    '<tpl for=".">',
-        '<div class="thumb-wrap" id="{name}">',
-        '<div class="thumb"><img src="{url}" title="{name}"></div>',
-        '<span class="x-editable">{shortName}</span></div>',
-    '</tpl>',
-    '<div class="x-clear"></div>'
-);
-
-var panel = new Ext.Panel({
-    id:'images-view',
-    frame:true,
-    width:535,
-    autoHeight:true,
-    collapsible:true,
-    layout:'fit',
-    title:'Simple DataView',
-
-    items: new Ext.DataView({
-        store: store,
-        tpl: tpl,
-        autoHeight:true,
-        multiSelect: true,
-        overClass:'x-view-over',
-        itemSelector:'div.thumb-wrap',
-        emptyText: 'No images to display'
-    })
-});
-panel.render(document.body);
-
- * @constructor - * Create a new DataView - * @param {Object} config The config object - * @xtype dataview - */ -Ext.DataView = Ext.extend(Ext.BoxComponent, { - /** - * @cfg {String/Array} tpl - * The HTML fragment or an array of fragments that will make up the template used by this DataView. This should - * be specified in the same format expected by the constructor of {@link Ext.XTemplate}. - */ - /** - * @cfg {Ext.data.Store} store - * The {@link Ext.data.Store} to bind this DataView to. - */ - /** - * @cfg {String} itemSelector - * This is a required setting. A simple CSS selector (e.g. div.some-class or - * span:first-child) that will be used to determine what nodes this DataView will be - * working with. - */ - /** - * @cfg {Boolean} multiSelect - * True to allow selection of more than one item at a time, false to allow selection of only a single item - * at a time or no selection at all, depending on the value of {@link #singleSelect} (defaults to false). - */ - /** - * @cfg {Boolean} singleSelect - * True to allow selection of exactly one item at a time, false to allow no selection at all (defaults to false). - * Note that if {@link #multiSelect} = true, this value will be ignored. - */ - /** - * @cfg {Boolean} simpleSelect - * True to enable multiselection by clicking on multiple items without requiring the user to hold Shift or Ctrl, - * false to force the user to hold Ctrl or Shift to select more than on item (defaults to false). - */ - /** - * @cfg {String} overClass - * A CSS class to apply to each item in the view on mouseover (defaults to undefined). - */ - /** - * @cfg {String} loadingText - * A string to display during data load operations (defaults to undefined). If specified, this text will be - * displayed in a loading div and the view's contents will be cleared while loading, otherwise the view's - * contents will continue to display normally until the new data is loaded and the contents are replaced. - */ - /** - * @cfg {String} selectedClass - * A CSS class to apply to each selected item in the view (defaults to 'x-view-selected'). - */ - selectedClass : "x-view-selected", - /** - * @cfg {String} emptyText - * The text to display in the view when there is no data to display (defaults to ''). - */ - emptyText : "", - - /** - * @cfg {Boolean} deferEmptyText True to defer emptyText being applied until the store's first load - */ - deferEmptyText: true, - /** - * @cfg {Boolean} trackOver True to enable mouseenter and mouseleave events - */ - trackOver: false, - - /** - * @cfg {Boolean} blockRefresh Set this to true to ignore datachanged events on the bound store. This is useful if - * you wish to provide custom transition animations via a plugin (defaults to false) - */ - blockRefresh: false, - - //private - last: false, - - // private - initComponent : function(){ - Ext.DataView.superclass.initComponent.call(this); - if(Ext.isString(this.tpl) || Ext.isArray(this.tpl)){ - this.tpl = new Ext.XTemplate(this.tpl); - } - - this.addEvents( - /** - * @event beforeclick - * Fires before a click is processed. Returns false to cancel the default action. - * @param {Ext.DataView} this - * @param {Number} index The index of the target node - * @param {HTMLElement} node The target node - * @param {Ext.EventObject} e The raw event object - */ - "beforeclick", - /** - * @event click - * Fires when a template node is clicked. - * @param {Ext.DataView} this - * @param {Number} index The index of the target node - * @param {HTMLElement} node The target node - * @param {Ext.EventObject} e The raw event object - */ - "click", - /** - * @event mouseenter - * Fires when the mouse enters a template node. trackOver:true or an overClass must be set to enable this event. - * @param {Ext.DataView} this - * @param {Number} index The index of the target node - * @param {HTMLElement} node The target node - * @param {Ext.EventObject} e The raw event object - */ - "mouseenter", - /** - * @event mouseleave - * Fires when the mouse leaves a template node. trackOver:true or an overClass must be set to enable this event. - * @param {Ext.DataView} this - * @param {Number} index The index of the target node - * @param {HTMLElement} node The target node - * @param {Ext.EventObject} e The raw event object - */ - "mouseleave", - /** - * @event containerclick - * Fires when a click occurs and it is not on a template node. - * @param {Ext.DataView} this - * @param {Ext.EventObject} e The raw event object - */ - "containerclick", - /** - * @event dblclick - * Fires when a template node is double clicked. - * @param {Ext.DataView} this - * @param {Number} index The index of the target node - * @param {HTMLElement} node The target node - * @param {Ext.EventObject} e The raw event object - */ - "dblclick", - /** - * @event contextmenu - * Fires when a template node is right clicked. - * @param {Ext.DataView} this - * @param {Number} index The index of the target node - * @param {HTMLElement} node The target node - * @param {Ext.EventObject} e The raw event object - */ - "contextmenu", - /** - * @event containercontextmenu - * Fires when a right click occurs that is not on a template node. - * @param {Ext.DataView} this - * @param {Ext.EventObject} e The raw event object - */ - "containercontextmenu", - /** - * @event selectionchange - * Fires when the selected nodes change. - * @param {Ext.DataView} this - * @param {Array} selections Array of the selected nodes - */ - "selectionchange", - - /** - * @event beforeselect - * Fires before a selection is made. If any handlers return false, the selection is cancelled. - * @param {Ext.DataView} this - * @param {HTMLElement} node The node to be selected - * @param {Array} selections Array of currently selected nodes - */ - "beforeselect" - ); - - this.store = Ext.StoreMgr.lookup(this.store); - this.all = new Ext.CompositeElementLite(); - this.selected = new Ext.CompositeElementLite(); - }, - - // private - afterRender : function(){ - Ext.DataView.superclass.afterRender.call(this); - - this.mon(this.getTemplateTarget(), { - "click": this.onClick, - "dblclick": this.onDblClick, - "contextmenu": this.onContextMenu, - scope:this - }); - - if(this.overClass || this.trackOver){ - this.mon(this.getTemplateTarget(), { - "mouseover": this.onMouseOver, - "mouseout": this.onMouseOut, - scope:this - }); - } - - if(this.store){ - this.bindStore(this.store, true); - } - }, - - /** - * Refreshes the view by reloading the data from the store and re-rendering the template. - */ - refresh : function() { - this.clearSelections(false, true); - var el = this.getTemplateTarget(), - records = this.store.getRange(); - - el.update(''); - if(records.length < 1){ - if(!this.deferEmptyText || this.hasSkippedEmptyText){ - el.update(this.emptyText); - } - this.all.clear(); - }else{ - this.tpl.overwrite(el, this.collectData(records, 0)); - this.all.fill(Ext.query(this.itemSelector, el.dom)); - this.updateIndexes(0); - } - this.hasSkippedEmptyText = true; - }, - - getTemplateTarget: function(){ - return this.el; - }, - - /** - * Function which can be overridden to provide custom formatting for each Record that is used by this - * DataView's {@link #tpl template} to render each node. - * @param {Array/Object} data The raw data object that was used to create the Record. - * @param {Number} recordIndex the index number of the Record being prepared for rendering. - * @param {Record} record The Record being prepared for rendering. - * @return {Array/Object} The formatted data in a format expected by the internal {@link #tpl template}'s overwrite() method. - * (either an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'})) - */ - prepareData : function(data){ - return data; - }, - - /** - *

Function which can be overridden which returns the data object passed to this - * DataView's {@link #tpl template} to render the whole DataView.

- *

This is usually an Array of data objects, each element of which is processed by an - * {@link Ext.XTemplate XTemplate} which uses '<tpl for=".">' to iterate over its supplied - * data object as an Array. However, named properties may be placed into the data object to - * provide non-repeating data such as headings, totals etc.

- * @param {Array} records An Array of {@link Ext.data.Record}s to be rendered into the DataView. - * @param {Number} startIndex the index number of the Record being prepared for rendering. - * @return {Array} An Array of data objects to be processed by a repeating XTemplate. May also - * contain named properties. - */ - collectData : function(records, startIndex){ - var r = [], - i = 0, - len = records.length; - for(; i < len; i++){ - r[r.length] = this.prepareData(records[i].data, startIndex + i, records[i]); - } - return r; - }, - - // private - bufferRender : function(records, index){ - var div = document.createElement('div'); - this.tpl.overwrite(div, this.collectData(records, index)); - return Ext.query(this.itemSelector, div); - }, - - // private - onUpdate : function(ds, record){ - var index = this.store.indexOf(record); - if(index > -1){ - var sel = this.isSelected(index), - original = this.all.elements[index], - node = this.bufferRender([record], index)[0]; - - this.all.replaceElement(index, node, true); - if(sel){ - this.selected.replaceElement(original, node); - this.all.item(index).addClass(this.selectedClass); - } - this.updateIndexes(index, index); - } - }, - - // private - onAdd : function(ds, records, index){ - if(this.all.getCount() === 0){ - this.refresh(); - return; - } - var nodes = this.bufferRender(records, index), n, a = this.all.elements; - if(index < this.all.getCount()){ - n = this.all.item(index).insertSibling(nodes, 'before', true); - a.splice.apply(a, [index, 0].concat(nodes)); - }else{ - n = this.all.last().insertSibling(nodes, 'after', true); - a.push.apply(a, nodes); - } - this.updateIndexes(index); - }, - - // private - onRemove : function(ds, record, index){ - this.deselect(index); - this.all.removeElement(index, true); - this.updateIndexes(index); - if (this.store.getCount() === 0){ - this.refresh(); - } - }, - - /** - * Refreshes an individual node's data from the store. - * @param {Number} index The item's data index in the store - */ - refreshNode : function(index){ - this.onUpdate(this.store, this.store.getAt(index)); - }, - - // private - updateIndexes : function(startIndex, endIndex){ - var ns = this.all.elements; - startIndex = startIndex || 0; - endIndex = endIndex || ((endIndex === 0) ? 0 : (ns.length - 1)); - for(var i = startIndex; i <= endIndex; i++){ - ns[i].viewIndex = i; - } - }, - - /** - * Returns the store associated with this DataView. - * @return {Ext.data.Store} The store - */ - getStore : function(){ - return this.store; - }, - - /** - * Changes the data store bound to this view and refreshes it. - * @param {Store} store The store to bind to this view - */ - bindStore : function(store, initial){ - if(!initial && this.store){ - if(store !== this.store && this.store.autoDestroy){ - this.store.destroy(); - }else{ - this.store.un("beforeload", this.onBeforeLoad, this); - this.store.un("datachanged", this.onDataChanged, this); - this.store.un("add", this.onAdd, this); - this.store.un("remove", this.onRemove, this); - this.store.un("update", this.onUpdate, this); - this.store.un("clear", this.refresh, this); - } - if(!store){ - this.store = null; - } - } - if(store){ - store = Ext.StoreMgr.lookup(store); - store.on({ - scope: this, - beforeload: this.onBeforeLoad, - datachanged: this.onDataChanged, - add: this.onAdd, - remove: this.onRemove, - update: this.onUpdate, - clear: this.refresh - }); - } - this.store = store; - if(store){ - this.refresh(); - } - }, - - /** - * @private - * Calls this.refresh if this.blockRefresh is not true - */ - onDataChanged: function() { - if (this.blockRefresh !== true) { - this.refresh.apply(this, arguments); - } - }, - - /** - * Returns the template node the passed child belongs to, or null if it doesn't belong to one. - * @param {HTMLElement} node - * @return {HTMLElement} The template node - */ - findItemFromChild : function(node){ - return Ext.fly(node).findParent(this.itemSelector, this.getTemplateTarget()); - }, - - // private - onClick : function(e){ - var item = e.getTarget(this.itemSelector, this.getTemplateTarget()), - index; - if(item){ - index = this.indexOf(item); - if(this.onItemClick(item, index, e) !== false){ - this.fireEvent("click", this, index, item, e); - } - }else{ - if(this.fireEvent("containerclick", this, e) !== false){ - this.onContainerClick(e); - } - } - }, - - onContainerClick : function(e){ - this.clearSelections(); - }, - - // private - onContextMenu : function(e){ - var item = e.getTarget(this.itemSelector, this.getTemplateTarget()); - if(item){ - this.fireEvent("contextmenu", this, this.indexOf(item), item, e); - }else{ - this.fireEvent("containercontextmenu", this, e); - } - }, - - // private - onDblClick : function(e){ - var item = e.getTarget(this.itemSelector, this.getTemplateTarget()); - if(item){ - this.fireEvent("dblclick", this, this.indexOf(item), item, e); - } - }, - - // private - onMouseOver : function(e){ - var item = e.getTarget(this.itemSelector, this.getTemplateTarget()); - if(item && item !== this.lastItem){ - this.lastItem = item; - Ext.fly(item).addClass(this.overClass); - this.fireEvent("mouseenter", this, this.indexOf(item), item, e); - } - }, - - // private - onMouseOut : function(e){ - if(this.lastItem){ - if(!e.within(this.lastItem, true, true)){ - Ext.fly(this.lastItem).removeClass(this.overClass); - this.fireEvent("mouseleave", this, this.indexOf(this.lastItem), this.lastItem, e); - delete this.lastItem; - } - } - }, - - // private - onItemClick : function(item, index, e){ - if(this.fireEvent("beforeclick", this, index, item, e) === false){ - return false; - } - if(this.multiSelect){ - this.doMultiSelection(item, index, e); - e.preventDefault(); - }else if(this.singleSelect){ - this.doSingleSelection(item, index, e); - e.preventDefault(); - } - return true; - }, - - // private - doSingleSelection : function(item, index, e){ - if(e.ctrlKey && this.isSelected(index)){ - this.deselect(index); - }else{ - this.select(index, false); - } - }, - - // private - doMultiSelection : function(item, index, e){ - if(e.shiftKey && this.last !== false){ - var last = this.last; - this.selectRange(last, index, e.ctrlKey); - this.last = last; // reset the last - }else{ - if((e.ctrlKey||this.simpleSelect) && this.isSelected(index)){ - this.deselect(index); - }else{ - this.select(index, e.ctrlKey || e.shiftKey || this.simpleSelect); - } - } - }, - - /** - * Gets the number of selected nodes. - * @return {Number} The node count - */ - getSelectionCount : function(){ - return this.selected.getCount(); - }, - - /** - * Gets the currently selected nodes. - * @return {Array} An array of HTMLElements - */ - getSelectedNodes : function(){ - return this.selected.elements; - }, - - /** - * Gets the indexes of the selected nodes. - * @return {Array} An array of numeric indexes - */ - getSelectedIndexes : function(){ - var indexes = [], - selected = this.selected.elements, - i = 0, - len = selected.length; - - for(; i < len; i++){ - indexes.push(selected[i].viewIndex); - } - return indexes; - }, - - /** - * Gets an array of the selected records - * @return {Array} An array of {@link Ext.data.Record} objects - */ - getSelectedRecords : function(){ - return this.getRecords(this.selected.elements); - }, - - /** - * Gets an array of the records from an array of nodes - * @param {Array} nodes The nodes to evaluate - * @return {Array} records The {@link Ext.data.Record} objects - */ - getRecords : function(nodes){ - var records = [], - i = 0, - len = nodes.length; - - for(; i < len; i++){ - records[records.length] = this.store.getAt(nodes[i].viewIndex); - } - return records; - }, - - /** - * Gets a record from a node - * @param {HTMLElement} node The node to evaluate - * @return {Record} record The {@link Ext.data.Record} object - */ - getRecord : function(node){ - return this.store.getAt(node.viewIndex); - }, - - /** - * Clears all selections. - * @param {Boolean} suppressEvent (optional) True to skip firing of the selectionchange event - */ - clearSelections : function(suppressEvent, skipUpdate){ - if((this.multiSelect || this.singleSelect) && this.selected.getCount() > 0){ - if(!skipUpdate){ - this.selected.removeClass(this.selectedClass); - } - this.selected.clear(); - this.last = false; - if(!suppressEvent){ - this.fireEvent("selectionchange", this, this.selected.elements); - } - } - }, - - /** - * Returns true if the passed node is selected, else false. - * @param {HTMLElement/Number/Ext.data.Record} node The node, node index or record to check - * @return {Boolean} True if selected, else false - */ - isSelected : function(node){ - return this.selected.contains(this.getNode(node)); - }, - - /** - * Deselects a node. - * @param {HTMLElement/Number/Record} node The node, node index or record to deselect - */ - deselect : function(node){ - if(this.isSelected(node)){ - node = this.getNode(node); - this.selected.removeElement(node); - if(this.last == node.viewIndex){ - this.last = false; - } - Ext.fly(node).removeClass(this.selectedClass); - this.fireEvent("selectionchange", this, this.selected.elements); - } - }, - - /** - * Selects a set of nodes. - * @param {Array/HTMLElement/String/Number/Ext.data.Record} nodeInfo An HTMLElement template node, index of a template node, - * id of a template node, record associated with a node or an array of any of those to select - * @param {Boolean} keepExisting (optional) true to keep existing selections - * @param {Boolean} suppressEvent (optional) true to skip firing of the selectionchange vent - */ - select : function(nodeInfo, keepExisting, suppressEvent){ - if(Ext.isArray(nodeInfo)){ - if(!keepExisting){ - this.clearSelections(true); - } - for(var i = 0, len = nodeInfo.length; i < len; i++){ - this.select(nodeInfo[i], true, true); - } - if(!suppressEvent){ - this.fireEvent("selectionchange", this, this.selected.elements); - } - } else{ - var node = this.getNode(nodeInfo); - if(!keepExisting){ - this.clearSelections(true); - } - if(node && !this.isSelected(node)){ - if(this.fireEvent("beforeselect", this, node, this.selected.elements) !== false){ - Ext.fly(node).addClass(this.selectedClass); - this.selected.add(node); - this.last = node.viewIndex; - if(!suppressEvent){ - this.fireEvent("selectionchange", this, this.selected.elements); - } - } - } - } - }, - - /** - * Selects a range of nodes. All nodes between start and end are selected. - * @param {Number} start The index of the first node in the range - * @param {Number} end The index of the last node in the range - * @param {Boolean} keepExisting (optional) True to retain existing selections - */ - selectRange : function(start, end, keepExisting){ - if(!keepExisting){ - this.clearSelections(true); - } - this.select(this.getNodes(start, end), true); - }, - - /** - * Gets a template node. - * @param {HTMLElement/String/Number/Ext.data.Record} nodeInfo An HTMLElement template node, index of a template node, - * the id of a template node or the record associated with the node. - * @return {HTMLElement} The node or null if it wasn't found - */ - getNode : function(nodeInfo){ - if(Ext.isString(nodeInfo)){ - return document.getElementById(nodeInfo); - }else if(Ext.isNumber(nodeInfo)){ - return this.all.elements[nodeInfo]; - }else if(nodeInfo instanceof Ext.data.Record){ - var idx = this.store.indexOf(nodeInfo); - return this.all.elements[idx]; - } - return nodeInfo; - }, - - /** - * Gets a range nodes. - * @param {Number} start (optional) The index of the first node in the range - * @param {Number} end (optional) The index of the last node in the range - * @return {Array} An array of nodes - */ - getNodes : function(start, end){ - var ns = this.all.elements, - nodes = [], - i; - - start = start || 0; - end = !Ext.isDefined(end) ? Math.max(ns.length - 1, 0) : end; - if(start <= end){ - for(i = start; i <= end && ns[i]; i++){ - nodes.push(ns[i]); - } - } else{ - for(i = start; i >= end && ns[i]; i--){ - nodes.push(ns[i]); - } - } - return nodes; - }, - - /** - * Finds the index of the passed node. - * @param {HTMLElement/String/Number/Record} nodeInfo An HTMLElement template node, index of a template node, the id of a template node - * or a record associated with a node. - * @return {Number} The index of the node or -1 - */ - indexOf : function(node){ - node = this.getNode(node); - if(Ext.isNumber(node.viewIndex)){ - return node.viewIndex; - } - return this.all.indexOf(node); - }, - - // private - onBeforeLoad : function(){ - if(this.loadingText){ - this.clearSelections(false, true); - this.getTemplateTarget().update('
'+this.loadingText+'
'); - this.all.clear(); - } - }, - - onDestroy : function(){ - this.all.clear(); - this.selected.clear(); - Ext.DataView.superclass.onDestroy.call(this); - this.bindStore(null); - } -}); - -/** - * Changes the data store bound to this view and refreshes it. (deprecated in favor of bindStore) - * @param {Store} store The store to bind to this view - */ -Ext.DataView.prototype.setStore = Ext.DataView.prototype.bindStore; - -Ext.reg('dataview', Ext.DataView); -/** - * @class Ext.list.ListView - * @extends Ext.DataView - *

Ext.list.ListView is a fast and light-weight implentation of a - * {@link Ext.grid.GridPanel Grid} like view with the following characteristics:

- *
    - *
  • resizable columns
  • - *
  • selectable
  • - *
  • column widths are initially proportioned by percentage based on the container - * width and number of columns
  • - *
  • uses templates to render the data in any required format
  • - *
  • no horizontal scrolling
  • - *
  • no editing
  • - *
- *

Example usage:

- *

-// consume JSON of this form:
-{
-   "images":[
-      {
-         "name":"dance_fever.jpg",
-         "size":2067,
-         "lastmod":1236974993000,
-         "url":"images\/thumbs\/dance_fever.jpg"
-      },
-      {
-         "name":"zack_sink.jpg",
-         "size":2303,
-         "lastmod":1236974993000,
-         "url":"images\/thumbs\/zack_sink.jpg"
-      }
-   ]
-}
-var store = new Ext.data.JsonStore({
-    url: 'get-images.php',
-    root: 'images',
-    fields: [
-        'name', 'url',
-        {name:'size', type: 'float'},
-        {name:'lastmod', type:'date', dateFormat:'timestamp'}
-    ]
-});
-store.load();
-
-var listView = new Ext.list.ListView({
-    store: store,
-    multiSelect: true,
-    emptyText: 'No images to display',
-    reserveScrollOffset: true,
-    columns: [{
-        header: 'File',
-        width: .5,
-        dataIndex: 'name'
-    },{
-        header: 'Last Modified',
-        width: .35,
-        dataIndex: 'lastmod',
-        tpl: '{lastmod:date("m-d h:i a")}'
-    },{
-        header: 'Size',
-        dataIndex: 'size',
-        tpl: '{size:fileSize}', // format using Ext.util.Format.fileSize()
-        align: 'right'
-    }]
-});
-
-// put it in a Panel so it looks pretty
-var panel = new Ext.Panel({
-    id:'images-view',
-    width:425,
-    height:250,
-    collapsible:true,
-    layout:'fit',
-    title:'Simple ListView (0 items selected)',
-    items: listView
-});
-panel.render(document.body);
-
-// little bit of feedback
-listView.on('selectionchange', function(view, nodes){
-    var l = nodes.length;
-    var s = l != 1 ? 's' : '';
-    panel.setTitle('Simple ListView ('+l+' item'+s+' selected)');
-});
- * 
- * @constructor - * @param {Object} config - * @xtype listview - */ -Ext.list.ListView = Ext.extend(Ext.DataView, { - /** - * Set this property to true to disable the header click handler disabling sort - * (defaults to false). - * @type Boolean - * @property disableHeaders - */ - /** - * @cfg {Boolean} hideHeaders - * true to hide the {@link #internalTpl header row} (defaults to false so - * the {@link #internalTpl header row} will be shown). - */ - /** - * @cfg {String} itemSelector - * Defaults to 'dl' to work with the preconfigured {@link Ext.DataView#tpl tpl}. - * This setting specifies the CSS selector (e.g. div.some-class or span:first-child) - * that will be used to determine what nodes the ListView will be working with. - */ - itemSelector: 'dl', - /** - * @cfg {String} selectedClass The CSS class applied to a selected row (defaults to - * 'x-list-selected'). An example overriding the default styling: -

-    .x-list-selected {background-color: yellow;}
-    
- * @type String - */ - selectedClass:'x-list-selected', - /** - * @cfg {String} overClass The CSS class applied when over a row (defaults to - * 'x-list-over'). An example overriding the default styling: -

-    .x-list-over {background-color: orange;}
-    
- * @type String - */ - overClass:'x-list-over', - /** - * @cfg {Boolean} reserveScrollOffset - * By default will defer accounting for the configured {@link #scrollOffset} - * for 10 milliseconds. Specify true to account for the configured - * {@link #scrollOffset} immediately. - */ - /** - * @cfg {Number} scrollOffset The amount of space to reserve for the scrollbar (defaults to - * undefined). If an explicit value isn't specified, this will be automatically - * calculated. - */ - scrollOffset : undefined, - /** - * @cfg {Boolean/Object} columnResize - * Specify true or specify a configuration object for {@link Ext.list.ListView.ColumnResizer} - * to enable the columns to be resizable (defaults to true). - */ - columnResize: true, - /** - * @cfg {Array} columns An array of column configuration objects, for example: - *

-{
-    align: 'right',
-    dataIndex: 'size',
-    header: 'Size',
-    tpl: '{size:fileSize}',
-    width: .35
-}
-     * 
- * Acceptable properties for each column configuration object are: - *
    - *
  • align : String
    Set the CSS text-align property - * of the column. Defaults to 'left'.
  • - *
  • dataIndex : String
    See {@link Ext.grid.Column}. - * {@link Ext.grid.Column#dataIndex dataIndex} for details.
  • - *
  • header : String
    See {@link Ext.grid.Column}. - * {@link Ext.grid.Column#header header} for details.
  • - *
  • tpl : String
    Specify a string to pass as the - * configuration string for {@link Ext.XTemplate}. By default an {@link Ext.XTemplate} - * will be implicitly created using the dataIndex.
  • - *
  • width : Number
    Percentage of the container width - * this column should be allocated. Columns that have no width specified will be - * allocated with an equal percentage to fill 100% of the container width. To easily take - * advantage of the full container width, leave the width of at least one column undefined. - * Note that if you do not want to take up the full width of the container, the width of - * every column needs to be explicitly defined.
  • - *
- */ - /** - * @cfg {Boolean/Object} columnSort - * Specify true or specify a configuration object for {@link Ext.list.ListView.Sorter} - * to enable the columns to be sortable (defaults to true). - */ - columnSort: true, - /** - * @cfg {String/Array} internalTpl - * The template to be used for the header row. See {@link #tpl} for more details. - */ - - /* - * IE has issues when setting percentage based widths to 100%. Default to 99. - */ - maxColumnWidth: Ext.isIE ? 99 : 100, - - initComponent : function(){ - if(this.columnResize){ - this.colResizer = new Ext.list.ColumnResizer(this.colResizer); - this.colResizer.init(this); - } - if(this.columnSort){ - this.colSorter = new Ext.list.Sorter(this.columnSort); - this.colSorter.init(this); - } - if(!this.internalTpl){ - this.internalTpl = new Ext.XTemplate( - '
', - '', - '
', - '{header}', - '
', - '
', - '
', - '
', - '
', - '
' - ); - } - if(!this.tpl){ - this.tpl = new Ext.XTemplate( - '', - '
', - '', - '
', - ' class="{cls}">', - '{[values.tpl.apply(parent)]}', - '
', - '
', - '
', - '
', - '
' - ); - }; - - var cs = this.columns, - allocatedWidth = 0, - colsWithWidth = 0, - len = cs.length, - columns = []; - - for(var i = 0; i < len; i++){ - var c = cs[i]; - if(!c.isColumn) { - c.xtype = c.xtype ? (/^lv/.test(c.xtype) ? c.xtype : 'lv' + c.xtype) : 'lvcolumn'; - c = Ext.create(c); - } - if(c.width) { - allocatedWidth += c.width*100; - if(allocatedWidth > this.maxColumnWidth){ - c.width -= (allocatedWidth - this.maxColumnWidth) / 100; - } - colsWithWidth++; - } - columns.push(c); - } - - cs = this.columns = columns; - - // auto calculate missing column widths - if(colsWithWidth < len){ - var remaining = len - colsWithWidth; - if(allocatedWidth < this.maxColumnWidth){ - var perCol = ((this.maxColumnWidth-allocatedWidth) / remaining)/100; - for(var j = 0; j < len; j++){ - var c = cs[j]; - if(!c.width){ - c.width = perCol; - } - } - } - } - Ext.list.ListView.superclass.initComponent.call(this); - }, - - onRender : function(){ - this.autoEl = { - cls: 'x-list-wrap' - }; - Ext.list.ListView.superclass.onRender.apply(this, arguments); - - this.internalTpl.overwrite(this.el, {columns: this.columns}); - - this.innerBody = Ext.get(this.el.dom.childNodes[1].firstChild); - this.innerHd = Ext.get(this.el.dom.firstChild.firstChild); - - if(this.hideHeaders){ - this.el.dom.firstChild.style.display = 'none'; - } - }, - - getTemplateTarget : function(){ - return this.innerBody; - }, - - /** - *

Function which can be overridden which returns the data object passed to this - * view's {@link #tpl template} to render the whole ListView. The returned object - * shall contain the following properties:

- *
    - *
  • columns : String
    See {@link #columns}
  • - *
  • rows : String
    See - * {@link Ext.DataView}.{@link Ext.DataView#collectData collectData}
  • - *
- * @param {Array} records An Array of {@link Ext.data.Record}s to be rendered into the DataView. - * @param {Number} startIndex the index number of the Record being prepared for rendering. - * @return {Object} A data object containing properties to be processed by a repeating - * XTemplate as described above. - */ - collectData : function(){ - var rs = Ext.list.ListView.superclass.collectData.apply(this, arguments); - return { - columns: this.columns, - rows: rs - }; - }, - - verifyInternalSize : function(){ - if(this.lastSize){ - this.onResize(this.lastSize.width, this.lastSize.height); - } - }, - - // private - onResize : function(w, h){ - var body = this.innerBody.dom, - header = this.innerHd.dom, - scrollWidth = w - Ext.num(this.scrollOffset, Ext.getScrollBarWidth()) + 'px', - parentNode; - - if(!body){ - return; - } - parentNode = body.parentNode; - if(Ext.isNumber(w)){ - if(this.reserveScrollOffset || ((parentNode.offsetWidth - parentNode.clientWidth) > 10)){ - body.style.width = scrollWidth; - header.style.width = scrollWidth; - }else{ - body.style.width = w + 'px'; - header.style.width = w + 'px'; - setTimeout(function(){ - if((parentNode.offsetWidth - parentNode.clientWidth) > 10){ - body.style.width = scrollWidth; - header.style.width = scrollWidth; - } - }, 10); - } - } - if(Ext.isNumber(h)){ - parentNode.style.height = Math.max(0, h - header.parentNode.offsetHeight) + 'px'; - } - }, - - updateIndexes : function(){ - Ext.list.ListView.superclass.updateIndexes.apply(this, arguments); - this.verifyInternalSize(); - }, - - findHeaderIndex : function(header){ - header = header.dom || header; - var parentNode = header.parentNode, - children = parentNode.parentNode.childNodes, - i = 0, - c; - for(; c = children[i]; i++){ - if(c == parentNode){ - return i; - } - } - return -1; - }, - - setHdWidths : function(){ - var els = this.innerHd.dom.getElementsByTagName('div'), - i = 0, - columns = this.columns, - len = columns.length; - - for(; i < len; i++){ - els[i].style.width = (columns[i].width*100) + '%'; - } - } -}); - -Ext.reg('listview', Ext.list.ListView); - -// Backwards compatibility alias -Ext.ListView = Ext.list.ListView;/** - * @class Ext.list.Column - *

This class encapsulates column configuration data to be used in the initialization of a - * {@link Ext.list.ListView ListView}.

- *

While subclasses are provided to render data in different ways, this class renders a passed - * data field unchanged and is usually used for textual columns.

- */ -Ext.list.Column = Ext.extend(Object, { - /** - * @private - * @cfg {Boolean} isColumn - * Used by ListView constructor method to avoid reprocessing a Column - * if isColumn is not set ListView will recreate a new Ext.list.Column - * Defaults to true. - */ - isColumn: true, - - /** - * @cfg {String} align - * Set the CSS text-align property of the column. Defaults to 'left'. - */ - align: 'left', - /** - * @cfg {String} header Optional. The header text to be used as innerHTML - * (html tags are accepted) to display in the ListView. Note: to - * have a clickable header with no text displayed use ' '. - */ - header: '', - - /** - * @cfg {Number} width Optional. Percentage of the container width - * this column should be allocated. Columns that have no width specified will be - * allocated with an equal percentage to fill 100% of the container width. To easily take - * advantage of the full container width, leave the width of at least one column undefined. - * Note that if you do not want to take up the full width of the container, the width of - * every column needs to be explicitly defined. - */ - width: null, - - /** - * @cfg {String} cls Optional. This option can be used to add a CSS class to the cell of each - * row for this column. - */ - cls: '', - - /** - * @cfg {String} tpl Optional. Specify a string to pass as the - * configuration string for {@link Ext.XTemplate}. By default an {@link Ext.XTemplate} - * will be implicitly created using the dataIndex. - */ - - /** - * @cfg {String} dataIndex

Required. The name of the field in the - * ListViews's {@link Ext.data.Store}'s {@link Ext.data.Record} definition from - * which to draw the column's value.

- */ - - constructor : function(c){ - if(!c.tpl){ - c.tpl = new Ext.XTemplate('{' + c.dataIndex + '}'); - } - else if(Ext.isString(c.tpl)){ - c.tpl = new Ext.XTemplate(c.tpl); - } - - Ext.apply(this, c); - } -}); - -Ext.reg('lvcolumn', Ext.list.Column); - -/** - * @class Ext.list.NumberColumn - * @extends Ext.list.Column - *

A Column definition class which renders a numeric data field according to a {@link #format} string. See the - * {@link Ext.list.Column#xtype xtype} config option of {@link Ext.list.Column} for more details.

- */ -Ext.list.NumberColumn = Ext.extend(Ext.list.Column, { - /** - * @cfg {String} format - * A formatting string as used by {@link Ext.util.Format#number} to format a numeric value for this Column - * (defaults to '0,000.00'). - */ - format: '0,000.00', - - constructor : function(c) { - c.tpl = c.tpl || new Ext.XTemplate('{' + c.dataIndex + ':number("' + (c.format || this.format) + '")}'); - Ext.list.NumberColumn.superclass.constructor.call(this, c); - } -}); - -Ext.reg('lvnumbercolumn', Ext.list.NumberColumn); - -/** - * @class Ext.list.DateColumn - * @extends Ext.list.Column - *

A Column definition class which renders a passed date according to the default locale, or a configured - * {@link #format}. See the {@link Ext.list.Column#xtype xtype} config option of {@link Ext.list.Column} - * for more details.

- */ -Ext.list.DateColumn = Ext.extend(Ext.list.Column, { - format: 'm/d/Y', - constructor : function(c) { - c.tpl = c.tpl || new Ext.XTemplate('{' + c.dataIndex + ':date("' + (c.format || this.format) + '")}'); - Ext.list.DateColumn.superclass.constructor.call(this, c); - } -}); -Ext.reg('lvdatecolumn', Ext.list.DateColumn); - -/** - * @class Ext.list.BooleanColumn - * @extends Ext.list.Column - *

A Column definition class which renders boolean data fields. See the {@link Ext.list.Column#xtype xtype} - * config option of {@link Ext.list.Column} for more details.

- */ -Ext.list.BooleanColumn = Ext.extend(Ext.list.Column, { - /** - * @cfg {String} trueText - * The string returned by the renderer when the column value is not falsey (defaults to 'true'). - */ - trueText: 'true', - /** - * @cfg {String} falseText - * The string returned by the renderer when the column value is falsey (but not undefined) (defaults to - * 'false'). - */ - falseText: 'false', - /** - * @cfg {String} undefinedText - * The string returned by the renderer when the column value is undefined (defaults to ' '). - */ - undefinedText: ' ', - - constructor : function(c) { - c.tpl = c.tpl || new Ext.XTemplate('{' + c.dataIndex + ':this.format}'); - - var t = this.trueText, f = this.falseText, u = this.undefinedText; - c.tpl.format = function(v){ - if(v === undefined){ - return u; - } - if(!v || v === 'false'){ - return f; - } - return t; - }; - - Ext.list.DateColumn.superclass.constructor.call(this, c); - } -}); - -Ext.reg('lvbooleancolumn', Ext.list.BooleanColumn);/** - * @class Ext.list.ColumnResizer - * @extends Ext.util.Observable - *

Supporting Class for Ext.list.ListView

- * @constructor - * @param {Object} config - */ -Ext.list.ColumnResizer = Ext.extend(Ext.util.Observable, { - /** - * @cfg {Number} minPct The minimum percentage to allot for any column (defaults to .05) - */ - minPct: .05, - - constructor: function(config){ - Ext.apply(this, config); - Ext.list.ColumnResizer.superclass.constructor.call(this); - }, - init : function(listView){ - this.view = listView; - listView.on('render', this.initEvents, this); - }, - - initEvents : function(view){ - view.mon(view.innerHd, 'mousemove', this.handleHdMove, this); - this.tracker = new Ext.dd.DragTracker({ - onBeforeStart: this.onBeforeStart.createDelegate(this), - onStart: this.onStart.createDelegate(this), - onDrag: this.onDrag.createDelegate(this), - onEnd: this.onEnd.createDelegate(this), - tolerance: 3, - autoStart: 300 - }); - this.tracker.initEl(view.innerHd); - view.on('beforedestroy', this.tracker.destroy, this.tracker); - }, - - handleHdMove : function(e, t){ - var handleWidth = 5, - x = e.getPageX(), - header = e.getTarget('em', 3, true); - if(header){ - var region = header.getRegion(), - style = header.dom.style, - parentNode = header.dom.parentNode; - - if(x - region.left <= handleWidth && parentNode != parentNode.parentNode.firstChild){ - this.activeHd = Ext.get(parentNode.previousSibling.firstChild); - style.cursor = Ext.isWebKit ? 'e-resize' : 'col-resize'; - } else if(region.right - x <= handleWidth && parentNode != parentNode.parentNode.lastChild.previousSibling){ - this.activeHd = header; - style.cursor = Ext.isWebKit ? 'w-resize' : 'col-resize'; - } else{ - delete this.activeHd; - style.cursor = ''; - } - } - }, - - onBeforeStart : function(e){ - this.dragHd = this.activeHd; - return !!this.dragHd; - }, - - onStart: function(e){ - - var me = this, - view = me.view, - dragHeader = me.dragHd, - x = me.tracker.getXY()[0]; - - me.proxy = view.el.createChild({cls:'x-list-resizer'}); - me.dragX = dragHeader.getX(); - me.headerIndex = view.findHeaderIndex(dragHeader); - - me.headersDisabled = view.disableHeaders; - view.disableHeaders = true; - - me.proxy.setHeight(view.el.getHeight()); - me.proxy.setX(me.dragX); - me.proxy.setWidth(x - me.dragX); - - this.setBoundaries(); - - }, - - // Sets up the boundaries for the drag/drop operation - setBoundaries: function(relativeX){ - var view = this.view, - headerIndex = this.headerIndex, - width = view.innerHd.getWidth(), - relativeX = view.innerHd.getX(), - minWidth = Math.ceil(width * this.minPct), - maxWidth = width - minWidth, - numColumns = view.columns.length, - headers = view.innerHd.select('em', true), - minX = minWidth + relativeX, - maxX = maxWidth + relativeX, - header; - - if (numColumns == 2) { - this.minX = minX; - this.maxX = maxX; - }else{ - header = headers.item(headerIndex + 2); - this.minX = headers.item(headerIndex).getX() + minWidth; - this.maxX = header ? header.getX() - minWidth : maxX; - if (headerIndex == 0) { - // First - this.minX = minX; - } else if (headerIndex == numColumns - 2) { - // Last - this.maxX = maxX; - } - } - }, - - onDrag: function(e){ - var me = this, - cursorX = me.tracker.getXY()[0].constrain(me.minX, me.maxX); - - me.proxy.setWidth(cursorX - this.dragX); - }, - - onEnd: function(e){ - /* calculate desired width by measuring proxy and then remove it */ - var newWidth = this.proxy.getWidth(), - index = this.headerIndex, - view = this.view, - columns = view.columns, - width = view.innerHd.getWidth(), - newPercent = Math.ceil(newWidth * view.maxColumnWidth / width) / 100, - disabled = this.headersDisabled, - headerCol = columns[index], - otherCol = columns[index + 1], - totalPercent = headerCol.width + otherCol.width; - - this.proxy.remove(); - - headerCol.width = newPercent; - otherCol.width = totalPercent - newPercent; - - delete this.dragHd; - view.setHdWidths(); - view.refresh(); - - setTimeout(function(){ - view.disableHeaders = disabled; - }, 100); - } -}); - -// Backwards compatibility alias -Ext.ListView.ColumnResizer = Ext.list.ColumnResizer;/** - * @class Ext.list.Sorter - * @extends Ext.util.Observable - *

Supporting Class for Ext.list.ListView

- * @constructor - * @param {Object} config - */ -Ext.list.Sorter = Ext.extend(Ext.util.Observable, { - /** - * @cfg {Array} sortClasses - * The CSS classes applied to a header when it is sorted. (defaults to ["sort-asc", "sort-desc"]) - */ - sortClasses : ["sort-asc", "sort-desc"], - - constructor: function(config){ - Ext.apply(this, config); - Ext.list.Sorter.superclass.constructor.call(this); - }, - - init : function(listView){ - this.view = listView; - listView.on('render', this.initEvents, this); - }, - - initEvents : function(view){ - view.mon(view.innerHd, 'click', this.onHdClick, this); - view.innerHd.setStyle('cursor', 'pointer'); - view.mon(view.store, 'datachanged', this.updateSortState, this); - this.updateSortState.defer(10, this, [view.store]); - }, - - updateSortState : function(store){ - var state = store.getSortState(); - if(!state){ - return; - } - this.sortState = state; - var cs = this.view.columns, sortColumn = -1; - for(var i = 0, len = cs.length; i < len; i++){ - if(cs[i].dataIndex == state.field){ - sortColumn = i; - break; - } - } - if(sortColumn != -1){ - var sortDir = state.direction; - this.updateSortIcon(sortColumn, sortDir); - } - }, - - updateSortIcon : function(col, dir){ - var sc = this.sortClasses; - var hds = this.view.innerHd.select('em').removeClass(sc); - hds.item(col).addClass(sc[dir == "DESC" ? 1 : 0]); - }, - - onHdClick : function(e){ - var hd = e.getTarget('em', 3); - if(hd && !this.view.disableHeaders){ - var index = this.view.findHeaderIndex(hd); - this.view.store.sort(this.view.columns[index].dataIndex); - } - } -}); - -// Backwards compatibility alias -Ext.ListView.Sorter = Ext.list.Sorter;/** - * @class Ext.TabPanel - *

A basic tab container. TabPanels can be used exactly like a standard {@link Ext.Panel} - * for layout purposes, but also have special support for containing child Components - * ({@link Ext.Container#items items}) that are managed using a - * {@link Ext.layout.CardLayout CardLayout layout manager}, and displayed as separate tabs.

- * - * Note: By default, a tab's close tool destroys the child tab Component - * and all its descendants. This makes the child tab Component, and all its descendants unusable. To enable - * re-use of a tab, configure the TabPanel with {@link #autoDestroy autoDestroy: false}. - * - *

TabPanel header/footer elements

- *

TabPanels use their {@link Ext.Panel#header header} or {@link Ext.Panel#footer footer} element - * (depending on the {@link #tabPosition} configuration) to accommodate the tab selector buttons. - * This means that a TabPanel will not display any configured title, and will not display any - * configured header {@link Ext.Panel#tools tools}.

- *

To display a header, embed the TabPanel in a {@link Ext.Panel Panel} which uses - * {@link Ext.Container#layout layout:'fit'}.

- * - *

Tab Events

- *

There is no actual tab class — each tab is simply a {@link Ext.BoxComponent Component} - * such as a {@link Ext.Panel Panel}. However, when rendered in a TabPanel, each child Component - * can fire additional events that only exist for tabs and are not available from other Components. - * These events are:

- *
    - *
  • {@link Ext.Panel#activate activate} : Fires when this Component becomes - * the active tab.
  • - *
  • {@link Ext.Panel#deactivate deactivate} : Fires when the Component that - * was the active tab becomes deactivated.
  • - *
  • {@link Ext.Panel#beforeclose beforeclose} : Fires when the user clicks on the close tool of a closeable tab. - * May be vetoed by returning false from a handler.
  • - *
  • {@link Ext.Panel#close close} : Fires a closeable tab has been closed by the user.
  • - *
- *

Creating TabPanels from Code

- *

TabPanels can be created and rendered completely in code, as in this example:

- *

-var tabs = new Ext.TabPanel({
-    renderTo: Ext.getBody(),
-    activeTab: 0,
-    items: [{
-        title: 'Tab 1',
-        html: 'A simple tab'
-    },{
-        title: 'Tab 2',
-        html: 'Another one'
-    }]
-});
-
- *

Creating TabPanels from Existing Markup

- *

TabPanels can also be rendered from pre-existing markup in a couple of ways.

- *
    - * - *
  • Pre-Structured Markup
  • - *
    - *

    A container div with one or more nested tab divs with class 'x-tab' can be rendered entirely - * from existing markup (See the {@link #autoTabs} example).

    - *
    - * - *
  • Un-Structured Markup
  • - *
    - *

    A TabPanel can also be rendered from markup that is not strictly structured by simply specifying by id - * which elements should be the container and the tabs. Using this method tab content can be pulled from different - * elements within the page by id regardless of page structure. For example:

    - *
    
    -var tabs = new Ext.TabPanel({
    -    renderTo: 'my-tabs',
    -    activeTab: 0,
    -    items:[
    -        {contentEl:'tab1', title:'Tab 1'},
    -        {contentEl:'tab2', title:'Tab 2'}
    -    ]
    -});
    -
    -// Note that the tabs do not have to be nested within the container (although they can be)
    -<div id="my-tabs"></div>
    -<div id="tab1" class="x-hide-display">A simple tab</div>
    -<div id="tab2" class="x-hide-display">Another one</div>
    -
    - * Note that the tab divs in this example contain the class 'x-hide-display' so that they can be rendered - * deferred without displaying outside the tabs. You could alternately set {@link #deferredRender} = false - * to render all content tabs on page load. - *
    - * - *
- * - * @extends Ext.Panel - * @constructor - * @param {Object} config The configuration options - * @xtype tabpanel - */ -Ext.TabPanel = Ext.extend(Ext.Panel, { - /** - * @cfg {Boolean} layoutOnTabChange - * Set to true to force a layout of the active tab when the tab is changed. Defaults to false. - * See {@link Ext.layout.CardLayout}.{@link Ext.layout.CardLayout#layoutOnCardChange layoutOnCardChange}. - */ - /** - * @cfg {String} tabCls This config option is used on child Components of ths TabPanel. A CSS - * class name applied to the tab strip item representing the child Component, allowing special - * styling to be applied. - */ - /** - * @cfg {Boolean} deferredRender - *

true by default to defer the rendering of child {@link Ext.Container#items items} - * to the browsers DOM until a tab is activated. false will render all contained - * {@link Ext.Container#items items} as soon as the {@link Ext.layout.CardLayout layout} - * is rendered. If there is a significant amount of content or a lot of heavy controls being - * rendered into panels that are not displayed by default, setting this to true might - * improve performance.

- *

The deferredRender property is internally passed to the layout manager for - * TabPanels ({@link Ext.layout.CardLayout}) as its {@link Ext.layout.CardLayout#deferredRender} - * configuration value.

- *

Note: leaving deferredRender as true means that the content - * within an unactivated tab will not be available. For example, this means that if the TabPanel - * is within a {@link Ext.form.FormPanel form}, then until a tab is activated, any Fields within - * unactivated tabs will not be rendered, and will therefore not be submitted and will not be - * available to either {@link Ext.form.BasicForm#getValues getValues} or - * {@link Ext.form.BasicForm#setValues setValues}.

- */ - deferredRender : true, - /** - * @cfg {Number} tabWidth The initial width in pixels of each new tab (defaults to 120). - */ - tabWidth : 120, - /** - * @cfg {Number} minTabWidth The minimum width in pixels for each tab when {@link #resizeTabs} = true (defaults to 30). - */ - minTabWidth : 30, - /** - * @cfg {Boolean} resizeTabs True to automatically resize each tab so that the tabs will completely fill the - * tab strip (defaults to false). Setting this to true may cause specific widths that might be set per tab to - * be overridden in order to fit them all into view (although {@link #minTabWidth} will always be honored). - */ - resizeTabs : false, - /** - * @cfg {Boolean} enableTabScroll True to enable scrolling to tabs that may be invisible due to overflowing the - * overall TabPanel width. Only available with tabPosition:'top' (defaults to false). - */ - enableTabScroll : false, - /** - * @cfg {Number} scrollIncrement The number of pixels to scroll each time a tab scroll button is pressed - * (defaults to 100, or if {@link #resizeTabs} = true, the calculated tab width). Only - * applies when {@link #enableTabScroll} = true. - */ - scrollIncrement : 0, - /** - * @cfg {Number} scrollRepeatInterval Number of milliseconds between each scroll while a tab scroll button is - * continuously pressed (defaults to 400). - */ - scrollRepeatInterval : 400, - /** - * @cfg {Float} scrollDuration The number of milliseconds that each scroll animation should last (defaults - * to .35). Only applies when {@link #animScroll} = true. - */ - scrollDuration : 0.35, - /** - * @cfg {Boolean} animScroll True to animate tab scrolling so that hidden tabs slide smoothly into view (defaults - * to true). Only applies when {@link #enableTabScroll} = true. - */ - animScroll : true, - /** - * @cfg {String} tabPosition The position where the tab strip should be rendered (defaults to 'top'). - * The only other supported value is 'bottom'. Note: tab scrolling is only supported for - * tabPosition: 'top'. - */ - tabPosition : 'top', - /** - * @cfg {String} baseCls The base CSS class applied to the panel (defaults to 'x-tab-panel'). - */ - baseCls : 'x-tab-panel', - /** - * @cfg {Boolean} autoTabs - *

true to query the DOM for any divs with a class of 'x-tab' to be automatically converted - * to tabs and added to this panel (defaults to false). Note that the query will be executed within - * the scope of the container element only (so that multiple tab panels from markup can be supported via this - * method).

- *

This method is only possible when the markup is structured correctly as a container with nested divs - * containing the class 'x-tab'. To create TabPanels without these limitations, or to pull tab content - * from other elements on the page, see the example at the top of the class for generating tabs from markup.

- *

There are a couple of things to note when using this method:

    - *
  • When using the autoTabs config (as opposed to passing individual tab configs in the TabPanel's - * {@link #items} collection), you must use {@link #applyTo} to correctly use the specified id - * as the tab container. The autoTabs method replaces existing content with the TabPanel - * components.
  • - *
  • Make sure that you set {@link #deferredRender}: false so that the content elements for each - * tab will be rendered into the TabPanel immediately upon page load, otherwise they will not be transformed - * until each tab is activated and will be visible outside the TabPanel.
  • - *
Example usage:

- *

-var tabs = new Ext.TabPanel({
-    applyTo: 'my-tabs',
-    activeTab: 0,
-    deferredRender: false,
-    autoTabs: true
-});
-
-// This markup will be converted to a TabPanel from the code above
-<div id="my-tabs">
-    <div class="x-tab" title="Tab 1">A simple tab</div>
-    <div class="x-tab" title="Tab 2">Another one</div>
-</div>
-
- */ - autoTabs : false, - /** - * @cfg {String} autoTabSelector The CSS selector used to search for tabs in existing markup when - * {@link #autoTabs} = true (defaults to 'div.x-tab'). This can be any valid selector - * supported by {@link Ext.DomQuery#select}. Note that the query will be executed within the scope of this - * tab panel only (so that multiple tab panels from markup can be supported on a page). - */ - autoTabSelector : 'div.x-tab', - /** - * @cfg {String/Number} activeTab A string id or the numeric index of the tab that should be initially - * activated on render (defaults to undefined). - */ - activeTab : undefined, - /** - * @cfg {Number} tabMargin The number of pixels of space to calculate into the sizing and scrolling of - * tabs. If you change the margin in CSS, you will need to update this value so calculations are correct - * with either {@link #resizeTabs} or scrolling tabs. (defaults to 2) - */ - tabMargin : 2, - /** - * @cfg {Boolean} plain
true to render the tab strip without a background container image - * (defaults to false). - */ - plain : false, - /** - * @cfg {Number} wheelIncrement For scrolling tabs, the number of pixels to increment on mouse wheel - * scrolling (defaults to 20). - */ - wheelIncrement : 20, - - /* - * This is a protected property used when concatenating tab ids to the TabPanel id for internal uniqueness. - * It does not generally need to be changed, but can be if external code also uses an id scheme that can - * potentially clash with this one. - */ - idDelimiter : '__', - - // private - itemCls : 'x-tab-item', - - // private config overrides - elements : 'body', - headerAsText : false, - frame : false, - hideBorders :true, - - // private - initComponent : function(){ - this.frame = false; - Ext.TabPanel.superclass.initComponent.call(this); - this.addEvents( - /** - * @event beforetabchange - * Fires before the active tab changes. Handlers can return false to cancel the tab change. - * @param {TabPanel} this - * @param {Panel} newTab The tab being activated - * @param {Panel} currentTab The current active tab - */ - 'beforetabchange', - /** - * @event tabchange - * Fires after the active tab has changed. - * @param {TabPanel} this - * @param {Panel} tab The new active tab - */ - 'tabchange', - /** - * @event contextmenu - * Relays the contextmenu event from a tab selector element in the tab strip. - * @param {TabPanel} this - * @param {Panel} tab The target tab - * @param {EventObject} e - */ - 'contextmenu' - ); - /** - * @cfg {Object} layoutConfig - * TabPanel implicitly uses {@link Ext.layout.CardLayout} as its layout manager. - * layoutConfig may be used to configure this layout manager. - * {@link #deferredRender} and {@link #layoutOnTabChange} - * configured on the TabPanel will be applied as configs to the layout manager. - */ - this.setLayout(new Ext.layout.CardLayout(Ext.apply({ - layoutOnCardChange: this.layoutOnTabChange, - deferredRender: this.deferredRender - }, this.layoutConfig))); - - if(this.tabPosition == 'top'){ - this.elements += ',header'; - this.stripTarget = 'header'; - }else { - this.elements += ',footer'; - this.stripTarget = 'footer'; - } - if(!this.stack){ - this.stack = Ext.TabPanel.AccessStack(); - } - this.initItems(); - }, - - // private - onRender : function(ct, position){ - Ext.TabPanel.superclass.onRender.call(this, ct, position); - - if(this.plain){ - var pos = this.tabPosition == 'top' ? 'header' : 'footer'; - this[pos].addClass('x-tab-panel-'+pos+'-plain'); - } - - var st = this[this.stripTarget]; - - this.stripWrap = st.createChild({cls:'x-tab-strip-wrap', cn:{ - tag:'ul', cls:'x-tab-strip x-tab-strip-'+this.tabPosition}}); - - var beforeEl = (this.tabPosition=='bottom' ? this.stripWrap : null); - st.createChild({cls:'x-tab-strip-spacer'}, beforeEl); - this.strip = new Ext.Element(this.stripWrap.dom.firstChild); - - // create an empty span with class x-tab-strip-text to force the height of the header element when there's no tabs. - this.edge = this.strip.createChild({tag:'li', cls:'x-tab-edge', cn: [{tag: 'span', cls: 'x-tab-strip-text', cn: ' '}]}); - this.strip.createChild({cls:'x-clear'}); - - this.body.addClass('x-tab-panel-body-'+this.tabPosition); - - /** - * @cfg {Template/XTemplate} itemTpl

(Optional) A {@link Ext.Template Template} or - * {@link Ext.XTemplate XTemplate} which may be provided to process the data object returned from - * {@link #getTemplateArgs} to produce a clickable selector element in the tab strip.

- *

The main element created should be a <li> element. In order for a click event on - * a selector element to be connected to its item, it must take its id from the TabPanel's - * native {@link #getTemplateArgs}.

- *

The child element which contains the title text must be marked by the CSS class - * x-tab-strip-inner.

- *

To enable closability, the created element should contain an element marked by the CSS class - * x-tab-strip-close.

- *

If a custom itemTpl is supplied, it is the developer's responsibility to create CSS - * style rules to create the desired appearance.

- * Below is an example of how to create customized tab selector items:

-new Ext.TabPanel({
-    renderTo: document.body,
-    minTabWidth: 115,
-    tabWidth: 135,
-    enableTabScroll: true,
-    width: 600,
-    height: 250,
-    defaults: {autoScroll:true},
-    itemTpl: new Ext.XTemplate(
-    '<li class="{cls}" id="{id}" style="overflow:hidden">',
-         '<tpl if="closable">',
-            '<a class="x-tab-strip-close"></a>',
-         '</tpl>',
-         '<a class="x-tab-right" href="#" style="padding-left:6px">',
-            '<em class="x-tab-left">',
-                '<span class="x-tab-strip-inner">',
-                    '<img src="{src}" style="float:left;margin:3px 3px 0 0">',
-                    '<span style="margin-left:20px" class="x-tab-strip-text {iconCls}">{text} {extra}</span>',
-                '</span>',
-            '</em>',
-        '</a>',
-    '</li>'
-    ),
-    getTemplateArgs: function(item) {
-//      Call the native method to collect the base data. Like the ID!
-        var result = Ext.TabPanel.prototype.getTemplateArgs.call(this, item);
-
-//      Add stuff used in our template
-        return Ext.apply(result, {
-            closable: item.closable,
-            src: item.iconSrc,
-            extra: item.extraText || ''
-        });
-    },
-    items: [{
-        title: 'New Tab 1',
-        iconSrc: '../shared/icons/fam/grid.png',
-        html: 'Tab Body 1',
-        closable: true
-    }, {
-        title: 'New Tab 2',
-        iconSrc: '../shared/icons/fam/grid.png',
-        html: 'Tab Body 2',
-        extraText: 'Extra stuff in the tab button'
-    }]
-});
-
- */ - if(!this.itemTpl){ - var tt = new Ext.Template( - '
  • ', - '', - '{text}', - '
  • ' - ); - tt.disableFormats = true; - tt.compile(); - Ext.TabPanel.prototype.itemTpl = tt; - } - - this.items.each(this.initTab, this); - }, - - // private - afterRender : function(){ - Ext.TabPanel.superclass.afterRender.call(this); - if(this.autoTabs){ - this.readTabs(false); - } - if(this.activeTab !== undefined){ - var item = Ext.isObject(this.activeTab) ? this.activeTab : this.items.get(this.activeTab); - delete this.activeTab; - this.setActiveTab(item); - } - }, - - // private - initEvents : function(){ - Ext.TabPanel.superclass.initEvents.call(this); - this.mon(this.strip, { - scope: this, - mousedown: this.onStripMouseDown, - contextmenu: this.onStripContextMenu - }); - if(this.enableTabScroll){ - this.mon(this.strip, 'mousewheel', this.onWheel, this); - } - }, - - // private - findTargets : function(e){ - var item = null, - itemEl = e.getTarget('li:not(.x-tab-edge)', this.strip); - - if(itemEl){ - item = this.getComponent(itemEl.id.split(this.idDelimiter)[1]); - if(item.disabled){ - return { - close : null, - item : null, - el : null - }; - } - } - return { - close : e.getTarget('.x-tab-strip-close', this.strip), - item : item, - el : itemEl - }; - }, - - // private - onStripMouseDown : function(e){ - if(e.button !== 0){ - return; - } - e.preventDefault(); - var t = this.findTargets(e); - if(t.close){ - if (t.item.fireEvent('beforeclose', t.item) !== false) { - t.item.fireEvent('close', t.item); - this.remove(t.item); - } - return; - } - if(t.item && t.item != this.activeTab){ - this.setActiveTab(t.item); - } - }, - - // private - onStripContextMenu : function(e){ - e.preventDefault(); - var t = this.findTargets(e); - if(t.item){ - this.fireEvent('contextmenu', this, t.item, e); - } - }, - - /** - * True to scan the markup in this tab panel for {@link #autoTabs} using the - * {@link #autoTabSelector} - * @param {Boolean} removeExisting True to remove existing tabs - */ - readTabs : function(removeExisting){ - if(removeExisting === true){ - this.items.each(function(item){ - this.remove(item); - }, this); - } - var tabs = this.el.query(this.autoTabSelector); - for(var i = 0, len = tabs.length; i < len; i++){ - var tab = tabs[i], - title = tab.getAttribute('title'); - tab.removeAttribute('title'); - this.add({ - title: title, - contentEl: tab - }); - } - }, - - // private - initTab : function(item, index){ - var before = this.strip.dom.childNodes[index], - p = this.getTemplateArgs(item), - el = before ? - this.itemTpl.insertBefore(before, p) : - this.itemTpl.append(this.strip, p), - cls = 'x-tab-strip-over', - tabEl = Ext.get(el); - - tabEl.hover(function(){ - if(!item.disabled){ - tabEl.addClass(cls); - } - }, function(){ - tabEl.removeClass(cls); - }); - - if(item.tabTip){ - tabEl.child('span.x-tab-strip-text', true).qtip = item.tabTip; - } - item.tabEl = el; - - // Route *keyboard triggered* click events to the tab strip mouse handler. - tabEl.select('a').on('click', function(e){ - if(!e.getPageX()){ - this.onStripMouseDown(e); - } - }, this, {preventDefault: true}); - - item.on({ - scope: this, - disable: this.onItemDisabled, - enable: this.onItemEnabled, - titlechange: this.onItemTitleChanged, - iconchange: this.onItemIconChanged, - beforeshow: this.onBeforeShowItem - }); - }, - - - - /** - *

    Provides template arguments for rendering a tab selector item in the tab strip.

    - *

    This method returns an object hash containing properties used by the TabPanel's {@link #itemTpl} - * to create a formatted, clickable tab selector element. The properties which must be returned - * are:

      - *
    • id : String
      A unique identifier which links to the item
    • - *
    • text : String
      The text to display
    • - *
    • cls : String
      The CSS class name
    • - *
    • iconCls : String
      A CSS class to provide appearance for an icon.
    • - *
    - * @param {Ext.BoxComponent} item The {@link Ext.BoxComponent BoxComponent} for which to create a selector element in the tab strip. - * @return {Object} An object hash containing the properties required to render the selector element. - */ - getTemplateArgs : function(item) { - var cls = item.closable ? 'x-tab-strip-closable' : ''; - if(item.disabled){ - cls += ' x-item-disabled'; - } - if(item.iconCls){ - cls += ' x-tab-with-icon'; - } - if(item.tabCls){ - cls += ' ' + item.tabCls; - } - - return { - id: this.id + this.idDelimiter + item.getItemId(), - text: item.title, - cls: cls, - iconCls: item.iconCls || '' - }; - }, - - // private - onAdd : function(c){ - Ext.TabPanel.superclass.onAdd.call(this, c); - if(this.rendered){ - var items = this.items; - this.initTab(c, items.indexOf(c)); - this.delegateUpdates(); - } - }, - - // private - onBeforeAdd : function(item){ - var existing = item.events ? (this.items.containsKey(item.getItemId()) ? item : null) : this.items.get(item); - if(existing){ - this.setActiveTab(item); - return false; - } - Ext.TabPanel.superclass.onBeforeAdd.apply(this, arguments); - var es = item.elements; - item.elements = es ? es.replace(',header', '') : es; - item.border = (item.border === true); - }, - - // private - onRemove : function(c){ - var te = Ext.get(c.tabEl); - // check if the tabEl exists, it won't if the tab isn't rendered - if(te){ - te.select('a').removeAllListeners(); - Ext.destroy(te); - } - Ext.TabPanel.superclass.onRemove.call(this, c); - this.stack.remove(c); - delete c.tabEl; - c.un('disable', this.onItemDisabled, this); - c.un('enable', this.onItemEnabled, this); - c.un('titlechange', this.onItemTitleChanged, this); - c.un('iconchange', this.onItemIconChanged, this); - c.un('beforeshow', this.onBeforeShowItem, this); - if(c == this.activeTab){ - var next = this.stack.next(); - if(next){ - this.setActiveTab(next); - }else if(this.items.getCount() > 0){ - this.setActiveTab(0); - }else{ - this.setActiveTab(null); - } - } - if(!this.destroying){ - this.delegateUpdates(); - } - }, - - // private - onBeforeShowItem : function(item){ - if(item != this.activeTab){ - this.setActiveTab(item); - return false; - } - }, - - // private - onItemDisabled : function(item){ - var el = this.getTabEl(item); - if(el){ - Ext.fly(el).addClass('x-item-disabled'); - } - this.stack.remove(item); - }, - - // private - onItemEnabled : function(item){ - var el = this.getTabEl(item); - if(el){ - Ext.fly(el).removeClass('x-item-disabled'); - } - }, - - // private - onItemTitleChanged : function(item){ - var el = this.getTabEl(item); - if(el){ - Ext.fly(el).child('span.x-tab-strip-text', true).innerHTML = item.title; - } - }, - - //private - onItemIconChanged : function(item, iconCls, oldCls){ - var el = this.getTabEl(item); - if(el){ - el = Ext.get(el); - el.child('span.x-tab-strip-text').replaceClass(oldCls, iconCls); - el[Ext.isEmpty(iconCls) ? 'removeClass' : 'addClass']('x-tab-with-icon'); - } - }, - - /** - * Gets the DOM element for the tab strip item which activates the child panel with the specified - * ID. Access this to change the visual treatment of the item, for example by changing the CSS class name. - * @param {Panel/Number/String} tab The tab component, or the tab's index, or the tabs id or itemId. - * @return {HTMLElement} The DOM node - */ - getTabEl : function(item){ - var c = this.getComponent(item); - return c ? c.tabEl : null; - }, - - // private - onResize : function(){ - Ext.TabPanel.superclass.onResize.apply(this, arguments); - this.delegateUpdates(); - }, - - /** - * Suspends any internal calculations or scrolling while doing a bulk operation. See {@link #endUpdate} - */ - beginUpdate : function(){ - this.suspendUpdates = true; - }, - - /** - * Resumes calculations and scrolling at the end of a bulk operation. See {@link #beginUpdate} - */ - endUpdate : function(){ - this.suspendUpdates = false; - this.delegateUpdates(); - }, - - /** - * Hides the tab strip item for the passed tab - * @param {Number/String/Panel} item The tab index, id or item - */ - hideTabStripItem : function(item){ - item = this.getComponent(item); - var el = this.getTabEl(item); - if(el){ - el.style.display = 'none'; - this.delegateUpdates(); - } - this.stack.remove(item); - }, - - /** - * Unhides the tab strip item for the passed tab - * @param {Number/String/Panel} item The tab index, id or item - */ - unhideTabStripItem : function(item){ - item = this.getComponent(item); - var el = this.getTabEl(item); - if(el){ - el.style.display = ''; - this.delegateUpdates(); - } - }, - - // private - delegateUpdates : function(){ - var rendered = this.rendered; - if(this.suspendUpdates){ - return; - } - if(this.resizeTabs && rendered){ - this.autoSizeTabs(); - } - if(this.enableTabScroll && rendered){ - this.autoScrollTabs(); - } - }, - - // private - autoSizeTabs : function(){ - var count = this.items.length, - ce = this.tabPosition != 'bottom' ? 'header' : 'footer', - ow = this[ce].dom.offsetWidth, - aw = this[ce].dom.clientWidth; - - if(!this.resizeTabs || count < 1 || !aw){ // !aw for display:none - return; - } - - var each = Math.max(Math.min(Math.floor((aw-4) / count) - this.tabMargin, this.tabWidth), this.minTabWidth); // -4 for float errors in IE - this.lastTabWidth = each; - var lis = this.strip.query('li:not(.x-tab-edge)'); - for(var i = 0, len = lis.length; i < len; i++) { - var li = lis[i], - inner = Ext.fly(li).child('.x-tab-strip-inner', true), - tw = li.offsetWidth, - iw = inner.offsetWidth; - inner.style.width = (each - (tw-iw)) + 'px'; - } - }, - - // private - adjustBodyWidth : function(w){ - if(this.header){ - this.header.setWidth(w); - } - if(this.footer){ - this.footer.setWidth(w); - } - return w; - }, - - /** - * Sets the specified tab as the active tab. This method fires the {@link #beforetabchange} event which - * can return false to cancel the tab change. - * @param {String/Number} item - * The id or tab Panel to activate. This parameter may be any of the following: - *
      - *
    • a String : representing the {@link Ext.Component#itemId itemId} - * or {@link Ext.Component#id id} of the child component
    • - *
    • a Number : representing the position of the child component - * within the {@link Ext.Container#items items} property
    • - *
    - *

    For additional information see {@link Ext.util.MixedCollection#get}. - */ - setActiveTab : function(item){ - item = this.getComponent(item); - if(this.fireEvent('beforetabchange', this, item, this.activeTab) === false){ - return; - } - if(!this.rendered){ - this.activeTab = item; - return; - } - if(this.activeTab != item){ - if(this.activeTab){ - var oldEl = this.getTabEl(this.activeTab); - if(oldEl){ - Ext.fly(oldEl).removeClass('x-tab-strip-active'); - } - } - this.activeTab = item; - if(item){ - var el = this.getTabEl(item); - Ext.fly(el).addClass('x-tab-strip-active'); - this.stack.add(item); - - this.layout.setActiveItem(item); - // Need to do this here, since setting the active tab slightly changes the size - this.delegateUpdates(); - if(this.scrolling){ - this.scrollToTab(item, this.animScroll); - } - } - this.fireEvent('tabchange', this, item); - } - }, - - /** - * Returns the Component which is the currently active tab. Note that before the TabPanel - * first activates a child Component, this method will return whatever was configured in the - * {@link #activeTab} config option. - * @return {BoxComponent} The currently active child Component if one is active, or the {@link #activeTab} config value. - */ - getActiveTab : function(){ - return this.activeTab || null; - }, - - /** - * Gets the specified tab by id. - * @param {String} id The tab id - * @return {Panel} The tab - */ - getItem : function(item){ - return this.getComponent(item); - }, - - // private - autoScrollTabs : function(){ - this.pos = this.tabPosition=='bottom' ? this.footer : this.header; - var count = this.items.length, - ow = this.pos.dom.offsetWidth, - tw = this.pos.dom.clientWidth, - wrap = this.stripWrap, - wd = wrap.dom, - cw = wd.offsetWidth, - pos = this.getScrollPos(), - l = this.edge.getOffsetsTo(this.stripWrap)[0] + pos; - - if(!this.enableTabScroll || cw < 20){ // 20 to prevent display:none issues - return; - } - if(count == 0 || l <= tw){ - // ensure the width is set if there's no tabs - wd.scrollLeft = 0; - wrap.setWidth(tw); - if(this.scrolling){ - this.scrolling = false; - this.pos.removeClass('x-tab-scrolling'); - this.scrollLeft.hide(); - this.scrollRight.hide(); - // See here: http://extjs.com/forum/showthread.php?t=49308&highlight=isSafari - if(Ext.isAir || Ext.isWebKit){ - wd.style.marginLeft = ''; - wd.style.marginRight = ''; - } - } - }else{ - if(!this.scrolling){ - this.pos.addClass('x-tab-scrolling'); - // See here: http://extjs.com/forum/showthread.php?t=49308&highlight=isSafari - if(Ext.isAir || Ext.isWebKit){ - wd.style.marginLeft = '18px'; - wd.style.marginRight = '18px'; - } - } - tw -= wrap.getMargins('lr'); - wrap.setWidth(tw > 20 ? tw : 20); - if(!this.scrolling){ - if(!this.scrollLeft){ - this.createScrollers(); - }else{ - this.scrollLeft.show(); - this.scrollRight.show(); - } - } - this.scrolling = true; - if(pos > (l-tw)){ // ensure it stays within bounds - wd.scrollLeft = l-tw; - }else{ // otherwise, make sure the active tab is still visible - this.scrollToTab(this.activeTab, false); - } - this.updateScrollButtons(); - } - }, - - // private - createScrollers : function(){ - this.pos.addClass('x-tab-scrolling-' + this.tabPosition); - var h = this.stripWrap.dom.offsetHeight; - - // left - var sl = this.pos.insertFirst({ - cls:'x-tab-scroller-left' - }); - sl.setHeight(h); - sl.addClassOnOver('x-tab-scroller-left-over'); - this.leftRepeater = new Ext.util.ClickRepeater(sl, { - interval : this.scrollRepeatInterval, - handler: this.onScrollLeft, - scope: this - }); - this.scrollLeft = sl; - - // right - var sr = this.pos.insertFirst({ - cls:'x-tab-scroller-right' - }); - sr.setHeight(h); - sr.addClassOnOver('x-tab-scroller-right-over'); - this.rightRepeater = new Ext.util.ClickRepeater(sr, { - interval : this.scrollRepeatInterval, - handler: this.onScrollRight, - scope: this - }); - this.scrollRight = sr; - }, - - // private - getScrollWidth : function(){ - return this.edge.getOffsetsTo(this.stripWrap)[0] + this.getScrollPos(); - }, - - // private - getScrollPos : function(){ - return parseInt(this.stripWrap.dom.scrollLeft, 10) || 0; - }, - - // private - getScrollArea : function(){ - return parseInt(this.stripWrap.dom.clientWidth, 10) || 0; - }, - - // private - getScrollAnim : function(){ - return {duration:this.scrollDuration, callback: this.updateScrollButtons, scope: this}; - }, - - // private - getScrollIncrement : function(){ - return this.scrollIncrement || (this.resizeTabs ? this.lastTabWidth+2 : 100); - }, - - /** - * Scrolls to a particular tab if tab scrolling is enabled - * @param {Panel} item The item to scroll to - * @param {Boolean} animate True to enable animations - */ - - scrollToTab : function(item, animate){ - if(!item){ - return; - } - var el = this.getTabEl(item), - pos = this.getScrollPos(), - area = this.getScrollArea(), - left = Ext.fly(el).getOffsetsTo(this.stripWrap)[0] + pos, - right = left + el.offsetWidth; - if(left < pos){ - this.scrollTo(left, animate); - }else if(right > (pos + area)){ - this.scrollTo(right - area, animate); - } - }, - - // private - scrollTo : function(pos, animate){ - this.stripWrap.scrollTo('left', pos, animate ? this.getScrollAnim() : false); - if(!animate){ - this.updateScrollButtons(); - } - }, - - onWheel : function(e){ - var d = e.getWheelDelta()*this.wheelIncrement*-1; - e.stopEvent(); - - var pos = this.getScrollPos(), - newpos = pos + d, - sw = this.getScrollWidth()-this.getScrollArea(); - - var s = Math.max(0, Math.min(sw, newpos)); - if(s != pos){ - this.scrollTo(s, false); - } - }, - - // private - onScrollRight : function(){ - var sw = this.getScrollWidth()-this.getScrollArea(), - pos = this.getScrollPos(), - s = Math.min(sw, pos + this.getScrollIncrement()); - if(s != pos){ - this.scrollTo(s, this.animScroll); - } - }, - - // private - onScrollLeft : function(){ - var pos = this.getScrollPos(), - s = Math.max(0, pos - this.getScrollIncrement()); - if(s != pos){ - this.scrollTo(s, this.animScroll); - } - }, - - // private - updateScrollButtons : function(){ - var pos = this.getScrollPos(); - this.scrollLeft[pos === 0 ? 'addClass' : 'removeClass']('x-tab-scroller-left-disabled'); - this.scrollRight[pos >= (this.getScrollWidth()-this.getScrollArea()) ? 'addClass' : 'removeClass']('x-tab-scroller-right-disabled'); - }, - - // private - beforeDestroy : function() { - Ext.destroy(this.leftRepeater, this.rightRepeater); - this.deleteMembers('strip', 'edge', 'scrollLeft', 'scrollRight', 'stripWrap'); - this.activeTab = null; - Ext.TabPanel.superclass.beforeDestroy.apply(this); - } - - /** - * @cfg {Boolean} collapsible - * @hide - */ - /** - * @cfg {String} header - * @hide - */ - /** - * @cfg {Boolean} headerAsText - * @hide - */ - /** - * @property header - * @hide - */ - /** - * @cfg title - * @hide - */ - /** - * @cfg {Array} tools - * @hide - */ - /** - * @cfg {Array} toolTemplate - * @hide - */ - /** - * @cfg {Boolean} hideCollapseTool - * @hide - */ - /** - * @cfg {Boolean} titleCollapse - * @hide - */ - /** - * @cfg {Boolean} collapsed - * @hide - */ - /** - * @cfg {String} layout - * @hide - */ - /** - * @cfg {Boolean} preventBodyReset - * @hide - */ -}); -Ext.reg('tabpanel', Ext.TabPanel); - -/** - * See {@link #setActiveTab}. Sets the specified tab as the active tab. This method fires - * the {@link #beforetabchange} event which can return false to cancel the tab change. - * @param {String/Panel} tab The id or tab Panel to activate - * @method activate - */ -Ext.TabPanel.prototype.activate = Ext.TabPanel.prototype.setActiveTab; - -// private utility class used by TabPanel -Ext.TabPanel.AccessStack = function(){ - var items = []; - return { - add : function(item){ - items.push(item); - if(items.length > 10){ - items.shift(); - } - }, - - remove : function(item){ - var s = []; - for(var i = 0, len = items.length; i < len; i++) { - if(items[i] != item){ - s.push(items[i]); - } - } - items = s; - }, - - next : function(){ - return items.pop(); - } - }; -}; -/** - * @class Ext.Button - * @extends Ext.BoxComponent - * Simple Button class - * @cfg {String} text The button text to be used as innerHTML (html tags are accepted) - * @cfg {String} icon The path to an image to display in the button (the image will be set as the background-image - * CSS property of the button by default, so if you want a mixed icon/text button, set cls:'x-btn-text-icon') - * @cfg {Function} handler A function called when the button is clicked (can be used instead of click event). - * The handler is passed the following parameters:

      - *
    • b : Button
      This Button.
    • - *
    • e : EventObject
      The click event.
    • - *
    - * @cfg {Number} minWidth The minimum width for this button (used to give a set of buttons a common width). - * See also {@link Ext.Panel}.{@link Ext.Panel#minButtonWidth minButtonWidth}. - * @cfg {String/Object} tooltip The tooltip for the button - can be a string to be used as innerHTML (html tags are accepted) or QuickTips config object - * @cfg {Boolean} hidden True to start hidden (defaults to false) - * @cfg {Boolean} disabled True to start disabled (defaults to false) - * @cfg {Boolean} pressed True to start pressed (only if enableToggle = true) - * @cfg {String} toggleGroup The group this toggle button is a member of (only 1 per group can be pressed) - * @cfg {Boolean/Object} repeat True to repeat fire the click event while the mouse is down. This can also be - * a {@link Ext.util.ClickRepeater ClickRepeater} config object (defaults to false). - * @constructor - * Create a new button - * @param {Object} config The config object - * @xtype button - */ -Ext.Button = Ext.extend(Ext.BoxComponent, { - /** - * Read-only. True if this button is hidden - * @type Boolean - */ - hidden : false, - /** - * Read-only. True if this button is disabled - * @type Boolean - */ - disabled : false, - /** - * Read-only. True if this button is pressed (only if enableToggle = true) - * @type Boolean - */ - pressed : false, - - /** - * @cfg {Number} tabIndex Set a DOM tabIndex for this button (defaults to undefined) - */ - - /** - * @cfg {Boolean} allowDepress - * False to not allow a pressed Button to be depressed (defaults to undefined). Only valid when {@link #enableToggle} is true. - */ - - /** - * @cfg {Boolean} enableToggle - * True to enable pressed/not pressed toggling (defaults to false) - */ - enableToggle : false, - /** - * @cfg {Function} toggleHandler - * Function called when a Button with {@link #enableToggle} set to true is clicked. Two arguments are passed:
      - *
    • button : Ext.Button
      this Button object
    • - *
    • state : Boolean
      The next state of the Button, true means pressed.
    • - *
    - */ - /** - * @cfg {Mixed} menu - * Standard menu attribute consisting of a reference to a menu object, a menu id or a menu config blob (defaults to undefined). - */ - /** - * @cfg {String} menuAlign - * The position to align the menu to (see {@link Ext.Element#alignTo} for more details, defaults to 'tl-bl?'). - */ - menuAlign : 'tl-bl?', - - /** - * @cfg {String} overflowText If used in a {@link Ext.Toolbar Toolbar}, the - * text to be used if this item is shown in the overflow menu. See also - * {@link Ext.Toolbar.Item}.{@link Ext.Toolbar.Item#overflowText overflowText}. - */ - /** - * @cfg {String} iconCls - * A css class which sets a background image to be used as the icon for this button - */ - /** - * @cfg {String} type - * submit, reset or button - defaults to 'button' - */ - type : 'button', - - // private - menuClassTarget : 'tr:nth(2)', - - /** - * @cfg {String} clickEvent - * The DOM event that will fire the handler of the button. This can be any valid event name (dblclick, contextmenu). - * Defaults to 'click'. - */ - clickEvent : 'click', - - /** - * @cfg {Boolean} handleMouseEvents - * False to disable visual cues on mouseover, mouseout and mousedown (defaults to true) - */ - handleMouseEvents : true, - - /** - * @cfg {String} tooltipType - * The type of tooltip to use. Either 'qtip' (default) for QuickTips or 'title' for title attribute. - */ - tooltipType : 'qtip', - - /** - * @cfg {String} buttonSelector - *

    (Optional) A {@link Ext.DomQuery DomQuery} selector which is used to extract the active, clickable element from the - * DOM structure created.

    - *

    When a custom {@link #template} is used, you must ensure that this selector results in the selection of - * a focussable element.

    - *

    Defaults to 'button:first-child'.

    - */ - buttonSelector : 'button:first-child', - - /** - * @cfg {String} scale - *

    (Optional) The size of the Button. Three values are allowed:

    - *
      - *
    • 'small'
      Results in the button element being 16px high.
    • - *
    • 'medium'
      Results in the button element being 24px high.
    • - *
    • 'large'
      Results in the button element being 32px high.
    • - *
    - *

    Defaults to 'small'.

    - */ - scale : 'small', - - /** - * @cfg {Object} scope The scope (this reference) in which the - * {@link #handler} and {@link #toggleHandler} is - * executed. Defaults to this Button. - */ - - /** - * @cfg {String} iconAlign - *

    (Optional) The side of the Button box to render the icon. Four values are allowed:

    - *
      - *
    • 'top'
    • - *
    • 'right'
    • - *
    • 'bottom'
    • - *
    • 'left'
    • - *
    - *

    Defaults to 'left'.

    - */ - iconAlign : 'left', - - /** - * @cfg {String} arrowAlign - *

    (Optional) The side of the Button box to render the arrow if the button has an associated {@link #menu}. - * Two values are allowed:

    - *
      - *
    • 'right'
    • - *
    • 'bottom'
    • - *
    - *

    Defaults to 'right'.

    - */ - arrowAlign : 'right', - - /** - * @cfg {Ext.Template} template (Optional) - *

    A {@link Ext.Template Template} used to create the Button's DOM structure.

    - * Instances, or subclasses which need a different DOM structure may provide a different - * template layout in conjunction with an implementation of {@link #getTemplateArgs}. - * @type Ext.Template - * @property template - */ - /** - * @cfg {String} cls - * A CSS class string to apply to the button's main element. - */ - /** - * @property menu - * @type Menu - * The {@link Ext.menu.Menu Menu} object associated with this Button when configured with the {@link #menu} config option. - */ - /** - * @cfg {Boolean} autoWidth - * By default, if a width is not specified the button will attempt to stretch horizontally to fit its content. - * If the button is being managed by a width sizing layout (hbox, fit, anchor), set this to false to prevent - * the button from doing this automatic sizing. - * Defaults to undefined. - */ - - initComponent : function(){ - if(this.menu){ - // If array of items, turn it into an object config so we - // can set the ownerCt property in the config - if (Ext.isArray(this.menu)){ - this.menu = { items: this.menu }; - } - - // An object config will work here, but an instance of a menu - // will have already setup its ref's and have no effect - if (Ext.isObject(this.menu)){ - this.menu.ownerCt = this; - } - - this.menu = Ext.menu.MenuMgr.get(this.menu); - this.menu.ownerCt = undefined; - } - - Ext.Button.superclass.initComponent.call(this); - - this.addEvents( - /** - * @event click - * Fires when this button is clicked - * @param {Button} this - * @param {EventObject} e The click event - */ - 'click', - /** - * @event toggle - * Fires when the 'pressed' state of this button changes (only if enableToggle = true) - * @param {Button} this - * @param {Boolean} pressed - */ - 'toggle', - /** - * @event mouseover - * Fires when the mouse hovers over the button - * @param {Button} this - * @param {Event} e The event object - */ - 'mouseover', - /** - * @event mouseout - * Fires when the mouse exits the button - * @param {Button} this - * @param {Event} e The event object - */ - 'mouseout', - /** - * @event menushow - * If this button has a menu, this event fires when it is shown - * @param {Button} this - * @param {Menu} menu - */ - 'menushow', - /** - * @event menuhide - * If this button has a menu, this event fires when it is hidden - * @param {Button} this - * @param {Menu} menu - */ - 'menuhide', - /** - * @event menutriggerover - * If this button has a menu, this event fires when the mouse enters the menu triggering element - * @param {Button} this - * @param {Menu} menu - * @param {EventObject} e - */ - 'menutriggerover', - /** - * @event menutriggerout - * If this button has a menu, this event fires when the mouse leaves the menu triggering element - * @param {Button} this - * @param {Menu} menu - * @param {EventObject} e - */ - 'menutriggerout' - ); - - if(Ext.isString(this.toggleGroup)){ - this.enableToggle = true; - } - }, - -/** - *

    This method returns an Array which provides substitution parameters for the {@link #template Template} used - * to create this Button's DOM structure.

    - *

    Instances or subclasses which use a different Template to create a different DOM structure may need to provide their - * own implementation of this method.

    - *

    The default implementation which provides data for the default {@link #template} returns an Array containing the - * following items:

      - *
    • The <button>'s {@link #type}
    • - *
    • A CSS class name applied to the Button's main <tbody> element which determines the button's scale and icon alignment.
    • - *
    • A CSS class to determine the presence and position of an arrow icon. ('x-btn-arrow' or 'x-btn-arrow-bottom' or '')
    • - *
    • The {@link #cls} CSS class name applied to the button's wrapping <table> element.
    • - *
    • The Component id which is applied to the button's wrapping <table> element.
    • - *
    - * @return {Array} Substitution data for a Template. - */ - getTemplateArgs : function(){ - return [this.type, 'x-btn-' + this.scale + ' x-btn-icon-' + this.scale + '-' + this.iconAlign, this.getMenuClass(), this.cls, this.id]; - }, - - // private - setButtonClass : function(){ - if(this.useSetClass){ - if(!Ext.isEmpty(this.oldCls)){ - this.el.removeClass([this.oldCls, 'x-btn-pressed']); - } - this.oldCls = (this.iconCls || this.icon) ? (this.text ? 'x-btn-text-icon' : 'x-btn-icon') : 'x-btn-noicon'; - this.el.addClass([this.oldCls, this.pressed ? 'x-btn-pressed' : null]); - } - }, - - // protected - getMenuClass : function(){ - return this.menu ? (this.arrowAlign != 'bottom' ? 'x-btn-arrow' : 'x-btn-arrow-bottom') : ''; - }, - - // private - onRender : function(ct, position){ - if(!this.template){ - if(!Ext.Button.buttonTemplate){ - // hideous table template - Ext.Button.buttonTemplate = new Ext.Template( - '', - '', - '', - '', - '
      
      
      
    '); - Ext.Button.buttonTemplate.compile(); - } - this.template = Ext.Button.buttonTemplate; - } - - var btn, targs = this.getTemplateArgs(); - - if(position){ - btn = this.template.insertBefore(position, targs, true); - }else{ - btn = this.template.append(ct, targs, true); - } - /** - * An {@link Ext.Element Element} encapsulating the Button's clickable element. By default, - * this references a <button> element. Read only. - * @type Ext.Element - * @property btnEl - */ - this.btnEl = btn.child(this.buttonSelector); - this.mon(this.btnEl, { - scope: this, - focus: this.onFocus, - blur: this.onBlur - }); - - this.initButtonEl(btn, this.btnEl); - - Ext.ButtonToggleMgr.register(this); - }, - - // private - initButtonEl : function(btn, btnEl){ - this.el = btn; - this.setIcon(this.icon); - this.setText(this.text); - this.setIconClass(this.iconCls); - if(Ext.isDefined(this.tabIndex)){ - btnEl.dom.tabIndex = this.tabIndex; - } - if(this.tooltip){ - this.setTooltip(this.tooltip, true); - } - - if(this.handleMouseEvents){ - this.mon(btn, { - scope: this, - mouseover: this.onMouseOver, - mousedown: this.onMouseDown - }); - - // new functionality for monitoring on the document level - //this.mon(btn, 'mouseout', this.onMouseOut, this); - } - - if(this.menu){ - this.mon(this.menu, { - scope: this, - show: this.onMenuShow, - hide: this.onMenuHide - }); - } - - if(this.repeat){ - var repeater = new Ext.util.ClickRepeater(btn, Ext.isObject(this.repeat) ? this.repeat : {}); - this.mon(repeater, 'click', this.onRepeatClick, this); - }else{ - this.mon(btn, this.clickEvent, this.onClick, this); - } - }, - - // private - afterRender : function(){ - Ext.Button.superclass.afterRender.call(this); - this.useSetClass = true; - this.setButtonClass(); - this.doc = Ext.getDoc(); - this.doAutoWidth(); - }, - - /** - * Sets the CSS class that provides a background image to use as the button's icon. This method also changes - * the value of the {@link iconCls} config internally. - * @param {String} cls The CSS class providing the icon image - * @return {Ext.Button} this - */ - setIconClass : function(cls){ - this.iconCls = cls; - if(this.el){ - this.btnEl.dom.className = ''; - this.btnEl.addClass(['x-btn-text', cls || '']); - this.setButtonClass(); - } - return this; - }, - - /** - * Sets the tooltip for this Button. - * @param {String/Object} tooltip. This may be:
      - *
    • String : A string to be used as innerHTML (html tags are accepted) to show in a tooltip
    • - *
    • Object : A configuration object for {@link Ext.QuickTips#register}.
    • - *
    - * @return {Ext.Button} this - */ - setTooltip : function(tooltip, /* private */ initial){ - if(this.rendered){ - if(!initial){ - this.clearTip(); - } - if(Ext.isObject(tooltip)){ - Ext.QuickTips.register(Ext.apply({ - target: this.btnEl.id - }, tooltip)); - this.tooltip = tooltip; - }else{ - this.btnEl.dom[this.tooltipType] = tooltip; - } - }else{ - this.tooltip = tooltip; - } - return this; - }, - - // private - clearTip : function(){ - if(Ext.isObject(this.tooltip)){ - Ext.QuickTips.unregister(this.btnEl); - } - }, - - // private - beforeDestroy : function(){ - if(this.rendered){ - this.clearTip(); - } - if(this.menu && this.destroyMenu !== false) { - Ext.destroy(this.btnEl, this.menu); - } - Ext.destroy(this.repeater); - }, - - // private - onDestroy : function(){ - if(this.rendered){ - this.doc.un('mouseover', this.monitorMouseOver, this); - this.doc.un('mouseup', this.onMouseUp, this); - delete this.doc; - delete this.btnEl; - Ext.ButtonToggleMgr.unregister(this); - } - Ext.Button.superclass.onDestroy.call(this); - }, - - // private - doAutoWidth : function(){ - if(this.autoWidth !== false && this.el && this.text && this.width === undefined){ - this.el.setWidth('auto'); - if(Ext.isIE7 && Ext.isStrict){ - var ib = this.btnEl; - if(ib && ib.getWidth() > 20){ - ib.clip(); - ib.setWidth(Ext.util.TextMetrics.measure(ib, this.text).width+ib.getFrameWidth('lr')); - } - } - if(this.minWidth){ - if(this.el.getWidth() < this.minWidth){ - this.el.setWidth(this.minWidth); - } - } - } - }, - - /** - * Assigns this Button's click handler - * @param {Function} handler The function to call when the button is clicked - * @param {Object} scope (optional) The scope (this reference) in which the handler function is executed. - * Defaults to this Button. - * @return {Ext.Button} this - */ - setHandler : function(handler, scope){ - this.handler = handler; - this.scope = scope; - return this; - }, - - /** - * Sets this Button's text - * @param {String} text The button text - * @return {Ext.Button} this - */ - setText : function(text){ - this.text = text; - if(this.el){ - this.btnEl.update(text || ' '); - this.setButtonClass(); - } - this.doAutoWidth(); - return this; - }, - - /** - * Sets the background image (inline style) of the button. This method also changes - * the value of the {@link icon} config internally. - * @param {String} icon The path to an image to display in the button - * @return {Ext.Button} this - */ - setIcon : function(icon){ - this.icon = icon; - if(this.el){ - this.btnEl.setStyle('background-image', icon ? 'url(' + icon + ')' : ''); - this.setButtonClass(); - } - return this; - }, - - /** - * Gets the text for this Button - * @return {String} The button text - */ - getText : function(){ - return this.text; - }, - - /** - * If a state it passed, it becomes the pressed state otherwise the current state is toggled. - * @param {Boolean} state (optional) Force a particular state - * @param {Boolean} supressEvent (optional) True to stop events being fired when calling this method. - * @return {Ext.Button} this - */ - toggle : function(state, suppressEvent){ - state = state === undefined ? !this.pressed : !!state; - if(state != this.pressed){ - if(this.rendered){ - this.el[state ? 'addClass' : 'removeClass']('x-btn-pressed'); - } - this.pressed = state; - if(!suppressEvent){ - this.fireEvent('toggle', this, state); - if(this.toggleHandler){ - this.toggleHandler.call(this.scope || this, this, state); - } - } - } - return this; - }, - - // private - onDisable : function(){ - this.onDisableChange(true); - }, - - // private - onEnable : function(){ - this.onDisableChange(false); - }, - - onDisableChange : function(disabled){ - if(this.el){ - if(!Ext.isIE6 || !this.text){ - this.el[disabled ? 'addClass' : 'removeClass'](this.disabledClass); - } - this.el.dom.disabled = disabled; - } - this.disabled = disabled; - }, - - /** - * Show this button's menu (if it has one) - */ - showMenu : function(){ - if(this.rendered && this.menu){ - if(this.tooltip){ - Ext.QuickTips.getQuickTip().cancelShow(this.btnEl); - } - if(this.menu.isVisible()){ - this.menu.hide(); - } - this.menu.ownerCt = this; - this.menu.show(this.el, this.menuAlign); - } - return this; - }, - - /** - * Hide this button's menu (if it has one) - */ - hideMenu : function(){ - if(this.hasVisibleMenu()){ - this.menu.hide(); - } - return this; - }, - - /** - * Returns true if the button has a menu and it is visible - * @return {Boolean} - */ - hasVisibleMenu : function(){ - return this.menu && this.menu.ownerCt == this && this.menu.isVisible(); - }, - - // private - onRepeatClick : function(repeat, e){ - this.onClick(e); - }, - - // private - onClick : function(e){ - if(e){ - e.preventDefault(); - } - if(e.button !== 0){ - return; - } - if(!this.disabled){ - this.doToggle(); - if(this.menu && !this.hasVisibleMenu() && !this.ignoreNextClick){ - this.showMenu(); - } - this.fireEvent('click', this, e); - if(this.handler){ - //this.el.removeClass('x-btn-over'); - this.handler.call(this.scope || this, this, e); - } - } - }, - - // private - doToggle: function(){ - if (this.enableToggle && (this.allowDepress !== false || !this.pressed)) { - this.toggle(); - } - }, - - // private - isMenuTriggerOver : function(e, internal){ - return this.menu && !internal; - }, - - // private - isMenuTriggerOut : function(e, internal){ - return this.menu && !internal; - }, - - // private - onMouseOver : function(e){ - if(!this.disabled){ - var internal = e.within(this.el, true); - if(!internal){ - this.el.addClass('x-btn-over'); - if(!this.monitoringMouseOver){ - this.doc.on('mouseover', this.monitorMouseOver, this); - this.monitoringMouseOver = true; - } - this.fireEvent('mouseover', this, e); - } - if(this.isMenuTriggerOver(e, internal)){ - this.fireEvent('menutriggerover', this, this.menu, e); - } - } - }, - - // private - monitorMouseOver : function(e){ - if(e.target != this.el.dom && !e.within(this.el)){ - if(this.monitoringMouseOver){ - this.doc.un('mouseover', this.monitorMouseOver, this); - this.monitoringMouseOver = false; - } - this.onMouseOut(e); - } - }, - - // private - onMouseOut : function(e){ - var internal = e.within(this.el) && e.target != this.el.dom; - this.el.removeClass('x-btn-over'); - this.fireEvent('mouseout', this, e); - if(this.isMenuTriggerOut(e, internal)){ - this.fireEvent('menutriggerout', this, this.menu, e); - } - }, - - focus : function() { - this.btnEl.focus(); - }, - - blur : function() { - this.btnEl.blur(); - }, - - // private - onFocus : function(e){ - if(!this.disabled){ - this.el.addClass('x-btn-focus'); - } - }, - // private - onBlur : function(e){ - this.el.removeClass('x-btn-focus'); - }, - - // private - getClickEl : function(e, isUp){ - return this.el; - }, - - // private - onMouseDown : function(e){ - if(!this.disabled && e.button === 0){ - this.getClickEl(e).addClass('x-btn-click'); - this.doc.on('mouseup', this.onMouseUp, this); - } - }, - // private - onMouseUp : function(e){ - if(e.button === 0){ - this.getClickEl(e, true).removeClass('x-btn-click'); - this.doc.un('mouseup', this.onMouseUp, this); - } - }, - // private - onMenuShow : function(e){ - if(this.menu.ownerCt == this){ - this.menu.ownerCt = this; - this.ignoreNextClick = 0; - this.el.addClass('x-btn-menu-active'); - this.fireEvent('menushow', this, this.menu); - } - }, - // private - onMenuHide : function(e){ - if(this.menu.ownerCt == this){ - this.el.removeClass('x-btn-menu-active'); - this.ignoreNextClick = this.restoreClick.defer(250, this); - this.fireEvent('menuhide', this, this.menu); - delete this.menu.ownerCt; - } - }, - - // private - restoreClick : function(){ - this.ignoreNextClick = 0; - } - - /** - * @cfg {String} autoEl @hide - */ - /** - * @cfg {String/Object} html @hide - */ - /** - * @cfg {String} contentEl @hide - */ - /** - * @cfg {Mixed} data @hide - */ - /** - * @cfg {Mixed} tpl @hide - */ - /** - * @cfg {String} tplWriteMode @hide - */ -}); -Ext.reg('button', Ext.Button); - -// Private utility class used by Button -Ext.ButtonToggleMgr = function(){ - var groups = {}; - - function toggleGroup(btn, state){ - if(state){ - var g = groups[btn.toggleGroup]; - for(var i = 0, l = g.length; i < l; i++){ - if(g[i] != btn){ - g[i].toggle(false); - } - } - } - } - - return { - register : function(btn){ - if(!btn.toggleGroup){ - return; - } - var g = groups[btn.toggleGroup]; - if(!g){ - g = groups[btn.toggleGroup] = []; - } - g.push(btn); - btn.on('toggle', toggleGroup); - }, - - unregister : function(btn){ - if(!btn.toggleGroup){ - return; - } - var g = groups[btn.toggleGroup]; - if(g){ - g.remove(btn); - btn.un('toggle', toggleGroup); - } - }, - - /** - * Gets the pressed button in the passed group or null - * @param {String} group - * @return Button - */ - getPressed : function(group){ - var g = groups[group]; - if(g){ - for(var i = 0, len = g.length; i < len; i++){ - if(g[i].pressed === true){ - return g[i]; - } - } - } - return null; - } - }; -}(); -/** - * @class Ext.SplitButton - * @extends Ext.Button - * A split button that provides a built-in dropdown arrow that can fire an event separately from the default - * click event of the button. Typically this would be used to display a dropdown menu that provides additional - * options to the primary button action, but any custom handler can provide the arrowclick implementation. Example usage: - *
    
    -// display a dropdown menu:
    -new Ext.SplitButton({
    -	renderTo: 'button-ct', // the container id
    -   	text: 'Options',
    -   	handler: optionsHandler, // handle a click on the button itself
    -   	menu: new Ext.menu.Menu({
    -        items: [
    -        	// these items will render as dropdown menu items when the arrow is clicked:
    -	        {text: 'Item 1', handler: item1Handler},
    -	        {text: 'Item 2', handler: item2Handler}
    -        ]
    -   	})
    -});
    -
    -// Instead of showing a menu, you provide any type of custom
    -// functionality you want when the dropdown arrow is clicked:
    -new Ext.SplitButton({
    -	renderTo: 'button-ct',
    -   	text: 'Options',
    -   	handler: optionsHandler,
    -   	arrowHandler: myCustomHandler
    -});
    -
    - * @cfg {Function} arrowHandler A function called when the arrow button is clicked (can be used instead of click event) - * @cfg {String} arrowTooltip The title attribute of the arrow - * @constructor - * Create a new menu button - * @param {Object} config The config object - * @xtype splitbutton - */ -Ext.SplitButton = Ext.extend(Ext.Button, { - // private - arrowSelector : 'em', - split: true, - - // private - initComponent : function(){ - Ext.SplitButton.superclass.initComponent.call(this); - /** - * @event arrowclick - * Fires when this button's arrow is clicked - * @param {MenuButton} this - * @param {EventObject} e The click event - */ - this.addEvents("arrowclick"); - }, - - // private - onRender : function(){ - Ext.SplitButton.superclass.onRender.apply(this, arguments); - if(this.arrowTooltip){ - this.el.child(this.arrowSelector).dom[this.tooltipType] = this.arrowTooltip; - } - }, - - /** - * Sets this button's arrow click handler. - * @param {Function} handler The function to call when the arrow is clicked - * @param {Object} scope (optional) Scope for the function passed above - */ - setArrowHandler : function(handler, scope){ - this.arrowHandler = handler; - this.scope = scope; - }, - - getMenuClass : function(){ - return 'x-btn-split' + (this.arrowAlign == 'bottom' ? '-bottom' : ''); - }, - - isClickOnArrow : function(e){ - if (this.arrowAlign != 'bottom') { - var visBtn = this.el.child('em.x-btn-split'); - var right = visBtn.getRegion().right - visBtn.getPadding('r'); - return e.getPageX() > right; - } else { - return e.getPageY() > this.btnEl.getRegion().bottom; - } - }, - - // private - onClick : function(e, t){ - e.preventDefault(); - if(!this.disabled){ - if(this.isClickOnArrow(e)){ - if(this.menu && !this.menu.isVisible() && !this.ignoreNextClick){ - this.showMenu(); - } - this.fireEvent("arrowclick", this, e); - if(this.arrowHandler){ - this.arrowHandler.call(this.scope || this, this, e); - } - }else{ - this.doToggle(); - this.fireEvent("click", this, e); - if(this.handler){ - this.handler.call(this.scope || this, this, e); - } - } - } - }, - - // private - isMenuTriggerOver : function(e){ - return this.menu && e.target.tagName == this.arrowSelector; - }, - - // private - isMenuTriggerOut : function(e, internal){ - return this.menu && e.target.tagName != this.arrowSelector; - } -}); - -Ext.reg('splitbutton', Ext.SplitButton);/** - * @class Ext.CycleButton - * @extends Ext.SplitButton - * A specialized SplitButton that contains a menu of {@link Ext.menu.CheckItem} elements. The button automatically - * cycles through each menu item on click, raising the button's {@link #change} event (or calling the button's - * {@link #changeHandler} function, if supplied) for the active menu item. Clicking on the arrow section of the - * button displays the dropdown menu just like a normal SplitButton. Example usage: - *
    
    -var btn = new Ext.CycleButton({
    -    showText: true,
    -    prependText: 'View as ',
    -    items: [{
    -        text:'text only',
    -        iconCls:'view-text',
    -        checked:true
    -    },{
    -        text:'HTML',
    -        iconCls:'view-html'
    -    }],
    -    changeHandler:function(btn, item){
    -        Ext.Msg.alert('Change View', item.text);
    -    }
    -});
    -
    - * @constructor - * Create a new split button - * @param {Object} config The config object - * @xtype cycle - */ -Ext.CycleButton = Ext.extend(Ext.SplitButton, { - /** - * @cfg {Array} items An array of {@link Ext.menu.CheckItem} config objects to be used when creating the - * button's menu items (e.g., {text:'Foo', iconCls:'foo-icon'}) - */ - /** - * @cfg {Boolean} showText True to display the active item's text as the button text (defaults to false) - */ - /** - * @cfg {String} prependText A static string to prepend before the active item's text when displayed as the - * button's text (only applies when showText = true, defaults to '') - */ - /** - * @cfg {Function} changeHandler A callback function that will be invoked each time the active menu - * item in the button's menu has changed. If this callback is not supplied, the SplitButton will instead - * fire the {@link #change} event on active item change. The changeHandler function will be called with the - * following argument list: (SplitButton this, Ext.menu.CheckItem item) - */ - /** - * @cfg {String} forceIcon A css class which sets an image to be used as the static icon for this button. This - * icon will always be displayed regardless of which item is selected in the dropdown list. This overrides the - * default behavior of changing the button's icon to match the selected item's icon on change. - */ - /** - * @property menu - * @type Menu - * The {@link Ext.menu.Menu Menu} object used to display the {@link Ext.menu.CheckItem CheckItems} representing the available choices. - */ - - // private - getItemText : function(item){ - if(item && this.showText === true){ - var text = ''; - if(this.prependText){ - text += this.prependText; - } - text += item.text; - return text; - } - return undefined; - }, - - /** - * Sets the button's active menu item. - * @param {Ext.menu.CheckItem} item The item to activate - * @param {Boolean} suppressEvent True to prevent the button's change event from firing (defaults to false) - */ - setActiveItem : function(item, suppressEvent){ - if(!Ext.isObject(item)){ - item = this.menu.getComponent(item); - } - if(item){ - if(!this.rendered){ - this.text = this.getItemText(item); - this.iconCls = item.iconCls; - }else{ - var t = this.getItemText(item); - if(t){ - this.setText(t); - } - this.setIconClass(item.iconCls); - } - this.activeItem = item; - if(!item.checked){ - item.setChecked(true, suppressEvent); - } - if(this.forceIcon){ - this.setIconClass(this.forceIcon); - } - if(!suppressEvent){ - this.fireEvent('change', this, item); - } - } - }, - - /** - * Gets the currently active menu item. - * @return {Ext.menu.CheckItem} The active item - */ - getActiveItem : function(){ - return this.activeItem; - }, - - // private - initComponent : function(){ - this.addEvents( - /** - * @event change - * Fires after the button's active menu item has changed. Note that if a {@link #changeHandler} function - * is set on this CycleButton, it will be called instead on active item change and this change event will - * not be fired. - * @param {Ext.CycleButton} this - * @param {Ext.menu.CheckItem} item The menu item that was selected - */ - "change" - ); - - if(this.changeHandler){ - this.on('change', this.changeHandler, this.scope||this); - delete this.changeHandler; - } - - this.itemCount = this.items.length; - - this.menu = {cls:'x-cycle-menu', items:[]}; - var checked = 0; - Ext.each(this.items, function(item, i){ - Ext.apply(item, { - group: item.group || this.id, - itemIndex: i, - checkHandler: this.checkHandler, - scope: this, - checked: item.checked || false - }); - this.menu.items.push(item); - if(item.checked){ - checked = i; - } - }, this); - Ext.CycleButton.superclass.initComponent.call(this); - this.on('click', this.toggleSelected, this); - this.setActiveItem(checked, true); - }, - - // private - checkHandler : function(item, pressed){ - if(pressed){ - this.setActiveItem(item); - } - }, - - /** - * This is normally called internally on button click, but can be called externally to advance the button's - * active item programmatically to the next one in the menu. If the current item is the last one in the menu - * the active item will be set to the first item in the menu. - */ - toggleSelected : function(){ - var m = this.menu; - m.render(); - // layout if we haven't before so the items are active - if(!m.hasLayout){ - m.doLayout(); - } - - var nextIdx, checkItem; - for (var i = 1; i < this.itemCount; i++) { - nextIdx = (this.activeItem.itemIndex + i) % this.itemCount; - // check the potential item - checkItem = m.items.itemAt(nextIdx); - // if its not disabled then check it. - if (!checkItem.disabled) { - checkItem.setChecked(true); - break; - } - } - } -}); -Ext.reg('cycle', Ext.CycleButton);/** - * @class Ext.Toolbar - * @extends Ext.Container - *

    Basic Toolbar class. Although the {@link Ext.Container#defaultType defaultType} for Toolbar - * is {@link Ext.Button button}, Toolbar elements (child items for the Toolbar container) may - * be virtually any type of Component. Toolbar elements can be created explicitly via their constructors, - * or implicitly via their xtypes, and can be {@link #add}ed dynamically.

    - *

    Some items have shortcut strings for creation:

    - *
    -Shortcut  xtype          Class                  Description
    -'->'      'tbfill'       {@link Ext.Toolbar.Fill}       begin using the right-justified button container
    -'-'       'tbseparator'  {@link Ext.Toolbar.Separator}  add a vertical separator bar between toolbar items
    -' '       'tbspacer'     {@link Ext.Toolbar.Spacer}     add horiztonal space between elements
    - * 
    - * - * Example usage of various elements: - *
    
    -var tb = new Ext.Toolbar({
    -    renderTo: document.body,
    -    width: 600,
    -    height: 100,
    -    items: [
    -        {
    -            // xtype: 'button', // default for Toolbars, same as 'tbbutton'
    -            text: 'Button'
    -        },
    -        {
    -            xtype: 'splitbutton', // same as 'tbsplitbutton'
    -            text: 'Split Button'
    -        },
    -        // begin using the right-justified button container
    -        '->', // same as {xtype: 'tbfill'}, // Ext.Toolbar.Fill
    -        {
    -            xtype: 'textfield',
    -            name: 'field1',
    -            emptyText: 'enter search term'
    -        },
    -        // add a vertical separator bar between toolbar items
    -        '-', // same as {xtype: 'tbseparator'} to create Ext.Toolbar.Separator
    -        'text 1', // same as {xtype: 'tbtext', text: 'text1'} to create Ext.Toolbar.TextItem
    -        {xtype: 'tbspacer'},// same as ' ' to create Ext.Toolbar.Spacer
    -        'text 2',
    -        {xtype: 'tbspacer', width: 50}, // add a 50px space
    -        'text 3'
    -    ]
    -});
    - * 
    - * Example adding a ComboBox within a menu of a button: - *
    
    -// ComboBox creation
    -var combo = new Ext.form.ComboBox({
    -    store: new Ext.data.ArrayStore({
    -        autoDestroy: true,
    -        fields: ['initials', 'fullname'],
    -        data : [
    -            ['FF', 'Fred Flintstone'],
    -            ['BR', 'Barney Rubble']
    -        ]
    -    }),
    -    displayField: 'fullname',
    -    typeAhead: true,
    -    mode: 'local',
    -    forceSelection: true,
    -    triggerAction: 'all',
    -    emptyText: 'Select a name...',
    -    selectOnFocus: true,
    -    width: 135,
    -    getListParent: function() {
    -        return this.el.up('.x-menu');
    -    },
    -    iconCls: 'no-icon' //use iconCls if placing within menu to shift to right side of menu
    -});
    -
    -// put ComboBox in a Menu
    -var menu = new Ext.menu.Menu({
    -    id: 'mainMenu',
    -    items: [
    -        combo // A Field in a Menu
    -    ]
    -});
    -
    -// add a Button with the menu
    -tb.add({
    -        text:'Button w/ Menu',
    -        menu: menu  // assign menu by instance
    -    });
    -tb.doLayout();
    - * 
    - * @constructor - * Creates a new Toolbar - * @param {Object/Array} config A config object or an array of buttons to {@link #add} - * @xtype toolbar - */ -Ext.Toolbar = function(config){ - if(Ext.isArray(config)){ - config = {items: config, layout: 'toolbar'}; - } else { - config = Ext.apply({ - layout: 'toolbar' - }, config); - if(config.buttons) { - config.items = config.buttons; - } - } - Ext.Toolbar.superclass.constructor.call(this, config); -}; - -(function(){ - -var T = Ext.Toolbar; - -Ext.extend(T, Ext.Container, { - - defaultType: 'button', - - /** - * @cfg {String/Object} layout - * This class assigns a default layout (layout:'toolbar'). - * Developers may override this configuration option if another layout - * is required (the constructor must be passed a configuration object in this - * case instead of an array). - * See {@link Ext.Container#layout} for additional information. - */ - - enableOverflow : false, - - /** - * @cfg {Boolean} enableOverflow - * Defaults to false. Configure true to make the toolbar provide a button - * which activates a dropdown Menu to show items which overflow the Toolbar's width. - */ - /** - * @cfg {String} buttonAlign - *

    The default position at which to align child items. Defaults to "left"

    - *

    May be specified as "center" to cause items added before a Fill (A "->") item - * to be centered in the Toolbar. Items added after a Fill are still right-aligned.

    - *

    Specify as "right" to right align all child items.

    - */ - - trackMenus : true, - internalDefaults: {removeMode: 'container', hideParent: true}, - toolbarCls: 'x-toolbar', - - initComponent : function(){ - T.superclass.initComponent.call(this); - - /** - * @event overflowchange - * Fires after the overflow state has changed. - * @param {Object} c The Container - * @param {Boolean} lastOverflow overflow state - */ - this.addEvents('overflowchange'); - }, - - // private - onRender : function(ct, position){ - if(!this.el){ - if(!this.autoCreate){ - this.autoCreate = { - cls: this.toolbarCls + ' x-small-editor' - }; - } - this.el = ct.createChild(Ext.apply({ id: this.id },this.autoCreate), position); - Ext.Toolbar.superclass.onRender.apply(this, arguments); - } - }, - - /** - *

    Adds element(s) to the toolbar -- this function takes a variable number of - * arguments of mixed type and adds them to the toolbar.

    - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Mixed} arg1 The following types of arguments are all valid:
    - *
      - *
    • {@link Ext.Button} config: A valid button config object (equivalent to {@link #addButton})
    • - *
    • HtmlElement: Any standard HTML element (equivalent to {@link #addElement})
    • - *
    • Field: Any form field (equivalent to {@link #addField})
    • - *
    • Item: Any subclass of {@link Ext.Toolbar.Item} (equivalent to {@link #addItem})
    • - *
    • String: Any generic string (gets wrapped in a {@link Ext.Toolbar.TextItem}, equivalent to {@link #addText}). - * Note that there are a few special strings that are treated differently as explained next.
    • - *
    • '-': Creates a separator element (equivalent to {@link #addSeparator})
    • - *
    • ' ': Creates a spacer element (equivalent to {@link #addSpacer})
    • - *
    • '->': Creates a fill element (equivalent to {@link #addFill})
    • - *
    - * @param {Mixed} arg2 - * @param {Mixed} etc. - * @method add - */ - - // private - lookupComponent : function(c){ - if(Ext.isString(c)){ - if(c == '-'){ - c = new T.Separator(); - }else if(c == ' '){ - c = new T.Spacer(); - }else if(c == '->'){ - c = new T.Fill(); - }else{ - c = new T.TextItem(c); - } - this.applyDefaults(c); - }else{ - if(c.isFormField || c.render){ // some kind of form field, some kind of Toolbar.Item - c = this.createComponent(c); - }else if(c.tag){ // DomHelper spec - c = new T.Item({autoEl: c}); - }else if(c.tagName){ // element - c = new T.Item({el:c}); - }else if(Ext.isObject(c)){ // must be button config? - c = c.xtype ? this.createComponent(c) : this.constructButton(c); - } - } - return c; - }, - - // private - applyDefaults : function(c){ - if(!Ext.isString(c)){ - c = Ext.Toolbar.superclass.applyDefaults.call(this, c); - var d = this.internalDefaults; - if(c.events){ - Ext.applyIf(c.initialConfig, d); - Ext.apply(c, d); - }else{ - Ext.applyIf(c, d); - } - } - return c; - }, - - /** - * Adds a separator - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @return {Ext.Toolbar.Item} The separator {@link Ext.Toolbar.Item item} - */ - addSeparator : function(){ - return this.add(new T.Separator()); - }, - - /** - * Adds a spacer element - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @return {Ext.Toolbar.Spacer} The spacer item - */ - addSpacer : function(){ - return this.add(new T.Spacer()); - }, - - /** - * Forces subsequent additions into the float:right toolbar - *

    Note: See the notes within {@link Ext.Container#add}.

    - */ - addFill : function(){ - this.add(new T.Fill()); - }, - - /** - * Adds any standard HTML element to the toolbar - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Mixed} el The element or id of the element to add - * @return {Ext.Toolbar.Item} The element's item - */ - addElement : function(el){ - return this.addItem(new T.Item({el:el})); - }, - - /** - * Adds any Toolbar.Item or subclass - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Ext.Toolbar.Item} item - * @return {Ext.Toolbar.Item} The item - */ - addItem : function(item){ - return this.add.apply(this, arguments); - }, - - /** - * Adds a button (or buttons). See {@link Ext.Button} for more info on the config. - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Object/Array} config A button config or array of configs - * @return {Ext.Button/Array} - */ - addButton : function(config){ - if(Ext.isArray(config)){ - var buttons = []; - for(var i = 0, len = config.length; i < len; i++) { - buttons.push(this.addButton(config[i])); - } - return buttons; - } - return this.add(this.constructButton(config)); - }, - - /** - * Adds text to the toolbar - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {String} text The text to add - * @return {Ext.Toolbar.Item} The element's item - */ - addText : function(text){ - return this.addItem(new T.TextItem(text)); - }, - - /** - * Adds a new element to the toolbar from the passed {@link Ext.DomHelper} config - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Object} config - * @return {Ext.Toolbar.Item} The element's item - */ - addDom : function(config){ - return this.add(new T.Item({autoEl: config})); - }, - - /** - * Adds a dynamically rendered Ext.form field (TextField, ComboBox, etc). Note: the field should not have - * been rendered yet. For a field that has already been rendered, use {@link #addElement}. - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Ext.form.Field} field - * @return {Ext.Toolbar.Item} - */ - addField : function(field){ - return this.add(field); - }, - - /** - * Inserts any {@link Ext.Toolbar.Item}/{@link Ext.Button} at the specified index. - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Number} index The index where the item is to be inserted - * @param {Object/Ext.Toolbar.Item/Ext.Button/Array} item The button, or button config object to be - * inserted, or an array of buttons/configs. - * @return {Ext.Button/Item} - */ - insertButton : function(index, item){ - if(Ext.isArray(item)){ - var buttons = []; - for(var i = 0, len = item.length; i < len; i++) { - buttons.push(this.insertButton(index + i, item[i])); - } - return buttons; - } - return Ext.Toolbar.superclass.insert.call(this, index, item); - }, - - // private - trackMenu : function(item, remove){ - if(this.trackMenus && item.menu){ - var method = remove ? 'mun' : 'mon'; - this[method](item, 'menutriggerover', this.onButtonTriggerOver, this); - this[method](item, 'menushow', this.onButtonMenuShow, this); - this[method](item, 'menuhide', this.onButtonMenuHide, this); - } - }, - - // private - constructButton : function(item){ - var b = item.events ? item : this.createComponent(item, item.split ? 'splitbutton' : this.defaultType); - return b; - }, - - // private - onAdd : function(c){ - Ext.Toolbar.superclass.onAdd.call(this); - this.trackMenu(c); - if(this.disabled){ - c.disable(); - } - }, - - // private - onRemove : function(c){ - Ext.Toolbar.superclass.onRemove.call(this); - if (c == this.activeMenuBtn) { - delete this.activeMenuBtn; - } - this.trackMenu(c, true); - }, - - // private - onDisable : function(){ - this.items.each(function(item){ - if(item.disable){ - item.disable(); - } - }); - }, - - // private - onEnable : function(){ - this.items.each(function(item){ - if(item.enable){ - item.enable(); - } - }); - }, - - // private - onButtonTriggerOver : function(btn){ - if(this.activeMenuBtn && this.activeMenuBtn != btn){ - this.activeMenuBtn.hideMenu(); - btn.showMenu(); - this.activeMenuBtn = btn; - } - }, - - // private - onButtonMenuShow : function(btn){ - this.activeMenuBtn = btn; - }, - - // private - onButtonMenuHide : function(btn){ - delete this.activeMenuBtn; - } -}); -Ext.reg('toolbar', Ext.Toolbar); - -/** - * @class Ext.Toolbar.Item - * @extends Ext.BoxComponent - * The base class that other non-interacting Toolbar Item classes should extend in order to - * get some basic common toolbar item functionality. - * @constructor - * Creates a new Item - * @param {HTMLElement} el - * @xtype tbitem - */ -T.Item = Ext.extend(Ext.BoxComponent, { - hideParent: true, // Hiding a Toolbar.Item hides its containing TD - enable:Ext.emptyFn, - disable:Ext.emptyFn, - focus:Ext.emptyFn - /** - * @cfg {String} overflowText Text to be used for the menu if the item is overflowed. - */ -}); -Ext.reg('tbitem', T.Item); - -/** - * @class Ext.Toolbar.Separator - * @extends Ext.Toolbar.Item - * A simple class that adds a vertical separator bar between toolbar items - * (css class:'xtb-sep'). Example usage: - *
    
    -new Ext.Panel({
    -    tbar : [
    -        'Item 1',
    -        {xtype: 'tbseparator'}, // or '-'
    -        'Item 2'
    -    ]
    -});
    -
    - * @constructor - * Creates a new Separator - * @xtype tbseparator - */ -T.Separator = Ext.extend(T.Item, { - onRender : function(ct, position){ - this.el = ct.createChild({tag:'span', cls:'xtb-sep'}, position); - } -}); -Ext.reg('tbseparator', T.Separator); - -/** - * @class Ext.Toolbar.Spacer - * @extends Ext.Toolbar.Item - * A simple element that adds extra horizontal space between items in a toolbar. - * By default a 2px wide space is added via css specification:
    
    -.x-toolbar .xtb-spacer {
    -    width:2px;
    -}
    - * 
    - *

    Example usage:

    - *
    
    -new Ext.Panel({
    -    tbar : [
    -        'Item 1',
    -        {xtype: 'tbspacer'}, // or ' '
    -        'Item 2',
    -        // space width is also configurable via javascript
    -        {xtype: 'tbspacer', width: 50}, // add a 50px space
    -        'Item 3'
    -    ]
    -});
    -
    - * @constructor - * Creates a new Spacer - * @xtype tbspacer - */ -T.Spacer = Ext.extend(T.Item, { - /** - * @cfg {Number} width - * The width of the spacer in pixels (defaults to 2px via css style .x-toolbar .xtb-spacer). - */ - - onRender : function(ct, position){ - this.el = ct.createChild({tag:'div', cls:'xtb-spacer', style: this.width?'width:'+this.width+'px':''}, position); - } -}); -Ext.reg('tbspacer', T.Spacer); - -/** - * @class Ext.Toolbar.Fill - * @extends Ext.Toolbar.Spacer - * A non-rendering placeholder item which instructs the Toolbar's Layout to begin using - * the right-justified button container. - *
    
    -new Ext.Panel({
    -    tbar : [
    -        'Item 1',
    -        {xtype: 'tbfill'}, // or '->'
    -        'Item 2'
    -    ]
    -});
    -
    - * @constructor - * Creates a new Fill - * @xtype tbfill - */ -T.Fill = Ext.extend(T.Item, { - // private - render : Ext.emptyFn, - isFill : true -}); -Ext.reg('tbfill', T.Fill); - -/** - * @class Ext.Toolbar.TextItem - * @extends Ext.Toolbar.Item - * A simple class that renders text directly into a toolbar - * (with css class:'xtb-text'). Example usage: - *
    
    -new Ext.Panel({
    -    tbar : [
    -        {xtype: 'tbtext', text: 'Item 1'} // or simply 'Item 1'
    -    ]
    -});
    -
    - * @constructor - * Creates a new TextItem - * @param {String/Object} text A text string, or a config object containing a text property - * @xtype tbtext - */ -T.TextItem = Ext.extend(T.Item, { - /** - * @cfg {String} text The text to be used as innerHTML (html tags are accepted) - */ - - constructor: function(config){ - T.TextItem.superclass.constructor.call(this, Ext.isString(config) ? {text: config} : config); - }, - - // private - onRender : function(ct, position) { - this.autoEl = {cls: 'xtb-text', html: this.text || ''}; - T.TextItem.superclass.onRender.call(this, ct, position); - }, - - /** - * Updates this item's text, setting the text to be used as innerHTML. - * @param {String} t The text to display (html accepted). - */ - setText : function(t) { - if(this.rendered){ - this.el.update(t); - }else{ - this.text = t; - } - } -}); -Ext.reg('tbtext', T.TextItem); - -// backwards compat -T.Button = Ext.extend(Ext.Button, {}); -T.SplitButton = Ext.extend(Ext.SplitButton, {}); -Ext.reg('tbbutton', T.Button); -Ext.reg('tbsplit', T.SplitButton); - -})(); -/** - * @class Ext.ButtonGroup - * @extends Ext.Panel - * Container for a group of buttons. Example usage: - *
    
    -var p = new Ext.Panel({
    -    title: 'Panel with Button Group',
    -    width: 300,
    -    height:200,
    -    renderTo: document.body,
    -    html: 'whatever',
    -    tbar: [{
    -        xtype: 'buttongroup',
    -        {@link #columns}: 3,
    -        title: 'Clipboard',
    -        items: [{
    -            text: 'Paste',
    -            scale: 'large',
    -            rowspan: 3, iconCls: 'add',
    -            iconAlign: 'top',
    -            cls: 'x-btn-as-arrow'
    -        },{
    -            xtype:'splitbutton',
    -            text: 'Menu Button',
    -            scale: 'large',
    -            rowspan: 3,
    -            iconCls: 'add',
    -            iconAlign: 'top',
    -            arrowAlign:'bottom',
    -            menu: [{text: 'Menu Item 1'}]
    -        },{
    -            xtype:'splitbutton', text: 'Cut', iconCls: 'add16', menu: [{text: 'Cut Menu Item'}]
    -        },{
    -            text: 'Copy', iconCls: 'add16'
    -        },{
    -            text: 'Format', iconCls: 'add16'
    -        }]
    -    }]
    -});
    - * 
    - * @constructor - * Create a new ButtonGroup. - * @param {Object} config The config object - * @xtype buttongroup - */ -Ext.ButtonGroup = Ext.extend(Ext.Panel, { - /** - * @cfg {Number} columns The columns configuration property passed to the - * {@link #layout configured layout manager}. See {@link Ext.layout.TableLayout#columns}. - */ - /** - * @cfg {String} baseCls Defaults to 'x-btn-group'. See {@link Ext.Panel#baseCls}. - */ - baseCls: 'x-btn-group', - /** - * @cfg {String} layout Defaults to 'table'. See {@link Ext.Container#layout}. - */ - layout:'table', - defaultType: 'button', - /** - * @cfg {Boolean} frame Defaults to true. See {@link Ext.Panel#frame}. - */ - frame: true, - internalDefaults: {removeMode: 'container', hideParent: true}, - - initComponent : function(){ - this.layoutConfig = this.layoutConfig || {}; - Ext.applyIf(this.layoutConfig, { - columns : this.columns - }); - if(!this.title){ - this.addClass('x-btn-group-notitle'); - } - this.on('afterlayout', this.onAfterLayout, this); - Ext.ButtonGroup.superclass.initComponent.call(this); - }, - - applyDefaults : function(c){ - c = Ext.ButtonGroup.superclass.applyDefaults.call(this, c); - var d = this.internalDefaults; - if(c.events){ - Ext.applyIf(c.initialConfig, d); - Ext.apply(c, d); - }else{ - Ext.applyIf(c, d); - } - return c; - }, - - onAfterLayout : function(){ - var bodyWidth = this.body.getFrameWidth('lr') + this.body.dom.firstChild.offsetWidth; - this.body.setWidth(bodyWidth); - this.el.setWidth(bodyWidth + this.getFrameWidth()); - } - /** - * @cfg {Array} tools @hide - */ -}); - -Ext.reg('buttongroup', Ext.ButtonGroup); -/** - * @class Ext.PagingToolbar - * @extends Ext.Toolbar - *

    As the amount of records increases, the time required for the browser to render - * them increases. Paging is used to reduce the amount of data exchanged with the client. - * Note: if there are more records/rows than can be viewed in the available screen area, vertical - * scrollbars will be added.

    - *

    Paging is typically handled on the server side (see exception below). The client sends - * parameters to the server side, which the server needs to interpret and then respond with the - * approprate data.

    - *

    Ext.PagingToolbar is a specialized toolbar that is bound to a {@link Ext.data.Store} - * and provides automatic paging control. This Component {@link Ext.data.Store#load load}s blocks - * of data into the {@link #store} by passing {@link Ext.data.Store#paramNames paramNames} used for - * paging criteria.

    - *

    PagingToolbar is typically used as one of the Grid's toolbars:

    - *
    
    -Ext.QuickTips.init(); // to display button quicktips
    -
    -var myStore = new Ext.data.Store({
    -    reader: new Ext.data.JsonReader({
    -        {@link Ext.data.JsonReader#totalProperty totalProperty}: 'results', 
    -        ...
    -    }),
    -    ...
    -});
    -
    -var myPageSize = 25;  // server script should only send back 25 items at a time
    -
    -var grid = new Ext.grid.GridPanel({
    -    ...
    -    store: myStore,
    -    bbar: new Ext.PagingToolbar({
    -        {@link #store}: myStore,       // grid and PagingToolbar using same store
    -        {@link #displayInfo}: true,
    -        {@link #pageSize}: myPageSize,
    -        {@link #prependButtons}: true,
    -        items: [
    -            'text 1'
    -        ]
    -    })
    -});
    - * 
    - * - *

    To use paging, pass the paging requirements to the server when the store is first loaded.

    - *
    
    -store.load({
    -    params: {
    -        // specify params for the first page load if using paging
    -        start: 0,          
    -        limit: myPageSize,
    -        // other params
    -        foo:   'bar'
    -    }
    -});
    - * 
    - * - *

    If using {@link Ext.data.Store#autoLoad store's autoLoad} configuration:

    - *
    
    -var myStore = new Ext.data.Store({
    -    {@link Ext.data.Store#autoLoad autoLoad}: {params:{start: 0, limit: 25}},
    -    ...
    -});
    - * 
    - * - *

    The packet sent back from the server would have this form:

    - *
    
    -{
    -    "success": true,
    -    "results": 2000, 
    -    "rows": [ // *Note: this must be an Array 
    -        { "id":  1, "name": "Bill", "occupation": "Gardener" },
    -        { "id":  2, "name":  "Ben", "occupation": "Horticulturalist" },
    -        ...
    -        { "id": 25, "name":  "Sue", "occupation": "Botanist" }
    -    ]
    -}
    - * 
    - *

    Paging with Local Data

    - *

    Paging can also be accomplished with local data using extensions:

    - *
    - * @constructor Create a new PagingToolbar - * @param {Object} config The config object - * @xtype paging - */ -(function() { - -var T = Ext.Toolbar; - -Ext.PagingToolbar = Ext.extend(Ext.Toolbar, { - /** - * @cfg {Ext.data.Store} store - * The {@link Ext.data.Store} the paging toolbar should use as its data source (required). - */ - /** - * @cfg {Boolean} displayInfo - * true to display the displayMsg (defaults to false) - */ - /** - * @cfg {Number} pageSize - * The number of records to display per page (defaults to 20) - */ - pageSize : 20, - /** - * @cfg {Boolean} prependButtons - * true to insert any configured items before the paging buttons. - * Defaults to false. - */ - /** - * @cfg {String} displayMsg - * The paging status message to display (defaults to 'Displaying {0} - {1} of {2}'). - * Note that this string is formatted using the braced numbers {0}-{2} as tokens - * that are replaced by the values for start, end and total respectively. These tokens should - * be preserved when overriding this string if showing those values is desired. - */ - displayMsg : 'Displaying {0} - {1} of {2}', - /** - * @cfg {String} emptyMsg - * The message to display when no records are found (defaults to 'No data to display') - */ - emptyMsg : 'No data to display', - /** - * @cfg {String} beforePageText - * The text displayed before the input item (defaults to 'Page'). - */ - beforePageText : 'Page', - /** - * @cfg {String} afterPageText - * Customizable piece of the default paging text (defaults to 'of {0}'). Note that - * this string is formatted using {0} as a token that is replaced by the number of - * total pages. This token should be preserved when overriding this string if showing the - * total page count is desired. - */ - afterPageText : 'of {0}', - /** - * @cfg {String} firstText - * The quicktip text displayed for the first page button (defaults to 'First Page'). - * Note: quick tips must be initialized for the quicktip to show. - */ - firstText : 'First Page', - /** - * @cfg {String} prevText - * The quicktip text displayed for the previous page button (defaults to 'Previous Page'). - * Note: quick tips must be initialized for the quicktip to show. - */ - prevText : 'Previous Page', - /** - * @cfg {String} nextText - * The quicktip text displayed for the next page button (defaults to 'Next Page'). - * Note: quick tips must be initialized for the quicktip to show. - */ - nextText : 'Next Page', - /** - * @cfg {String} lastText - * The quicktip text displayed for the last page button (defaults to 'Last Page'). - * Note: quick tips must be initialized for the quicktip to show. - */ - lastText : 'Last Page', - /** - * @cfg {String} refreshText - * The quicktip text displayed for the Refresh button (defaults to 'Refresh'). - * Note: quick tips must be initialized for the quicktip to show. - */ - refreshText : 'Refresh', - - /** - *

    Deprecated. paramNames should be set in the data store - * (see {@link Ext.data.Store#paramNames}).

    - *

    Object mapping of parameter names used for load calls, initially set to:

    - *
    {start: 'start', limit: 'limit'}
    - * @type Object - * @property paramNames - * @deprecated - */ - - /** - * The number of records to display per page. See also {@link #cursor}. - * @type Number - * @property pageSize - */ - - /** - * Indicator for the record position. This property might be used to get the active page - * number for example:
    
    -     * // t is reference to the paging toolbar instance
    -     * var activePage = Math.ceil((t.cursor + t.pageSize) / t.pageSize);
    -     * 
    - * @type Number - * @property cursor - */ - - initComponent : function(){ - var pagingItems = [this.first = new T.Button({ - tooltip: this.firstText, - overflowText: this.firstText, - iconCls: 'x-tbar-page-first', - disabled: true, - handler: this.moveFirst, - scope: this - }), this.prev = new T.Button({ - tooltip: this.prevText, - overflowText: this.prevText, - iconCls: 'x-tbar-page-prev', - disabled: true, - handler: this.movePrevious, - scope: this - }), '-', this.beforePageText, - this.inputItem = new Ext.form.NumberField({ - cls: 'x-tbar-page-number', - allowDecimals: false, - allowNegative: false, - enableKeyEvents: true, - selectOnFocus: true, - submitValue: false, - listeners: { - scope: this, - keydown: this.onPagingKeyDown, - blur: this.onPagingBlur - } - }), this.afterTextItem = new T.TextItem({ - text: String.format(this.afterPageText, 1) - }), '-', this.next = new T.Button({ - tooltip: this.nextText, - overflowText: this.nextText, - iconCls: 'x-tbar-page-next', - disabled: true, - handler: this.moveNext, - scope: this - }), this.last = new T.Button({ - tooltip: this.lastText, - overflowText: this.lastText, - iconCls: 'x-tbar-page-last', - disabled: true, - handler: this.moveLast, - scope: this - }), '-', this.refresh = new T.Button({ - tooltip: this.refreshText, - overflowText: this.refreshText, - iconCls: 'x-tbar-loading', - handler: this.doRefresh, - scope: this - })]; - - - var userItems = this.items || this.buttons || []; - if (this.prependButtons) { - this.items = userItems.concat(pagingItems); - }else{ - this.items = pagingItems.concat(userItems); - } - delete this.buttons; - if(this.displayInfo){ - this.items.push('->'); - this.items.push(this.displayItem = new T.TextItem({})); - } - Ext.PagingToolbar.superclass.initComponent.call(this); - this.addEvents( - /** - * @event change - * Fires after the active page has been changed. - * @param {Ext.PagingToolbar} this - * @param {Object} pageData An object that has these properties:
      - *
    • total : Number
      The total number of records in the dataset as - * returned by the server
    • - *
    • activePage : Number
      The current page number
    • - *
    • pages : Number
      The total number of pages (calculated from - * the total number of records in the dataset as returned by the server and the current {@link #pageSize})
    • - *
    - */ - 'change', - /** - * @event beforechange - * Fires just before the active page is changed. - * Return false to prevent the active page from being changed. - * @param {Ext.PagingToolbar} this - * @param {Object} params An object hash of the parameters which the PagingToolbar will send when - * loading the required page. This will contain:
      - *
    • start : Number
      The starting row number for the next page of records to - * be retrieved from the server
    • - *
    • limit : Number
      The number of records to be retrieved from the server
    • - *
    - *

    (note: the names of the start and limit properties are determined - * by the store's {@link Ext.data.Store#paramNames paramNames} property.)

    - *

    Parameters may be added as required in the event handler.

    - */ - 'beforechange' - ); - this.on('afterlayout', this.onFirstLayout, this, {single: true}); - this.cursor = 0; - this.bindStore(this.store, true); - }, - - // private - onFirstLayout : function(){ - if(this.dsLoaded){ - this.onLoad.apply(this, this.dsLoaded); - } - }, - - // private - updateInfo : function(){ - if(this.displayItem){ - var count = this.store.getCount(); - var msg = count == 0 ? - this.emptyMsg : - String.format( - this.displayMsg, - this.cursor+1, this.cursor+count, this.store.getTotalCount() - ); - this.displayItem.setText(msg); - } - }, - - // private - onLoad : function(store, r, o){ - if(!this.rendered){ - this.dsLoaded = [store, r, o]; - return; - } - var p = this.getParams(); - this.cursor = (o.params && o.params[p.start]) ? o.params[p.start] : 0; - var d = this.getPageData(), ap = d.activePage, ps = d.pages; - - this.afterTextItem.setText(String.format(this.afterPageText, d.pages)); - this.inputItem.setValue(ap); - this.first.setDisabled(ap == 1); - this.prev.setDisabled(ap == 1); - this.next.setDisabled(ap == ps); - this.last.setDisabled(ap == ps); - this.refresh.enable(); - this.updateInfo(); - this.fireEvent('change', this, d); - }, - - // private - getPageData : function(){ - var total = this.store.getTotalCount(); - return { - total : total, - activePage : Math.ceil((this.cursor+this.pageSize)/this.pageSize), - pages : total < this.pageSize ? 1 : Math.ceil(total/this.pageSize) - }; - }, - - /** - * Change the active page - * @param {Integer} page The page to display - */ - changePage : function(page){ - this.doLoad(((page-1) * this.pageSize).constrain(0, this.store.getTotalCount())); - }, - - // private - onLoadError : function(){ - if(!this.rendered){ - return; - } - this.refresh.enable(); - }, - - // private - readPage : function(d){ - var v = this.inputItem.getValue(), pageNum; - if (!v || isNaN(pageNum = parseInt(v, 10))) { - this.inputItem.setValue(d.activePage); - return false; - } - return pageNum; - }, - - onPagingFocus : function(){ - this.inputItem.select(); - }, - - //private - onPagingBlur : function(e){ - this.inputItem.setValue(this.getPageData().activePage); - }, - - // private - onPagingKeyDown : function(field, e){ - var k = e.getKey(), d = this.getPageData(), pageNum; - if (k == e.RETURN) { - e.stopEvent(); - pageNum = this.readPage(d); - if(pageNum !== false){ - pageNum = Math.min(Math.max(1, pageNum), d.pages) - 1; - this.doLoad(pageNum * this.pageSize); - } - }else if (k == e.HOME || k == e.END){ - e.stopEvent(); - pageNum = k == e.HOME ? 1 : d.pages; - field.setValue(pageNum); - }else if (k == e.UP || k == e.PAGEUP || k == e.DOWN || k == e.PAGEDOWN){ - e.stopEvent(); - if((pageNum = this.readPage(d))){ - var increment = e.shiftKey ? 10 : 1; - if(k == e.DOWN || k == e.PAGEDOWN){ - increment *= -1; - } - pageNum += increment; - if(pageNum >= 1 & pageNum <= d.pages){ - field.setValue(pageNum); - } - } - } - }, - - // private - getParams : function(){ - //retain backwards compat, allow params on the toolbar itself, if they exist. - return this.paramNames || this.store.paramNames; - }, - - // private - beforeLoad : function(){ - if(this.rendered && this.refresh){ - this.refresh.disable(); - } - }, - - // private - doLoad : function(start){ - var o = {}, pn = this.getParams(); - o[pn.start] = start; - o[pn.limit] = this.pageSize; - if(this.fireEvent('beforechange', this, o) !== false){ - this.store.load({params:o}); - } - }, - - /** - * Move to the first page, has the same effect as clicking the 'first' button. - */ - moveFirst : function(){ - this.doLoad(0); - }, - - /** - * Move to the previous page, has the same effect as clicking the 'previous' button. - */ - movePrevious : function(){ - this.doLoad(Math.max(0, this.cursor-this.pageSize)); - }, - - /** - * Move to the next page, has the same effect as clicking the 'next' button. - */ - moveNext : function(){ - this.doLoad(this.cursor+this.pageSize); - }, - - /** - * Move to the last page, has the same effect as clicking the 'last' button. - */ - moveLast : function(){ - var total = this.store.getTotalCount(), - extra = total % this.pageSize; - - this.doLoad(extra ? (total - extra) : total - this.pageSize); - }, - - /** - * Refresh the current page, has the same effect as clicking the 'refresh' button. - */ - doRefresh : function(){ - this.doLoad(this.cursor); - }, - - /** - * Binds the paging toolbar to the specified {@link Ext.data.Store} - * @param {Store} store The store to bind to this toolbar - * @param {Boolean} initial (Optional) true to not remove listeners - */ - bindStore : function(store, initial){ - var doLoad; - if(!initial && this.store){ - if(store !== this.store && this.store.autoDestroy){ - this.store.destroy(); - }else{ - this.store.un('beforeload', this.beforeLoad, this); - this.store.un('load', this.onLoad, this); - this.store.un('exception', this.onLoadError, this); - } - if(!store){ - this.store = null; - } - } - if(store){ - store = Ext.StoreMgr.lookup(store); - store.on({ - scope: this, - beforeload: this.beforeLoad, - load: this.onLoad, - exception: this.onLoadError - }); - doLoad = true; - } - this.store = store; - if(doLoad){ - this.onLoad(store, null, {}); - } - }, - - /** - * Unbinds the paging toolbar from the specified {@link Ext.data.Store} (deprecated) - * @param {Ext.data.Store} store The data store to unbind - */ - unbind : function(store){ - this.bindStore(null); - }, - - /** - * Binds the paging toolbar to the specified {@link Ext.data.Store} (deprecated) - * @param {Ext.data.Store} store The data store to bind - */ - bind : function(store){ - this.bindStore(store); - }, - - // private - onDestroy : function(){ - this.bindStore(null); - Ext.PagingToolbar.superclass.onDestroy.call(this); - } -}); - -})(); -Ext.reg('paging', Ext.PagingToolbar);/** - * @class Ext.History - * @extends Ext.util.Observable - * History management component that allows you to register arbitrary tokens that signify application - * history state on navigation actions. You can then handle the history {@link #change} event in order - * to reset your application UI to the appropriate state when the user navigates forward or backward through - * the browser history stack. - * @singleton - */ -Ext.History = (function () { - var iframe, hiddenField; - var ready = false; - var currentToken; - - function getHash() { - var href = location.href, i = href.indexOf("#"), - hash = i >= 0 ? href.substr(i + 1) : null; - - if (Ext.isGecko) { - hash = decodeURIComponent(hash); - } - return hash; - } - - function doSave() { - hiddenField.value = currentToken; - } - - function handleStateChange(token) { - currentToken = token; - Ext.History.fireEvent('change', token); - } - - function updateIFrame (token) { - var html = ['
    ',Ext.util.Format.htmlEncode(token),'
    '].join(''); - try { - var doc = iframe.contentWindow.document; - doc.open(); - doc.write(html); - doc.close(); - return true; - } catch (e) { - return false; - } - } - - function checkIFrame() { - if (!iframe.contentWindow || !iframe.contentWindow.document) { - setTimeout(checkIFrame, 10); - return; - } - - var doc = iframe.contentWindow.document; - var elem = doc.getElementById("state"); - var token = elem ? elem.innerText : null; - - var hash = getHash(); - - setInterval(function () { - - doc = iframe.contentWindow.document; - elem = doc.getElementById("state"); - - var newtoken = elem ? elem.innerText : null; - - var newHash = getHash(); - - if (newtoken !== token) { - token = newtoken; - handleStateChange(token); - location.hash = token; - hash = token; - doSave(); - } else if (newHash !== hash) { - hash = newHash; - updateIFrame(newHash); - } - - }, 50); - - ready = true; - - Ext.History.fireEvent('ready', Ext.History); - } - - function startUp() { - currentToken = hiddenField.value ? hiddenField.value : getHash(); - - if (Ext.isIE) { - checkIFrame(); - } else { - var hash = getHash(); - setInterval(function () { - var newHash = getHash(); - if (newHash !== hash) { - hash = newHash; - handleStateChange(hash); - doSave(); - } - }, 50); - ready = true; - Ext.History.fireEvent('ready', Ext.History); - } - } - - return { - /** - * The id of the hidden field required for storing the current history token. - * @type String - * @property - */ - fieldId: 'x-history-field', - /** - * The id of the iframe required by IE to manage the history stack. - * @type String - * @property - */ - iframeId: 'x-history-frame', - - events:{}, - - /** - * Initialize the global History instance. - * @param {Boolean} onReady (optional) A callback function that will be called once the history - * component is fully initialized. - * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser window. - */ - init: function (onReady, scope) { - if(ready) { - Ext.callback(onReady, scope, [this]); - return; - } - if(!Ext.isReady){ - Ext.onReady(function(){ - Ext.History.init(onReady, scope); - }); - return; - } - hiddenField = Ext.getDom(Ext.History.fieldId); - if (Ext.isIE) { - iframe = Ext.getDom(Ext.History.iframeId); - } - this.addEvents( - /** - * @event ready - * Fires when the Ext.History singleton has been initialized and is ready for use. - * @param {Ext.History} The Ext.History singleton. - */ - 'ready', - /** - * @event change - * Fires when navigation back or forwards within the local page's history occurs. - * @param {String} token An identifier associated with the page state at that point in its history. - */ - 'change' - ); - if(onReady){ - this.on('ready', onReady, scope, {single:true}); - } - startUp(); - }, - - /** - * Add a new token to the history stack. This can be any arbitrary value, although it would - * commonly be the concatenation of a component id and another id marking the specifc history - * state of that component. Example usage: - *
    
    -// Handle tab changes on a TabPanel
    -tabPanel.on('tabchange', function(tabPanel, tab){
    -    Ext.History.add(tabPanel.id + ':' + tab.id);
    -});
    -
    - * @param {String} token The value that defines a particular application-specific history state - * @param {Boolean} preventDuplicates When true, if the passed token matches the current token - * it will not save a new history step. Set to false if the same state can be saved more than once - * at the same history stack location (defaults to true). - */ - add: function (token, preventDup) { - if(preventDup !== false){ - if(this.getToken() == token){ - return true; - } - } - if (Ext.isIE) { - return updateIFrame(token); - } else { - location.hash = token; - return true; - } - }, - - /** - * Programmatically steps back one step in browser history (equivalent to the user pressing the Back button). - */ - back: function(){ - history.go(-1); - }, - - /** - * Programmatically steps forward one step in browser history (equivalent to the user pressing the Forward button). - */ - forward: function(){ - history.go(1); - }, - - /** - * Retrieves the currently-active history token. - * @return {String} The token - */ - getToken: function() { - return ready ? currentToken : getHash(); - } - }; -})(); -Ext.apply(Ext.History, new Ext.util.Observable());/** - * @class Ext.Tip - * @extends Ext.Panel - * @xtype tip - * This is the base class for {@link Ext.QuickTip} and {@link Ext.Tooltip} that provides the basic layout and - * positioning that all tip-based classes require. This class can be used directly for simple, statically-positioned - * tips that are displayed programmatically, or it can be extended to provide custom tip implementations. - * @constructor - * Create a new Tip - * @param {Object} config The configuration options - */ -Ext.Tip = Ext.extend(Ext.Panel, { - /** - * @cfg {Boolean} closable True to render a close tool button into the tooltip header (defaults to false). - */ - /** - * @cfg {Number} width - * Width in pixels of the tip (defaults to auto). Width will be ignored if it exceeds the bounds of - * {@link #minWidth} or {@link #maxWidth}. The maximum supported value is 500. - */ - /** - * @cfg {Number} minWidth The minimum width of the tip in pixels (defaults to 40). - */ - minWidth : 40, - /** - * @cfg {Number} maxWidth The maximum width of the tip in pixels (defaults to 300). The maximum supported value is 500. - */ - maxWidth : 300, - /** - * @cfg {Boolean/String} shadow True or "sides" for the default effect, "frame" for 4-way shadow, and "drop" - * for bottom-right shadow (defaults to "sides"). - */ - shadow : "sides", - /** - * @cfg {String} defaultAlign Experimental. The default {@link Ext.Element#alignTo} anchor position value - * for this tip relative to its element of origin (defaults to "tl-bl?"). - */ - defaultAlign : "tl-bl?", - autoRender: true, - quickShowInterval : 250, - - // private panel overrides - frame:true, - hidden:true, - baseCls: 'x-tip', - floating:{shadow:true,shim:true,useDisplay:true,constrain:false}, - autoHeight:true, - - closeAction: 'hide', - - // private - initComponent : function(){ - Ext.Tip.superclass.initComponent.call(this); - if(this.closable && !this.title){ - this.elements += ',header'; - } - }, - - // private - afterRender : function(){ - Ext.Tip.superclass.afterRender.call(this); - if(this.closable){ - this.addTool({ - id: 'close', - handler: this[this.closeAction], - scope: this - }); - } - }, - - /** - * Shows this tip at the specified XY position. Example usage: - *
    
    -// Show the tip at x:50 and y:100
    -tip.showAt([50,100]);
    -
    - * @param {Array} xy An array containing the x and y coordinates - */ - showAt : function(xy){ - Ext.Tip.superclass.show.call(this); - if(this.measureWidth !== false && (!this.initialConfig || typeof this.initialConfig.width != 'number')){ - this.doAutoWidth(); - } - if(this.constrainPosition){ - xy = this.el.adjustForConstraints(xy); - } - this.setPagePosition(xy[0], xy[1]); - }, - - // protected - doAutoWidth : function(adjust){ - adjust = adjust || 0; - var bw = this.body.getTextWidth(); - if(this.title){ - bw = Math.max(bw, this.header.child('span').getTextWidth(this.title)); - } - bw += this.getFrameWidth() + (this.closable ? 20 : 0) + this.body.getPadding("lr") + adjust; - this.setWidth(bw.constrain(this.minWidth, this.maxWidth)); - - // IE7 repaint bug on initial show - if(Ext.isIE7 && !this.repainted){ - this.el.repaint(); - this.repainted = true; - } - }, - - /** - * Experimental. Shows this tip at a position relative to another element using a standard {@link Ext.Element#alignTo} - * anchor position value. Example usage: - *
    
    -// Show the tip at the default position ('tl-br?')
    -tip.showBy('my-el');
    -
    -// Show the tip's top-left corner anchored to the element's top-right corner
    -tip.showBy('my-el', 'tl-tr');
    -
    - * @param {Mixed} el An HTMLElement, Ext.Element or string id of the target element to align to - * @param {String} position (optional) A valid {@link Ext.Element#alignTo} anchor position (defaults to 'tl-br?' or - * {@link #defaultAlign} if specified). - */ - showBy : function(el, pos){ - if(!this.rendered){ - this.render(Ext.getBody()); - } - this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign)); - }, - - initDraggable : function(){ - this.dd = new Ext.Tip.DD(this, typeof this.draggable == 'boolean' ? null : this.draggable); - this.header.addClass('x-tip-draggable'); - } -}); - -Ext.reg('tip', Ext.Tip); - -// private - custom Tip DD implementation -Ext.Tip.DD = function(tip, config){ - Ext.apply(this, config); - this.tip = tip; - Ext.Tip.DD.superclass.constructor.call(this, tip.el.id, 'WindowDD-'+tip.id); - this.setHandleElId(tip.header.id); - this.scroll = false; -}; - -Ext.extend(Ext.Tip.DD, Ext.dd.DD, { - moveOnly:true, - scroll:false, - headerOffsets:[100, 25], - startDrag : function(){ - this.tip.el.disableShadow(); - }, - endDrag : function(e){ - this.tip.el.enableShadow(true); - } -});/** - * @class Ext.ToolTip - * @extends Ext.Tip - * A standard tooltip implementation for providing additional information when hovering over a target element. - * @xtype tooltip - * @constructor - * Create a new Tooltip - * @param {Object} config The configuration options - */ -Ext.ToolTip = Ext.extend(Ext.Tip, { - /** - * When a Tooltip is configured with the {@link #delegate} - * option to cause selected child elements of the {@link #target} - * Element to each trigger a seperate show event, this property is set to - * the DOM element which triggered the show. - * @type DOMElement - * @property triggerElement - */ - /** - * @cfg {Mixed} target The target HTMLElement, Ext.Element or id to monitor - * for mouseover events to trigger showing this ToolTip. - */ - /** - * @cfg {Boolean} autoHide True to automatically hide the tooltip after the - * mouse exits the target element or after the {@link #dismissDelay} - * has expired if set (defaults to true). If {@link closable} = true - * a close tool button will be rendered into the tooltip header. - */ - /** - * @cfg {Number} showDelay Delay in milliseconds before the tooltip displays - * after the mouse enters the target element (defaults to 500) - */ - showDelay : 500, - /** - * @cfg {Number} hideDelay Delay in milliseconds after the mouse exits the - * target element but before the tooltip actually hides (defaults to 200). - * Set to 0 for the tooltip to hide immediately. - */ - hideDelay : 200, - /** - * @cfg {Number} dismissDelay Delay in milliseconds before the tooltip - * automatically hides (defaults to 5000). To disable automatic hiding, set - * dismissDelay = 0. - */ - dismissDelay : 5000, - /** - * @cfg {Array} mouseOffset An XY offset from the mouse position where the - * tooltip should be shown (defaults to [15,18]). - */ - /** - * @cfg {Boolean} trackMouse True to have the tooltip follow the mouse as it - * moves over the target element (defaults to false). - */ - trackMouse : false, - /** - * @cfg {Boolean} anchorToTarget True to anchor the tooltip to the target - * element, false to anchor it relative to the mouse coordinates (defaults - * to true). When anchorToTarget is true, use - * {@link #defaultAlign} to control tooltip alignment to the - * target element. When anchorToTarget is false, use - * {@link #anchorPosition} instead to control alignment. - */ - anchorToTarget : true, - /** - * @cfg {Number} anchorOffset A numeric pixel value used to offset the - * default position of the anchor arrow (defaults to 0). When the anchor - * position is on the top or bottom of the tooltip, anchorOffset - * will be used as a horizontal offset. Likewise, when the anchor position - * is on the left or right side, anchorOffset will be used as - * a vertical offset. - */ - anchorOffset : 0, - /** - * @cfg {String} delegate

    Optional. A {@link Ext.DomQuery DomQuery} - * selector which allows selection of individual elements within the - * {@link #target} element to trigger showing and hiding the - * ToolTip as the mouse moves within the target.

    - *

    When specified, the child element of the target which caused a show - * event is placed into the {@link #triggerElement} property - * before the ToolTip is shown.

    - *

    This may be useful when a Component has regular, repeating elements - * in it, each of which need a Tooltip which contains information specific - * to that element. For example:

    
    -var myGrid = new Ext.grid.gridPanel(gridConfig);
    -myGrid.on('render', function(grid) {
    -    var store = grid.getStore();  // Capture the Store.
    -    var view = grid.getView();    // Capture the GridView.
    -    myGrid.tip = new Ext.ToolTip({
    -        target: view.mainBody,    // The overall target element.
    -        delegate: '.x-grid3-row', // Each grid row causes its own seperate show and hide.
    -        trackMouse: true,         // Moving within the row should not hide the tip.
    -        renderTo: document.body,  // Render immediately so that tip.body can be
    -                                  //  referenced prior to the first show.
    -        listeners: {              // Change content dynamically depending on which element
    -                                  //  triggered the show.
    -            beforeshow: function updateTipBody(tip) {
    -                var rowIndex = view.findRowIndex(tip.triggerElement);
    -                tip.body.dom.innerHTML = 'Over Record ID ' + store.getAt(rowIndex).id;
    -            }
    -        }
    -    });
    -});
    -     *
    - */ - - // private - targetCounter : 0, - - constrainPosition : false, - - // private - initComponent : function(){ - Ext.ToolTip.superclass.initComponent.call(this); - this.lastActive = new Date(); - this.initTarget(this.target); - this.origAnchor = this.anchor; - }, - - // private - onRender : function(ct, position){ - Ext.ToolTip.superclass.onRender.call(this, ct, position); - this.anchorCls = 'x-tip-anchor-' + this.getAnchorPosition(); - this.anchorEl = this.el.createChild({ - cls: 'x-tip-anchor ' + this.anchorCls - }); - }, - - // private - afterRender : function(){ - Ext.ToolTip.superclass.afterRender.call(this); - this.anchorEl.setStyle('z-index', this.el.getZIndex() + 1).setVisibilityMode(Ext.Element.DISPLAY); - }, - - /** - * Binds this ToolTip to the specified element. The tooltip will be displayed when the mouse moves over the element. - * @param {Mixed} t The Element, HtmlElement, or ID of an element to bind to - */ - initTarget : function(target){ - var t; - if((t = Ext.get(target))){ - if(this.target){ - var tg = Ext.get(this.target); - this.mun(tg, 'mouseover', this.onTargetOver, this); - this.mun(tg, 'mouseout', this.onTargetOut, this); - this.mun(tg, 'mousemove', this.onMouseMove, this); - } - this.mon(t, { - mouseover: this.onTargetOver, - mouseout: this.onTargetOut, - mousemove: this.onMouseMove, - scope: this - }); - this.target = t; - } - if(this.anchor){ - this.anchorTarget = this.target; - } - }, - - // private - onMouseMove : function(e){ - var t = this.delegate ? e.getTarget(this.delegate) : this.triggerElement = true; - if (t) { - this.targetXY = e.getXY(); - if (t === this.triggerElement) { - if(!this.hidden && this.trackMouse){ - this.setPagePosition(this.getTargetXY()); - } - } else { - this.hide(); - this.lastActive = new Date(0); - this.onTargetOver(e); - } - } else if (!this.closable && this.isVisible()) { - this.hide(); - } - }, - - // private - getTargetXY : function(){ - if(this.delegate){ - this.anchorTarget = this.triggerElement; - } - if(this.anchor){ - this.targetCounter++; - var offsets = this.getOffsets(), - xy = (this.anchorToTarget && !this.trackMouse) ? this.el.getAlignToXY(this.anchorTarget, this.getAnchorAlign()) : this.targetXY, - dw = Ext.lib.Dom.getViewWidth() - 5, - dh = Ext.lib.Dom.getViewHeight() - 5, - de = document.documentElement, - bd = document.body, - scrollX = (de.scrollLeft || bd.scrollLeft || 0) + 5, - scrollY = (de.scrollTop || bd.scrollTop || 0) + 5, - axy = [xy[0] + offsets[0], xy[1] + offsets[1]], - sz = this.getSize(); - - this.anchorEl.removeClass(this.anchorCls); - - if(this.targetCounter < 2){ - if(axy[0] < scrollX){ - if(this.anchorToTarget){ - this.defaultAlign = 'l-r'; - if(this.mouseOffset){this.mouseOffset[0] *= -1;} - } - this.anchor = 'left'; - return this.getTargetXY(); - } - if(axy[0]+sz.width > dw){ - if(this.anchorToTarget){ - this.defaultAlign = 'r-l'; - if(this.mouseOffset){this.mouseOffset[0] *= -1;} - } - this.anchor = 'right'; - return this.getTargetXY(); - } - if(axy[1] < scrollY){ - if(this.anchorToTarget){ - this.defaultAlign = 't-b'; - if(this.mouseOffset){this.mouseOffset[1] *= -1;} - } - this.anchor = 'top'; - return this.getTargetXY(); - } - if(axy[1]+sz.height > dh){ - if(this.anchorToTarget){ - this.defaultAlign = 'b-t'; - if(this.mouseOffset){this.mouseOffset[1] *= -1;} - } - this.anchor = 'bottom'; - return this.getTargetXY(); - } - } - - this.anchorCls = 'x-tip-anchor-'+this.getAnchorPosition(); - this.anchorEl.addClass(this.anchorCls); - this.targetCounter = 0; - return axy; - }else{ - var mouseOffset = this.getMouseOffset(); - return [this.targetXY[0]+mouseOffset[0], this.targetXY[1]+mouseOffset[1]]; - } - }, - - getMouseOffset : function(){ - var offset = this.anchor ? [0,0] : [15,18]; - if(this.mouseOffset){ - offset[0] += this.mouseOffset[0]; - offset[1] += this.mouseOffset[1]; - } - return offset; - }, - - // private - getAnchorPosition : function(){ - if(this.anchor){ - this.tipAnchor = this.anchor.charAt(0); - }else{ - var m = this.defaultAlign.match(/^([a-z]+)-([a-z]+)(\?)?$/); - if(!m){ - throw 'AnchorTip.defaultAlign is invalid'; - } - this.tipAnchor = m[1].charAt(0); - } - - switch(this.tipAnchor){ - case 't': return 'top'; - case 'b': return 'bottom'; - case 'r': return 'right'; - } - return 'left'; - }, - - // private - getAnchorAlign : function(){ - switch(this.anchor){ - case 'top' : return 'tl-bl'; - case 'left' : return 'tl-tr'; - case 'right': return 'tr-tl'; - default : return 'bl-tl'; - } - }, - - // private - getOffsets : function(){ - var offsets, - ap = this.getAnchorPosition().charAt(0); - if(this.anchorToTarget && !this.trackMouse){ - switch(ap){ - case 't': - offsets = [0, 9]; - break; - case 'b': - offsets = [0, -13]; - break; - case 'r': - offsets = [-13, 0]; - break; - default: - offsets = [9, 0]; - break; - } - }else{ - switch(ap){ - case 't': - offsets = [-15-this.anchorOffset, 30]; - break; - case 'b': - offsets = [-19-this.anchorOffset, -13-this.el.dom.offsetHeight]; - break; - case 'r': - offsets = [-15-this.el.dom.offsetWidth, -13-this.anchorOffset]; - break; - default: - offsets = [25, -13-this.anchorOffset]; - break; - } - } - var mouseOffset = this.getMouseOffset(); - offsets[0] += mouseOffset[0]; - offsets[1] += mouseOffset[1]; - - return offsets; - }, - - // private - onTargetOver : function(e){ - if(this.disabled || e.within(this.target.dom, true)){ - return; - } - var t = e.getTarget(this.delegate); - if (t) { - this.triggerElement = t; - this.clearTimer('hide'); - this.targetXY = e.getXY(); - this.delayShow(); - } - }, - - // private - delayShow : function(){ - if(this.hidden && !this.showTimer){ - if(this.lastActive.getElapsed() < this.quickShowInterval){ - this.show(); - }else{ - this.showTimer = this.show.defer(this.showDelay, this); - } - }else if(!this.hidden && this.autoHide !== false){ - this.show(); - } - }, - - // private - onTargetOut : function(e){ - if(this.disabled || e.within(this.target.dom, true)){ - return; - } - this.clearTimer('show'); - if(this.autoHide !== false){ - this.delayHide(); - } - }, - - // private - delayHide : function(){ - if(!this.hidden && !this.hideTimer){ - this.hideTimer = this.hide.defer(this.hideDelay, this); - } - }, - - /** - * Hides this tooltip if visible. - */ - hide: function(){ - this.clearTimer('dismiss'); - this.lastActive = new Date(); - if(this.anchorEl){ - this.anchorEl.hide(); - } - Ext.ToolTip.superclass.hide.call(this); - delete this.triggerElement; - }, - - /** - * Shows this tooltip at the current event target XY position. - */ - show : function(){ - if(this.anchor){ - // pre-show it off screen so that the el will have dimensions - // for positioning calcs when getting xy next - this.showAt([-1000,-1000]); - this.origConstrainPosition = this.constrainPosition; - this.constrainPosition = false; - this.anchor = this.origAnchor; - } - this.showAt(this.getTargetXY()); - - if(this.anchor){ - this.anchorEl.show(); - this.syncAnchor(); - this.constrainPosition = this.origConstrainPosition; - }else{ - this.anchorEl.hide(); - } - }, - - // inherit docs - showAt : function(xy){ - this.lastActive = new Date(); - this.clearTimers(); - Ext.ToolTip.superclass.showAt.call(this, xy); - if(this.dismissDelay && this.autoHide !== false){ - this.dismissTimer = this.hide.defer(this.dismissDelay, this); - } - if(this.anchor && !this.anchorEl.isVisible()){ - this.syncAnchor(); - this.anchorEl.show(); - }else{ - this.anchorEl.hide(); - } - }, - - // private - syncAnchor : function(){ - var anchorPos, targetPos, offset; - switch(this.tipAnchor.charAt(0)){ - case 't': - anchorPos = 'b'; - targetPos = 'tl'; - offset = [20+this.anchorOffset, 2]; - break; - case 'r': - anchorPos = 'l'; - targetPos = 'tr'; - offset = [-2, 11+this.anchorOffset]; - break; - case 'b': - anchorPos = 't'; - targetPos = 'bl'; - offset = [20+this.anchorOffset, -2]; - break; - default: - anchorPos = 'r'; - targetPos = 'tl'; - offset = [2, 11+this.anchorOffset]; - break; - } - this.anchorEl.alignTo(this.el, anchorPos+'-'+targetPos, offset); - }, - - // private - setPagePosition : function(x, y){ - Ext.ToolTip.superclass.setPagePosition.call(this, x, y); - if(this.anchor){ - this.syncAnchor(); - } - }, - - // private - clearTimer : function(name){ - name = name + 'Timer'; - clearTimeout(this[name]); - delete this[name]; - }, - - // private - clearTimers : function(){ - this.clearTimer('show'); - this.clearTimer('dismiss'); - this.clearTimer('hide'); - }, - - // private - onShow : function(){ - Ext.ToolTip.superclass.onShow.call(this); - Ext.getDoc().on('mousedown', this.onDocMouseDown, this); - }, - - // private - onHide : function(){ - Ext.ToolTip.superclass.onHide.call(this); - Ext.getDoc().un('mousedown', this.onDocMouseDown, this); - }, - - // private - onDocMouseDown : function(e){ - if(this.autoHide !== true && !this.closable && !e.within(this.el.dom)){ - this.disable(); - this.doEnable.defer(100, this); - } - }, - - // private - doEnable : function(){ - if(!this.isDestroyed){ - this.enable(); - } - }, - - // private - onDisable : function(){ - this.clearTimers(); - this.hide(); - }, - - // private - adjustPosition : function(x, y){ - if(this.constrainPosition){ - var ay = this.targetXY[1], h = this.getSize().height; - if(y <= ay && (y+h) >= ay){ - y = ay-h-5; - } - } - return {x : x, y: y}; - }, - - beforeDestroy : function(){ - this.clearTimers(); - Ext.destroy(this.anchorEl); - delete this.anchorEl; - delete this.target; - delete this.anchorTarget; - delete this.triggerElement; - Ext.ToolTip.superclass.beforeDestroy.call(this); - }, - - // private - onDestroy : function(){ - Ext.getDoc().un('mousedown', this.onDocMouseDown, this); - Ext.ToolTip.superclass.onDestroy.call(this); - } -}); - -Ext.reg('tooltip', Ext.ToolTip);/** - * @class Ext.QuickTip - * @extends Ext.ToolTip - * @xtype quicktip - * A specialized tooltip class for tooltips that can be specified in markup and automatically managed by the global - * {@link Ext.QuickTips} instance. See the QuickTips class header for additional usage details and examples. - * @constructor - * Create a new Tip - * @param {Object} config The configuration options - */ -Ext.QuickTip = Ext.extend(Ext.ToolTip, { - /** - * @cfg {Mixed} target The target HTMLElement, Ext.Element or id to associate with this quicktip (defaults to the document). - */ - /** - * @cfg {Boolean} interceptTitles True to automatically use the element's DOM title value if available (defaults to false). - */ - interceptTitles : false, - - // private - tagConfig : { - namespace : "ext", - attribute : "qtip", - width : "qwidth", - target : "target", - title : "qtitle", - hide : "hide", - cls : "qclass", - align : "qalign", - anchor : "anchor" - }, - - // private - initComponent : function(){ - this.target = this.target || Ext.getDoc(); - this.targets = this.targets || {}; - Ext.QuickTip.superclass.initComponent.call(this); - }, - - /** - * Configures a new quick tip instance and assigns it to a target element. The following config values are - * supported (for example usage, see the {@link Ext.QuickTips} class header): - *
      - *
    • autoHide
    • - *
    • cls
    • - *
    • dismissDelay (overrides the singleton value)
    • - *
    • target (required)
    • - *
    • text (required)
    • - *
    • title
    • - *
    • width
    - * @param {Object} config The config object - */ - register : function(config){ - var cs = Ext.isArray(config) ? config : arguments; - for(var i = 0, len = cs.length; i < len; i++){ - var c = cs[i]; - var target = c.target; - if(target){ - if(Ext.isArray(target)){ - for(var j = 0, jlen = target.length; j < jlen; j++){ - this.targets[Ext.id(target[j])] = c; - } - } else{ - this.targets[Ext.id(target)] = c; - } - } - } - }, - - /** - * Removes this quick tip from its element and destroys it. - * @param {String/HTMLElement/Element} el The element from which the quick tip is to be removed. - */ - unregister : function(el){ - delete this.targets[Ext.id(el)]; - }, - - /** - * Hides a visible tip or cancels an impending show for a particular element. - * @param {String/HTMLElement/Element} el The element that is the target of the tip. - */ - cancelShow: function(el){ - var at = this.activeTarget; - el = Ext.get(el).dom; - if(this.isVisible()){ - if(at && at.el == el){ - this.hide(); - } - }else if(at && at.el == el){ - this.clearTimer('show'); - } - }, - - getTipCfg: function(e) { - var t = e.getTarget(), - ttp, - cfg; - if(this.interceptTitles && t.title && Ext.isString(t.title)){ - ttp = t.title; - t.qtip = ttp; - t.removeAttribute("title"); - e.preventDefault(); - }else{ - cfg = this.tagConfig; - ttp = t.qtip || Ext.fly(t).getAttribute(cfg.attribute, cfg.namespace); - } - return ttp; - }, - - // private - onTargetOver : function(e){ - if(this.disabled){ - return; - } - this.targetXY = e.getXY(); - var t = e.getTarget(); - if(!t || t.nodeType !== 1 || t == document || t == document.body){ - return; - } - if(this.activeTarget && ((t == this.activeTarget.el) || Ext.fly(this.activeTarget.el).contains(t))){ - this.clearTimer('hide'); - this.show(); - return; - } - if(t && this.targets[t.id]){ - this.activeTarget = this.targets[t.id]; - this.activeTarget.el = t; - this.anchor = this.activeTarget.anchor; - if(this.anchor){ - this.anchorTarget = t; - } - this.delayShow(); - return; - } - var ttp, et = Ext.fly(t), cfg = this.tagConfig, ns = cfg.namespace; - if(ttp = this.getTipCfg(e)){ - var autoHide = et.getAttribute(cfg.hide, ns); - this.activeTarget = { - el: t, - text: ttp, - width: et.getAttribute(cfg.width, ns), - autoHide: autoHide != "user" && autoHide !== 'false', - title: et.getAttribute(cfg.title, ns), - cls: et.getAttribute(cfg.cls, ns), - align: et.getAttribute(cfg.align, ns) - - }; - this.anchor = et.getAttribute(cfg.anchor, ns); - if(this.anchor){ - this.anchorTarget = t; - } - this.delayShow(); - } - }, - - // private - onTargetOut : function(e){ - - // If moving within the current target, and it does not have a new tip, ignore the mouseout - if (this.activeTarget && e.within(this.activeTarget.el) && !this.getTipCfg(e)) { - return; - } - - this.clearTimer('show'); - if(this.autoHide !== false){ - this.delayHide(); - } - }, - - // inherit docs - showAt : function(xy){ - var t = this.activeTarget; - if(t){ - if(!this.rendered){ - this.render(Ext.getBody()); - this.activeTarget = t; - } - if(t.width){ - this.setWidth(t.width); - this.body.setWidth(this.adjustBodyWidth(t.width - this.getFrameWidth())); - this.measureWidth = false; - } else{ - this.measureWidth = true; - } - this.setTitle(t.title || ''); - this.body.update(t.text); - this.autoHide = t.autoHide; - this.dismissDelay = t.dismissDelay || this.dismissDelay; - if(this.lastCls){ - this.el.removeClass(this.lastCls); - delete this.lastCls; - } - if(t.cls){ - this.el.addClass(t.cls); - this.lastCls = t.cls; - } - if(this.anchor){ - this.constrainPosition = false; - }else if(t.align){ // TODO: this doesn't seem to work consistently - xy = this.el.getAlignToXY(t.el, t.align); - this.constrainPosition = false; - }else{ - this.constrainPosition = true; - } - } - Ext.QuickTip.superclass.showAt.call(this, xy); - }, - - // inherit docs - hide: function(){ - delete this.activeTarget; - Ext.QuickTip.superclass.hide.call(this); - } -}); -Ext.reg('quicktip', Ext.QuickTip);/** - * @class Ext.QuickTips - *

    Provides attractive and customizable tooltips for any element. The QuickTips - * singleton is used to configure and manage tooltips globally for multiple elements - * in a generic manner. To create individual tooltips with maximum customizability, - * you should consider either {@link Ext.Tip} or {@link Ext.ToolTip}.

    - *

    Quicktips can be configured via tag attributes directly in markup, or by - * registering quick tips programmatically via the {@link #register} method.

    - *

    The singleton's instance of {@link Ext.QuickTip} is available via - * {@link #getQuickTip}, and supports all the methods, and all the all the - * configuration properties of Ext.QuickTip. These settings will apply to all - * tooltips shown by the singleton.

    - *

    Below is the summary of the configuration properties which can be used. - * For detailed descriptions see the config options for the {@link Ext.QuickTip QuickTip} class

    - *

    QuickTips singleton configs (all are optional)

    - *
    • dismissDelay
    • - *
    • hideDelay
    • - *
    • maxWidth
    • - *
    • minWidth
    • - *
    • showDelay
    • - *
    • trackMouse
    - *

    Target element configs (optional unless otherwise noted)

    - *
    • autoHide
    • - *
    • cls
    • - *
    • dismissDelay (overrides singleton value)
    • - *
    • target (required)
    • - *
    • text (required)
    • - *
    • title
    • - *
    • width
    - *

    Here is an example showing how some of these config options could be used:

    - *
    
    -// Init the singleton.  Any tag-based quick tips will start working.
    -Ext.QuickTips.init();
    -
    -// Apply a set of config properties to the singleton
    -Ext.apply(Ext.QuickTips.getQuickTip(), {
    -    maxWidth: 200,
    -    minWidth: 100,
    -    showDelay: 50,      // Show 50ms after entering target
    -    trackMouse: true
    -});
    -
    -// Manually register a quick tip for a specific element
    -Ext.QuickTips.register({
    -    target: 'my-div',
    -    title: 'My Tooltip',
    -    text: 'This tooltip was added in code',
    -    width: 100,
    -    dismissDelay: 10000 // Hide after 10 seconds hover
    -});
    -
    - *

    To register a quick tip in markup, you simply add one or more of the valid QuickTip attributes prefixed with - * the ext: namespace. The HTML element itself is automatically set as the quick tip target. Here is the summary - * of supported attributes (optional unless otherwise noted):

    - *
    • hide: Specifying "user" is equivalent to setting autoHide = false. Any other value will be the - * same as autoHide = true.
    • - *
    • qclass: A CSS class to be applied to the quick tip (equivalent to the 'cls' target element config).
    • - *
    • qtip (required): The quick tip text (equivalent to the 'text' target element config).
    • - *
    • qtitle: The quick tip title (equivalent to the 'title' target element config).
    • - *
    • qwidth: The quick tip width (equivalent to the 'width' target element config).
    - *

    Here is an example of configuring an HTML element to display a tooltip from markup:

    - *
    
    -// Add a quick tip to an HTML button
    -<input type="button" value="OK" ext:qtitle="OK Button" ext:qwidth="100"
    -     ext:qtip="This is a quick tip from markup!"></input>
    -
    - * @singleton - */ -Ext.QuickTips = function(){ - var tip, - disabled = false; - - return { - /** - * Initialize the global QuickTips instance and prepare any quick tips. - * @param {Boolean} autoRender True to render the QuickTips container immediately to preload images. (Defaults to true) - */ - init : function(autoRender){ - if(!tip){ - if(!Ext.isReady){ - Ext.onReady(function(){ - Ext.QuickTips.init(autoRender); - }); - return; - } - tip = new Ext.QuickTip({ - elements:'header,body', - disabled: disabled - }); - if(autoRender !== false){ - tip.render(Ext.getBody()); - } - } - }, - - // Protected method called by the dd classes - ddDisable : function(){ - // don't disable it if we don't need to - if(tip && !disabled){ - tip.disable(); - } - }, - - // Protected method called by the dd classes - ddEnable : function(){ - // only enable it if it hasn't been disabled - if(tip && !disabled){ - tip.enable(); - } - }, - - /** - * Enable quick tips globally. - */ - enable : function(){ - if(tip){ - tip.enable(); - } - disabled = false; - }, - - /** - * Disable quick tips globally. - */ - disable : function(){ - if(tip){ - tip.disable(); - } - disabled = true; - }, - - /** - * Returns true if quick tips are enabled, else false. - * @return {Boolean} - */ - isEnabled : function(){ - return tip !== undefined && !tip.disabled; - }, - - /** - * Gets the single {@link Ext.QuickTip QuickTip} instance used to show tips from all registered elements. - * @return {Ext.QuickTip} - */ - getQuickTip : function(){ - return tip; - }, - - /** - * Configures a new quick tip instance and assigns it to a target element. See - * {@link Ext.QuickTip#register} for details. - * @param {Object} config The config object - */ - register : function(){ - tip.register.apply(tip, arguments); - }, - - /** - * Removes any registered quick tip from the target element and destroys it. - * @param {String/HTMLElement/Element} el The element from which the quick tip is to be removed. - */ - unregister : function(){ - tip.unregister.apply(tip, arguments); - }, - - /** - * Alias of {@link #register}. - * @param {Object} config The config object - */ - tips : function(){ - tip.register.apply(tip, arguments); - } - }; -}();/** - * @class Ext.slider.Tip - * @extends Ext.Tip - * Simple plugin for using an Ext.Tip with a slider to show the slider value. Example usage: -
    -new Ext.Slider({
    -    width: 214,
    -    minValue: 0,
    -    maxValue: 100,
    -    plugins: new Ext.slider.Tip()
    -});
    -
    - * Optionally provide your own tip text by overriding getText: -
    - new Ext.Slider({
    -     width: 214,
    -     minValue: 0,
    -     maxValue: 100,
    -     plugins: new Ext.slider.Tip({
    -         getText: function(thumb){
    -             return String.format('{0}% complete', thumb.value);
    -         }
    -     })
    - });
    - 
    - */ -Ext.slider.Tip = Ext.extend(Ext.Tip, { - minWidth: 10, - offsets : [0, -10], - - init: function(slider) { - slider.on({ - scope : this, - dragstart: this.onSlide, - drag : this.onSlide, - dragend : this.hide, - destroy : this.destroy - }); - }, - - /** - * @private - * Called whenever a dragstart or drag event is received on the associated Thumb. - * Aligns the Tip with the Thumb's new position. - * @param {Ext.slider.MultiSlider} slider The slider - * @param {Ext.EventObject} e The Event object - * @param {Ext.slider.Thumb} thumb The thumb that the Tip is attached to - */ - onSlide : function(slider, e, thumb) { - this.show(); - this.body.update(this.getText(thumb)); - this.doAutoWidth(); - this.el.alignTo(thumb.el, 'b-t?', this.offsets); - }, - - /** - * Used to create the text that appears in the Tip's body. By default this just returns - * the value of the Slider Thumb that the Tip is attached to. Override to customize. - * @param {Ext.slider.Thumb} thumb The Thumb that the Tip is attached to - * @return {String} The text to display in the tip - */ - getText : function(thumb) { - return String(thumb.value); - } -}); - -//backwards compatibility - SliderTip used to be a ux before 3.2 -Ext.ux.SliderTip = Ext.slider.Tip;/** - * @class Ext.tree.TreePanel - * @extends Ext.Panel - *

    The TreePanel provides tree-structured UI representation of tree-structured data.

    - *

    {@link Ext.tree.TreeNode TreeNode}s added to the TreePanel may each contain metadata - * used by your application in their {@link Ext.tree.TreeNode#attributes attributes} property.

    - *

    A TreePanel must have a {@link #root} node before it is rendered. This may either be - * specified using the {@link #root} config option, or using the {@link #setRootNode} method. - *

    An example of tree rendered to an existing div:

    
    -var tree = new Ext.tree.TreePanel({
    -    renderTo: 'tree-div',
    -    useArrows: true,
    -    autoScroll: true,
    -    animate: true,
    -    enableDD: true,
    -    containerScroll: true,
    -    border: false,
    -    // auto create TreeLoader
    -    dataUrl: 'get-nodes.php',
    -
    -    root: {
    -        nodeType: 'async',
    -        text: 'Ext JS',
    -        draggable: false,
    -        id: 'source'
    -    }
    -});
    -
    -tree.getRootNode().expand();
    - * 
    - *

    The example above would work with a data packet similar to this:

    
    -[{
    -    "text": "adapter",
    -    "id": "source\/adapter",
    -    "cls": "folder"
    -}, {
    -    "text": "dd",
    -    "id": "source\/dd",
    -    "cls": "folder"
    -}, {
    -    "text": "debug.js",
    -    "id": "source\/debug.js",
    -    "leaf": true,
    -    "cls": "file"
    -}]
    - * 
    - *

    An example of tree within a Viewport:

    
    -new Ext.Viewport({
    -    layout: 'border',
    -    items: [{
    -        region: 'west',
    -        collapsible: true,
    -        title: 'Navigation',
    -        xtype: 'treepanel',
    -        width: 200,
    -        autoScroll: true,
    -        split: true,
    -        loader: new Ext.tree.TreeLoader(),
    -        root: new Ext.tree.AsyncTreeNode({
    -            expanded: true,
    -            children: [{
    -                text: 'Menu Option 1',
    -                leaf: true
    -            }, {
    -                text: 'Menu Option 2',
    -                leaf: true
    -            }, {
    -                text: 'Menu Option 3',
    -                leaf: true
    -            }]
    -        }),
    -        rootVisible: false,
    -        listeners: {
    -            click: function(n) {
    -                Ext.Msg.alert('Navigation Tree Click', 'You clicked: "' + n.attributes.text + '"');
    -            }
    -        }
    -    }, {
    -        region: 'center',
    -        xtype: 'tabpanel',
    -        // remaining code not shown ...
    -    }]
    -});
    -
    - * - * @cfg {Ext.tree.TreeNode} root The root node for the tree. - * @cfg {Boolean} rootVisible false to hide the root node (defaults to true) - * @cfg {Boolean} lines false to disable tree lines (defaults to true) - * @cfg {Boolean} enableDD true to enable drag and drop - * @cfg {Boolean} enableDrag true to enable just drag - * @cfg {Boolean} enableDrop true to enable just drop - * @cfg {Object} dragConfig Custom config to pass to the {@link Ext.tree.TreeDragZone} instance - * @cfg {Object} dropConfig Custom config to pass to the {@link Ext.tree.TreeDropZone} instance - * @cfg {String} ddGroup The DD group this TreePanel belongs to - * @cfg {Boolean} ddAppendOnly true if the tree should only allow append drops (use for trees which are sorted) - * @cfg {Boolean} ddScroll true to enable body scrolling - * @cfg {Boolean} containerScroll true to register this container with ScrollManager - * @cfg {Boolean} hlDrop false to disable node highlight on drop (defaults to the value of {@link Ext#enableFx}) - * @cfg {String} hlColor The color of the node highlight (defaults to 'C3DAF9') - * @cfg {Boolean} animate true to enable animated expand/collapse (defaults to the value of {@link Ext#enableFx}) - * @cfg {Boolean} singleExpand true if only 1 node per branch may be expanded - * @cfg {Object} selModel A tree selection model to use with this TreePanel (defaults to an {@link Ext.tree.DefaultSelectionModel}) - * @cfg {Boolean} trackMouseOver false to disable mouse over highlighting - * @cfg {Ext.tree.TreeLoader} loader A {@link Ext.tree.TreeLoader} for use with this TreePanel - * @cfg {String} pathSeparator The token used to separate sub-paths in path strings (defaults to '/') - * @cfg {Boolean} useArrows true to use Vista-style arrows in the tree (defaults to false) - * @cfg {String} requestMethod The HTTP request method for loading data (defaults to the value of {@link Ext.Ajax#method}). - * - * @constructor - * @param {Object} config - * @xtype treepanel - */ -Ext.tree.TreePanel = Ext.extend(Ext.Panel, { - rootVisible : true, - animate : Ext.enableFx, - lines : true, - enableDD : false, - hlDrop : Ext.enableFx, - pathSeparator : '/', - - /** - * @cfg {Array} bubbleEvents - *

    An array of events that, when fired, should be bubbled to any parent container. - * See {@link Ext.util.Observable#enableBubble}. - * Defaults to []. - */ - bubbleEvents : [], - - initComponent : function(){ - Ext.tree.TreePanel.superclass.initComponent.call(this); - - if(!this.eventModel){ - this.eventModel = new Ext.tree.TreeEventModel(this); - } - - // initialize the loader - var l = this.loader; - if(!l){ - l = new Ext.tree.TreeLoader({ - dataUrl: this.dataUrl, - requestMethod: this.requestMethod - }); - }else if(Ext.isObject(l) && !l.load){ - l = new Ext.tree.TreeLoader(l); - } - this.loader = l; - - this.nodeHash = {}; - - /** - * The root node of this tree. - * @type Ext.tree.TreeNode - * @property root - */ - if(this.root){ - var r = this.root; - delete this.root; - this.setRootNode(r); - } - - - this.addEvents( - - /** - * @event append - * Fires when a new child node is appended to a node in this tree. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The newly appended node - * @param {Number} index The index of the newly appended node - */ - 'append', - /** - * @event remove - * Fires when a child node is removed from a node in this tree. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node removed - */ - 'remove', - /** - * @event movenode - * Fires when a node is moved to a new location in the tree - * @param {Tree} tree The owner tree - * @param {Node} node The node moved - * @param {Node} oldParent The old parent of this node - * @param {Node} newParent The new parent of this node - * @param {Number} index The index it was moved to - */ - 'movenode', - /** - * @event insert - * Fires when a new child node is inserted in a node in this tree. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node inserted - * @param {Node} refNode The child node the node was inserted before - */ - 'insert', - /** - * @event beforeappend - * Fires before a new child is appended to a node in this tree, return false to cancel the append. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node to be appended - */ - 'beforeappend', - /** - * @event beforeremove - * Fires before a child is removed from a node in this tree, return false to cancel the remove. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node to be removed - */ - 'beforeremove', - /** - * @event beforemovenode - * Fires before a node is moved to a new location in the tree. Return false to cancel the move. - * @param {Tree} tree The owner tree - * @param {Node} node The node being moved - * @param {Node} oldParent The parent of the node - * @param {Node} newParent The new parent the node is moving to - * @param {Number} index The index it is being moved to - */ - 'beforemovenode', - /** - * @event beforeinsert - * Fires before a new child is inserted in a node in this tree, return false to cancel the insert. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node to be inserted - * @param {Node} refNode The child node the node is being inserted before - */ - 'beforeinsert', - - /** - * @event beforeload - * Fires before a node is loaded, return false to cancel - * @param {Node} node The node being loaded - */ - 'beforeload', - /** - * @event load - * Fires when a node is loaded - * @param {Node} node The node that was loaded - */ - 'load', - /** - * @event textchange - * Fires when the text for a node is changed - * @param {Node} node The node - * @param {String} text The new text - * @param {String} oldText The old text - */ - 'textchange', - /** - * @event beforeexpandnode - * Fires before a node is expanded, return false to cancel. - * @param {Node} node The node - * @param {Boolean} deep - * @param {Boolean} anim - */ - 'beforeexpandnode', - /** - * @event beforecollapsenode - * Fires before a node is collapsed, return false to cancel. - * @param {Node} node The node - * @param {Boolean} deep - * @param {Boolean} anim - */ - 'beforecollapsenode', - /** - * @event expandnode - * Fires when a node is expanded - * @param {Node} node The node - */ - 'expandnode', - /** - * @event disabledchange - * Fires when the disabled status of a node changes - * @param {Node} node The node - * @param {Boolean} disabled - */ - 'disabledchange', - /** - * @event collapsenode - * Fires when a node is collapsed - * @param {Node} node The node - */ - 'collapsenode', - /** - * @event beforeclick - * Fires before click processing on a node. Return false to cancel the default action. - * @param {Node} node The node - * @param {Ext.EventObject} e The event object - */ - 'beforeclick', - /** - * @event click - * Fires when a node is clicked - * @param {Node} node The node - * @param {Ext.EventObject} e The event object - */ - 'click', - /** - * @event containerclick - * Fires when the tree container is clicked - * @param {Tree} this - * @param {Ext.EventObject} e The event object - */ - 'containerclick', - /** - * @event checkchange - * Fires when a node with a checkbox's checked property changes - * @param {Node} this This node - * @param {Boolean} checked - */ - 'checkchange', - /** - * @event beforedblclick - * Fires before double click processing on a node. Return false to cancel the default action. - * @param {Node} node The node - * @param {Ext.EventObject} e The event object - */ - 'beforedblclick', - /** - * @event dblclick - * Fires when a node is double clicked - * @param {Node} node The node - * @param {Ext.EventObject} e The event object - */ - 'dblclick', - /** - * @event containerdblclick - * Fires when the tree container is double clicked - * @param {Tree} this - * @param {Ext.EventObject} e The event object - */ - 'containerdblclick', - /** - * @event contextmenu - * Fires when a node is right clicked. To display a context menu in response to this - * event, first create a Menu object (see {@link Ext.menu.Menu} for details), then add - * a handler for this event:

    
    -new Ext.tree.TreePanel({
    -    title: 'My TreePanel',
    -    root: new Ext.tree.AsyncTreeNode({
    -        text: 'The Root',
    -        children: [
    -            { text: 'Child node 1', leaf: true },
    -            { text: 'Child node 2', leaf: true }
    -        ]
    -    }),
    -    contextMenu: new Ext.menu.Menu({
    -        items: [{
    -            id: 'delete-node',
    -            text: 'Delete Node'
    -        }],
    -        listeners: {
    -            itemclick: function(item) {
    -                switch (item.id) {
    -                    case 'delete-node':
    -                        var n = item.parentMenu.contextNode;
    -                        if (n.parentNode) {
    -                            n.remove();
    -                        }
    -                        break;
    -                }
    -            }
    -        }
    -    }),
    -    listeners: {
    -        contextmenu: function(node, e) {
    -//          Register the context node with the menu so that a Menu Item's handler function can access
    -//          it via its {@link Ext.menu.BaseItem#parentMenu parentMenu} property.
    -            node.select();
    -            var c = node.getOwnerTree().contextMenu;
    -            c.contextNode = node;
    -            c.showAt(e.getXY());
    -        }
    -    }
    -});
    -
    - * @param {Node} node The node - * @param {Ext.EventObject} e The event object - */ - 'contextmenu', - /** - * @event containercontextmenu - * Fires when the tree container is right clicked - * @param {Tree} this - * @param {Ext.EventObject} e The event object - */ - 'containercontextmenu', - /** - * @event beforechildrenrendered - * Fires right before the child nodes for a node are rendered - * @param {Node} node The node - */ - 'beforechildrenrendered', - /** - * @event startdrag - * Fires when a node starts being dragged - * @param {Ext.tree.TreePanel} this - * @param {Ext.tree.TreeNode} node - * @param {event} e The raw browser event - */ - 'startdrag', - /** - * @event enddrag - * Fires when a drag operation is complete - * @param {Ext.tree.TreePanel} this - * @param {Ext.tree.TreeNode} node - * @param {event} e The raw browser event - */ - 'enddrag', - /** - * @event dragdrop - * Fires when a dragged node is dropped on a valid DD target - * @param {Ext.tree.TreePanel} this - * @param {Ext.tree.TreeNode} node - * @param {DD} dd The dd it was dropped on - * @param {event} e The raw browser event - */ - 'dragdrop', - /** - * @event beforenodedrop - * Fires when a DD object is dropped on a node in this tree for preprocessing. Return false to cancel the drop. The dropEvent - * passed to handlers has the following properties:
    - *
      - *
    • tree - The TreePanel
    • - *
    • target - The node being targeted for the drop
    • - *
    • data - The drag data from the drag source
    • - *
    • point - The point of the drop - append, above or below
    • - *
    • source - The drag source
    • - *
    • rawEvent - Raw mouse event
    • - *
    • dropNode - Drop node(s) provided by the source OR you can supply node(s) - * to be inserted by setting them on this object.
    • - *
    • cancel - Set this to true to cancel the drop.
    • - *
    • dropStatus - If the default drop action is cancelled but the drop is valid, setting this to true - * will prevent the animated 'repair' from appearing.
    • - *
    - * @param {Object} dropEvent - */ - 'beforenodedrop', - /** - * @event nodedrop - * Fires after a DD object is dropped on a node in this tree. The dropEvent - * passed to handlers has the following properties:
    - *
      - *
    • tree - The TreePanel
    • - *
    • target - The node being targeted for the drop
    • - *
    • data - The drag data from the drag source
    • - *
    • point - The point of the drop - append, above or below
    • - *
    • source - The drag source
    • - *
    • rawEvent - Raw mouse event
    • - *
    • dropNode - Dropped node(s).
    • - *
    - * @param {Object} dropEvent - */ - 'nodedrop', - /** - * @event nodedragover - * Fires when a tree node is being targeted for a drag drop, return false to signal drop not allowed. The dragOverEvent - * passed to handlers has the following properties:
    - *
      - *
    • tree - The TreePanel
    • - *
    • target - The node being targeted for the drop
    • - *
    • data - The drag data from the drag source
    • - *
    • point - The point of the drop - append, above or below
    • - *
    • source - The drag source
    • - *
    • rawEvent - Raw mouse event
    • - *
    • dropNode - Drop node(s) provided by the source.
    • - *
    • cancel - Set this to true to signal drop not allowed.
    • - *
    - * @param {Object} dragOverEvent - */ - 'nodedragover' - ); - if(this.singleExpand){ - this.on('beforeexpandnode', this.restrictExpand, this); - } - }, - - // private - proxyNodeEvent : function(ename, a1, a2, a3, a4, a5, a6){ - if(ename == 'collapse' || ename == 'expand' || ename == 'beforecollapse' || ename == 'beforeexpand' || ename == 'move' || ename == 'beforemove'){ - ename = ename+'node'; - } - // args inline for performance while bubbling events - return this.fireEvent(ename, a1, a2, a3, a4, a5, a6); - }, - - - /** - * Returns this root node for this tree - * @return {Node} - */ - getRootNode : function(){ - return this.root; - }, - - /** - * Sets the root node for this tree. If the TreePanel has already rendered a root node, the - * previous root node (and all of its descendants) are destroyed before the new root node is rendered. - * @param {Node} node - * @return {Node} - */ - setRootNode : function(node){ - this.destroyRoot(); - if(!node.render){ // attributes passed - node = this.loader.createNode(node); - } - this.root = node; - node.ownerTree = this; - node.isRoot = true; - this.registerNode(node); - if(!this.rootVisible){ - var uiP = node.attributes.uiProvider; - node.ui = uiP ? new uiP(node) : new Ext.tree.RootTreeNodeUI(node); - } - if(this.innerCt){ - this.clearInnerCt(); - this.renderRoot(); - } - return node; - }, - - clearInnerCt : function(){ - this.innerCt.update(''); - }, - - // private - renderRoot : function(){ - this.root.render(); - if(!this.rootVisible){ - this.root.renderChildren(); - } - }, - - /** - * Gets a node in this tree by its id - * @param {String} id - * @return {Node} - */ - getNodeById : function(id){ - return this.nodeHash[id]; - }, - - // private - registerNode : function(node){ - this.nodeHash[node.id] = node; - }, - - // private - unregisterNode : function(node){ - delete this.nodeHash[node.id]; - }, - - // private - toString : function(){ - return '[Tree'+(this.id?' '+this.id:'')+']'; - }, - - // private - restrictExpand : function(node){ - var p = node.parentNode; - if(p){ - if(p.expandedChild && p.expandedChild.parentNode == p){ - p.expandedChild.collapse(); - } - p.expandedChild = node; - } - }, - - /** - * Retrieve an array of checked nodes, or an array of a specific attribute of checked nodes (e.g. 'id') - * @param {String} attribute (optional) Defaults to null (return the actual nodes) - * @param {TreeNode} startNode (optional) The node to start from, defaults to the root - * @return {Array} - */ - getChecked : function(a, startNode){ - startNode = startNode || this.root; - var r = []; - var f = function(){ - if(this.attributes.checked){ - r.push(!a ? this : (a == 'id' ? this.id : this.attributes[a])); - } - }; - startNode.cascade(f); - return r; - }, - - /** - * Returns the default {@link Ext.tree.TreeLoader} for this TreePanel. - * @return {Ext.tree.TreeLoader} The TreeLoader for this TreePanel. - */ - getLoader : function(){ - return this.loader; - }, - - /** - * Expand all nodes - */ - expandAll : function(){ - this.root.expand(true); - }, - - /** - * Collapse all nodes - */ - collapseAll : function(){ - this.root.collapse(true); - }, - - /** - * Returns the selection model used by this TreePanel. - * @return {TreeSelectionModel} The selection model used by this TreePanel - */ - getSelectionModel : function(){ - if(!this.selModel){ - this.selModel = new Ext.tree.DefaultSelectionModel(); - } - return this.selModel; - }, - - /** - * Expands a specified path in this TreePanel. A path can be retrieved from a node with {@link Ext.data.Node#getPath} - * @param {String} path - * @param {String} attr (optional) The attribute used in the path (see {@link Ext.data.Node#getPath} for more info) - * @param {Function} callback (optional) The callback to call when the expand is complete. The callback will be called with - * (bSuccess, oLastNode) where bSuccess is if the expand was successful and oLastNode is the last node that was expanded. - */ - expandPath : function(path, attr, callback){ - if(Ext.isEmpty(path)){ - if(callback){ - callback(false, undefined); - } - return; - } - attr = attr || 'id'; - var keys = path.split(this.pathSeparator); - var curNode = this.root; - if(curNode.attributes[attr] != keys[1]){ // invalid root - if(callback){ - callback(false, null); - } - return; - } - var index = 1; - var f = function(){ - if(++index == keys.length){ - if(callback){ - callback(true, curNode); - } - return; - } - var c = curNode.findChild(attr, keys[index]); - if(!c){ - if(callback){ - callback(false, curNode); - } - return; - } - curNode = c; - c.expand(false, false, f); - }; - curNode.expand(false, false, f); - }, - - /** - * Selects the node in this tree at the specified path. A path can be retrieved from a node with {@link Ext.data.Node#getPath} - * @param {String} path - * @param {String} attr (optional) The attribute used in the path (see {@link Ext.data.Node#getPath} for more info) - * @param {Function} callback (optional) The callback to call when the selection is complete. The callback will be called with - * (bSuccess, oSelNode) where bSuccess is if the selection was successful and oSelNode is the selected node. - */ - selectPath : function(path, attr, callback){ - if(Ext.isEmpty(path)){ - if(callback){ - callback(false, undefined); - } - return; - } - attr = attr || 'id'; - var keys = path.split(this.pathSeparator), - v = keys.pop(); - if(keys.length > 1){ - var f = function(success, node){ - if(success && node){ - var n = node.findChild(attr, v); - if(n){ - n.select(); - if(callback){ - callback(true, n); - } - }else if(callback){ - callback(false, n); - } - }else{ - if(callback){ - callback(false, n); - } - } - }; - this.expandPath(keys.join(this.pathSeparator), attr, f); - }else{ - this.root.select(); - if(callback){ - callback(true, this.root); - } - } - }, - - /** - * Returns the underlying Element for this tree - * @return {Ext.Element} The Element - */ - getTreeEl : function(){ - return this.body; - }, - - // private - onRender : function(ct, position){ - Ext.tree.TreePanel.superclass.onRender.call(this, ct, position); - this.el.addClass('x-tree'); - this.innerCt = this.body.createChild({tag:'ul', - cls:'x-tree-root-ct ' + - (this.useArrows ? 'x-tree-arrows' : this.lines ? 'x-tree-lines' : 'x-tree-no-lines')}); - }, - - // private - initEvents : function(){ - Ext.tree.TreePanel.superclass.initEvents.call(this); - - if(this.containerScroll){ - Ext.dd.ScrollManager.register(this.body); - } - if((this.enableDD || this.enableDrop) && !this.dropZone){ - /** - * The dropZone used by this tree if drop is enabled (see {@link #enableDD} or {@link #enableDrop}) - * @property dropZone - * @type Ext.tree.TreeDropZone - */ - this.dropZone = new Ext.tree.TreeDropZone(this, this.dropConfig || { - ddGroup: this.ddGroup || 'TreeDD', appendOnly: this.ddAppendOnly === true - }); - } - if((this.enableDD || this.enableDrag) && !this.dragZone){ - /** - * The dragZone used by this tree if drag is enabled (see {@link #enableDD} or {@link #enableDrag}) - * @property dragZone - * @type Ext.tree.TreeDragZone - */ - this.dragZone = new Ext.tree.TreeDragZone(this, this.dragConfig || { - ddGroup: this.ddGroup || 'TreeDD', - scroll: this.ddScroll - }); - } - this.getSelectionModel().init(this); - }, - - // private - afterRender : function(){ - Ext.tree.TreePanel.superclass.afterRender.call(this); - this.renderRoot(); - }, - - beforeDestroy : function(){ - if(this.rendered){ - Ext.dd.ScrollManager.unregister(this.body); - Ext.destroy(this.dropZone, this.dragZone); - } - this.destroyRoot(); - Ext.destroy(this.loader); - this.nodeHash = this.root = this.loader = null; - Ext.tree.TreePanel.superclass.beforeDestroy.call(this); - }, - - /** - * Destroy the root node. Not included by itself because we need to pass the silent parameter. - * @private - */ - destroyRoot : function(){ - if(this.root && this.root.destroy){ - this.root.destroy(true); - } - } - - /** - * @cfg {String/Number} activeItem - * @hide - */ - /** - * @cfg {Boolean} autoDestroy - * @hide - */ - /** - * @cfg {Object/String/Function} autoLoad - * @hide - */ - /** - * @cfg {Boolean} autoWidth - * @hide - */ - /** - * @cfg {Boolean/Number} bufferResize - * @hide - */ - /** - * @cfg {String} defaultType - * @hide - */ - /** - * @cfg {Object} defaults - * @hide - */ - /** - * @cfg {Boolean} hideBorders - * @hide - */ - /** - * @cfg {Mixed} items - * @hide - */ - /** - * @cfg {String} layout - * @hide - */ - /** - * @cfg {Object} layoutConfig - * @hide - */ - /** - * @cfg {Boolean} monitorResize - * @hide - */ - /** - * @property items - * @hide - */ - /** - * @method cascade - * @hide - */ - /** - * @method doLayout - * @hide - */ - /** - * @method find - * @hide - */ - /** - * @method findBy - * @hide - */ - /** - * @method findById - * @hide - */ - /** - * @method findByType - * @hide - */ - /** - * @method getComponent - * @hide - */ - /** - * @method getLayout - * @hide - */ - /** - * @method getUpdater - * @hide - */ - /** - * @method insert - * @hide - */ - /** - * @method load - * @hide - */ - /** - * @method remove - * @hide - */ - /** - * @event add - * @hide - */ - /** - * @method removeAll - * @hide - */ - /** - * @event afterLayout - * @hide - */ - /** - * @event beforeadd - * @hide - */ - /** - * @event beforeremove - * @hide - */ - /** - * @event remove - * @hide - */ - - - - /** - * @cfg {String} allowDomMove @hide - */ - /** - * @cfg {String} autoEl @hide - */ - /** - * @cfg {String} applyTo @hide - */ - /** - * @cfg {String} contentEl @hide - */ - /** - * @cfg {Mixed} data @hide - */ - /** - * @cfg {Mixed} tpl @hide - */ - /** - * @cfg {String} tplWriteMode @hide - */ - /** - * @cfg {String} disabledClass @hide - */ - /** - * @cfg {String} elements @hide - */ - /** - * @cfg {String} html @hide - */ - /** - * @cfg {Boolean} preventBodyReset - * @hide - */ - /** - * @property disabled - * @hide - */ - /** - * @method applyToMarkup - * @hide - */ - /** - * @method enable - * @hide - */ - /** - * @method disable - * @hide - */ - /** - * @method setDisabled - * @hide - */ -}); - -Ext.tree.TreePanel.nodeTypes = {}; - -Ext.reg('treepanel', Ext.tree.TreePanel);Ext.tree.TreeEventModel = function(tree){ - this.tree = tree; - this.tree.on('render', this.initEvents, this); -}; - -Ext.tree.TreeEventModel.prototype = { - initEvents : function(){ - var t = this.tree; - - if(t.trackMouseOver !== false){ - t.mon(t.innerCt, { - scope: this, - mouseover: this.delegateOver, - mouseout: this.delegateOut - }); - } - t.mon(t.getTreeEl(), { - scope: this, - click: this.delegateClick, - dblclick: this.delegateDblClick, - contextmenu: this.delegateContextMenu - }); - }, - - getNode : function(e){ - var t; - if(t = e.getTarget('.x-tree-node-el', 10)){ - var id = Ext.fly(t, '_treeEvents').getAttribute('tree-node-id', 'ext'); - if(id){ - return this.tree.getNodeById(id); - } - } - return null; - }, - - getNodeTarget : function(e){ - var t = e.getTarget('.x-tree-node-icon', 1); - if(!t){ - t = e.getTarget('.x-tree-node-el', 6); - } - return t; - }, - - delegateOut : function(e, t){ - if(!this.beforeEvent(e)){ - return; - } - if(e.getTarget('.x-tree-ec-icon', 1)){ - var n = this.getNode(e); - this.onIconOut(e, n); - if(n == this.lastEcOver){ - delete this.lastEcOver; - } - } - if((t = this.getNodeTarget(e)) && !e.within(t, true)){ - this.onNodeOut(e, this.getNode(e)); - } - }, - - delegateOver : function(e, t){ - if(!this.beforeEvent(e)){ - return; - } - if(Ext.isGecko && !this.trackingDoc){ // prevent hanging in FF - Ext.getBody().on('mouseover', this.trackExit, this); - this.trackingDoc = true; - } - if(this.lastEcOver){ // prevent hung highlight - this.onIconOut(e, this.lastEcOver); - delete this.lastEcOver; - } - if(e.getTarget('.x-tree-ec-icon', 1)){ - this.lastEcOver = this.getNode(e); - this.onIconOver(e, this.lastEcOver); - } - if(t = this.getNodeTarget(e)){ - this.onNodeOver(e, this.getNode(e)); - } - }, - - trackExit : function(e){ - if(this.lastOverNode){ - if(this.lastOverNode.ui && !e.within(this.lastOverNode.ui.getEl())){ - this.onNodeOut(e, this.lastOverNode); - } - delete this.lastOverNode; - Ext.getBody().un('mouseover', this.trackExit, this); - this.trackingDoc = false; - } - - }, - - delegateClick : function(e, t){ - if(this.beforeEvent(e)){ - if(e.getTarget('input[type=checkbox]', 1)){ - this.onCheckboxClick(e, this.getNode(e)); - }else if(e.getTarget('.x-tree-ec-icon', 1)){ - this.onIconClick(e, this.getNode(e)); - }else if(this.getNodeTarget(e)){ - this.onNodeClick(e, this.getNode(e)); - } - }else{ - this.checkContainerEvent(e, 'click'); - } - }, - - delegateDblClick : function(e, t){ - if(this.beforeEvent(e)){ - if(this.getNodeTarget(e)){ - this.onNodeDblClick(e, this.getNode(e)); - } - }else{ - this.checkContainerEvent(e, 'dblclick'); - } - }, - - delegateContextMenu : function(e, t){ - if(this.beforeEvent(e)){ - if(this.getNodeTarget(e)){ - this.onNodeContextMenu(e, this.getNode(e)); - } - }else{ - this.checkContainerEvent(e, 'contextmenu'); - } - }, - - checkContainerEvent: function(e, type){ - if(this.disabled){ - e.stopEvent(); - return false; - } - this.onContainerEvent(e, type); - }, - - onContainerEvent: function(e, type){ - this.tree.fireEvent('container' + type, this.tree, e); - }, - - onNodeClick : function(e, node){ - node.ui.onClick(e); - }, - - onNodeOver : function(e, node){ - this.lastOverNode = node; - node.ui.onOver(e); - }, - - onNodeOut : function(e, node){ - node.ui.onOut(e); - }, - - onIconOver : function(e, node){ - node.ui.addClass('x-tree-ec-over'); - }, - - onIconOut : function(e, node){ - node.ui.removeClass('x-tree-ec-over'); - }, - - onIconClick : function(e, node){ - node.ui.ecClick(e); - }, - - onCheckboxClick : function(e, node){ - node.ui.onCheckChange(e); - }, - - onNodeDblClick : function(e, node){ - node.ui.onDblClick(e); - }, - - onNodeContextMenu : function(e, node){ - node.ui.onContextMenu(e); - }, - - beforeEvent : function(e){ - var node = this.getNode(e); - if(this.disabled || !node || !node.ui){ - e.stopEvent(); - return false; - } - return true; - }, - - disable: function(){ - this.disabled = true; - }, - - enable: function(){ - this.disabled = false; - } -};/** - * @class Ext.tree.DefaultSelectionModel - * @extends Ext.util.Observable - * The default single selection for a TreePanel. - */ -Ext.tree.DefaultSelectionModel = Ext.extend(Ext.util.Observable, { - - constructor : function(config){ - this.selNode = null; - - this.addEvents( - /** - * @event selectionchange - * Fires when the selected node changes - * @param {DefaultSelectionModel} this - * @param {TreeNode} node the new selection - */ - 'selectionchange', - - /** - * @event beforeselect - * Fires before the selected node changes, return false to cancel the change - * @param {DefaultSelectionModel} this - * @param {TreeNode} node the new selection - * @param {TreeNode} node the old selection - */ - 'beforeselect' - ); - - Ext.apply(this, config); - Ext.tree.DefaultSelectionModel.superclass.constructor.call(this); - }, - - init : function(tree){ - this.tree = tree; - tree.mon(tree.getTreeEl(), 'keydown', this.onKeyDown, this); - tree.on('click', this.onNodeClick, this); - }, - - onNodeClick : function(node, e){ - this.select(node); - }, - - /** - * Select a node. - * @param {TreeNode} node The node to select - * @return {TreeNode} The selected node - */ - select : function(node, /* private*/ selectNextNode){ - // If node is hidden, select the next node in whatever direction was being moved in. - if (!Ext.fly(node.ui.wrap).isVisible() && selectNextNode) { - return selectNextNode.call(this, node); - } - var last = this.selNode; - if(node == last){ - node.ui.onSelectedChange(true); - }else if(this.fireEvent('beforeselect', this, node, last) !== false){ - if(last && last.ui){ - last.ui.onSelectedChange(false); - } - this.selNode = node; - node.ui.onSelectedChange(true); - this.fireEvent('selectionchange', this, node, last); - } - return node; - }, - - /** - * Deselect a node. - * @param {TreeNode} node The node to unselect - * @param {Boolean} silent True to stop the selectionchange event from firing. - */ - unselect : function(node, silent){ - if(this.selNode == node){ - this.clearSelections(silent); - } - }, - - /** - * Clear all selections - * @param {Boolean} silent True to stop the selectionchange event from firing. - */ - clearSelections : function(silent){ - var n = this.selNode; - if(n){ - n.ui.onSelectedChange(false); - this.selNode = null; - if(silent !== true){ - this.fireEvent('selectionchange', this, null); - } - } - return n; - }, - - /** - * Get the selected node - * @return {TreeNode} The selected node - */ - getSelectedNode : function(){ - return this.selNode; - }, - - /** - * Returns true if the node is selected - * @param {TreeNode} node The node to check - * @return {Boolean} - */ - isSelected : function(node){ - return this.selNode == node; - }, - - /** - * Selects the node above the selected node in the tree, intelligently walking the nodes - * @return TreeNode The new selection - */ - selectPrevious : function(/* private */ s){ - if(!(s = s || this.selNode || this.lastSelNode)){ - return null; - } - // Here we pass in the current function to select to indicate the direction we're moving - var ps = s.previousSibling; - if(ps){ - if(!ps.isExpanded() || ps.childNodes.length < 1){ - return this.select(ps, this.selectPrevious); - } else{ - var lc = ps.lastChild; - while(lc && lc.isExpanded() && Ext.fly(lc.ui.wrap).isVisible() && lc.childNodes.length > 0){ - lc = lc.lastChild; - } - return this.select(lc, this.selectPrevious); - } - } else if(s.parentNode && (this.tree.rootVisible || !s.parentNode.isRoot)){ - return this.select(s.parentNode, this.selectPrevious); - } - return null; - }, - - /** - * Selects the node above the selected node in the tree, intelligently walking the nodes - * @return TreeNode The new selection - */ - selectNext : function(/* private */ s){ - if(!(s = s || this.selNode || this.lastSelNode)){ - return null; - } - // Here we pass in the current function to select to indicate the direction we're moving - if(s.firstChild && s.isExpanded() && Ext.fly(s.ui.wrap).isVisible()){ - return this.select(s.firstChild, this.selectNext); - }else if(s.nextSibling){ - return this.select(s.nextSibling, this.selectNext); - }else if(s.parentNode){ - var newS = null; - s.parentNode.bubble(function(){ - if(this.nextSibling){ - newS = this.getOwnerTree().selModel.select(this.nextSibling, this.selectNext); - return false; - } - }); - return newS; - } - return null; - }, - - onKeyDown : function(e){ - var s = this.selNode || this.lastSelNode; - // undesirable, but required - var sm = this; - if(!s){ - return; - } - var k = e.getKey(); - switch(k){ - case e.DOWN: - e.stopEvent(); - this.selectNext(); - break; - case e.UP: - e.stopEvent(); - this.selectPrevious(); - break; - case e.RIGHT: - e.preventDefault(); - if(s.hasChildNodes()){ - if(!s.isExpanded()){ - s.expand(); - }else if(s.firstChild){ - this.select(s.firstChild, e); - } - } - break; - case e.LEFT: - e.preventDefault(); - if(s.hasChildNodes() && s.isExpanded()){ - s.collapse(); - }else if(s.parentNode && (this.tree.rootVisible || s.parentNode != this.tree.getRootNode())){ - this.select(s.parentNode, e); - } - break; - }; - } -}); - -/** - * @class Ext.tree.MultiSelectionModel - * @extends Ext.util.Observable - * Multi selection for a TreePanel. - */ -Ext.tree.MultiSelectionModel = Ext.extend(Ext.util.Observable, { - - constructor : function(config){ - this.selNodes = []; - this.selMap = {}; - this.addEvents( - /** - * @event selectionchange - * Fires when the selected nodes change - * @param {MultiSelectionModel} this - * @param {Array} nodes Array of the selected nodes - */ - 'selectionchange' - ); - Ext.apply(this, config); - Ext.tree.MultiSelectionModel.superclass.constructor.call(this); - }, - - init : function(tree){ - this.tree = tree; - tree.mon(tree.getTreeEl(), 'keydown', this.onKeyDown, this); - tree.on('click', this.onNodeClick, this); - }, - - onNodeClick : function(node, e){ - if(e.ctrlKey && this.isSelected(node)){ - this.unselect(node); - }else{ - this.select(node, e, e.ctrlKey); - } - }, - - /** - * Select a node. - * @param {TreeNode} node The node to select - * @param {EventObject} e (optional) An event associated with the selection - * @param {Boolean} keepExisting True to retain existing selections - * @return {TreeNode} The selected node - */ - select : function(node, e, keepExisting){ - if(keepExisting !== true){ - this.clearSelections(true); - } - if(this.isSelected(node)){ - this.lastSelNode = node; - return node; - } - this.selNodes.push(node); - this.selMap[node.id] = node; - this.lastSelNode = node; - node.ui.onSelectedChange(true); - this.fireEvent('selectionchange', this, this.selNodes); - return node; - }, - - /** - * Deselect a node. - * @param {TreeNode} node The node to unselect - */ - unselect : function(node){ - if(this.selMap[node.id]){ - node.ui.onSelectedChange(false); - var sn = this.selNodes; - var index = sn.indexOf(node); - if(index != -1){ - this.selNodes.splice(index, 1); - } - delete this.selMap[node.id]; - this.fireEvent('selectionchange', this, this.selNodes); - } - }, - - /** - * Clear all selections - */ - clearSelections : function(suppressEvent){ - var sn = this.selNodes; - if(sn.length > 0){ - for(var i = 0, len = sn.length; i < len; i++){ - sn[i].ui.onSelectedChange(false); - } - this.selNodes = []; - this.selMap = {}; - if(suppressEvent !== true){ - this.fireEvent('selectionchange', this, this.selNodes); - } - } - }, - - /** - * Returns true if the node is selected - * @param {TreeNode} node The node to check - * @return {Boolean} - */ - isSelected : function(node){ - return this.selMap[node.id] ? true : false; - }, - - /** - * Returns an array of the selected nodes - * @return {Array} - */ - getSelectedNodes : function(){ - return this.selNodes.concat([]); - }, - - onKeyDown : Ext.tree.DefaultSelectionModel.prototype.onKeyDown, - - selectNext : Ext.tree.DefaultSelectionModel.prototype.selectNext, - - selectPrevious : Ext.tree.DefaultSelectionModel.prototype.selectPrevious -});/** - * @class Ext.data.Tree - * @extends Ext.util.Observable - * Represents a tree data structure and bubbles all the events for its nodes. The nodes - * in the tree have most standard DOM functionality. - * @constructor - * @param {Node} root (optional) The root node - */ -Ext.data.Tree = Ext.extend(Ext.util.Observable, { - - constructor: function(root){ - this.nodeHash = {}; - /** - * The root node for this tree - * @type Node - */ - this.root = null; - if(root){ - this.setRootNode(root); - } - this.addEvents( - /** - * @event append - * Fires when a new child node is appended to a node in this tree. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The newly appended node - * @param {Number} index The index of the newly appended node - */ - "append", - /** - * @event remove - * Fires when a child node is removed from a node in this tree. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node removed - */ - "remove", - /** - * @event move - * Fires when a node is moved to a new location in the tree - * @param {Tree} tree The owner tree - * @param {Node} node The node moved - * @param {Node} oldParent The old parent of this node - * @param {Node} newParent The new parent of this node - * @param {Number} index The index it was moved to - */ - "move", - /** - * @event insert - * Fires when a new child node is inserted in a node in this tree. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node inserted - * @param {Node} refNode The child node the node was inserted before - */ - "insert", - /** - * @event beforeappend - * Fires before a new child is appended to a node in this tree, return false to cancel the append. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node to be appended - */ - "beforeappend", - /** - * @event beforeremove - * Fires before a child is removed from a node in this tree, return false to cancel the remove. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node to be removed - */ - "beforeremove", - /** - * @event beforemove - * Fires before a node is moved to a new location in the tree. Return false to cancel the move. - * @param {Tree} tree The owner tree - * @param {Node} node The node being moved - * @param {Node} oldParent The parent of the node - * @param {Node} newParent The new parent the node is moving to - * @param {Number} index The index it is being moved to - */ - "beforemove", - /** - * @event beforeinsert - * Fires before a new child is inserted in a node in this tree, return false to cancel the insert. - * @param {Tree} tree The owner tree - * @param {Node} parent The parent node - * @param {Node} node The child node to be inserted - * @param {Node} refNode The child node the node is being inserted before - */ - "beforeinsert" - ); - Ext.data.Tree.superclass.constructor.call(this); - }, - - /** - * @cfg {String} pathSeparator - * The token used to separate paths in node ids (defaults to '/'). - */ - pathSeparator: "/", - - // private - proxyNodeEvent : function(){ - return this.fireEvent.apply(this, arguments); - }, - - /** - * Returns the root node for this tree. - * @return {Node} - */ - getRootNode : function(){ - return this.root; - }, - - /** - * Sets the root node for this tree. - * @param {Node} node - * @return {Node} - */ - setRootNode : function(node){ - this.root = node; - node.ownerTree = this; - node.isRoot = true; - this.registerNode(node); - return node; - }, - - /** - * Gets a node in this tree by its id. - * @param {String} id - * @return {Node} - */ - getNodeById : function(id){ - return this.nodeHash[id]; - }, - - // private - registerNode : function(node){ - this.nodeHash[node.id] = node; - }, - - // private - unregisterNode : function(node){ - delete this.nodeHash[node.id]; - }, - - toString : function(){ - return "[Tree"+(this.id?" "+this.id:"")+"]"; - } -}); - -/** - * @class Ext.data.Node - * @extends Ext.util.Observable - * @cfg {Boolean} leaf true if this node is a leaf and does not have children - * @cfg {String} id The id for this node. If one is not specified, one is generated. - * @constructor - * @param {Object} attributes The attributes/config for the node - */ -Ext.data.Node = Ext.extend(Ext.util.Observable, { - - constructor: function(attributes){ - /** - * The attributes supplied for the node. You can use this property to access any custom attributes you supplied. - * @type {Object} - */ - this.attributes = attributes || {}; - this.leaf = this.attributes.leaf; - /** - * The node id. @type String - */ - this.id = this.attributes.id; - if(!this.id){ - this.id = Ext.id(null, "xnode-"); - this.attributes.id = this.id; - } - /** - * All child nodes of this node. @type Array - */ - this.childNodes = []; - /** - * The parent node for this node. @type Node - */ - this.parentNode = null; - /** - * The first direct child node of this node, or null if this node has no child nodes. @type Node - */ - this.firstChild = null; - /** - * The last direct child node of this node, or null if this node has no child nodes. @type Node - */ - this.lastChild = null; - /** - * The node immediately preceding this node in the tree, or null if there is no sibling node. @type Node - */ - this.previousSibling = null; - /** - * The node immediately following this node in the tree, or null if there is no sibling node. @type Node - */ - this.nextSibling = null; - - this.addEvents({ - /** - * @event append - * Fires when a new child node is appended - * @param {Tree} tree The owner tree - * @param {Node} this This node - * @param {Node} node The newly appended node - * @param {Number} index The index of the newly appended node - */ - "append" : true, - /** - * @event remove - * Fires when a child node is removed - * @param {Tree} tree The owner tree - * @param {Node} this This node - * @param {Node} node The removed node - */ - "remove" : true, - /** - * @event move - * Fires when this node is moved to a new location in the tree - * @param {Tree} tree The owner tree - * @param {Node} this This node - * @param {Node} oldParent The old parent of this node - * @param {Node} newParent The new parent of this node - * @param {Number} index The index it was moved to - */ - "move" : true, - /** - * @event insert - * Fires when a new child node is inserted. - * @param {Tree} tree The owner tree - * @param {Node} this This node - * @param {Node} node The child node inserted - * @param {Node} refNode The child node the node was inserted before - */ - "insert" : true, - /** - * @event beforeappend - * Fires before a new child is appended, return false to cancel the append. - * @param {Tree} tree The owner tree - * @param {Node} this This node - * @param {Node} node The child node to be appended - */ - "beforeappend" : true, - /** - * @event beforeremove - * Fires before a child is removed, return false to cancel the remove. - * @param {Tree} tree The owner tree - * @param {Node} this This node - * @param {Node} node The child node to be removed - */ - "beforeremove" : true, - /** - * @event beforemove - * Fires before this node is moved to a new location in the tree. Return false to cancel the move. - * @param {Tree} tree The owner tree - * @param {Node} this This node - * @param {Node} oldParent The parent of this node - * @param {Node} newParent The new parent this node is moving to - * @param {Number} index The index it is being moved to - */ - "beforemove" : true, - /** - * @event beforeinsert - * Fires before a new child is inserted, return false to cancel the insert. - * @param {Tree} tree The owner tree - * @param {Node} this This node - * @param {Node} node The child node to be inserted - * @param {Node} refNode The child node the node is being inserted before - */ - "beforeinsert" : true - }); - this.listeners = this.attributes.listeners; - Ext.data.Node.superclass.constructor.call(this); - }, - - // private - fireEvent : function(evtName){ - // first do standard event for this node - if(Ext.data.Node.superclass.fireEvent.apply(this, arguments) === false){ - return false; - } - // then bubble it up to the tree if the event wasn't cancelled - var ot = this.getOwnerTree(); - if(ot){ - if(ot.proxyNodeEvent.apply(ot, arguments) === false){ - return false; - } - } - return true; - }, - - /** - * Returns true if this node is a leaf - * @return {Boolean} - */ - isLeaf : function(){ - return this.leaf === true; - }, - - // private - setFirstChild : function(node){ - this.firstChild = node; - }, - - //private - setLastChild : function(node){ - this.lastChild = node; - }, - - - /** - * Returns true if this node is the last child of its parent - * @return {Boolean} - */ - isLast : function(){ - return (!this.parentNode ? true : this.parentNode.lastChild == this); - }, - - /** - * Returns true if this node is the first child of its parent - * @return {Boolean} - */ - isFirst : function(){ - return (!this.parentNode ? true : this.parentNode.firstChild == this); - }, - - /** - * Returns true if this node has one or more child nodes, else false. - * @return {Boolean} - */ - hasChildNodes : function(){ - return !this.isLeaf() && this.childNodes.length > 0; - }, - - /** - * Returns true if this node has one or more child nodes, or if the expandable - * node attribute is explicitly specified as true (see {@link #attributes}), otherwise returns false. - * @return {Boolean} - */ - isExpandable : function(){ - return this.attributes.expandable || this.hasChildNodes(); - }, - - /** - * Insert node(s) as the last child node of this node. - * @param {Node/Array} node The node or Array of nodes to append - * @return {Node} The appended node if single append, or null if an array was passed - */ - appendChild : function(node){ - var multi = false; - if(Ext.isArray(node)){ - multi = node; - }else if(arguments.length > 1){ - multi = arguments; - } - // if passed an array or multiple args do them one by one - if(multi){ - for(var i = 0, len = multi.length; i < len; i++) { - this.appendChild(multi[i]); - } - }else{ - if(this.fireEvent("beforeappend", this.ownerTree, this, node) === false){ - return false; - } - var index = this.childNodes.length; - var oldParent = node.parentNode; - // it's a move, make sure we move it cleanly - if(oldParent){ - if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index) === false){ - return false; - } - oldParent.removeChild(node); - } - index = this.childNodes.length; - if(index === 0){ - this.setFirstChild(node); - } - this.childNodes.push(node); - node.parentNode = this; - var ps = this.childNodes[index-1]; - if(ps){ - node.previousSibling = ps; - ps.nextSibling = node; - }else{ - node.previousSibling = null; - } - node.nextSibling = null; - this.setLastChild(node); - node.setOwnerTree(this.getOwnerTree()); - this.fireEvent("append", this.ownerTree, this, node, index); - if(oldParent){ - node.fireEvent("move", this.ownerTree, node, oldParent, this, index); - } - return node; - } - }, - - /** - * Removes a child node from this node. - * @param {Node} node The node to remove - * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. - * @return {Node} The removed node - */ - removeChild : function(node, destroy){ - var index = this.childNodes.indexOf(node); - if(index == -1){ - return false; - } - if(this.fireEvent("beforeremove", this.ownerTree, this, node) === false){ - return false; - } - - // remove it from childNodes collection - this.childNodes.splice(index, 1); - - // update siblings - if(node.previousSibling){ - node.previousSibling.nextSibling = node.nextSibling; - } - if(node.nextSibling){ - node.nextSibling.previousSibling = node.previousSibling; - } - - // update child refs - if(this.firstChild == node){ - this.setFirstChild(node.nextSibling); - } - if(this.lastChild == node){ - this.setLastChild(node.previousSibling); - } - - this.fireEvent("remove", this.ownerTree, this, node); - if(destroy){ - node.destroy(true); - }else{ - node.clear(); - } - return node; - }, - - // private - clear : function(destroy){ - // clear any references from the node - this.setOwnerTree(null, destroy); - this.parentNode = this.previousSibling = this.nextSibling = null; - if(destroy){ - this.firstChild = this.lastChild = null; - } - }, - - /** - * Destroys the node. - */ - destroy : function(/* private */ silent){ - /* - * Silent is to be used in a number of cases - * 1) When setRootNode is called. - * 2) When destroy on the tree is called - * 3) For destroying child nodes on a node - */ - if(silent === true){ - this.purgeListeners(); - this.clear(true); - Ext.each(this.childNodes, function(n){ - n.destroy(true); - }); - this.childNodes = null; - }else{ - this.remove(true); - } - }, - - /** - * Inserts the first node before the second node in this nodes childNodes collection. - * @param {Node} node The node to insert - * @param {Node} refNode The node to insert before (if null the node is appended) - * @return {Node} The inserted node - */ - insertBefore : function(node, refNode){ - if(!refNode){ // like standard Dom, refNode can be null for append - return this.appendChild(node); - } - // nothing to do - if(node == refNode){ - return false; - } - - if(this.fireEvent("beforeinsert", this.ownerTree, this, node, refNode) === false){ - return false; - } - var index = this.childNodes.indexOf(refNode); - var oldParent = node.parentNode; - var refIndex = index; - - // when moving internally, indexes will change after remove - if(oldParent == this && this.childNodes.indexOf(node) < index){ - refIndex--; - } - - // it's a move, make sure we move it cleanly - if(oldParent){ - if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index, refNode) === false){ - return false; - } - oldParent.removeChild(node); - } - if(refIndex === 0){ - this.setFirstChild(node); - } - this.childNodes.splice(refIndex, 0, node); - node.parentNode = this; - var ps = this.childNodes[refIndex-1]; - if(ps){ - node.previousSibling = ps; - ps.nextSibling = node; - }else{ - node.previousSibling = null; - } - node.nextSibling = refNode; - refNode.previousSibling = node; - node.setOwnerTree(this.getOwnerTree()); - this.fireEvent("insert", this.ownerTree, this, node, refNode); - if(oldParent){ - node.fireEvent("move", this.ownerTree, node, oldParent, this, refIndex, refNode); - } - return node; - }, - - /** - * Removes this node from its parent - * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. - * @return {Node} this - */ - remove : function(destroy){ - if (this.parentNode) { - this.parentNode.removeChild(this, destroy); - } - return this; - }, - - /** - * Removes all child nodes from this node. - * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. - * @return {Node} this - */ - removeAll : function(destroy){ - var cn = this.childNodes, - n; - while((n = cn[0])){ - this.removeChild(n, destroy); - } - return this; - }, - - /** - * Returns the child node at the specified index. - * @param {Number} index - * @return {Node} - */ - item : function(index){ - return this.childNodes[index]; - }, - - /** - * Replaces one child node in this node with another. - * @param {Node} newChild The replacement node - * @param {Node} oldChild The node to replace - * @return {Node} The replaced node - */ - replaceChild : function(newChild, oldChild){ - var s = oldChild ? oldChild.nextSibling : null; - this.removeChild(oldChild); - this.insertBefore(newChild, s); - return oldChild; - }, - - /** - * Returns the index of a child node - * @param {Node} node - * @return {Number} The index of the node or -1 if it was not found - */ - indexOf : function(child){ - return this.childNodes.indexOf(child); - }, - - /** - * Returns the tree this node is in. - * @return {Tree} - */ - getOwnerTree : function(){ - // if it doesn't have one, look for one - if(!this.ownerTree){ - var p = this; - while(p){ - if(p.ownerTree){ - this.ownerTree = p.ownerTree; - break; - } - p = p.parentNode; - } - } - return this.ownerTree; - }, - - /** - * Returns depth of this node (the root node has a depth of 0) - * @return {Number} - */ - getDepth : function(){ - var depth = 0; - var p = this; - while(p.parentNode){ - ++depth; - p = p.parentNode; - } - return depth; - }, - - // private - setOwnerTree : function(tree, destroy){ - // if it is a move, we need to update everyone - if(tree != this.ownerTree){ - if(this.ownerTree){ - this.ownerTree.unregisterNode(this); - } - this.ownerTree = tree; - // If we're destroying, we don't need to recurse since it will be called on each child node - if(destroy !== true){ - Ext.each(this.childNodes, function(n){ - n.setOwnerTree(tree); - }); - } - if(tree){ - tree.registerNode(this); - } - } - }, - - /** - * Changes the id of this node. - * @param {String} id The new id for the node. - */ - setId: function(id){ - if(id !== this.id){ - var t = this.ownerTree; - if(t){ - t.unregisterNode(this); - } - this.id = this.attributes.id = id; - if(t){ - t.registerNode(this); - } - this.onIdChange(id); - } - }, - - // private - onIdChange: Ext.emptyFn, - - /** - * Returns the path for this node. The path can be used to expand or select this node programmatically. - * @param {String} attr (optional) The attr to use for the path (defaults to the node's id) - * @return {String} The path - */ - getPath : function(attr){ - attr = attr || "id"; - var p = this.parentNode; - var b = [this.attributes[attr]]; - while(p){ - b.unshift(p.attributes[attr]); - p = p.parentNode; - } - var sep = this.getOwnerTree().pathSeparator; - return sep + b.join(sep); - }, - - /** - * Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function - * will be the args provided or the current node. If the function returns false at any point, - * the bubble is stopped. - * @param {Function} fn The function to call - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. - * @param {Array} args (optional) The args to call the function with (default to passing the current Node) - */ - bubble : function(fn, scope, args){ - var p = this; - while(p){ - if(fn.apply(scope || p, args || [p]) === false){ - break; - } - p = p.parentNode; - } - }, - - /** - * Cascades down the tree from this node, calling the specified function with each node. The arguments to the function - * will be the args provided or the current node. If the function returns false at any point, - * the cascade is stopped on that branch. - * @param {Function} fn The function to call - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. - * @param {Array} args (optional) The args to call the function with (default to passing the current Node) - */ - cascade : function(fn, scope, args){ - if(fn.apply(scope || this, args || [this]) !== false){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].cascade(fn, scope, args); - } - } - }, - - /** - * Interates the child nodes of this node, calling the specified function with each node. The arguments to the function - * will be the args provided or the current node. If the function returns false at any point, - * the iteration stops. - * @param {Function} fn The function to call - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node in the iteration. - * @param {Array} args (optional) The args to call the function with (default to passing the current Node) - */ - eachChild : function(fn, scope, args){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - if(fn.apply(scope || cs[i], args || [cs[i]]) === false){ - break; - } - } - }, - - /** - * Finds the first child that has the attribute with the specified value. - * @param {String} attribute The attribute name - * @param {Mixed} value The value to search for - * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children - * @return {Node} The found child or null if none was found - */ - findChild : function(attribute, value, deep){ - return this.findChildBy(function(){ - return this.attributes[attribute] == value; - }, null, deep); - }, - - /** - * Finds the first child by a custom function. The child matches if the function passed returns true. - * @param {Function} fn A function which must return true if the passed Node is the required Node. - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the Node being tested. - * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children - * @return {Node} The found child or null if none was found - */ - findChildBy : function(fn, scope, deep){ - var cs = this.childNodes, - len = cs.length, - i = 0, - n, - res; - for(; i < len; i++){ - n = cs[i]; - if(fn.call(scope || n, n) === true){ - return n; - }else if (deep){ - res = n.findChildBy(fn, scope, deep); - if(res != null){ - return res; - } - } - - } - return null; - }, - - /** - * Sorts this nodes children using the supplied sort function. - * @param {Function} fn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order. - * @param {Object} scope (optional)The scope (this reference) in which the function is executed. Defaults to the browser window. - */ - sort : function(fn, scope){ - var cs = this.childNodes; - var len = cs.length; - if(len > 0){ - var sortFn = scope ? function(){fn.apply(scope, arguments);} : fn; - cs.sort(sortFn); - for(var i = 0; i < len; i++){ - var n = cs[i]; - n.previousSibling = cs[i-1]; - n.nextSibling = cs[i+1]; - if(i === 0){ - this.setFirstChild(n); - } - if(i == len-1){ - this.setLastChild(n); - } - } - } - }, - - /** - * Returns true if this node is an ancestor (at any point) of the passed node. - * @param {Node} node - * @return {Boolean} - */ - contains : function(node){ - return node.isAncestor(this); - }, - - /** - * Returns true if the passed node is an ancestor (at any point) of this node. - * @param {Node} node - * @return {Boolean} - */ - isAncestor : function(node){ - var p = this.parentNode; - while(p){ - if(p == node){ - return true; - } - p = p.parentNode; - } - return false; - }, - - toString : function(){ - return "[Node"+(this.id?" "+this.id:"")+"]"; - } -});/** - * @class Ext.tree.TreeNode - * @extends Ext.data.Node - * @cfg {String} text The text for this node - * @cfg {Boolean} expanded true to start the node expanded - * @cfg {Boolean} allowDrag False to make this node undraggable if {@link #draggable} = true (defaults to true) - * @cfg {Boolean} allowDrop False if this node cannot have child nodes dropped on it (defaults to true) - * @cfg {Boolean} disabled true to start the node disabled - * @cfg {String} icon The path to an icon for the node. The preferred way to do this - * is to use the cls or iconCls attributes and add the icon via a CSS background image. - * @cfg {String} cls A css class to be added to the node - * @cfg {String} iconCls A css class to be added to the nodes icon element for applying css background images - * @cfg {String} href URL of the link used for the node (defaults to #) - * @cfg {String} hrefTarget target frame for the link - * @cfg {Boolean} hidden True to render hidden. (Defaults to false). - * @cfg {String} qtip An Ext QuickTip for the node - * @cfg {Boolean} expandable If set to true, the node will always show a plus/minus icon, even when empty - * @cfg {String} qtipCfg An Ext QuickTip config for the node (used instead of qtip) - * @cfg {Boolean} singleClickExpand True for single click expand on this node - * @cfg {Function} uiProvider A UI class to use for this node (defaults to Ext.tree.TreeNodeUI) - * @cfg {Boolean} checked True to render a checked checkbox for this node, false to render an unchecked checkbox - * (defaults to undefined with no checkbox rendered) - * @cfg {Boolean} draggable True to make this node draggable (defaults to false) - * @cfg {Boolean} isTarget False to not allow this node to act as a drop target (defaults to true) - * @cfg {Boolean} allowChildren False to not allow this node to have child nodes (defaults to true) - * @cfg {Boolean} editable False to not allow this node to be edited by an {@link Ext.tree.TreeEditor} (defaults to true) - * @constructor - * @param {Object/String} attributes The attributes/config for the node or just a string with the text for the node - */ -Ext.tree.TreeNode = Ext.extend(Ext.data.Node, { - - constructor : function(attributes){ - attributes = attributes || {}; - if(Ext.isString(attributes)){ - attributes = {text: attributes}; - } - this.childrenRendered = false; - this.rendered = false; - Ext.tree.TreeNode.superclass.constructor.call(this, attributes); - this.expanded = attributes.expanded === true; - this.isTarget = attributes.isTarget !== false; - this.draggable = attributes.draggable !== false && attributes.allowDrag !== false; - this.allowChildren = attributes.allowChildren !== false && attributes.allowDrop !== false; - - /** - * Read-only. The text for this node. To change it use {@link #setText}. - * @type String - */ - this.text = attributes.text; - /** - * True if this node is disabled. - * @type Boolean - */ - this.disabled = attributes.disabled === true; - /** - * True if this node is hidden. - * @type Boolean - */ - this.hidden = attributes.hidden === true; - - this.addEvents( - /** - * @event textchange - * Fires when the text for this node is changed - * @param {Node} this This node - * @param {String} text The new text - * @param {String} oldText The old text - */ - 'textchange', - /** - * @event beforeexpand - * Fires before this node is expanded, return false to cancel. - * @param {Node} this This node - * @param {Boolean} deep - * @param {Boolean} anim - */ - 'beforeexpand', - /** - * @event beforecollapse - * Fires before this node is collapsed, return false to cancel. - * @param {Node} this This node - * @param {Boolean} deep - * @param {Boolean} anim - */ - 'beforecollapse', - /** - * @event expand - * Fires when this node is expanded - * @param {Node} this This node - */ - 'expand', - /** - * @event disabledchange - * Fires when the disabled status of this node changes - * @param {Node} this This node - * @param {Boolean} disabled - */ - 'disabledchange', - /** - * @event collapse - * Fires when this node is collapsed - * @param {Node} this This node - */ - 'collapse', - /** - * @event beforeclick - * Fires before click processing. Return false to cancel the default action. - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'beforeclick', - /** - * @event click - * Fires when this node is clicked - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'click', - /** - * @event checkchange - * Fires when a node with a checkbox's checked property changes - * @param {Node} this This node - * @param {Boolean} checked - */ - 'checkchange', - /** - * @event beforedblclick - * Fires before double click processing. Return false to cancel the default action. - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'beforedblclick', - /** - * @event dblclick - * Fires when this node is double clicked - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'dblclick', - /** - * @event contextmenu - * Fires when this node is right clicked - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'contextmenu', - /** - * @event beforechildrenrendered - * Fires right before the child nodes for this node are rendered - * @param {Node} this This node - */ - 'beforechildrenrendered' - ); - - var uiClass = this.attributes.uiProvider || this.defaultUI || Ext.tree.TreeNodeUI; - - /** - * Read-only. The UI for this node - * @type TreeNodeUI - */ - this.ui = new uiClass(this); - }, - - preventHScroll : true, - /** - * Returns true if this node is expanded - * @return {Boolean} - */ - isExpanded : function(){ - return this.expanded; - }, - -/** - * Returns the UI object for this node. - * @return {TreeNodeUI} The object which is providing the user interface for this tree - * node. Unless otherwise specified in the {@link #uiProvider}, this will be an instance - * of {@link Ext.tree.TreeNodeUI} - */ - getUI : function(){ - return this.ui; - }, - - getLoader : function(){ - var owner; - return this.loader || ((owner = this.getOwnerTree()) && owner.loader ? owner.loader : (this.loader = new Ext.tree.TreeLoader())); - }, - - // private override - setFirstChild : function(node){ - var of = this.firstChild; - Ext.tree.TreeNode.superclass.setFirstChild.call(this, node); - if(this.childrenRendered && of && node != of){ - of.renderIndent(true, true); - } - if(this.rendered){ - this.renderIndent(true, true); - } - }, - - // private override - setLastChild : function(node){ - var ol = this.lastChild; - Ext.tree.TreeNode.superclass.setLastChild.call(this, node); - if(this.childrenRendered && ol && node != ol){ - ol.renderIndent(true, true); - } - if(this.rendered){ - this.renderIndent(true, true); - } - }, - - // these methods are overridden to provide lazy rendering support - // private override - appendChild : function(n){ - if(!n.render && !Ext.isArray(n)){ - n = this.getLoader().createNode(n); - } - var node = Ext.tree.TreeNode.superclass.appendChild.call(this, n); - if(node && this.childrenRendered){ - node.render(); - } - this.ui.updateExpandIcon(); - return node; - }, - - // private override - removeChild : function(node, destroy){ - this.ownerTree.getSelectionModel().unselect(node); - Ext.tree.TreeNode.superclass.removeChild.apply(this, arguments); - // only update the ui if we're not destroying - if(!destroy){ - var rendered = node.ui.rendered; - // if it's been rendered remove dom node - if(rendered){ - node.ui.remove(); - } - if(rendered && this.childNodes.length < 1){ - this.collapse(false, false); - }else{ - this.ui.updateExpandIcon(); - } - if(!this.firstChild && !this.isHiddenRoot()){ - this.childrenRendered = false; - } - } - return node; - }, - - // private override - insertBefore : function(node, refNode){ - if(!node.render){ - node = this.getLoader().createNode(node); - } - var newNode = Ext.tree.TreeNode.superclass.insertBefore.call(this, node, refNode); - if(newNode && refNode && this.childrenRendered){ - node.render(); - } - this.ui.updateExpandIcon(); - return newNode; - }, - - /** - * Sets the text for this node - * @param {String} text - */ - setText : function(text){ - var oldText = this.text; - this.text = this.attributes.text = text; - if(this.rendered){ // event without subscribing - this.ui.onTextChange(this, text, oldText); - } - this.fireEvent('textchange', this, text, oldText); - }, - - /** - * Sets the icon class for this node. - * @param {String} cls - */ - setIconCls : function(cls){ - var old = this.attributes.iconCls; - this.attributes.iconCls = cls; - if(this.rendered){ - this.ui.onIconClsChange(this, cls, old); - } - }, - - /** - * Sets the tooltip for this node. - * @param {String} tip The text for the tip - * @param {String} title (Optional) The title for the tip - */ - setTooltip : function(tip, title){ - this.attributes.qtip = tip; - this.attributes.qtipTitle = title; - if(this.rendered){ - this.ui.onTipChange(this, tip, title); - } - }, - - /** - * Sets the icon for this node. - * @param {String} icon - */ - setIcon : function(icon){ - this.attributes.icon = icon; - if(this.rendered){ - this.ui.onIconChange(this, icon); - } - }, - - /** - * Sets the href for the node. - * @param {String} href The href to set - * @param {String} (Optional) target The target of the href - */ - setHref : function(href, target){ - this.attributes.href = href; - this.attributes.hrefTarget = target; - if(this.rendered){ - this.ui.onHrefChange(this, href, target); - } - }, - - /** - * Sets the class on this node. - * @param {String} cls - */ - setCls : function(cls){ - var old = this.attributes.cls; - this.attributes.cls = cls; - if(this.rendered){ - this.ui.onClsChange(this, cls, old); - } - }, - - /** - * Triggers selection of this node - */ - select : function(){ - var t = this.getOwnerTree(); - if(t){ - t.getSelectionModel().select(this); - } - }, - - /** - * Triggers deselection of this node - * @param {Boolean} silent (optional) True to stop selection change events from firing. - */ - unselect : function(silent){ - var t = this.getOwnerTree(); - if(t){ - t.getSelectionModel().unselect(this, silent); - } - }, - - /** - * Returns true if this node is selected - * @return {Boolean} - */ - isSelected : function(){ - var t = this.getOwnerTree(); - return t ? t.getSelectionModel().isSelected(this) : false; - }, - - /** - * Expand this node. - * @param {Boolean} deep (optional) True to expand all children as well - * @param {Boolean} anim (optional) false to cancel the default animation - * @param {Function} callback (optional) A callback to be called when - * expanding this node completes (does not wait for deep expand to complete). - * Called with 1 parameter, this node. - * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this TreeNode. - */ - expand : function(deep, anim, callback, scope){ - if(!this.expanded){ - if(this.fireEvent('beforeexpand', this, deep, anim) === false){ - return; - } - if(!this.childrenRendered){ - this.renderChildren(); - } - this.expanded = true; - if(!this.isHiddenRoot() && (this.getOwnerTree().animate && anim !== false) || anim){ - this.ui.animExpand(function(){ - this.fireEvent('expand', this); - this.runCallback(callback, scope || this, [this]); - if(deep === true){ - this.expandChildNodes(true, true); - } - }.createDelegate(this)); - return; - }else{ - this.ui.expand(); - this.fireEvent('expand', this); - this.runCallback(callback, scope || this, [this]); - } - }else{ - this.runCallback(callback, scope || this, [this]); - } - if(deep === true){ - this.expandChildNodes(true); - } - }, - - runCallback : function(cb, scope, args){ - if(Ext.isFunction(cb)){ - cb.apply(scope, args); - } - }, - - isHiddenRoot : function(){ - return this.isRoot && !this.getOwnerTree().rootVisible; - }, - - /** - * Collapse this node. - * @param {Boolean} deep (optional) True to collapse all children as well - * @param {Boolean} anim (optional) false to cancel the default animation - * @param {Function} callback (optional) A callback to be called when - * expanding this node completes (does not wait for deep expand to complete). - * Called with 1 parameter, this node. - * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this TreeNode. - */ - collapse : function(deep, anim, callback, scope){ - if(this.expanded && !this.isHiddenRoot()){ - if(this.fireEvent('beforecollapse', this, deep, anim) === false){ - return; - } - this.expanded = false; - if((this.getOwnerTree().animate && anim !== false) || anim){ - this.ui.animCollapse(function(){ - this.fireEvent('collapse', this); - this.runCallback(callback, scope || this, [this]); - if(deep === true){ - this.collapseChildNodes(true); - } - }.createDelegate(this)); - return; - }else{ - this.ui.collapse(); - this.fireEvent('collapse', this); - this.runCallback(callback, scope || this, [this]); - } - }else if(!this.expanded){ - this.runCallback(callback, scope || this, [this]); - } - if(deep === true){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].collapse(true, false); - } - } - }, - - // private - delayedExpand : function(delay){ - if(!this.expandProcId){ - this.expandProcId = this.expand.defer(delay, this); - } - }, - - // private - cancelExpand : function(){ - if(this.expandProcId){ - clearTimeout(this.expandProcId); - } - this.expandProcId = false; - }, - - /** - * Toggles expanded/collapsed state of the node - */ - toggle : function(){ - if(this.expanded){ - this.collapse(); - }else{ - this.expand(); - } - }, - - /** - * Ensures all parent nodes are expanded, and if necessary, scrolls - * the node into view. - * @param {Function} callback (optional) A function to call when the node has been made visible. - * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this TreeNode. - */ - ensureVisible : function(callback, scope){ - var tree = this.getOwnerTree(); - tree.expandPath(this.parentNode ? this.parentNode.getPath() : this.getPath(), false, function(){ - var node = tree.getNodeById(this.id); // Somehow if we don't do this, we lose changes that happened to node in the meantime - tree.getTreeEl().scrollChildIntoView(node.ui.anchor); - this.runCallback(callback, scope || this, [this]); - }.createDelegate(this)); - }, - - /** - * Expand all child nodes - * @param {Boolean} deep (optional) true if the child nodes should also expand their child nodes - */ - expandChildNodes : function(deep, anim) { - var cs = this.childNodes, - i, - len = cs.length; - for (i = 0; i < len; i++) { - cs[i].expand(deep, anim); - } - }, - - /** - * Collapse all child nodes - * @param {Boolean} deep (optional) true if the child nodes should also collapse their child nodes - */ - collapseChildNodes : function(deep){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].collapse(deep); - } - }, - - /** - * Disables this node - */ - disable : function(){ - this.disabled = true; - this.unselect(); - if(this.rendered && this.ui.onDisableChange){ // event without subscribing - this.ui.onDisableChange(this, true); - } - this.fireEvent('disabledchange', this, true); - }, - - /** - * Enables this node - */ - enable : function(){ - this.disabled = false; - if(this.rendered && this.ui.onDisableChange){ // event without subscribing - this.ui.onDisableChange(this, false); - } - this.fireEvent('disabledchange', this, false); - }, - - // private - renderChildren : function(suppressEvent){ - if(suppressEvent !== false){ - this.fireEvent('beforechildrenrendered', this); - } - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].render(true); - } - this.childrenRendered = true; - }, - - // private - sort : function(fn, scope){ - Ext.tree.TreeNode.superclass.sort.apply(this, arguments); - if(this.childrenRendered){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].render(true); - } - } - }, - - // private - render : function(bulkRender){ - this.ui.render(bulkRender); - if(!this.rendered){ - // make sure it is registered - this.getOwnerTree().registerNode(this); - this.rendered = true; - if(this.expanded){ - this.expanded = false; - this.expand(false, false); - } - } - }, - - // private - renderIndent : function(deep, refresh){ - if(refresh){ - this.ui.childIndent = null; - } - this.ui.renderIndent(); - if(deep === true && this.childrenRendered){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].renderIndent(true, refresh); - } - } - }, - - beginUpdate : function(){ - this.childrenRendered = false; - }, - - endUpdate : function(){ - if(this.expanded && this.rendered){ - this.renderChildren(); - } - }, - - //inherit docs - destroy : function(silent){ - if(silent === true){ - this.unselect(true); - } - Ext.tree.TreeNode.superclass.destroy.call(this, silent); - Ext.destroy(this.ui, this.loader); - this.ui = this.loader = null; - }, - - // private - onIdChange : function(id){ - this.ui.onIdChange(id); - } -}); - -Ext.tree.TreePanel.nodeTypes.node = Ext.tree.TreeNode;/** - * @class Ext.tree.AsyncTreeNode - * @extends Ext.tree.TreeNode - * @cfg {TreeLoader} loader A TreeLoader to be used by this node (defaults to the loader defined on the tree) - * @constructor - * @param {Object/String} attributes The attributes/config for the node or just a string with the text for the node - */ - Ext.tree.AsyncTreeNode = function(config){ - this.loaded = config && config.loaded === true; - this.loading = false; - Ext.tree.AsyncTreeNode.superclass.constructor.apply(this, arguments); - /** - * @event beforeload - * Fires before this node is loaded, return false to cancel - * @param {Node} this This node - */ - this.addEvents('beforeload', 'load'); - /** - * @event load - * Fires when this node is loaded - * @param {Node} this This node - */ - /** - * The loader used by this node (defaults to using the tree's defined loader) - * @type TreeLoader - * @property loader - */ -}; -Ext.extend(Ext.tree.AsyncTreeNode, Ext.tree.TreeNode, { - expand : function(deep, anim, callback, scope){ - if(this.loading){ // if an async load is already running, waiting til it's done - var timer; - var f = function(){ - if(!this.loading){ // done loading - clearInterval(timer); - this.expand(deep, anim, callback, scope); - } - }.createDelegate(this); - timer = setInterval(f, 200); - return; - } - if(!this.loaded){ - if(this.fireEvent("beforeload", this) === false){ - return; - } - this.loading = true; - this.ui.beforeLoad(this); - var loader = this.loader || this.attributes.loader || this.getOwnerTree().getLoader(); - if(loader){ - loader.load(this, this.loadComplete.createDelegate(this, [deep, anim, callback, scope]), this); - return; - } - } - Ext.tree.AsyncTreeNode.superclass.expand.call(this, deep, anim, callback, scope); - }, - - /** - * Returns true if this node is currently loading - * @return {Boolean} - */ - isLoading : function(){ - return this.loading; - }, - - loadComplete : function(deep, anim, callback, scope){ - this.loading = false; - this.loaded = true; - this.ui.afterLoad(this); - this.fireEvent("load", this); - this.expand(deep, anim, callback, scope); - }, - - /** - * Returns true if this node has been loaded - * @return {Boolean} - */ - isLoaded : function(){ - return this.loaded; - }, - - hasChildNodes : function(){ - if(!this.isLeaf() && !this.loaded){ - return true; - }else{ - return Ext.tree.AsyncTreeNode.superclass.hasChildNodes.call(this); - } - }, - - /** - * Trigger a reload for this node - * @param {Function} callback - * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this Node. - */ - reload : function(callback, scope){ - this.collapse(false, false); - while(this.firstChild){ - this.removeChild(this.firstChild).destroy(); - } - this.childrenRendered = false; - this.loaded = false; - if(this.isHiddenRoot()){ - this.expanded = false; - } - this.expand(false, false, callback, scope); - } -}); - -Ext.tree.TreePanel.nodeTypes.async = Ext.tree.AsyncTreeNode;/** - * @class Ext.tree.TreeNodeUI - * This class provides the default UI implementation for Ext TreeNodes. - * The TreeNode UI implementation is separate from the - * tree implementation, and allows customizing of the appearance of - * tree nodes.
    - *

    - * If you are customizing the Tree's user interface, you - * may need to extend this class, but you should never need to instantiate this class.
    - *

    - * This class provides access to the user interface components of an Ext TreeNode, through - * {@link Ext.tree.TreeNode#getUI} - */ -Ext.tree.TreeNodeUI = Ext.extend(Object, { - - constructor : function(node){ - Ext.apply(this, { - node: node, - rendered: false, - animating: false, - wasLeaf: true, - ecc: 'x-tree-ec-icon x-tree-elbow', - emptyIcon: Ext.BLANK_IMAGE_URL - }); - }, - - // private - removeChild : function(node){ - if(this.rendered){ - this.ctNode.removeChild(node.ui.getEl()); - } - }, - - // private - beforeLoad : function(){ - this.addClass("x-tree-node-loading"); - }, - - // private - afterLoad : function(){ - this.removeClass("x-tree-node-loading"); - }, - - // private - onTextChange : function(node, text, oldText){ - if(this.rendered){ - this.textNode.innerHTML = text; - } - }, - - // private - onIconClsChange : function(node, cls, oldCls){ - if(this.rendered){ - Ext.fly(this.iconNode).replaceClass(oldCls, cls); - } - }, - - // private - onIconChange : function(node, icon){ - if(this.rendered){ - //'', - var empty = Ext.isEmpty(icon); - this.iconNode.src = empty ? this.emptyIcon : icon; - Ext.fly(this.iconNode)[empty ? 'removeClass' : 'addClass']('x-tree-node-inline-icon'); - } - }, - - // private - onTipChange : function(node, tip, title){ - if(this.rendered){ - var hasTitle = Ext.isDefined(title); - if(this.textNode.setAttributeNS){ - this.textNode.setAttributeNS("ext", "qtip", tip); - if(hasTitle){ - this.textNode.setAttributeNS("ext", "qtitle", title); - } - }else{ - this.textNode.setAttribute("ext:qtip", tip); - if(hasTitle){ - this.textNode.setAttribute("ext:qtitle", title); - } - } - } - }, - - // private - onHrefChange : function(node, href, target){ - if(this.rendered){ - this.anchor.href = this.getHref(href); - if(Ext.isDefined(target)){ - this.anchor.target = target; - } - } - }, - - // private - onClsChange : function(node, cls, oldCls){ - if(this.rendered){ - Ext.fly(this.elNode).replaceClass(oldCls, cls); - } - }, - - // private - onDisableChange : function(node, state){ - this.disabled = state; - if (this.checkbox) { - this.checkbox.disabled = state; - } - this[state ? 'addClass' : 'removeClass']('x-tree-node-disabled'); - }, - - // private - onSelectedChange : function(state){ - if(state){ - this.focus(); - this.addClass("x-tree-selected"); - }else{ - //this.blur(); - this.removeClass("x-tree-selected"); - } - }, - - // private - onMove : function(tree, node, oldParent, newParent, index, refNode){ - this.childIndent = null; - if(this.rendered){ - var targetNode = newParent.ui.getContainer(); - if(!targetNode){//target not rendered - this.holder = document.createElement("div"); - this.holder.appendChild(this.wrap); - return; - } - var insertBefore = refNode ? refNode.ui.getEl() : null; - if(insertBefore){ - targetNode.insertBefore(this.wrap, insertBefore); - }else{ - targetNode.appendChild(this.wrap); - } - this.node.renderIndent(true, oldParent != newParent); - } - }, - -/** - * Adds one or more CSS classes to the node's UI element. - * Duplicate classes are automatically filtered out. - * @param {String/Array} className The CSS class to add, or an array of classes - */ - addClass : function(cls){ - if(this.elNode){ - Ext.fly(this.elNode).addClass(cls); - } - }, - -/** - * Removes one or more CSS classes from the node's UI element. - * @param {String/Array} className The CSS class to remove, or an array of classes - */ - removeClass : function(cls){ - if(this.elNode){ - Ext.fly(this.elNode).removeClass(cls); - } - }, - - // private - remove : function(){ - if(this.rendered){ - this.holder = document.createElement("div"); - this.holder.appendChild(this.wrap); - } - }, - - // private - fireEvent : function(){ - return this.node.fireEvent.apply(this.node, arguments); - }, - - // private - initEvents : function(){ - this.node.on("move", this.onMove, this); - - if(this.node.disabled){ - this.onDisableChange(this.node, true); - } - if(this.node.hidden){ - this.hide(); - } - var ot = this.node.getOwnerTree(); - var dd = ot.enableDD || ot.enableDrag || ot.enableDrop; - if(dd && (!this.node.isRoot || ot.rootVisible)){ - Ext.dd.Registry.register(this.elNode, { - node: this.node, - handles: this.getDDHandles(), - isHandle: false - }); - } - }, - - // private - getDDHandles : function(){ - return [this.iconNode, this.textNode, this.elNode]; - }, - -/** - * Hides this node. - */ - hide : function(){ - this.node.hidden = true; - if(this.wrap){ - this.wrap.style.display = "none"; - } - }, - -/** - * Shows this node. - */ - show : function(){ - this.node.hidden = false; - if(this.wrap){ - this.wrap.style.display = ""; - } - }, - - // private - onContextMenu : function(e){ - if (this.node.hasListener("contextmenu") || this.node.getOwnerTree().hasListener("contextmenu")) { - e.preventDefault(); - this.focus(); - this.fireEvent("contextmenu", this.node, e); - } - }, - - // private - onClick : function(e){ - if(this.dropping){ - e.stopEvent(); - return; - } - if(this.fireEvent("beforeclick", this.node, e) !== false){ - var a = e.getTarget('a'); - if(!this.disabled && this.node.attributes.href && a){ - this.fireEvent("click", this.node, e); - return; - }else if(a && e.ctrlKey){ - e.stopEvent(); - } - e.preventDefault(); - if(this.disabled){ - return; - } - - if(this.node.attributes.singleClickExpand && !this.animating && this.node.isExpandable()){ - this.node.toggle(); - } - - this.fireEvent("click", this.node, e); - }else{ - e.stopEvent(); - } - }, - - // private - onDblClick : function(e){ - e.preventDefault(); - if(this.disabled){ - return; - } - if(this.fireEvent("beforedblclick", this.node, e) !== false){ - if(this.checkbox){ - this.toggleCheck(); - } - if(!this.animating && this.node.isExpandable()){ - this.node.toggle(); - } - this.fireEvent("dblclick", this.node, e); - } - }, - - onOver : function(e){ - this.addClass('x-tree-node-over'); - }, - - onOut : function(e){ - this.removeClass('x-tree-node-over'); - }, - - // private - onCheckChange : function(){ - var checked = this.checkbox.checked; - // fix for IE6 - this.checkbox.defaultChecked = checked; - this.node.attributes.checked = checked; - this.fireEvent('checkchange', this.node, checked); - }, - - // private - ecClick : function(e){ - if(!this.animating && this.node.isExpandable()){ - this.node.toggle(); - } - }, - - // private - startDrop : function(){ - this.dropping = true; - }, - - // delayed drop so the click event doesn't get fired on a drop - endDrop : function(){ - setTimeout(function(){ - this.dropping = false; - }.createDelegate(this), 50); - }, - - // private - expand : function(){ - this.updateExpandIcon(); - this.ctNode.style.display = ""; - }, - - // private - focus : function(){ - if(!this.node.preventHScroll){ - try{this.anchor.focus(); - }catch(e){} - }else{ - try{ - var noscroll = this.node.getOwnerTree().getTreeEl().dom; - var l = noscroll.scrollLeft; - this.anchor.focus(); - noscroll.scrollLeft = l; - }catch(e){} - } - }, - -/** - * Sets the checked status of the tree node to the passed value, or, if no value was passed, - * toggles the checked status. If the node was rendered with no checkbox, this has no effect. - * @param {Boolean} value (optional) The new checked status. - */ - toggleCheck : function(value){ - var cb = this.checkbox; - if(cb){ - cb.checked = (value === undefined ? !cb.checked : value); - this.onCheckChange(); - } - }, - - // private - blur : function(){ - try{ - this.anchor.blur(); - }catch(e){} - }, - - // private - animExpand : function(callback){ - var ct = Ext.get(this.ctNode); - ct.stopFx(); - if(!this.node.isExpandable()){ - this.updateExpandIcon(); - this.ctNode.style.display = ""; - Ext.callback(callback); - return; - } - this.animating = true; - this.updateExpandIcon(); - - ct.slideIn('t', { - callback : function(){ - this.animating = false; - Ext.callback(callback); - }, - scope: this, - duration: this.node.ownerTree.duration || .25 - }); - }, - - // private - highlight : function(){ - var tree = this.node.getOwnerTree(); - Ext.fly(this.wrap).highlight( - tree.hlColor || "C3DAF9", - {endColor: tree.hlBaseColor} - ); - }, - - // private - collapse : function(){ - this.updateExpandIcon(); - this.ctNode.style.display = "none"; - }, - - // private - animCollapse : function(callback){ - var ct = Ext.get(this.ctNode); - ct.enableDisplayMode('block'); - ct.stopFx(); - - this.animating = true; - this.updateExpandIcon(); - - ct.slideOut('t', { - callback : function(){ - this.animating = false; - Ext.callback(callback); - }, - scope: this, - duration: this.node.ownerTree.duration || .25 - }); - }, - - // private - getContainer : function(){ - return this.ctNode; - }, - -/** - * Returns the element which encapsulates this node. - * @return {HtmlElement} The DOM element. The default implementation uses a <li>. - */ - getEl : function(){ - return this.wrap; - }, - - // private - appendDDGhost : function(ghostNode){ - ghostNode.appendChild(this.elNode.cloneNode(true)); - }, - - // private - getDDRepairXY : function(){ - return Ext.lib.Dom.getXY(this.iconNode); - }, - - // private - onRender : function(){ - this.render(); - }, - - // private - render : function(bulkRender){ - var n = this.node, a = n.attributes; - var targetNode = n.parentNode ? - n.parentNode.ui.getContainer() : n.ownerTree.innerCt.dom; - - if(!this.rendered){ - this.rendered = true; - - this.renderElements(n, a, targetNode, bulkRender); - - if(a.qtip){ - this.onTipChange(n, a.qtip, a.qtipTitle); - }else if(a.qtipCfg){ - a.qtipCfg.target = Ext.id(this.textNode); - Ext.QuickTips.register(a.qtipCfg); - } - this.initEvents(); - if(!this.node.expanded){ - this.updateExpandIcon(true); - } - }else{ - if(bulkRender === true) { - targetNode.appendChild(this.wrap); - } - } - }, - - // private - renderElements : function(n, a, targetNode, bulkRender){ - // add some indent caching, this helps performance when rendering a large tree - this.indentMarkup = n.parentNode ? n.parentNode.ui.getChildIndent() : ''; - - var cb = Ext.isBoolean(a.checked), - nel, - href = this.getHref(a.href), - buf = ['

  • ', - '',this.indentMarkup,"", - '', - '', - cb ? ('' : '/>')) : '', - '',n.text,"
    ", - '', - "
  • "].join(''); - - if(bulkRender !== true && n.nextSibling && (nel = n.nextSibling.ui.getEl())){ - this.wrap = Ext.DomHelper.insertHtml("beforeBegin", nel, buf); - }else{ - this.wrap = Ext.DomHelper.insertHtml("beforeEnd", targetNode, buf); - } - - this.elNode = this.wrap.childNodes[0]; - this.ctNode = this.wrap.childNodes[1]; - var cs = this.elNode.childNodes; - this.indentNode = cs[0]; - this.ecNode = cs[1]; - this.iconNode = cs[2]; - var index = 3; - if(cb){ - this.checkbox = cs[3]; - // fix for IE6 - this.checkbox.defaultChecked = this.checkbox.checked; - index++; - } - this.anchor = cs[index]; - this.textNode = cs[index].firstChild; - }, - - /** - * @private Gets a normalized href for the node. - * @param {String} href - */ - getHref : function(href){ - return Ext.isEmpty(href) ? (Ext.isGecko ? '' : '#') : href; - }, - -/** - * Returns the <a> element that provides focus for the node's UI. - * @return {HtmlElement} The DOM anchor element. - */ - getAnchor : function(){ - return this.anchor; - }, - -/** - * Returns the text node. - * @return {HtmlNode} The DOM text node. - */ - getTextEl : function(){ - return this.textNode; - }, - -/** - * Returns the icon <img> element. - * @return {HtmlElement} The DOM image element. - */ - getIconEl : function(){ - return this.iconNode; - }, - -/** - * Returns the checked status of the node. If the node was rendered with no - * checkbox, it returns false. - * @return {Boolean} The checked flag. - */ - isChecked : function(){ - return this.checkbox ? this.checkbox.checked : false; - }, - - // private - updateExpandIcon : function(){ - if(this.rendered){ - var n = this.node, - c1, - c2, - cls = n.isLast() ? "x-tree-elbow-end" : "x-tree-elbow", - hasChild = n.hasChildNodes(); - if(hasChild || n.attributes.expandable){ - if(n.expanded){ - cls += "-minus"; - c1 = "x-tree-node-collapsed"; - c2 = "x-tree-node-expanded"; - }else{ - cls += "-plus"; - c1 = "x-tree-node-expanded"; - c2 = "x-tree-node-collapsed"; - } - if(this.wasLeaf){ - this.removeClass("x-tree-node-leaf"); - this.wasLeaf = false; - } - if(this.c1 != c1 || this.c2 != c2){ - Ext.fly(this.elNode).replaceClass(c1, c2); - this.c1 = c1; this.c2 = c2; - } - }else{ - if(!this.wasLeaf){ - Ext.fly(this.elNode).replaceClass("x-tree-node-expanded", "x-tree-node-collapsed"); - delete this.c1; - delete this.c2; - this.wasLeaf = true; - } - } - var ecc = "x-tree-ec-icon "+cls; - if(this.ecc != ecc){ - this.ecNode.className = ecc; - this.ecc = ecc; - } - } - }, - - // private - onIdChange: function(id){ - if(this.rendered){ - this.elNode.setAttribute('ext:tree-node-id', id); - } - }, - - // private - getChildIndent : function(){ - if(!this.childIndent){ - var buf = [], - p = this.node; - while(p){ - if(!p.isRoot || (p.isRoot && p.ownerTree.rootVisible)){ - if(!p.isLast()) { - buf.unshift(''); - } else { - buf.unshift(''); - } - } - p = p.parentNode; - } - this.childIndent = buf.join(""); - } - return this.childIndent; - }, - - // private - renderIndent : function(){ - if(this.rendered){ - var indent = "", - p = this.node.parentNode; - if(p){ - indent = p.ui.getChildIndent(); - } - if(this.indentMarkup != indent){ // don't rerender if not required - this.indentNode.innerHTML = indent; - this.indentMarkup = indent; - } - this.updateExpandIcon(); - } - }, - - destroy : function(){ - if(this.elNode){ - Ext.dd.Registry.unregister(this.elNode.id); - } - - Ext.each(['textnode', 'anchor', 'checkbox', 'indentNode', 'ecNode', 'iconNode', 'elNode', 'ctNode', 'wrap', 'holder'], function(el){ - if(this[el]){ - Ext.fly(this[el]).remove(); - delete this[el]; - } - }, this); - delete this.node; - } -}); - -/** - * @class Ext.tree.RootTreeNodeUI - * This class provides the default UI implementation for root Ext TreeNodes. - * The RootTreeNode UI implementation allows customizing the appearance of the root tree node.
    - *

    - * If you are customizing the Tree's user interface, you - * may need to extend this class, but you should never need to instantiate this class.
    - */ -Ext.tree.RootTreeNodeUI = Ext.extend(Ext.tree.TreeNodeUI, { - // private - render : function(){ - if(!this.rendered){ - var targetNode = this.node.ownerTree.innerCt.dom; - this.node.expanded = true; - targetNode.innerHTML = '

    '; - this.wrap = this.ctNode = targetNode.firstChild; - } - }, - collapse : Ext.emptyFn, - expand : Ext.emptyFn -});/** - * @class Ext.tree.TreeLoader - * @extends Ext.util.Observable - * A TreeLoader provides for lazy loading of an {@link Ext.tree.TreeNode}'s child - * nodes from a specified URL. The response must be a JavaScript Array definition - * whose elements are node definition objects. e.g.: - *
    
    -    [{
    -        id: 1,
    -        text: 'A leaf Node',
    -        leaf: true
    -    },{
    -        id: 2,
    -        text: 'A folder Node',
    -        children: [{
    -            id: 3,
    -            text: 'A child Node',
    -            leaf: true
    -        }]
    -   }]
    -
    - *

    - * A server request is sent, and child nodes are loaded only when a node is expanded. - * The loading node's id is passed to the server under the parameter name "node" to - * enable the server to produce the correct child nodes. - *

    - * To pass extra parameters, an event handler may be attached to the "beforeload" - * event, and the parameters specified in the TreeLoader's baseParams property: - *
    
    -    myTreeLoader.on("beforeload", function(treeLoader, node) {
    -        this.baseParams.category = node.attributes.category;
    -    }, this);
    -
    - * This would pass an HTTP parameter called "category" to the server containing - * the value of the Node's "category" attribute. - * @constructor - * Creates a new Treeloader. - * @param {Object} config A config object containing config properties. - */ -Ext.tree.TreeLoader = function(config){ - this.baseParams = {}; - Ext.apply(this, config); - - this.addEvents( - /** - * @event beforeload - * Fires before a network request is made to retrieve the Json text which specifies a node's children. - * @param {Object} This TreeLoader object. - * @param {Object} node The {@link Ext.tree.TreeNode} object being loaded. - * @param {Object} callback The callback function specified in the {@link #load} call. - */ - "beforeload", - /** - * @event load - * Fires when the node has been successfuly loaded. - * @param {Object} This TreeLoader object. - * @param {Object} node The {@link Ext.tree.TreeNode} object being loaded. - * @param {Object} response The response object containing the data from the server. - */ - "load", - /** - * @event loadexception - * Fires if the network request failed. - * @param {Object} This TreeLoader object. - * @param {Object} node The {@link Ext.tree.TreeNode} object being loaded. - * @param {Object} response The response object containing the data from the server. - */ - "loadexception" - ); - Ext.tree.TreeLoader.superclass.constructor.call(this); - if(Ext.isString(this.paramOrder)){ - this.paramOrder = this.paramOrder.split(/[\s,|]/); - } -}; - -Ext.extend(Ext.tree.TreeLoader, Ext.util.Observable, { - /** - * @cfg {String} dataUrl The URL from which to request a Json string which - * specifies an array of node definition objects representing the child nodes - * to be loaded. - */ - /** - * @cfg {String} requestMethod The HTTP request method for loading data (defaults to the value of {@link Ext.Ajax#method}). - */ - /** - * @cfg {String} url Equivalent to {@link #dataUrl}. - */ - /** - * @cfg {Boolean} preloadChildren If set to true, the loader recursively loads "children" attributes when doing the first load on nodes. - */ - /** - * @cfg {Object} baseParams (optional) An object containing properties which - * specify HTTP parameters to be passed to each request for child nodes. - */ - /** - * @cfg {Object} baseAttrs (optional) An object containing attributes to be added to all nodes - * created by this loader. If the attributes sent by the server have an attribute in this object, - * they take priority. - */ - /** - * @cfg {Object} uiProviders (optional) An object containing properties which - * specify custom {@link Ext.tree.TreeNodeUI} implementations. If the optional - * uiProvider attribute of a returned child node is a string rather - * than a reference to a TreeNodeUI implementation, then that string value - * is used as a property name in the uiProviders object. - */ - uiProviders : {}, - - /** - * @cfg {Boolean} clearOnLoad (optional) Default to true. Remove previously existing - * child nodes before loading. - */ - clearOnLoad : true, - - /** - * @cfg {Array/String} paramOrder Defaults to undefined. Only used when using directFn. - * Specifies the params in the order in which they must be passed to the server-side Direct method - * as either (1) an Array of String values, or (2) a String of params delimited by either whitespace, - * comma, or pipe. For example, - * any of the following would be acceptable:
    
    -nodeParameter: 'node',
    -paramOrder: ['param1','param2','param3']
    -paramOrder: 'node param1 param2 param3'
    -paramOrder: 'param1,node,param2,param3'
    -paramOrder: 'param1|param2|param|node'
    -     
    - */ - paramOrder: undefined, - - /** - * @cfg {Boolean} paramsAsHash Only used when using directFn. - * Send parameters as a collection of named arguments (defaults to false). Providing a - * {@link #paramOrder} nullifies this configuration. - */ - paramsAsHash: false, - - /** - * @cfg {String} nodeParameter The name of the parameter sent to the server which contains - * the identifier of the node. Defaults to 'node'. - */ - nodeParameter: 'node', - - /** - * @cfg {Function} directFn - * Function to call when executing a request. - */ - directFn : undefined, - - /** - * Load an {@link Ext.tree.TreeNode} from the URL specified in the constructor. - * This is called automatically when a node is expanded, but may be used to reload - * a node (or append new children if the {@link #clearOnLoad} option is false.) - * @param {Ext.tree.TreeNode} node - * @param {Function} callback Function to call after the node has been loaded. The - * function is passed the TreeNode which was requested to be loaded. - * @param {Object} scope The scope (this reference) in which the callback is executed. - * defaults to the loaded TreeNode. - */ - load : function(node, callback, scope){ - if(this.clearOnLoad){ - while(node.firstChild){ - node.removeChild(node.firstChild); - } - } - if(this.doPreload(node)){ // preloaded json children - this.runCallback(callback, scope || node, [node]); - }else if(this.directFn || this.dataUrl || this.url){ - this.requestData(node, callback, scope || node); - } - }, - - doPreload : function(node){ - if(node.attributes.children){ - if(node.childNodes.length < 1){ // preloaded? - var cs = node.attributes.children; - node.beginUpdate(); - for(var i = 0, len = cs.length; i < len; i++){ - var cn = node.appendChild(this.createNode(cs[i])); - if(this.preloadChildren){ - this.doPreload(cn); - } - } - node.endUpdate(); - } - return true; - } - return false; - }, - - getParams: function(node){ - var bp = Ext.apply({}, this.baseParams), - np = this.nodeParameter, - po = this.paramOrder; - - np && (bp[ np ] = node.id); - - if(this.directFn){ - var buf = [node.id]; - if(po){ - // reset 'buf' if the nodeParameter was included in paramOrder - if(np && po.indexOf(np) > -1){ - buf = []; - } - - for(var i = 0, len = po.length; i < len; i++){ - buf.push(bp[ po[i] ]); - } - }else if(this.paramsAsHash){ - buf = [bp]; - } - return buf; - }else{ - return bp; - } - }, - - requestData : function(node, callback, scope){ - if(this.fireEvent("beforeload", this, node, callback) !== false){ - if(this.directFn){ - var args = this.getParams(node); - args.push(this.processDirectResponse.createDelegate(this, [{callback: callback, node: node, scope: scope}], true)); - this.directFn.apply(window, args); - }else{ - this.transId = Ext.Ajax.request({ - method:this.requestMethod, - url: this.dataUrl||this.url, - success: this.handleResponse, - failure: this.handleFailure, - scope: this, - argument: {callback: callback, node: node, scope: scope}, - params: this.getParams(node) - }); - } - }else{ - // if the load is cancelled, make sure we notify - // the node that we are done - this.runCallback(callback, scope || node, []); - } - }, - - processDirectResponse: function(result, response, args){ - if(response.status){ - this.handleResponse({ - responseData: Ext.isArray(result) ? result : null, - responseText: result, - argument: args - }); - }else{ - this.handleFailure({ - argument: args - }); - } - }, - - // private - runCallback: function(cb, scope, args){ - if(Ext.isFunction(cb)){ - cb.apply(scope, args); - } - }, - - isLoading : function(){ - return !!this.transId; - }, - - abort : function(){ - if(this.isLoading()){ - Ext.Ajax.abort(this.transId); - } - }, - - /** - *

    Override this function for custom TreeNode node implementation, or to - * modify the attributes at creation time.

    - * Example:
    
    -new Ext.tree.TreePanel({
    -    ...
    -    loader: new Ext.tree.TreeLoader({
    -        url: 'dataUrl',
    -        createNode: function(attr) {
    -//          Allow consolidation consignments to have
    -//          consignments dropped into them.
    -            if (attr.isConsolidation) {
    -                attr.iconCls = 'x-consol',
    -                attr.allowDrop = true;
    -            }
    -            return Ext.tree.TreeLoader.prototype.createNode.call(this, attr);
    -        }
    -    }),
    -    ...
    -});
    -
    - * @param attr {Object} The attributes from which to create the new node. - */ - createNode : function(attr){ - // apply baseAttrs, nice idea Corey! - if(this.baseAttrs){ - Ext.applyIf(attr, this.baseAttrs); - } - if(this.applyLoader !== false && !attr.loader){ - attr.loader = this; - } - if(Ext.isString(attr.uiProvider)){ - attr.uiProvider = this.uiProviders[attr.uiProvider] || eval(attr.uiProvider); - } - if(attr.nodeType){ - return new Ext.tree.TreePanel.nodeTypes[attr.nodeType](attr); - }else{ - return attr.leaf ? - new Ext.tree.TreeNode(attr) : - new Ext.tree.AsyncTreeNode(attr); - } - }, - - processResponse : function(response, node, callback, scope){ - var json = response.responseText; - try { - var o = response.responseData || Ext.decode(json); - node.beginUpdate(); - for(var i = 0, len = o.length; i < len; i++){ - var n = this.createNode(o[i]); - if(n){ - node.appendChild(n); - } - } - node.endUpdate(); - this.runCallback(callback, scope || node, [node]); - }catch(e){ - this.handleFailure(response); - } - }, - - handleResponse : function(response){ - this.transId = false; - var a = response.argument; - this.processResponse(response, a.node, a.callback, a.scope); - this.fireEvent("load", this, a.node, response); - }, - - handleFailure : function(response){ - this.transId = false; - var a = response.argument; - this.fireEvent("loadexception", this, a.node, response); - this.runCallback(a.callback, a.scope || a.node, [a.node]); - }, - - destroy : function(){ - this.abort(); - this.purgeListeners(); - } -});/** - * @class Ext.tree.TreeFilter - * Note this class is experimental and doesn't update the indent (lines) or expand collapse icons of the nodes - * @param {TreePanel} tree - * @param {Object} config (optional) - */ -Ext.tree.TreeFilter = function(tree, config){ - this.tree = tree; - this.filtered = {}; - Ext.apply(this, config); -}; - -Ext.tree.TreeFilter.prototype = { - clearBlank:false, - reverse:false, - autoClear:false, - remove:false, - - /** - * Filter the data by a specific attribute. - * @param {String/RegExp} value Either string that the attribute value - * should start with or a RegExp to test against the attribute - * @param {String} attr (optional) The attribute passed in your node's attributes collection. Defaults to "text". - * @param {TreeNode} startNode (optional) The node to start the filter at. - */ - filter : function(value, attr, startNode){ - attr = attr || "text"; - var f; - if(typeof value == "string"){ - var vlen = value.length; - // auto clear empty filter - if(vlen == 0 && this.clearBlank){ - this.clear(); - return; - } - value = value.toLowerCase(); - f = function(n){ - return n.attributes[attr].substr(0, vlen).toLowerCase() == value; - }; - }else if(value.exec){ // regex? - f = function(n){ - return value.test(n.attributes[attr]); - }; - }else{ - throw 'Illegal filter type, must be string or regex'; - } - this.filterBy(f, null, startNode); - }, - - /** - * Filter by a function. The passed function will be called with each - * node in the tree (or from the startNode). If the function returns true, the node is kept - * otherwise it is filtered. If a node is filtered, its children are also filtered. - * @param {Function} fn The filter function - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. - */ - filterBy : function(fn, scope, startNode){ - startNode = startNode || this.tree.root; - if(this.autoClear){ - this.clear(); - } - var af = this.filtered, rv = this.reverse; - var f = function(n){ - if(n == startNode){ - return true; - } - if(af[n.id]){ - return false; - } - var m = fn.call(scope || n, n); - if(!m || rv){ - af[n.id] = n; - n.ui.hide(); - return false; - } - return true; - }; - startNode.cascade(f); - if(this.remove){ - for(var id in af){ - if(typeof id != "function"){ - var n = af[id]; - if(n && n.parentNode){ - n.parentNode.removeChild(n); - } - } - } - } - }, - - /** - * Clears the current filter. Note: with the "remove" option - * set a filter cannot be cleared. - */ - clear : function(){ - var t = this.tree; - var af = this.filtered; - for(var id in af){ - if(typeof id != "function"){ - var n = af[id]; - if(n){ - n.ui.show(); - } - } - } - this.filtered = {}; - } -}; -/** - * @class Ext.tree.TreeSorter - * Provides sorting of nodes in a {@link Ext.tree.TreePanel}. The TreeSorter automatically monitors events on the - * associated TreePanel that might affect the tree's sort order (beforechildrenrendered, append, insert and textchange). - * Example usage:
    - *
    
    -new Ext.tree.TreeSorter(myTree, {
    -    folderSort: true,
    -    dir: "desc",
    -    sortType: function(node) {
    -        // sort by a custom, typed attribute:
    -        return parseInt(node.id, 10);
    -    }
    -});
    -
    - * @constructor - * @param {TreePanel} tree - * @param {Object} config - */ -Ext.tree.TreeSorter = Ext.extend(Object, { - - constructor: function(tree, config){ - /** - * @cfg {Boolean} folderSort True to sort leaf nodes under non-leaf nodes (defaults to false) - */ - /** - * @cfg {String} property The named attribute on the node to sort by (defaults to "text"). Note that this - * property is only used if no {@link #sortType} function is specified, otherwise it is ignored. - */ - /** - * @cfg {String} dir The direction to sort ("asc" or "desc," case-insensitive, defaults to "asc") - */ - /** - * @cfg {String} leafAttr The attribute used to determine leaf nodes when {@link #folderSort} = true (defaults to "leaf") - */ - /** - * @cfg {Boolean} caseSensitive true for case-sensitive sort (defaults to false) - */ - /** - * @cfg {Function} sortType A custom "casting" function used to convert node values before sorting. The function - * will be called with a single parameter (the {@link Ext.tree.TreeNode} being evaluated) and is expected to return - * the node's sort value cast to the specific data type required for sorting. This could be used, for example, when - * a node's text (or other attribute) should be sorted as a date or numeric value. See the class description for - * example usage. Note that if a sortType is specified, any {@link #property} config will be ignored. - */ - - Ext.apply(this, config); - tree.on({ - scope: this, - beforechildrenrendered: this.doSort, - append: this.updateSort, - insert: this.updateSort, - textchange: this.updateSortParent - }); - - var desc = this.dir && this.dir.toLowerCase() == 'desc', - prop = this.property || 'text', - sortType = this.sortType, - folderSort = this.folderSort, - caseSensitive = this.caseSensitive === true, - leafAttr = this.leafAttr || 'leaf'; - - if(Ext.isString(sortType)){ - sortType = Ext.data.SortTypes[sortType]; - } - this.sortFn = function(n1, n2){ - var attr1 = n1.attributes, - attr2 = n2.attributes; - - if(folderSort){ - if(attr1[leafAttr] && !attr2[leafAttr]){ - return 1; - } - if(!attr1[leafAttr] && attr2[leafAttr]){ - return -1; - } - } - var prop1 = attr1[prop], - prop2 = attr2[prop], - v1 = sortType ? sortType(prop1) : (caseSensitive ? prop1 : prop1.toUpperCase()), - v2 = sortType ? sortType(prop2) : (caseSensitive ? prop2 : prop2.toUpperCase()); - - if(v1 < v2){ - return desc ? 1 : -1; - }else if(v1 > v2){ - return desc ? -1 : 1; - } - return 0; - }; - }, - - doSort : function(node){ - node.sort(this.sortFn); - }, - - updateSort : function(tree, node){ - if(node.childrenRendered){ - this.doSort.defer(1, this, [node]); - } - }, - - updateSortParent : function(node){ - var p = node.parentNode; - if(p && p.childrenRendered){ - this.doSort.defer(1, this, [p]); - } - } -}); -/** - * @class Ext.tree.TreeDropZone - * @extends Ext.dd.DropZone - * @constructor - * @param {String/HTMLElement/Element} tree The {@link Ext.tree.TreePanel} for which to enable dropping - * @param {Object} config - */ -if(Ext.dd.DropZone){ - -Ext.tree.TreeDropZone = function(tree, config){ - /** - * @cfg {Boolean} allowParentInsert - * Allow inserting a dragged node between an expanded parent node and its first child that will become a - * sibling of the parent when dropped (defaults to false) - */ - this.allowParentInsert = config.allowParentInsert || false; - /** - * @cfg {String} allowContainerDrop - * True if drops on the tree container (outside of a specific tree node) are allowed (defaults to false) - */ - this.allowContainerDrop = config.allowContainerDrop || false; - /** - * @cfg {String} appendOnly - * True if the tree should only allow append drops (use for trees which are sorted, defaults to false) - */ - this.appendOnly = config.appendOnly || false; - - Ext.tree.TreeDropZone.superclass.constructor.call(this, tree.getTreeEl(), config); - /** - * The TreePanel for this drop zone - * @type Ext.tree.TreePanel - * @property - */ - this.tree = tree; - /** - * Arbitrary data that can be associated with this tree and will be included in the event object that gets - * passed to any nodedragover event handler (defaults to {}) - * @type Ext.tree.TreePanel - * @property - */ - this.dragOverData = {}; - // private - this.lastInsertClass = "x-tree-no-status"; -}; - -Ext.extend(Ext.tree.TreeDropZone, Ext.dd.DropZone, { - /** - * @cfg {String} ddGroup - * A named drag drop group to which this object belongs. If a group is specified, then this object will only - * interact with other drag drop objects in the same group (defaults to 'TreeDD'). - */ - ddGroup : "TreeDD", - - /** - * @cfg {String} expandDelay - * The delay in milliseconds to wait before expanding a target tree node while dragging a droppable node - * over the target (defaults to 1000) - */ - expandDelay : 1000, - - // private - expandNode : function(node){ - if(node.hasChildNodes() && !node.isExpanded()){ - node.expand(false, null, this.triggerCacheRefresh.createDelegate(this)); - } - }, - - // private - queueExpand : function(node){ - this.expandProcId = this.expandNode.defer(this.expandDelay, this, [node]); - }, - - // private - cancelExpand : function(){ - if(this.expandProcId){ - clearTimeout(this.expandProcId); - this.expandProcId = false; - } - }, - - // private - isValidDropPoint : function(n, pt, dd, e, data){ - if(!n || !data){ return false; } - var targetNode = n.node; - var dropNode = data.node; - // default drop rules - if(!(targetNode && targetNode.isTarget && pt)){ - return false; - } - if(pt == "append" && targetNode.allowChildren === false){ - return false; - } - if((pt == "above" || pt == "below") && (targetNode.parentNode && targetNode.parentNode.allowChildren === false)){ - return false; - } - if(dropNode && (targetNode == dropNode || dropNode.contains(targetNode))){ - return false; - } - // reuse the object - var overEvent = this.dragOverData; - overEvent.tree = this.tree; - overEvent.target = targetNode; - overEvent.data = data; - overEvent.point = pt; - overEvent.source = dd; - overEvent.rawEvent = e; - overEvent.dropNode = dropNode; - overEvent.cancel = false; - var result = this.tree.fireEvent("nodedragover", overEvent); - return overEvent.cancel === false && result !== false; - }, - - // private - getDropPoint : function(e, n, dd){ - var tn = n.node; - if(tn.isRoot){ - return tn.allowChildren !== false ? "append" : false; // always append for root - } - var dragEl = n.ddel; - var t = Ext.lib.Dom.getY(dragEl), b = t + dragEl.offsetHeight; - var y = Ext.lib.Event.getPageY(e); - var noAppend = tn.allowChildren === false || tn.isLeaf(); - if(this.appendOnly || tn.parentNode.allowChildren === false){ - return noAppend ? false : "append"; - } - var noBelow = false; - if(!this.allowParentInsert){ - noBelow = tn.hasChildNodes() && tn.isExpanded(); - } - var q = (b - t) / (noAppend ? 2 : 3); - if(y >= t && y < (t + q)){ - return "above"; - }else if(!noBelow && (noAppend || y >= b-q && y <= b)){ - return "below"; - }else{ - return "append"; - } - }, - - // private - onNodeEnter : function(n, dd, e, data){ - this.cancelExpand(); - }, - - onContainerOver : function(dd, e, data) { - if (this.allowContainerDrop && this.isValidDropPoint({ ddel: this.tree.getRootNode().ui.elNode, node: this.tree.getRootNode() }, "append", dd, e, data)) { - return this.dropAllowed; - } - return this.dropNotAllowed; - }, - - // private - onNodeOver : function(n, dd, e, data){ - var pt = this.getDropPoint(e, n, dd); - var node = n.node; - - // auto node expand check - if(!this.expandProcId && pt == "append" && node.hasChildNodes() && !n.node.isExpanded()){ - this.queueExpand(node); - }else if(pt != "append"){ - this.cancelExpand(); - } - - // set the insert point style on the target node - var returnCls = this.dropNotAllowed; - if(this.isValidDropPoint(n, pt, dd, e, data)){ - if(pt){ - var el = n.ddel; - var cls; - if(pt == "above"){ - returnCls = n.node.isFirst() ? "x-tree-drop-ok-above" : "x-tree-drop-ok-between"; - cls = "x-tree-drag-insert-above"; - }else if(pt == "below"){ - returnCls = n.node.isLast() ? "x-tree-drop-ok-below" : "x-tree-drop-ok-between"; - cls = "x-tree-drag-insert-below"; - }else{ - returnCls = "x-tree-drop-ok-append"; - cls = "x-tree-drag-append"; - } - if(this.lastInsertClass != cls){ - Ext.fly(el).replaceClass(this.lastInsertClass, cls); - this.lastInsertClass = cls; - } - } - } - return returnCls; - }, - - // private - onNodeOut : function(n, dd, e, data){ - this.cancelExpand(); - this.removeDropIndicators(n); - }, - - // private - onNodeDrop : function(n, dd, e, data){ - var point = this.getDropPoint(e, n, dd); - var targetNode = n.node; - targetNode.ui.startDrop(); - if(!this.isValidDropPoint(n, point, dd, e, data)){ - targetNode.ui.endDrop(); - return false; - } - // first try to find the drop node - var dropNode = data.node || (dd.getTreeNode ? dd.getTreeNode(data, targetNode, point, e) : null); - return this.processDrop(targetNode, data, point, dd, e, dropNode); - }, - - onContainerDrop : function(dd, e, data){ - if (this.allowContainerDrop && this.isValidDropPoint({ ddel: this.tree.getRootNode().ui.elNode, node: this.tree.getRootNode() }, "append", dd, e, data)) { - var targetNode = this.tree.getRootNode(); - targetNode.ui.startDrop(); - var dropNode = data.node || (dd.getTreeNode ? dd.getTreeNode(data, targetNode, 'append', e) : null); - return this.processDrop(targetNode, data, 'append', dd, e, dropNode); - } - return false; - }, - - // private - processDrop: function(target, data, point, dd, e, dropNode){ - var dropEvent = { - tree : this.tree, - target: target, - data: data, - point: point, - source: dd, - rawEvent: e, - dropNode: dropNode, - cancel: !dropNode, - dropStatus: false - }; - var retval = this.tree.fireEvent("beforenodedrop", dropEvent); - if(retval === false || dropEvent.cancel === true || !dropEvent.dropNode){ - target.ui.endDrop(); - return dropEvent.dropStatus; - } - - target = dropEvent.target; - if(point == 'append' && !target.isExpanded()){ - target.expand(false, null, function(){ - this.completeDrop(dropEvent); - }.createDelegate(this)); - }else{ - this.completeDrop(dropEvent); - } - return true; - }, - - // private - completeDrop : function(de){ - var ns = de.dropNode, p = de.point, t = de.target; - if(!Ext.isArray(ns)){ - ns = [ns]; - } - var n; - for(var i = 0, len = ns.length; i < len; i++){ - n = ns[i]; - if(p == "above"){ - t.parentNode.insertBefore(n, t); - }else if(p == "below"){ - t.parentNode.insertBefore(n, t.nextSibling); - }else{ - t.appendChild(n); - } - } - n.ui.focus(); - if(Ext.enableFx && this.tree.hlDrop){ - n.ui.highlight(); - } - t.ui.endDrop(); - this.tree.fireEvent("nodedrop", de); - }, - - // private - afterNodeMoved : function(dd, data, e, targetNode, dropNode){ - if(Ext.enableFx && this.tree.hlDrop){ - dropNode.ui.focus(); - dropNode.ui.highlight(); - } - this.tree.fireEvent("nodedrop", this.tree, targetNode, data, dd, e); - }, - - // private - getTree : function(){ - return this.tree; - }, - - // private - removeDropIndicators : function(n){ - if(n && n.ddel){ - var el = n.ddel; - Ext.fly(el).removeClass([ - "x-tree-drag-insert-above", - "x-tree-drag-insert-below", - "x-tree-drag-append"]); - this.lastInsertClass = "_noclass"; - } - }, - - // private - beforeDragDrop : function(target, e, id){ - this.cancelExpand(); - return true; - }, - - // private - afterRepair : function(data){ - if(data && Ext.enableFx){ - data.node.ui.highlight(); - } - this.hideProxy(); - } -}); - -}/** - * @class Ext.tree.TreeDragZone - * @extends Ext.dd.DragZone - * @constructor - * @param {String/HTMLElement/Element} tree The {@link Ext.tree.TreePanel} for which to enable dragging - * @param {Object} config - */ -if(Ext.dd.DragZone){ -Ext.tree.TreeDragZone = function(tree, config){ - Ext.tree.TreeDragZone.superclass.constructor.call(this, tree.innerCt, config); - /** - * The TreePanel for this drag zone - * @type Ext.tree.TreePanel - * @property - */ - this.tree = tree; -}; - -Ext.extend(Ext.tree.TreeDragZone, Ext.dd.DragZone, { - /** - * @cfg {String} ddGroup - * A named drag drop group to which this object belongs. If a group is specified, then this object will only - * interact with other drag drop objects in the same group (defaults to 'TreeDD'). - */ - ddGroup : "TreeDD", - - // private - onBeforeDrag : function(data, e){ - var n = data.node; - return n && n.draggable && !n.disabled; - }, - - // private - onInitDrag : function(e){ - var data = this.dragData; - this.tree.getSelectionModel().select(data.node); - this.tree.eventModel.disable(); - this.proxy.update(""); - data.node.ui.appendDDGhost(this.proxy.ghost.dom); - this.tree.fireEvent("startdrag", this.tree, data.node, e); - }, - - // private - getRepairXY : function(e, data){ - return data.node.ui.getDDRepairXY(); - }, - - // private - onEndDrag : function(data, e){ - this.tree.eventModel.enable.defer(100, this.tree.eventModel); - this.tree.fireEvent("enddrag", this.tree, data.node, e); - }, - - // private - onValidDrop : function(dd, e, id){ - this.tree.fireEvent("dragdrop", this.tree, this.dragData.node, dd, e); - this.hideProxy(); - }, - - // private - beforeInvalidDrop : function(e, id){ - // this scrolls the original position back into view - var sm = this.tree.getSelectionModel(); - sm.clearSelections(); - sm.select(this.dragData.node); - }, - - // private - afterRepair : function(){ - if (Ext.enableFx && this.tree.hlDrop) { - Ext.Element.fly(this.dragData.ddel).highlight(this.hlColor || "c3daf9"); - } - this.dragging = false; - } -}); -}/** - * @class Ext.tree.TreeEditor - * @extends Ext.Editor - * Provides editor functionality for inline tree node editing. Any valid {@link Ext.form.Field} subclass can be used - * as the editor field. - * @constructor - * @param {TreePanel} tree - * @param {Object} fieldConfig (optional) Either a prebuilt {@link Ext.form.Field} instance or a Field config object - * that will be applied to the default field instance (defaults to a {@link Ext.form.TextField}). - * @param {Object} config (optional) A TreeEditor config object - */ -Ext.tree.TreeEditor = function(tree, fc, config){ - fc = fc || {}; - var field = fc.events ? fc : new Ext.form.TextField(fc); - - Ext.tree.TreeEditor.superclass.constructor.call(this, field, config); - - this.tree = tree; - - if(!tree.rendered){ - tree.on('render', this.initEditor, this); - }else{ - this.initEditor(tree); - } -}; - -Ext.extend(Ext.tree.TreeEditor, Ext.Editor, { - /** - * @cfg {String} alignment - * The position to align to (see {@link Ext.Element#alignTo} for more details, defaults to "l-l"). - */ - alignment: "l-l", - // inherit - autoSize: false, - /** - * @cfg {Boolean} hideEl - * True to hide the bound element while the editor is displayed (defaults to false) - */ - hideEl : false, - /** - * @cfg {String} cls - * CSS class to apply to the editor (defaults to "x-small-editor x-tree-editor") - */ - cls: "x-small-editor x-tree-editor", - /** - * @cfg {Boolean} shim - * True to shim the editor if selects/iframes could be displayed beneath it (defaults to false) - */ - shim:false, - // inherit - shadow:"frame", - /** - * @cfg {Number} maxWidth - * The maximum width in pixels of the editor field (defaults to 250). Note that if the maxWidth would exceed - * the containing tree element's size, it will be automatically limited for you to the container width, taking - * scroll and client offsets into account prior to each edit. - */ - maxWidth: 250, - /** - * @cfg {Number} editDelay The number of milliseconds between clicks to register a double-click that will trigger - * editing on the current node (defaults to 350). If two clicks occur on the same node within this time span, - * the editor for the node will display, otherwise it will be processed as a regular click. - */ - editDelay : 350, - - initEditor : function(tree){ - tree.on({ - scope : this, - beforeclick: this.beforeNodeClick, - dblclick : this.onNodeDblClick - }); - - this.on({ - scope : this, - complete : this.updateNode, - beforestartedit: this.fitToTree, - specialkey : this.onSpecialKey - }); - - this.on('startedit', this.bindScroll, this, {delay:10}); - }, - - // private - fitToTree : function(ed, el){ - var td = this.tree.getTreeEl().dom, nd = el.dom; - if(td.scrollLeft > nd.offsetLeft){ // ensure the node left point is visible - td.scrollLeft = nd.offsetLeft; - } - var w = Math.min( - this.maxWidth, - (td.clientWidth > 20 ? td.clientWidth : td.offsetWidth) - Math.max(0, nd.offsetLeft-td.scrollLeft) - /*cushion*/5); - this.setSize(w, ''); - }, - - /** - * Edit the text of the passed {@link Ext.tree.TreeNode TreeNode}. - * @param node {Ext.tree.TreeNode} The TreeNode to edit. The TreeNode must be {@link Ext.tree.TreeNode#editable editable}. - */ - triggerEdit : function(node, defer){ - this.completeEdit(); - if(node.attributes.editable !== false){ - /** - * The {@link Ext.tree.TreeNode TreeNode} this editor is bound to. Read-only. - * @type Ext.tree.TreeNode - * @property editNode - */ - this.editNode = node; - if(this.tree.autoScroll){ - Ext.fly(node.ui.getEl()).scrollIntoView(this.tree.body); - } - var value = node.text || ''; - if (!Ext.isGecko && Ext.isEmpty(node.text)){ - node.setText(' '); - } - this.autoEditTimer = this.startEdit.defer(this.editDelay, this, [node.ui.textNode, value]); - return false; - } - }, - - // private - bindScroll : function(){ - this.tree.getTreeEl().on('scroll', this.cancelEdit, this); - }, - - // private - beforeNodeClick : function(node, e){ - clearTimeout(this.autoEditTimer); - if(this.tree.getSelectionModel().isSelected(node)){ - e.stopEvent(); - return this.triggerEdit(node); - } - }, - - onNodeDblClick : function(node, e){ - clearTimeout(this.autoEditTimer); - }, - - // private - updateNode : function(ed, value){ - this.tree.getTreeEl().un('scroll', this.cancelEdit, this); - this.editNode.setText(value); - }, - - // private - onHide : function(){ - Ext.tree.TreeEditor.superclass.onHide.call(this); - if(this.editNode){ - this.editNode.ui.focus.defer(50, this.editNode.ui); - } - }, - - // private - onSpecialKey : function(field, e){ - var k = e.getKey(); - if(k == e.ESC){ - e.stopEvent(); - this.cancelEdit(); - }else if(k == e.ENTER && !e.hasModifier()){ - e.stopEvent(); - this.completeEdit(); - } - }, - - onDestroy : function(){ - clearTimeout(this.autoEditTimer); - Ext.tree.TreeEditor.superclass.onDestroy.call(this); - var tree = this.tree; - tree.un('beforeclick', this.beforeNodeClick, this); - tree.un('dblclick', this.onNodeDblClick, this); - } -});/*! SWFObject v2.2 - is released under the MIT License -*/ - -var swfobject = function() { - - var UNDEF = "undefined", - OBJECT = "object", - SHOCKWAVE_FLASH = "Shockwave Flash", - SHOCKWAVE_FLASH_AX = "ShockwaveFlash.ShockwaveFlash", - FLASH_MIME_TYPE = "application/x-shockwave-flash", - EXPRESS_INSTALL_ID = "SWFObjectExprInst", - ON_READY_STATE_CHANGE = "onreadystatechange", - - win = window, - doc = document, - nav = navigator, - - plugin = false, - domLoadFnArr = [main], - regObjArr = [], - objIdArr = [], - listenersArr = [], - storedAltContent, - storedAltContentId, - storedCallbackFn, - storedCallbackObj, - isDomLoaded = false, - isExpressInstallActive = false, - dynamicStylesheet, - dynamicStylesheetMedia, - autoHideShow = true, - - /* Centralized function for browser feature detection - - User agent string detection is only used when no good alternative is possible - - Is executed directly for optimal performance - */ - ua = function() { - var w3cdom = typeof doc.getElementById != UNDEF && typeof doc.getElementsByTagName != UNDEF && typeof doc.createElement != UNDEF, - u = nav.userAgent.toLowerCase(), - p = nav.platform.toLowerCase(), - windows = p ? (/win/).test(p) : /win/.test(u), - mac = p ? (/mac/).test(p) : /mac/.test(u), - webkit = /webkit/.test(u) ? parseFloat(u.replace(/^.*webkit\/(\d+(\.\d+)?).*$/, "$1")) : false, // returns either the webkit version or false if not webkit - ie = !+"\v1", // feature detection based on Andrea Giammarchi's solution: http://webreflection.blogspot.com/2009/01/32-bytes-to-know-if-your-browser-is-ie.html - playerVersion = [0,0,0], - d = null; - if (typeof nav.plugins != UNDEF && typeof nav.plugins[SHOCKWAVE_FLASH] == OBJECT) { - d = nav.plugins[SHOCKWAVE_FLASH].description; - if (d && !(typeof nav.mimeTypes != UNDEF && nav.mimeTypes[FLASH_MIME_TYPE] && !nav.mimeTypes[FLASH_MIME_TYPE].enabledPlugin)) { // navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin indicates whether plug-ins are enabled or disabled in Safari 3+ - plugin = true; - ie = false; // cascaded feature detection for Internet Explorer - d = d.replace(/^.*\s+(\S+\s+\S+$)/, "$1"); - playerVersion[0] = parseInt(d.replace(/^(.*)\..*$/, "$1"), 10); - playerVersion[1] = parseInt(d.replace(/^.*\.(.*)\s.*$/, "$1"), 10); - playerVersion[2] = /[a-zA-Z]/.test(d) ? parseInt(d.replace(/^.*[a-zA-Z]+(.*)$/, "$1"), 10) : 0; - } - } - else if (typeof win.ActiveXObject != UNDEF) { - try { - var a = new ActiveXObject(SHOCKWAVE_FLASH_AX); - if (a) { // a will return null when ActiveX is disabled - d = a.GetVariable("$version"); - if (d) { - ie = true; // cascaded feature detection for Internet Explorer - d = d.split(" ")[1].split(","); - playerVersion = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)]; - } - } - } - catch(e) {} - } - return { w3:w3cdom, pv:playerVersion, wk:webkit, ie:ie, win:windows, mac:mac }; - }(), - - /* Cross-browser onDomLoad - - Will fire an event as soon as the DOM of a web page is loaded - - Internet Explorer workaround based on Diego Perini's solution: http://javascript.nwbox.com/IEContentLoaded/ - - Regular onload serves as fallback - */ - onDomLoad = function() { - if (!ua.w3) { return; } - if ((typeof doc.readyState != UNDEF && doc.readyState == "complete") || (typeof doc.readyState == UNDEF && (doc.getElementsByTagName("body")[0] || doc.body))) { // function is fired after onload, e.g. when script is inserted dynamically - callDomLoadFunctions(); - } - if (!isDomLoaded) { - if (typeof doc.addEventListener != UNDEF) { - doc.addEventListener("DOMContentLoaded", callDomLoadFunctions, false); - } - if (ua.ie && ua.win) { - doc.attachEvent(ON_READY_STATE_CHANGE, function() { - if (doc.readyState == "complete") { - doc.detachEvent(ON_READY_STATE_CHANGE, arguments.callee); - callDomLoadFunctions(); - } - }); - if (win == top) { // if not inside an iframe - (function(){ - if (isDomLoaded) { return; } - try { - doc.documentElement.doScroll("left"); - } - catch(e) { - setTimeout(arguments.callee, 0); - return; - } - callDomLoadFunctions(); - })(); - } - } - if (ua.wk) { - (function(){ - if (isDomLoaded) { return; } - if (!(/loaded|complete/).test(doc.readyState)) { - setTimeout(arguments.callee, 0); - return; - } - callDomLoadFunctions(); - })(); - } - addLoadEvent(callDomLoadFunctions); - } - }(); - - function callDomLoadFunctions() { - if (isDomLoaded) { return; } - try { // test if we can really add/remove elements to/from the DOM; we don't want to fire it too early - var t = doc.getElementsByTagName("body")[0].appendChild(createElement("span")); - t.parentNode.removeChild(t); - } - catch (e) { return; } - isDomLoaded = true; - var dl = domLoadFnArr.length; - for (var i = 0; i < dl; i++) { - domLoadFnArr[i](); - } - } - - function addDomLoadEvent(fn) { - if (isDomLoaded) { - fn(); - } - else { - domLoadFnArr[domLoadFnArr.length] = fn; // Array.push() is only available in IE5.5+ - } - } - - /* Cross-browser onload - - Based on James Edwards' solution: http://brothercake.com/site/resources/scripts/onload/ - - Will fire an event as soon as a web page including all of its assets are loaded - */ - function addLoadEvent(fn) { - if (typeof win.addEventListener != UNDEF) { - win.addEventListener("load", fn, false); - } - else if (typeof doc.addEventListener != UNDEF) { - doc.addEventListener("load", fn, false); - } - else if (typeof win.attachEvent != UNDEF) { - addListener(win, "onload", fn); - } - else if (typeof win.onload == "function") { - var fnOld = win.onload; - win.onload = function() { - fnOld(); - fn(); - }; - } - else { - win.onload = fn; - } - } - - /* Main function - - Will preferably execute onDomLoad, otherwise onload (as a fallback) - */ - function main() { - if (plugin) { - testPlayerVersion(); - } - else { - matchVersions(); - } - } - - /* Detect the Flash Player version for non-Internet Explorer browsers - - Detecting the plug-in version via the object element is more precise than using the plugins collection item's description: - a. Both release and build numbers can be detected - b. Avoid wrong descriptions by corrupt installers provided by Adobe - c. Avoid wrong descriptions by multiple Flash Player entries in the plugin Array, caused by incorrect browser imports - - Disadvantage of this method is that it depends on the availability of the DOM, while the plugins collection is immediately available - */ - function testPlayerVersion() { - var b = doc.getElementsByTagName("body")[0]; - var o = createElement(OBJECT); - o.setAttribute("type", FLASH_MIME_TYPE); - var t = b.appendChild(o); - if (t) { - var counter = 0; - (function(){ - if (typeof t.GetVariable != UNDEF) { - var d = t.GetVariable("$version"); - if (d) { - d = d.split(" ")[1].split(","); - ua.pv = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)]; - } - } - else if (counter < 10) { - counter++; - setTimeout(arguments.callee, 10); - return; - } - b.removeChild(o); - t = null; - matchVersions(); - })(); - } - else { - matchVersions(); - } - } - - /* Perform Flash Player and SWF version matching; static publishing only - */ - function matchVersions() { - var rl = regObjArr.length; - if (rl > 0) { - for (var i = 0; i < rl; i++) { // for each registered object element - var id = regObjArr[i].id; - var cb = regObjArr[i].callbackFn; - var cbObj = {success:false, id:id}; - if (ua.pv[0] > 0) { - var obj = getElementById(id); - if (obj) { - if (hasPlayerVersion(regObjArr[i].swfVersion) && !(ua.wk && ua.wk < 312)) { // Flash Player version >= published SWF version: Houston, we have a match! - setVisibility(id, true); - if (cb) { - cbObj.success = true; - cbObj.ref = getObjectById(id); - cb(cbObj); - } - } - else if (regObjArr[i].expressInstall && canExpressInstall()) { // show the Adobe Express Install dialog if set by the web page author and if supported - var att = {}; - att.data = regObjArr[i].expressInstall; - att.width = obj.getAttribute("width") || "0"; - att.height = obj.getAttribute("height") || "0"; - if (obj.getAttribute("class")) { att.styleclass = obj.getAttribute("class"); } - if (obj.getAttribute("align")) { att.align = obj.getAttribute("align"); } - // parse HTML object param element's name-value pairs - var par = {}; - var p = obj.getElementsByTagName("param"); - var pl = p.length; - for (var j = 0; j < pl; j++) { - if (p[j].getAttribute("name").toLowerCase() != "movie") { - par[p[j].getAttribute("name")] = p[j].getAttribute("value"); - } - } - showExpressInstall(att, par, id, cb); - } - else { // Flash Player and SWF version mismatch or an older Webkit engine that ignores the HTML object element's nested param elements: display alternative content instead of SWF - displayAltContent(obj); - if (cb) { cb(cbObj); } - } - } - } - else { // if no Flash Player is installed or the fp version cannot be detected we let the HTML object element do its job (either show a SWF or alternative content) - setVisibility(id, true); - if (cb) { - var o = getObjectById(id); // test whether there is an HTML object element or not - if (o && typeof o.SetVariable != UNDEF) { - cbObj.success = true; - cbObj.ref = o; - } - cb(cbObj); - } - } - } - } - } - - function getObjectById(objectIdStr) { - var r = null; - var o = getElementById(objectIdStr); - if (o && o.nodeName == "OBJECT") { - if (typeof o.SetVariable != UNDEF) { - r = o; - } - else { - var n = o.getElementsByTagName(OBJECT)[0]; - if (n) { - r = n; - } - } - } - return r; - } - - /* Requirements for Adobe Express Install - - only one instance can be active at a time - - fp 6.0.65 or higher - - Win/Mac OS only - - no Webkit engines older than version 312 - */ - function canExpressInstall() { - return !isExpressInstallActive && hasPlayerVersion("6.0.65") && (ua.win || ua.mac) && !(ua.wk && ua.wk < 312); - } - - /* Show the Adobe Express Install dialog - - Reference: http://www.adobe.com/cfusion/knowledgebase/index.cfm?id=6a253b75 - */ - function showExpressInstall(att, par, replaceElemIdStr, callbackFn) { - isExpressInstallActive = true; - storedCallbackFn = callbackFn || null; - storedCallbackObj = {success:false, id:replaceElemIdStr}; - var obj = getElementById(replaceElemIdStr); - if (obj) { - if (obj.nodeName == "OBJECT") { // static publishing - storedAltContent = abstractAltContent(obj); - storedAltContentId = null; - } - else { // dynamic publishing - storedAltContent = obj; - storedAltContentId = replaceElemIdStr; - } - att.id = EXPRESS_INSTALL_ID; - if (typeof att.width == UNDEF || (!(/%$/).test(att.width) && parseInt(att.width, 10) < 310)) { - att.width = "310"; - } - - if (typeof att.height == UNDEF || (!(/%$/).test(att.height) && parseInt(att.height, 10) < 137)) { - att.height = "137"; - } - doc.title = doc.title.slice(0, 47) + " - Flash Player Installation"; - var pt = ua.ie && ua.win ? "ActiveX" : "PlugIn", - fv = "MMredirectURL=" + win.location.toString().replace(/&/g,"%26") + "&MMplayerType=" + pt + "&MMdoctitle=" + doc.title; - if (typeof par.flashvars != UNDEF) { - par.flashvars += "&" + fv; - } - else { - par.flashvars = fv; - } - // IE only: when a SWF is loading (AND: not available in cache) wait for the readyState of the object element to become 4 before removing it, - // because you cannot properly cancel a loading SWF file without breaking browser load references, also obj.onreadystatechange doesn't work - if (ua.ie && ua.win && obj.readyState != 4) { - var newObj = createElement("div"); - replaceElemIdStr += "SWFObjectNew"; - newObj.setAttribute("id", replaceElemIdStr); - obj.parentNode.insertBefore(newObj, obj); // insert placeholder div that will be replaced by the object element that loads expressinstall.swf - obj.style.display = "none"; - (function(){ - if (obj.readyState == 4) { - obj.parentNode.removeChild(obj); - } - else { - setTimeout(arguments.callee, 10); - } - })(); - } - createSWF(att, par, replaceElemIdStr); - } - } - - /* Functions to abstract and display alternative content - */ - function displayAltContent(obj) { - if (ua.ie && ua.win && obj.readyState != 4) { - // IE only: when a SWF is loading (AND: not available in cache) wait for the readyState of the object element to become 4 before removing it, - // because you cannot properly cancel a loading SWF file without breaking browser load references, also obj.onreadystatechange doesn't work - var el = createElement("div"); - obj.parentNode.insertBefore(el, obj); // insert placeholder div that will be replaced by the alternative content - el.parentNode.replaceChild(abstractAltContent(obj), el); - obj.style.display = "none"; - (function(){ - if (obj.readyState == 4) { - obj.parentNode.removeChild(obj); - } - else { - setTimeout(arguments.callee, 10); - } - })(); - } - else { - obj.parentNode.replaceChild(abstractAltContent(obj), obj); - } - } - - function abstractAltContent(obj) { - var ac = createElement("div"); - if (ua.win && ua.ie) { - ac.innerHTML = obj.innerHTML; - } - else { - var nestedObj = obj.getElementsByTagName(OBJECT)[0]; - if (nestedObj) { - var c = nestedObj.childNodes; - if (c) { - var cl = c.length; - for (var i = 0; i < cl; i++) { - if (!(c[i].nodeType == 1 && c[i].nodeName == "PARAM") && !(c[i].nodeType == 8)) { - ac.appendChild(c[i].cloneNode(true)); - } - } - } - } - } - return ac; - } - - /* Cross-browser dynamic SWF creation - */ - function createSWF(attObj, parObj, id) { - var r, el = getElementById(id); - if (ua.wk && ua.wk < 312) { return r; } - if (el) { - if (typeof attObj.id == UNDEF) { // if no 'id' is defined for the object element, it will inherit the 'id' from the alternative content - attObj.id = id; - } - if (ua.ie && ua.win) { // Internet Explorer + the HTML object element + W3C DOM methods do not combine: fall back to outerHTML - var att = ""; - for (var i in attObj) { - if (attObj[i] != Object.prototype[i]) { // filter out prototype additions from other potential libraries - if (i.toLowerCase() == "data") { - parObj.movie = attObj[i]; - } - else if (i.toLowerCase() == "styleclass") { // 'class' is an ECMA4 reserved keyword - att += ' class="' + attObj[i] + '"'; - } - else if (i.toLowerCase() != "classid") { - att += ' ' + i + '="' + attObj[i] + '"'; - } - } - } - var par = ""; - for (var j in parObj) { - if (parObj[j] != Object.prototype[j]) { // filter out prototype additions from other potential libraries - par += ''; - } - } - el.outerHTML = '' + par + ''; - objIdArr[objIdArr.length] = attObj.id; // stored to fix object 'leaks' on unload (dynamic publishing only) - r = getElementById(attObj.id); - } - else { // well-behaving browsers - var o = createElement(OBJECT); - o.setAttribute("type", FLASH_MIME_TYPE); - for (var m in attObj) { - if (attObj[m] != Object.prototype[m]) { // filter out prototype additions from other potential libraries - if (m.toLowerCase() == "styleclass") { // 'class' is an ECMA4 reserved keyword - o.setAttribute("class", attObj[m]); - } - else if (m.toLowerCase() != "classid") { // filter out IE specific attribute - o.setAttribute(m, attObj[m]); - } - } - } - for (var n in parObj) { - if (parObj[n] != Object.prototype[n] && n.toLowerCase() != "movie") { // filter out prototype additions from other potential libraries and IE specific param element - createObjParam(o, n, parObj[n]); - } - } - el.parentNode.replaceChild(o, el); - r = o; - } - } - return r; - } - - function createObjParam(el, pName, pValue) { - var p = createElement("param"); - p.setAttribute("name", pName); - p.setAttribute("value", pValue); - el.appendChild(p); - } - - /* Cross-browser SWF removal - - Especially needed to safely and completely remove a SWF in Internet Explorer - */ - function removeSWF(id) { - var obj = getElementById(id); - if (obj && obj.nodeName == "OBJECT") { - if (ua.ie && ua.win) { - obj.style.display = "none"; - (function(){ - if (obj.readyState == 4) { - removeObjectInIE(id); - } - else { - setTimeout(arguments.callee, 10); - } - })(); - } - else { - obj.parentNode.removeChild(obj); - } - } - } - - function removeObjectInIE(id) { - var obj = getElementById(id); - if (obj) { - for (var i in obj) { - if (typeof obj[i] == "function") { - obj[i] = null; - } - } - obj.parentNode.removeChild(obj); - } - } - - /* Functions to optimize JavaScript compression - */ - function getElementById(id) { - var el = null; - try { - el = doc.getElementById(id); - } - catch (e) {} - return el; - } - - function createElement(el) { - return doc.createElement(el); - } - - /* Updated attachEvent function for Internet Explorer - - Stores attachEvent information in an Array, so on unload the detachEvent functions can be called to avoid memory leaks - */ - function addListener(target, eventType, fn) { - target.attachEvent(eventType, fn); - listenersArr[listenersArr.length] = [target, eventType, fn]; - } - - /* Flash Player and SWF content version matching - */ - function hasPlayerVersion(rv) { - var pv = ua.pv, v = rv.split("."); - v[0] = parseInt(v[0], 10); - v[1] = parseInt(v[1], 10) || 0; // supports short notation, e.g. "9" instead of "9.0.0" - v[2] = parseInt(v[2], 10) || 0; - return (pv[0] > v[0] || (pv[0] == v[0] && pv[1] > v[1]) || (pv[0] == v[0] && pv[1] == v[1] && pv[2] >= v[2])) ? true : false; - } - - /* Cross-browser dynamic CSS creation - - Based on Bobby van der Sluis' solution: http://www.bobbyvandersluis.com/articles/dynamicCSS.php - */ - function createCSS(sel, decl, media, newStyle) { - if (ua.ie && ua.mac) { return; } - var h = doc.getElementsByTagName("head")[0]; - if (!h) { return; } // to also support badly authored HTML pages that lack a head element - var m = (media && typeof media == "string") ? media : "screen"; - if (newStyle) { - dynamicStylesheet = null; - dynamicStylesheetMedia = null; - } - if (!dynamicStylesheet || dynamicStylesheetMedia != m) { - // create dynamic stylesheet + get a global reference to it - var s = createElement("style"); - s.setAttribute("type", "text/css"); - s.setAttribute("media", m); - dynamicStylesheet = h.appendChild(s); - if (ua.ie && ua.win && typeof doc.styleSheets != UNDEF && doc.styleSheets.length > 0) { - dynamicStylesheet = doc.styleSheets[doc.styleSheets.length - 1]; - } - dynamicStylesheetMedia = m; - } - // add style rule - if (ua.ie && ua.win) { - if (dynamicStylesheet && typeof dynamicStylesheet.addRule == OBJECT) { - dynamicStylesheet.addRule(sel, decl); - } - } - else { - if (dynamicStylesheet && typeof doc.createTextNode != UNDEF) { - dynamicStylesheet.appendChild(doc.createTextNode(sel + " {" + decl + "}")); - } - } - } - - function setVisibility(id, isVisible) { - if (!autoHideShow) { return; } - var v = isVisible ? "visible" : "hidden"; - if (isDomLoaded && getElementById(id)) { - getElementById(id).style.visibility = v; - } - else { - createCSS("#" + id, "visibility:" + v); - } - } - - /* Filter to avoid XSS attacks - */ - function urlEncodeIfNecessary(s) { - var regex = /[\\\"<>\.;]/; - var hasBadChars = regex.exec(s) != null; - return hasBadChars && typeof encodeURIComponent != UNDEF ? encodeURIComponent(s) : s; - } - - /* Release memory to avoid memory leaks caused by closures, fix hanging audio/video threads and force open sockets/NetConnections to disconnect (Internet Explorer only) - */ - var cleanup = function() { - if (ua.ie && ua.win) { - window.attachEvent("onunload", function() { - // remove listeners to avoid memory leaks - var ll = listenersArr.length; - for (var i = 0; i < ll; i++) { - listenersArr[i][0].detachEvent(listenersArr[i][1], listenersArr[i][2]); - } - // cleanup dynamically embedded objects to fix audio/video threads and force open sockets and NetConnections to disconnect - var il = objIdArr.length; - for (var j = 0; j < il; j++) { - removeSWF(objIdArr[j]); - } - // cleanup library's main closures to avoid memory leaks - for (var k in ua) { - ua[k] = null; - } - ua = null; - for (var l in swfobject) { - swfobject[l] = null; - } - swfobject = null; - window.detachEvent('onunload', arguments.callee); - }); - } - }(); - - return { - /* Public API - - Reference: http://code.google.com/p/swfobject/wiki/documentation - */ - registerObject: function(objectIdStr, swfVersionStr, xiSwfUrlStr, callbackFn) { - if (ua.w3 && objectIdStr && swfVersionStr) { - var regObj = {}; - regObj.id = objectIdStr; - regObj.swfVersion = swfVersionStr; - regObj.expressInstall = xiSwfUrlStr; - regObj.callbackFn = callbackFn; - regObjArr[regObjArr.length] = regObj; - setVisibility(objectIdStr, false); - } - else if (callbackFn) { - callbackFn({success:false, id:objectIdStr}); - } - }, - - getObjectById: function(objectIdStr) { - if (ua.w3) { - return getObjectById(objectIdStr); - } - }, - - embedSWF: function(swfUrlStr, replaceElemIdStr, widthStr, heightStr, swfVersionStr, xiSwfUrlStr, flashvarsObj, parObj, attObj, callbackFn) { - var callbackObj = {success:false, id:replaceElemIdStr}; - if (ua.w3 && !(ua.wk && ua.wk < 312) && swfUrlStr && replaceElemIdStr && widthStr && heightStr && swfVersionStr) { - setVisibility(replaceElemIdStr, false); - addDomLoadEvent(function() { - widthStr += ""; // auto-convert to string - heightStr += ""; - var att = {}; - if (attObj && typeof attObj === OBJECT) { - for (var i in attObj) { // copy object to avoid the use of references, because web authors often reuse attObj for multiple SWFs - att[i] = attObj[i]; - } - } - att.data = swfUrlStr; - att.width = widthStr; - att.height = heightStr; - var par = {}; - if (parObj && typeof parObj === OBJECT) { - for (var j in parObj) { // copy object to avoid the use of references, because web authors often reuse parObj for multiple SWFs - par[j] = parObj[j]; - } - } - if (flashvarsObj && typeof flashvarsObj === OBJECT) { - for (var k in flashvarsObj) { // copy object to avoid the use of references, because web authors often reuse flashvarsObj for multiple SWFs - if (typeof par.flashvars != UNDEF) { - par.flashvars += "&" + k + "=" + flashvarsObj[k]; - } - else { - par.flashvars = k + "=" + flashvarsObj[k]; - } - } - } - if (hasPlayerVersion(swfVersionStr)) { // create SWF - var obj = createSWF(att, par, replaceElemIdStr); - if (att.id == replaceElemIdStr) { - setVisibility(replaceElemIdStr, true); - } - callbackObj.success = true; - callbackObj.ref = obj; - } - else if (xiSwfUrlStr && canExpressInstall()) { // show Adobe Express Install - att.data = xiSwfUrlStr; - showExpressInstall(att, par, replaceElemIdStr, callbackFn); - return; - } - else { // show alternative content - setVisibility(replaceElemIdStr, true); - } - if (callbackFn) { callbackFn(callbackObj); } - }); - } - else if (callbackFn) { callbackFn(callbackObj); } - }, - - switchOffAutoHideShow: function() { - autoHideShow = false; - }, - - ua: ua, - - getFlashPlayerVersion: function() { - return { major:ua.pv[0], minor:ua.pv[1], release:ua.pv[2] }; - }, - - hasFlashPlayerVersion: hasPlayerVersion, - - createSWF: function(attObj, parObj, replaceElemIdStr) { - if (ua.w3) { - return createSWF(attObj, parObj, replaceElemIdStr); - } - else { - return undefined; - } - }, - - showExpressInstall: function(att, par, replaceElemIdStr, callbackFn) { - if (ua.w3 && canExpressInstall()) { - showExpressInstall(att, par, replaceElemIdStr, callbackFn); - } - }, - - removeSWF: function(objElemIdStr) { - if (ua.w3) { - removeSWF(objElemIdStr); - } - }, - - createCSS: function(selStr, declStr, mediaStr, newStyleBoolean) { - if (ua.w3) { - createCSS(selStr, declStr, mediaStr, newStyleBoolean); - } - }, - - addDomLoadEvent: addDomLoadEvent, - - addLoadEvent: addLoadEvent, - - getQueryParamValue: function(param) { - var q = doc.location.search || doc.location.hash; - if (q) { - if (/\?/.test(q)) { q = q.split("?")[1]; } // strip question mark - if (param == null) { - return urlEncodeIfNecessary(q); - } - var pairs = q.split("&"); - for (var i = 0; i < pairs.length; i++) { - if (pairs[i].substring(0, pairs[i].indexOf("=")) == param) { - return urlEncodeIfNecessary(pairs[i].substring((pairs[i].indexOf("=") + 1))); - } - } - } - return ""; - }, - - // For internal usage only - expressInstallCallback: function() { - if (isExpressInstallActive) { - var obj = getElementById(EXPRESS_INSTALL_ID); - if (obj && storedAltContent) { - obj.parentNode.replaceChild(storedAltContent, obj); - if (storedAltContentId) { - setVisibility(storedAltContentId, true); - if (ua.ie && ua.win) { storedAltContent.style.display = "block"; } - } - if (storedCallbackFn) { storedCallbackFn(storedCallbackObj); } - } - isExpressInstallActive = false; - } - } - }; -}(); -/** - * @class Ext.FlashComponent - * @extends Ext.BoxComponent - * @constructor - * @xtype flash - */ -Ext.FlashComponent = Ext.extend(Ext.BoxComponent, { - /** - * @cfg {String} flashVersion - * Indicates the version the flash content was published for. Defaults to '9.0.115'. - */ - flashVersion : '9.0.115', - - /** - * @cfg {String} backgroundColor - * The background color of the chart. Defaults to '#ffffff'. - */ - backgroundColor: '#ffffff', - - /** - * @cfg {String} wmode - * The wmode of the flash object. This can be used to control layering. Defaults to 'opaque'. - */ - wmode: 'opaque', - - /** - * @cfg {Object} flashVars - * A set of key value pairs to be passed to the flash object as flash variables. Defaults to undefined. - */ - flashVars: undefined, - - /** - * @cfg {Object} flashParams - * A set of key value pairs to be passed to the flash object as parameters. Possible parameters can be found here: - * http://kb2.adobe.com/cps/127/tn_12701.html Defaults to undefined. - */ - flashParams: undefined, - - /** - * @cfg {String} url - * The URL of the chart to include. Defaults to undefined. - */ - url: undefined, - swfId : undefined, - swfWidth: '100%', - swfHeight: '100%', - - /** - * @cfg {Boolean} expressInstall - * True to prompt the user to install flash if not installed. Note that this uses - * Ext.FlashComponent.EXPRESS_INSTALL_URL, which should be set to the local resource. Defaults to false. - */ - expressInstall: false, - - initComponent : function(){ - Ext.FlashComponent.superclass.initComponent.call(this); - - this.addEvents( - /** - * @event initialize - * - * @param {Chart} this - */ - 'initialize' - ); - }, - - onRender : function(){ - Ext.FlashComponent.superclass.onRender.apply(this, arguments); - - var params = Ext.apply({ - allowScriptAccess: 'always', - bgcolor: this.backgroundColor, - wmode: this.wmode - }, this.flashParams), vars = Ext.apply({ - allowedDomain: document.location.hostname, - YUISwfId: this.getId(), - YUIBridgeCallback: 'Ext.FlashEventProxy.onEvent' - }, this.flashVars); - - new swfobject.embedSWF(this.url, this.id, this.swfWidth, this.swfHeight, this.flashVersion, - this.expressInstall ? Ext.FlashComponent.EXPRESS_INSTALL_URL : undefined, vars, params); - - this.swf = Ext.getDom(this.id); - this.el = Ext.get(this.swf); - }, - - getSwfId : function(){ - return this.swfId || (this.swfId = "extswf" + (++Ext.Component.AUTO_ID)); - }, - - getId : function(){ - return this.id || (this.id = "extflashcmp" + (++Ext.Component.AUTO_ID)); - }, - - onFlashEvent : function(e){ - switch(e.type){ - case "swfReady": - this.initSwf(); - return; - case "log": - return; - } - e.component = this; - this.fireEvent(e.type.toLowerCase().replace(/event$/, ''), e); - }, - - initSwf : function(){ - this.onSwfReady(!!this.isInitialized); - this.isInitialized = true; - this.fireEvent('initialize', this); - }, - - beforeDestroy: function(){ - if(this.rendered){ - swfobject.removeSWF(this.swf.id); - } - Ext.FlashComponent.superclass.beforeDestroy.call(this); - }, - - onSwfReady : Ext.emptyFn -}); - -/** - * Sets the url for installing flash if it doesn't exist. This should be set to a local resource. - * @static - * @type String - */ -Ext.FlashComponent.EXPRESS_INSTALL_URL = 'http:/' + '/swfobject.googlecode.com/svn/trunk/swfobject/expressInstall.swf'; - -Ext.reg('flash', Ext.FlashComponent);/** - * @class Ext.FlashProxy - * @singleton - */ -Ext.FlashEventProxy = { - onEvent : function(id, e){ - var fp = Ext.getCmp(id); - if(fp){ - fp.onFlashEvent(e); - }else{ - arguments.callee.defer(10, this, [id, e]); - } - } -};/** - * @class Ext.chart.Chart - * @extends Ext.FlashComponent - * The Ext.chart package provides the capability to visualize data with flash based charting. - * Each chart binds directly to an Ext.data.Store enabling automatic updates of the chart. - * To change the look and feel of a chart, see the {@link #chartStyle} and {@link #extraStyle} config options. - * @constructor - * @xtype chart - */ - - Ext.chart.Chart = Ext.extend(Ext.FlashComponent, { - refreshBuffer: 100, - - /** - * @cfg {String} backgroundColor - * @hide - */ - - /** - * @cfg {Object} chartStyle - * Sets styles for this chart. This contains default styling, so modifying this property will override - * the built in styles of the chart. Use {@link #extraStyle} to add customizations to the default styling. - */ - chartStyle: { - padding: 10, - animationEnabled: true, - font: { - name: 'Tahoma', - color: 0x444444, - size: 11 - }, - dataTip: { - padding: 5, - border: { - color: 0x99bbe8, - size:1 - }, - background: { - color: 0xDAE7F6, - alpha: .9 - }, - font: { - name: 'Tahoma', - color: 0x15428B, - size: 10, - bold: true - } - } - }, - - /** - * @cfg {String} url - * The url to load the chart from. This defaults to Ext.chart.Chart.CHART_URL, which should - * be modified to point to the local charts resource. - */ - - /** - * @cfg {Object} extraStyle - * Contains extra styles that will be added or overwritten to the default chartStyle. Defaults to null. - * For a detailed list of the options available, visit the YUI Charts site - * at http://developer.yahoo.com/yui/charts/#basicstyles
    - * Some of the options availabe:
    - *
      - *
    • padding - The space around the edge of the chart's contents. Padding does not increase the size of the chart.
    • - *
    • animationEnabled - A Boolean value that specifies whether marker animations are enabled or not. Enabled by default.
    • - *
    • font - An Object defining the font style to be used in the chart. Defaults to { name: 'Tahoma', color: 0x444444, size: 11 }
      - *
        - *
      • name - font name
      • - *
      • color - font color (hex code, ie: "#ff0000", "ff0000" or 0xff0000)
      • - *
      • size - font size in points (numeric portion only, ie: 11)
      • - *
      • bold - boolean
      • - *
      • italic - boolean
      • - *
      • underline - boolean
      • - *
      - *
    • - *
    • border - An object defining the border style around the chart. The chart itself will decrease in dimensions to accomodate the border.
      - *
        - *
      • color - border color (hex code, ie: "#ff0000", "ff0000" or 0xff0000)
      • - *
      • size - border size in pixels (numeric portion only, ie: 1)
      • - *
      - *
    • - *
    • background - An object defining the background style of the chart.
      - *
        - *
      • color - border color (hex code, ie: "#ff0000", "ff0000" or 0xff0000)
      • - *
      • image - an image URL. May be relative to the current document or absolute.
      • - *
      - *
    • - *
    • legend - An object defining the legend style
      - *
        - *
      • display - location of the legend. Possible values are "none", "left", "right", "top", and "bottom".
      • - *
      • spacing - an image URL. May be relative to the current document or absolute.
      • - *
      • padding, border, background, font - same options as described above.
      • - *
    • - *
    • dataTip - An object defining the style of the data tip (tooltip).
      - *
        - *
      • padding, border, background, font - same options as described above.
      • - *
    • - *
    • xAxis and yAxis - An object defining the style of the style of either axis.
      - *
        - *
      • color - same option as described above.
      • - *
      • size - same option as described above.
      • - *
      • showLabels - boolean
      • - *
      • labelRotation - a value in degrees from -90 through 90. Default is zero.
      • - *
    • - *
    • majorGridLines and minorGridLines - An object defining the style of the style of the grid lines.
      - *
        - *
      • color, size - same options as described above.
      • - *
    • - *
    • zeroGridLine - An object defining the style of the style of the zero grid line.
      - *
        - *
      • color, size - same options as described above.
      • - *
    • - *
    • majorTicks and minorTicks - An object defining the style of the style of ticks in the chart.
      - *
        - *
      • color, size - same options as described above.
      • - *
      • length - the length of each tick in pixels extending from the axis.
      • - *
      • display - how the ticks are drawn. Possible values are "none", "inside", "outside", and "cross".
      • - *
    • - *
    - */ - extraStyle: null, - - /** - * @cfg {Object} seriesStyles - * Contains styles to apply to the series after a refresh. Defaults to null. - */ - seriesStyles: null, - - /** - * @cfg {Boolean} disableCaching - * True to add a "cache buster" to the end of the chart url. Defaults to true for Opera and IE. - */ - disableCaching: Ext.isIE || Ext.isOpera, - disableCacheParam: '_dc', - - initComponent : function(){ - Ext.chart.Chart.superclass.initComponent.call(this); - if(!this.url){ - this.url = Ext.chart.Chart.CHART_URL; - } - if(this.disableCaching){ - this.url = Ext.urlAppend(this.url, String.format('{0}={1}', this.disableCacheParam, new Date().getTime())); - } - this.addEvents( - 'itemmouseover', - 'itemmouseout', - 'itemclick', - 'itemdoubleclick', - 'itemdragstart', - 'itemdrag', - 'itemdragend', - /** - * @event beforerefresh - * Fires before a refresh to the chart data is called. If the beforerefresh handler returns - * false the {@link #refresh} action will be cancelled. - * @param {Chart} this - */ - 'beforerefresh', - /** - * @event refresh - * Fires after the chart data has been refreshed. - * @param {Chart} this - */ - 'refresh' - ); - this.store = Ext.StoreMgr.lookup(this.store); - }, - - /** - * Sets a single style value on the Chart instance. - * - * @param name {String} Name of the Chart style value to change. - * @param value {Object} New value to pass to the Chart style. - */ - setStyle: function(name, value){ - this.swf.setStyle(name, Ext.encode(value)); - }, - - /** - * Resets all styles on the Chart instance. - * - * @param styles {Object} Initializer for all Chart styles. - */ - setStyles: function(styles){ - this.swf.setStyles(Ext.encode(styles)); - }, - - /** - * Sets the styles on all series in the Chart. - * - * @param styles {Array} Initializer for all Chart series styles. - */ - setSeriesStyles: function(styles){ - this.seriesStyles = styles; - var s = []; - Ext.each(styles, function(style){ - s.push(Ext.encode(style)); - }); - this.swf.setSeriesStyles(s); - }, - - setCategoryNames : function(names){ - this.swf.setCategoryNames(names); - }, - - setLegendRenderer : function(fn, scope){ - var chart = this; - scope = scope || chart; - chart.removeFnProxy(chart.legendFnName); - chart.legendFnName = chart.createFnProxy(function(name){ - return fn.call(scope, name); - }); - chart.swf.setLegendLabelFunction(chart.legendFnName); - }, - - setTipRenderer : function(fn, scope){ - var chart = this; - scope = scope || chart; - chart.removeFnProxy(chart.tipFnName); - chart.tipFnName = chart.createFnProxy(function(item, index, series){ - var record = chart.store.getAt(index); - return fn.call(scope, chart, record, index, series); - }); - chart.swf.setDataTipFunction(chart.tipFnName); - }, - - setSeries : function(series){ - this.series = series; - this.refresh(); - }, - - /** - * Changes the data store bound to this chart and refreshes it. - * @param {Store} store The store to bind to this chart - */ - bindStore : function(store, initial){ - if(!initial && this.store){ - if(store !== this.store && this.store.autoDestroy){ - this.store.destroy(); - }else{ - this.store.un("datachanged", this.refresh, this); - this.store.un("add", this.delayRefresh, this); - this.store.un("remove", this.delayRefresh, this); - this.store.un("update", this.delayRefresh, this); - this.store.un("clear", this.refresh, this); - } - } - if(store){ - store = Ext.StoreMgr.lookup(store); - store.on({ - scope: this, - datachanged: this.refresh, - add: this.delayRefresh, - remove: this.delayRefresh, - update: this.delayRefresh, - clear: this.refresh - }); - } - this.store = store; - if(store && !initial){ - this.refresh(); - } - }, - - onSwfReady : function(isReset){ - Ext.chart.Chart.superclass.onSwfReady.call(this, isReset); - var ref; - this.swf.setType(this.type); - - if(this.chartStyle){ - this.setStyles(Ext.apply({}, this.extraStyle, this.chartStyle)); - } - - if(this.categoryNames){ - this.setCategoryNames(this.categoryNames); - } - - if(this.tipRenderer){ - ref = this.getFunctionRef(this.tipRenderer); - this.setTipRenderer(ref.fn, ref.scope); - } - if(this.legendRenderer){ - ref = this.getFunctionRef(this.legendRenderer); - this.setLegendRenderer(ref.fn, ref.scope); - } - if(!isReset){ - this.bindStore(this.store, true); - } - this.refresh.defer(10, this); - }, - - delayRefresh : function(){ - if(!this.refreshTask){ - this.refreshTask = new Ext.util.DelayedTask(this.refresh, this); - } - this.refreshTask.delay(this.refreshBuffer); - }, - - refresh : function(){ - if(this.fireEvent('beforerefresh', this) !== false){ - var styleChanged = false; - // convert the store data into something YUI charts can understand - var data = [], rs = this.store.data.items; - for(var j = 0, len = rs.length; j < len; j++){ - data[j] = rs[j].data; - } - //make a copy of the series definitions so that we aren't - //editing them directly. - var dataProvider = []; - var seriesCount = 0; - var currentSeries = null; - var i = 0; - if(this.series){ - seriesCount = this.series.length; - for(i = 0; i < seriesCount; i++){ - currentSeries = this.series[i]; - var clonedSeries = {}; - for(var prop in currentSeries){ - if(prop == "style" && currentSeries.style !== null){ - clonedSeries.style = Ext.encode(currentSeries.style); - styleChanged = true; - //we don't want to modify the styles again next time - //so null out the style property. - // this causes issues - // currentSeries.style = null; - } else{ - clonedSeries[prop] = currentSeries[prop]; - } - } - dataProvider.push(clonedSeries); - } - } - - if(seriesCount > 0){ - for(i = 0; i < seriesCount; i++){ - currentSeries = dataProvider[i]; - if(!currentSeries.type){ - currentSeries.type = this.type; - } - currentSeries.dataProvider = data; - } - } else{ - dataProvider.push({type: this.type, dataProvider: data}); - } - this.swf.setDataProvider(dataProvider); - if(this.seriesStyles){ - this.setSeriesStyles(this.seriesStyles); - } - this.fireEvent('refresh', this); - } - }, - - // private - createFnProxy : function(fn){ - var fnName = 'extFnProxy' + (++Ext.chart.Chart.PROXY_FN_ID); - Ext.chart.Chart.proxyFunction[fnName] = fn; - return 'Ext.chart.Chart.proxyFunction.' + fnName; - }, - - // private - removeFnProxy : function(fn){ - if(!Ext.isEmpty(fn)){ - fn = fn.replace('Ext.chart.Chart.proxyFunction.', ''); - delete Ext.chart.Chart.proxyFunction[fn]; - } - }, - - // private - getFunctionRef : function(val){ - if(Ext.isFunction(val)){ - return { - fn: val, - scope: this - }; - }else{ - return { - fn: val.fn, - scope: val.scope || this - }; - } - }, - - // private - onDestroy: function(){ - if (this.refreshTask && this.refreshTask.cancel){ - this.refreshTask.cancel(); - } - Ext.chart.Chart.superclass.onDestroy.call(this); - this.bindStore(null); - this.removeFnProxy(this.tipFnName); - this.removeFnProxy(this.legendFnName); - } -}); -Ext.reg('chart', Ext.chart.Chart); -Ext.chart.Chart.PROXY_FN_ID = 0; -Ext.chart.Chart.proxyFunction = {}; - -/** - * Sets the url to load the chart from. This should be set to a local resource. - * @static - * @type String - */ -Ext.chart.Chart.CHART_URL = 'http:/' + '/yui.yahooapis.com/2.8.2/build/charts/assets/charts.swf'; - -/** - * @class Ext.chart.PieChart - * @extends Ext.chart.Chart - * @constructor - * @xtype piechart - */ -Ext.chart.PieChart = Ext.extend(Ext.chart.Chart, { - type: 'pie', - - onSwfReady : function(isReset){ - Ext.chart.PieChart.superclass.onSwfReady.call(this, isReset); - - this.setDataField(this.dataField); - this.setCategoryField(this.categoryField); - }, - - setDataField : function(field){ - this.dataField = field; - this.swf.setDataField(field); - }, - - setCategoryField : function(field){ - this.categoryField = field; - this.swf.setCategoryField(field); - } -}); -Ext.reg('piechart', Ext.chart.PieChart); - -/** - * @class Ext.chart.CartesianChart - * @extends Ext.chart.Chart - * @constructor - * @xtype cartesianchart - */ -Ext.chart.CartesianChart = Ext.extend(Ext.chart.Chart, { - onSwfReady : function(isReset){ - Ext.chart.CartesianChart.superclass.onSwfReady.call(this, isReset); - this.labelFn = []; - if(this.xField){ - this.setXField(this.xField); - } - if(this.yField){ - this.setYField(this.yField); - } - if(this.xAxis){ - this.setXAxis(this.xAxis); - } - if(this.xAxes){ - this.setXAxes(this.xAxes); - } - if(this.yAxis){ - this.setYAxis(this.yAxis); - } - if(this.yAxes){ - this.setYAxes(this.yAxes); - } - if(Ext.isDefined(this.constrainViewport)){ - this.swf.setConstrainViewport(this.constrainViewport); - } - }, - - setXField : function(value){ - this.xField = value; - this.swf.setHorizontalField(value); - }, - - setYField : function(value){ - this.yField = value; - this.swf.setVerticalField(value); - }, - - setXAxis : function(value){ - this.xAxis = this.createAxis('xAxis', value); - this.swf.setHorizontalAxis(this.xAxis); - }, - - setXAxes : function(value){ - var axis; - for(var i = 0; i < value.length; i++) { - axis = this.createAxis('xAxis' + i, value[i]); - this.swf.setHorizontalAxis(axis); - } - }, - - setYAxis : function(value){ - this.yAxis = this.createAxis('yAxis', value); - this.swf.setVerticalAxis(this.yAxis); - }, - - setYAxes : function(value){ - var axis; - for(var i = 0; i < value.length; i++) { - axis = this.createAxis('yAxis' + i, value[i]); - this.swf.setVerticalAxis(axis); - } - }, - - createAxis : function(axis, value){ - var o = Ext.apply({}, value), - ref, - old; - - if(this[axis]){ - old = this[axis].labelFunction; - this.removeFnProxy(old); - this.labelFn.remove(old); - } - if(o.labelRenderer){ - ref = this.getFunctionRef(o.labelRenderer); - o.labelFunction = this.createFnProxy(function(v){ - return ref.fn.call(ref.scope, v); - }); - delete o.labelRenderer; - this.labelFn.push(o.labelFunction); - } - if(axis.indexOf('xAxis') > -1 && o.position == 'left'){ - o.position = 'bottom'; - } - return o; - }, - - onDestroy : function(){ - Ext.chart.CartesianChart.superclass.onDestroy.call(this); - Ext.each(this.labelFn, function(fn){ - this.removeFnProxy(fn); - }, this); - } -}); -Ext.reg('cartesianchart', Ext.chart.CartesianChart); - -/** - * @class Ext.chart.LineChart - * @extends Ext.chart.CartesianChart - * @constructor - * @xtype linechart - */ -Ext.chart.LineChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'line' -}); -Ext.reg('linechart', Ext.chart.LineChart); - -/** - * @class Ext.chart.ColumnChart - * @extends Ext.chart.CartesianChart - * @constructor - * @xtype columnchart - */ -Ext.chart.ColumnChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'column' -}); -Ext.reg('columnchart', Ext.chart.ColumnChart); - -/** - * @class Ext.chart.StackedColumnChart - * @extends Ext.chart.CartesianChart - * @constructor - * @xtype stackedcolumnchart - */ -Ext.chart.StackedColumnChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'stackcolumn' -}); -Ext.reg('stackedcolumnchart', Ext.chart.StackedColumnChart); - -/** - * @class Ext.chart.BarChart - * @extends Ext.chart.CartesianChart - * @constructor - * @xtype barchart - */ -Ext.chart.BarChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'bar' -}); -Ext.reg('barchart', Ext.chart.BarChart); - -/** - * @class Ext.chart.StackedBarChart - * @extends Ext.chart.CartesianChart - * @constructor - * @xtype stackedbarchart - */ -Ext.chart.StackedBarChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'stackbar' -}); -Ext.reg('stackedbarchart', Ext.chart.StackedBarChart); - - - -/** - * @class Ext.chart.Axis - * Defines a CartesianChart's vertical or horizontal axis. - * @constructor - */ -Ext.chart.Axis = function(config){ - Ext.apply(this, config); -}; - -Ext.chart.Axis.prototype = -{ - /** - * The type of axis. - * - * @property type - * @type String - */ - type: null, - - /** - * The direction in which the axis is drawn. May be "horizontal" or "vertical". - * - * @property orientation - * @type String - */ - orientation: "horizontal", - - /** - * If true, the items on the axis will be drawn in opposite direction. - * - * @property reverse - * @type Boolean - */ - reverse: false, - - /** - * A string reference to the globally-accessible function that may be called to - * determine each of the label values for this axis. - * - * @property labelFunction - * @type String - */ - labelFunction: null, - - /** - * If true, labels that overlap previously drawn labels on the axis will be hidden. - * - * @property hideOverlappingLabels - * @type Boolean - */ - hideOverlappingLabels: true, - - /** - * The space, in pixels, between labels on an axis. - * - * @property labelSpacing - * @type Number - */ - labelSpacing: 2 -}; - -/** - * @class Ext.chart.NumericAxis - * @extends Ext.chart.Axis - * A type of axis whose units are measured in numeric values. - * @constructor - */ -Ext.chart.NumericAxis = Ext.extend(Ext.chart.Axis, { - type: "numeric", - - /** - * The minimum value drawn by the axis. If not set explicitly, the axis - * minimum will be calculated automatically. - * - * @property minimum - * @type Number - */ - minimum: NaN, - - /** - * The maximum value drawn by the axis. If not set explicitly, the axis - * maximum will be calculated automatically. - * - * @property maximum - * @type Number - */ - maximum: NaN, - - /** - * The spacing between major intervals on this axis. - * - * @property majorUnit - * @type Number - */ - majorUnit: NaN, - - /** - * The spacing between minor intervals on this axis. - * - * @property minorUnit - * @type Number - */ - minorUnit: NaN, - - /** - * If true, the labels, ticks, gridlines, and other objects will snap to the - * nearest major or minor unit. If false, their position will be based on - * the minimum value. - * - * @property snapToUnits - * @type Boolean - */ - snapToUnits: true, - - /** - * If true, and the bounds are calculated automatically, either the minimum - * or maximum will be set to zero. - * - * @property alwaysShowZero - * @type Boolean - */ - alwaysShowZero: true, - - /** - * The scaling algorithm to use on this axis. May be "linear" or - * "logarithmic". - * - * @property scale - * @type String - */ - scale: "linear", - - /** - * Indicates whether to round the major unit. - * - * @property roundMajorUnit - * @type Boolean - */ - roundMajorUnit: true, - - /** - * Indicates whether to factor in the size of the labels when calculating a - * major unit. - * - * @property calculateByLabelSize - * @type Boolean - */ - calculateByLabelSize: true, - - /** - * Indicates the position of the axis relative to the chart - * - * @property position - * @type String - */ - position: 'left', - - /** - * Indicates whether to extend maximum beyond data's maximum to the nearest - * majorUnit. - * - * @property adjustMaximumByMajorUnit - * @type Boolean - */ - adjustMaximumByMajorUnit: true, - - /** - * Indicates whether to extend the minimum beyond data's minimum to the - * nearest majorUnit. - * - * @property adjustMinimumByMajorUnit - * @type Boolean - */ - adjustMinimumByMajorUnit: true - -}); - -/** - * @class Ext.chart.TimeAxis - * @extends Ext.chart.Axis - * A type of axis whose units are measured in time-based values. - * @constructor - */ -Ext.chart.TimeAxis = Ext.extend(Ext.chart.Axis, { - type: "time", - - /** - * The minimum value drawn by the axis. If not set explicitly, the axis - * minimum will be calculated automatically. - * - * @property minimum - * @type Date - */ - minimum: null, - - /** - * The maximum value drawn by the axis. If not set explicitly, the axis - * maximum will be calculated automatically. - * - * @property maximum - * @type Number - */ - maximum: null, - - /** - * The spacing between major intervals on this axis. - * - * @property majorUnit - * @type Number - */ - majorUnit: NaN, - - /** - * The time unit used by the majorUnit. - * - * @property majorTimeUnit - * @type String - */ - majorTimeUnit: null, - - /** - * The spacing between minor intervals on this axis. - * - * @property majorUnit - * @type Number - */ - minorUnit: NaN, - - /** - * The time unit used by the minorUnit. - * - * @property majorTimeUnit - * @type String - */ - minorTimeUnit: null, - - /** - * If true, the labels, ticks, gridlines, and other objects will snap to the - * nearest major or minor unit. If false, their position will be based on - * the minimum value. - * - * @property snapToUnits - * @type Boolean - */ - snapToUnits: true, - - /** - * Series that are stackable will only stack when this value is set to true. - * - * @property stackingEnabled - * @type Boolean - */ - stackingEnabled: false, - - /** - * Indicates whether to factor in the size of the labels when calculating a - * major unit. - * - * @property calculateByLabelSize - * @type Boolean - */ - calculateByLabelSize: true - -}); - -/** - * @class Ext.chart.CategoryAxis - * @extends Ext.chart.Axis - * A type of axis that displays items in categories. - * @constructor - */ -Ext.chart.CategoryAxis = Ext.extend(Ext.chart.Axis, { - type: "category", - - /** - * A list of category names to display along this axis. - * - * @property categoryNames - * @type Array - */ - categoryNames: null, - - /** - * Indicates whether or not to calculate the number of categories (ticks and - * labels) when there is not enough room to display all labels on the axis. - * If set to true, the axis will determine the number of categories to plot. - * If not, all categories will be plotted. - * - * @property calculateCategoryCount - * @type Boolean - */ - calculateCategoryCount: false - -}); - -/** - * @class Ext.chart.Series - * Series class for the charts widget. - * @constructor - */ -Ext.chart.Series = function(config) { Ext.apply(this, config); }; - -Ext.chart.Series.prototype = -{ - /** - * The type of series. - * - * @property type - * @type String - */ - type: null, - - /** - * The human-readable name of the series. - * - * @property displayName - * @type String - */ - displayName: null -}; - -/** - * @class Ext.chart.CartesianSeries - * @extends Ext.chart.Series - * CartesianSeries class for the charts widget. - * @constructor - */ -Ext.chart.CartesianSeries = Ext.extend(Ext.chart.Series, { - /** - * The field used to access the x-axis value from the items from the data - * source. - * - * @property xField - * @type String - */ - xField: null, - - /** - * The field used to access the y-axis value from the items from the data - * source. - * - * @property yField - * @type String - */ - yField: null, - - /** - * False to not show this series in the legend. Defaults to true. - * - * @property showInLegend - * @type Boolean - */ - showInLegend: true, - - /** - * Indicates which axis the series will bind to - * - * @property axis - * @type String - */ - axis: 'primary' -}); - -/** - * @class Ext.chart.ColumnSeries - * @extends Ext.chart.CartesianSeries - * ColumnSeries class for the charts widget. - * @constructor - */ -Ext.chart.ColumnSeries = Ext.extend(Ext.chart.CartesianSeries, { - type: "column" -}); - -/** - * @class Ext.chart.LineSeries - * @extends Ext.chart.CartesianSeries - * LineSeries class for the charts widget. - * @constructor - */ -Ext.chart.LineSeries = Ext.extend(Ext.chart.CartesianSeries, { - type: "line" -}); - -/** - * @class Ext.chart.BarSeries - * @extends Ext.chart.CartesianSeries - * BarSeries class for the charts widget. - * @constructor - */ -Ext.chart.BarSeries = Ext.extend(Ext.chart.CartesianSeries, { - type: "bar" -}); - - -/** - * @class Ext.chart.PieSeries - * @extends Ext.chart.Series - * PieSeries class for the charts widget. - * @constructor - */ -Ext.chart.PieSeries = Ext.extend(Ext.chart.Series, { - type: "pie", - dataField: null, - categoryField: null -});/** - * @class Ext.menu.Menu - * @extends Ext.Container - *

    A menu object. This is the container to which you may add menu items. Menu can also serve as a base class - * when you want a specialized menu based off of another component (like {@link Ext.menu.DateMenu} for example).

    - *

    Menus may contain either {@link Ext.menu.Item menu items}, or general {@link Ext.Component Component}s.

    - *

    To make a contained general {@link Ext.Component Component} line up with other {@link Ext.menu.Item menu items} - * specify iconCls: 'no-icon'. This reserves a space for an icon, and indents the Component in line - * with the other menu items. See {@link Ext.form.ComboBox}.{@link Ext.form.ComboBox#getListParent getListParent} - * for an example.

    - *

    By default, Menus are absolutely positioned, floating Components. By configuring a Menu with - * {@link #floating}:false, a Menu may be used as child of a Container.

    - * - * @xtype menu - */ -Ext.menu.Menu = Ext.extend(Ext.Container, { - /** - * @cfg {Object} defaults - * A config object that will be applied to all items added to this container either via the {@link #items} - * config or via the {@link #add} method. The defaults config can contain any number of - * name/value property pairs to be added to each item, and should be valid for the types of items - * being added to the menu. - */ - /** - * @cfg {Mixed} items - * An array of items to be added to this menu. Menus may contain either {@link Ext.menu.Item menu items}, - * or general {@link Ext.Component Component}s. - */ - /** - * @cfg {Number} minWidth The minimum width of the menu in pixels (defaults to 120) - */ - minWidth : 120, - /** - * @cfg {Boolean/String} shadow True or 'sides' for the default effect, 'frame' for 4-way shadow, and 'drop' - * for bottom-right shadow (defaults to 'sides') - */ - shadow : 'sides', - /** - * @cfg {String} subMenuAlign The {@link Ext.Element#alignTo} anchor position value to use for submenus of - * this menu (defaults to 'tl-tr?') - */ - subMenuAlign : 'tl-tr?', - /** - * @cfg {String} defaultAlign The default {@link Ext.Element#alignTo} anchor position value for this menu - * relative to its element of origin (defaults to 'tl-bl?') - */ - defaultAlign : 'tl-bl?', - /** - * @cfg {Boolean} allowOtherMenus True to allow multiple menus to be displayed at the same time (defaults to false) - */ - allowOtherMenus : false, - /** - * @cfg {Boolean} ignoreParentClicks True to ignore clicks on any item in this menu that is a parent item (displays - * a submenu) so that the submenu is not dismissed when clicking the parent item (defaults to false). - */ - ignoreParentClicks : false, - /** - * @cfg {Boolean} enableScrolling True to allow the menu container to have scroller controls if the menu is too long (defaults to true). - */ - enableScrolling : true, - /** - * @cfg {Number} maxHeight The maximum height of the menu. Only applies when enableScrolling is set to True (defaults to null). - */ - maxHeight : null, - /** - * @cfg {Number} scrollIncrement The amount to scroll the menu. Only applies when enableScrolling is set to True (defaults to 24). - */ - scrollIncrement : 24, - /** - * @cfg {Boolean} showSeparator True to show the icon separator. (defaults to true). - */ - showSeparator : true, - /** - * @cfg {Array} defaultOffsets An array specifying the [x, y] offset in pixels by which to - * change the default Menu popup position after aligning according to the {@link #defaultAlign} - * configuration. Defaults to [0, 0]. - */ - defaultOffsets : [0, 0], - - /** - * @cfg {Boolean} plain - * True to remove the incised line down the left side of the menu. Defaults to false. - */ - plain : false, - - /** - * @cfg {Boolean} floating - *

    By default, a Menu configured as floating:true - * will be rendered as an {@link Ext.Layer} (an absolutely positioned, - * floating Component with zindex=15000). - * If configured as floating:false, the Menu may be - * used as child item of another Container instead of a free-floating - * {@link Ext.Layer Layer}. - */ - floating : true, - - - /** - * @cfg {Number} zIndex - * zIndex to use when the menu is floating. - */ - zIndex: 15000, - - // private - hidden : true, - - /** - * @cfg {String/Object} layout - * This class assigns a default layout (layout:'menu'). - * Developers may override this configuration option if another layout is required. - * See {@link Ext.Container#layout} for additional information. - */ - layout : 'menu', - hideMode : 'offsets', // Important for laying out Components - scrollerHeight : 8, - autoLayout : true, // Provided for backwards compat - defaultType : 'menuitem', - bufferResize : false, - - initComponent : function(){ - if(Ext.isArray(this.initialConfig)){ - Ext.apply(this, {items:this.initialConfig}); - } - this.addEvents( - /** - * @event click - * Fires when this menu is clicked (or when the enter key is pressed while it is active) - * @param {Ext.menu.Menu} this - * @param {Ext.menu.Item} menuItem The menu item that was clicked - * @param {Ext.EventObject} e - */ - 'click', - /** - * @event mouseover - * Fires when the mouse is hovering over this menu - * @param {Ext.menu.Menu} this - * @param {Ext.EventObject} e - * @param {Ext.menu.Item} menuItem The menu item that was clicked - */ - 'mouseover', - /** - * @event mouseout - * Fires when the mouse exits this menu - * @param {Ext.menu.Menu} this - * @param {Ext.EventObject} e - * @param {Ext.menu.Item} menuItem The menu item that was clicked - */ - 'mouseout', - /** - * @event itemclick - * Fires when a menu item contained in this menu is clicked - * @param {Ext.menu.BaseItem} baseItem The BaseItem that was clicked - * @param {Ext.EventObject} e - */ - 'itemclick' - ); - Ext.menu.MenuMgr.register(this); - if(this.floating){ - Ext.EventManager.onWindowResize(this.hide, this); - }else{ - if(this.initialConfig.hidden !== false){ - this.hidden = false; - } - this.internalDefaults = {hideOnClick: false}; - } - Ext.menu.Menu.superclass.initComponent.call(this); - if(this.autoLayout){ - var fn = this.doLayout.createDelegate(this, []); - this.on({ - add: fn, - remove: fn - }); - } - }, - - //private - getLayoutTarget : function() { - return this.ul; - }, - - // private - onRender : function(ct, position){ - if(!ct){ - ct = Ext.getBody(); - } - - var dh = { - id: this.getId(), - cls: 'x-menu ' + ((this.floating) ? 'x-menu-floating x-layer ' : '') + (this.cls || '') + (this.plain ? ' x-menu-plain' : '') + (this.showSeparator ? '' : ' x-menu-nosep'), - style: this.style, - cn: [ - {tag: 'a', cls: 'x-menu-focus', href: '#', onclick: 'return false;', tabIndex: '-1'}, - {tag: 'ul', cls: 'x-menu-list'} - ] - }; - if(this.floating){ - this.el = new Ext.Layer({ - shadow: this.shadow, - dh: dh, - constrain: false, - parentEl: ct, - zindex: this.zIndex - }); - }else{ - this.el = ct.createChild(dh); - } - Ext.menu.Menu.superclass.onRender.call(this, ct, position); - - if(!this.keyNav){ - this.keyNav = new Ext.menu.MenuNav(this); - } - // generic focus element - this.focusEl = this.el.child('a.x-menu-focus'); - this.ul = this.el.child('ul.x-menu-list'); - this.mon(this.ul, { - scope: this, - click: this.onClick, - mouseover: this.onMouseOver, - mouseout: this.onMouseOut - }); - if(this.enableScrolling){ - this.mon(this.el, { - scope: this, - delegate: '.x-menu-scroller', - click: this.onScroll, - mouseover: this.deactivateActive - }); - } - }, - - // private - findTargetItem : function(e){ - var t = e.getTarget('.x-menu-list-item', this.ul, true); - if(t && t.menuItemId){ - return this.items.get(t.menuItemId); - } - }, - - // private - onClick : function(e){ - var t = this.findTargetItem(e); - if(t){ - if(t.isFormField){ - this.setActiveItem(t); - }else if(t instanceof Ext.menu.BaseItem){ - if(t.menu && this.ignoreParentClicks){ - t.expandMenu(); - e.preventDefault(); - }else if(t.onClick){ - t.onClick(e); - this.fireEvent('click', this, t, e); - } - } - } - }, - - // private - setActiveItem : function(item, autoExpand){ - if(item != this.activeItem){ - this.deactivateActive(); - if((this.activeItem = item).isFormField){ - item.focus(); - }else{ - item.activate(autoExpand); - } - }else if(autoExpand){ - item.expandMenu(); - } - }, - - deactivateActive : function(){ - var a = this.activeItem; - if(a){ - if(a.isFormField){ - //Fields cannot deactivate, but Combos must collapse - if(a.collapse){ - a.collapse(); - } - }else{ - a.deactivate(); - } - delete this.activeItem; - } - }, - - // private - tryActivate : function(start, step){ - var items = this.items; - for(var i = start, len = items.length; i >= 0 && i < len; i+= step){ - var item = items.get(i); - if(item.isVisible() && !item.disabled && (item.canActivate || item.isFormField)){ - this.setActiveItem(item, false); - return item; - } - } - return false; - }, - - // private - onMouseOver : function(e){ - var t = this.findTargetItem(e); - if(t){ - if(t.canActivate && !t.disabled){ - this.setActiveItem(t, true); - } - } - this.over = true; - this.fireEvent('mouseover', this, e, t); - }, - - // private - onMouseOut : function(e){ - var t = this.findTargetItem(e); - if(t){ - if(t == this.activeItem && t.shouldDeactivate && t.shouldDeactivate(e)){ - this.activeItem.deactivate(); - delete this.activeItem; - } - } - this.over = false; - this.fireEvent('mouseout', this, e, t); - }, - - // private - onScroll : function(e, t){ - if(e){ - e.stopEvent(); - } - var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top'); - ul.scrollTop += this.scrollIncrement * (top ? -1 : 1); - if(top ? ul.scrollTop <= 0 : ul.scrollTop + this.activeMax >= ul.scrollHeight){ - this.onScrollerOut(null, t); - } - }, - - // private - onScrollerIn : function(e, t){ - var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top'); - if(top ? ul.scrollTop > 0 : ul.scrollTop + this.activeMax < ul.scrollHeight){ - Ext.fly(t).addClass(['x-menu-item-active', 'x-menu-scroller-active']); - } - }, - - // private - onScrollerOut : function(e, t){ - Ext.fly(t).removeClass(['x-menu-item-active', 'x-menu-scroller-active']); - }, - - /** - * If {@link #floating}=true, shows this menu relative to - * another element using {@link #showat}, otherwise uses {@link Ext.Component#show}. - * @param {Mixed} element The element to align to - * @param {String} position (optional) The {@link Ext.Element#alignTo} anchor position to use in aligning to - * the element (defaults to this.defaultAlign) - * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined) - */ - show : function(el, pos, parentMenu){ - if(this.floating){ - this.parentMenu = parentMenu; - if(!this.el){ - this.render(); - this.doLayout(false, true); - } - this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign, this.defaultOffsets), parentMenu); - }else{ - Ext.menu.Menu.superclass.show.call(this); - } - }, - - /** - * Displays this menu at a specific xy position and fires the 'show' event if a - * handler for the 'beforeshow' event does not return false cancelling the operation. - * @param {Array} xyPosition Contains X & Y [x, y] values for the position at which to show the menu (coordinates are page-based) - * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined) - */ - showAt : function(xy, parentMenu){ - if(this.fireEvent('beforeshow', this) !== false){ - this.parentMenu = parentMenu; - if(!this.el){ - this.render(); - } - if(this.enableScrolling){ - // set the position so we can figure out the constrain value. - this.el.setXY(xy); - //constrain the value, keep the y coordinate the same - xy[1] = this.constrainScroll(xy[1]); - xy = [this.el.adjustForConstraints(xy)[0], xy[1]]; - }else{ - //constrain to the viewport. - xy = this.el.adjustForConstraints(xy); - } - this.el.setXY(xy); - this.el.show(); - Ext.menu.Menu.superclass.onShow.call(this); - if(Ext.isIE){ - // internal event, used so we don't couple the layout to the menu - this.fireEvent('autosize', this); - if(!Ext.isIE8){ - this.el.repaint(); - } - } - this.hidden = false; - this.focus(); - this.fireEvent('show', this); - } - }, - - constrainScroll : function(y){ - var max, full = this.ul.setHeight('auto').getHeight(), - returnY = y, normalY, parentEl, scrollTop, viewHeight; - if(this.floating){ - parentEl = Ext.fly(this.el.dom.parentNode); - scrollTop = parentEl.getScroll().top; - viewHeight = parentEl.getViewSize().height; - //Normalize y by the scroll position for the parent element. Need to move it into the coordinate space - //of the view. - normalY = y - scrollTop; - max = this.maxHeight ? this.maxHeight : viewHeight - normalY; - if(full > viewHeight) { - max = viewHeight; - //Set returnY equal to (0,0) in view space by reducing y by the value of normalY - returnY = y - normalY; - } else if(max < full) { - returnY = y - (full - max); - max = full; - } - }else{ - max = this.getHeight(); - } - // Always respect maxHeight - if (this.maxHeight){ - max = Math.min(this.maxHeight, max); - } - if(full > max && max > 0){ - this.activeMax = max - this.scrollerHeight * 2 - this.el.getFrameWidth('tb') - Ext.num(this.el.shadowOffset, 0); - this.ul.setHeight(this.activeMax); - this.createScrollers(); - this.el.select('.x-menu-scroller').setDisplayed(''); - }else{ - this.ul.setHeight(full); - this.el.select('.x-menu-scroller').setDisplayed('none'); - } - this.ul.dom.scrollTop = 0; - return returnY; - }, - - createScrollers : function(){ - if(!this.scroller){ - this.scroller = { - pos: 0, - top: this.el.insertFirst({ - tag: 'div', - cls: 'x-menu-scroller x-menu-scroller-top', - html: ' ' - }), - bottom: this.el.createChild({ - tag: 'div', - cls: 'x-menu-scroller x-menu-scroller-bottom', - html: ' ' - }) - }; - this.scroller.top.hover(this.onScrollerIn, this.onScrollerOut, this); - this.scroller.topRepeater = new Ext.util.ClickRepeater(this.scroller.top, { - listeners: { - click: this.onScroll.createDelegate(this, [null, this.scroller.top], false) - } - }); - this.scroller.bottom.hover(this.onScrollerIn, this.onScrollerOut, this); - this.scroller.bottomRepeater = new Ext.util.ClickRepeater(this.scroller.bottom, { - listeners: { - click: this.onScroll.createDelegate(this, [null, this.scroller.bottom], false) - } - }); - } - }, - - onLayout : function(){ - if(this.isVisible()){ - if(this.enableScrolling){ - this.constrainScroll(this.el.getTop()); - } - if(this.floating){ - this.el.sync(); - } - } - }, - - focus : function(){ - if(!this.hidden){ - this.doFocus.defer(50, this); - } - }, - - doFocus : function(){ - if(!this.hidden){ - this.focusEl.focus(); - } - }, - - /** - * Hides this menu and optionally all parent menus - * @param {Boolean} deep (optional) True to hide all parent menus recursively, if any (defaults to false) - */ - hide : function(deep){ - if (!this.isDestroyed) { - this.deepHide = deep; - Ext.menu.Menu.superclass.hide.call(this); - delete this.deepHide; - } - }, - - // private - onHide : function(){ - Ext.menu.Menu.superclass.onHide.call(this); - this.deactivateActive(); - if(this.el && this.floating){ - this.el.hide(); - } - var pm = this.parentMenu; - if(this.deepHide === true && pm){ - if(pm.floating){ - pm.hide(true); - }else{ - pm.deactivateActive(); - } - } - }, - - // private - lookupComponent : function(c){ - if(Ext.isString(c)){ - c = (c == 'separator' || c == '-') ? new Ext.menu.Separator() : new Ext.menu.TextItem(c); - this.applyDefaults(c); - }else{ - if(Ext.isObject(c)){ - c = this.getMenuItem(c); - }else if(c.tagName || c.el){ // element. Wrap it. - c = new Ext.BoxComponent({ - el: c - }); - } - } - return c; - }, - - applyDefaults : function(c) { - if (!Ext.isString(c)) { - c = Ext.menu.Menu.superclass.applyDefaults.call(this, c); - var d = this.internalDefaults; - if(d){ - if(c.events){ - Ext.applyIf(c.initialConfig, d); - Ext.apply(c, d); - }else{ - Ext.applyIf(c, d); - } - } - } - return c; - }, - - // private - getMenuItem : function(config) { - config.ownerCt = this; - - if (!config.isXType) { - if (!config.xtype && Ext.isBoolean(config.checked)) { - return new Ext.menu.CheckItem(config); - } - return Ext.create(config, this.defaultType); - } - return config; - }, - - /** - * Adds a separator bar to the menu - * @return {Ext.menu.Item} The menu item that was added - */ - addSeparator : function() { - return this.add(new Ext.menu.Separator()); - }, - - /** - * Adds an {@link Ext.Element} object to the menu - * @param {Mixed} el The element or DOM node to add, or its id - * @return {Ext.menu.Item} The menu item that was added - */ - addElement : function(el) { - return this.add(new Ext.menu.BaseItem({ - el: el - })); - }, - - /** - * Adds an existing object based on {@link Ext.menu.BaseItem} to the menu - * @param {Ext.menu.Item} item The menu item to add - * @return {Ext.menu.Item} The menu item that was added - */ - addItem : function(item) { - return this.add(item); - }, - - /** - * Creates a new {@link Ext.menu.Item} based an the supplied config object and adds it to the menu - * @param {Object} config A MenuItem config object - * @return {Ext.menu.Item} The menu item that was added - */ - addMenuItem : function(config) { - return this.add(this.getMenuItem(config)); - }, - - /** - * Creates a new {@link Ext.menu.TextItem} with the supplied text and adds it to the menu - * @param {String} text The text to display in the menu item - * @return {Ext.menu.Item} The menu item that was added - */ - addText : function(text){ - return this.add(new Ext.menu.TextItem(text)); - }, - - //private - onDestroy : function(){ - Ext.EventManager.removeResizeListener(this.hide, this); - var pm = this.parentMenu; - if(pm && pm.activeChild == this){ - delete pm.activeChild; - } - delete this.parentMenu; - Ext.menu.Menu.superclass.onDestroy.call(this); - Ext.menu.MenuMgr.unregister(this); - if(this.keyNav) { - this.keyNav.disable(); - } - var s = this.scroller; - if(s){ - Ext.destroy(s.topRepeater, s.bottomRepeater, s.top, s.bottom); - } - Ext.destroy( - this.el, - this.focusEl, - this.ul - ); - } -}); - -Ext.reg('menu', Ext.menu.Menu); - -// MenuNav is a private utility class used internally by the Menu -Ext.menu.MenuNav = Ext.extend(Ext.KeyNav, function(){ - function up(e, m){ - if(!m.tryActivate(m.items.indexOf(m.activeItem)-1, -1)){ - m.tryActivate(m.items.length-1, -1); - } - } - function down(e, m){ - if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){ - m.tryActivate(0, 1); - } - } - return { - constructor : function(menu){ - Ext.menu.MenuNav.superclass.constructor.call(this, menu.el); - this.scope = this.menu = menu; - }, - - doRelay : function(e, h){ - var k = e.getKey(); -// Keystrokes within a form Field (e.g.: down in a Combo) do not navigate. Allow only TAB - if (this.menu.activeItem && this.menu.activeItem.isFormField && k != e.TAB) { - return false; - } - if(!this.menu.activeItem && e.isNavKeyPress() && k != e.SPACE && k != e.RETURN){ - this.menu.tryActivate(0, 1); - return false; - } - return h.call(this.scope || this, e, this.menu); - }, - - tab: function(e, m) { - e.stopEvent(); - if (e.shiftKey) { - up(e, m); - } else { - down(e, m); - } - }, - - up : up, - - down : down, - - right : function(e, m){ - if(m.activeItem){ - m.activeItem.expandMenu(true); - } - }, - - left : function(e, m){ - m.hide(); - if(m.parentMenu && m.parentMenu.activeItem){ - m.parentMenu.activeItem.activate(); - } - }, - - enter : function(e, m){ - if(m.activeItem){ - e.stopPropagation(); - m.activeItem.onClick(e); - m.fireEvent('click', this, m.activeItem); - return true; - } - } - }; -}()); -/** - * @class Ext.menu.MenuMgr - * Provides a common registry of all menu items on a page so that they can be easily accessed by id. - * @singleton - */ -Ext.menu.MenuMgr = function(){ - var menus, - active, - map, - groups = {}, - attached = false, - lastShow = new Date(); - - - // private - called when first menu is created - function init(){ - menus = {}; - active = new Ext.util.MixedCollection(); - map = Ext.getDoc().addKeyListener(27, hideAll); - map.disable(); - } - - // private - function hideAll(){ - if(active && active.length > 0){ - var c = active.clone(); - c.each(function(m){ - m.hide(); - }); - return true; - } - return false; - } - - // private - function onHide(m){ - active.remove(m); - if(active.length < 1){ - map.disable(); - Ext.getDoc().un("mousedown", onMouseDown); - attached = false; - } - } - - // private - function onShow(m){ - var last = active.last(); - lastShow = new Date(); - active.add(m); - if(!attached){ - map.enable(); - Ext.getDoc().on("mousedown", onMouseDown); - attached = true; - } - if(m.parentMenu){ - m.getEl().setZIndex(parseInt(m.parentMenu.getEl().getStyle("z-index"), 10) + 3); - m.parentMenu.activeChild = m; - }else if(last && !last.isDestroyed && last.isVisible()){ - m.getEl().setZIndex(parseInt(last.getEl().getStyle("z-index"), 10) + 3); - } - } - - // private - function onBeforeHide(m){ - if(m.activeChild){ - m.activeChild.hide(); - } - if(m.autoHideTimer){ - clearTimeout(m.autoHideTimer); - delete m.autoHideTimer; - } - } - - // private - function onBeforeShow(m){ - var pm = m.parentMenu; - if(!pm && !m.allowOtherMenus){ - hideAll(); - }else if(pm && pm.activeChild){ - pm.activeChild.hide(); - } - } - - // private - function onMouseDown(e){ - if(lastShow.getElapsed() > 50 && active.length > 0 && !e.getTarget(".x-menu")){ - hideAll(); - } - } - - return { - - /** - * Hides all menus that are currently visible - * @return {Boolean} success True if any active menus were hidden. - */ - hideAll : function(){ - return hideAll(); - }, - - // private - register : function(menu){ - if(!menus){ - init(); - } - menus[menu.id] = menu; - menu.on({ - beforehide: onBeforeHide, - hide: onHide, - beforeshow: onBeforeShow, - show: onShow - }); - }, - - /** - * Returns a {@link Ext.menu.Menu} object - * @param {String/Object} menu The string menu id, an existing menu object reference, or a Menu config that will - * be used to generate and return a new Menu instance. - * @return {Ext.menu.Menu} The specified menu, or null if none are found - */ - get : function(menu){ - if(typeof menu == "string"){ // menu id - if(!menus){ // not initialized, no menus to return - return null; - } - return menus[menu]; - }else if(menu.events){ // menu instance - return menu; - }else if(typeof menu.length == 'number'){ // array of menu items? - return new Ext.menu.Menu({items:menu}); - }else{ // otherwise, must be a config - return Ext.create(menu, 'menu'); - } - }, - - // private - unregister : function(menu){ - delete menus[menu.id]; - menu.un("beforehide", onBeforeHide); - menu.un("hide", onHide); - menu.un("beforeshow", onBeforeShow); - menu.un("show", onShow); - }, - - // private - registerCheckable : function(menuItem){ - var g = menuItem.group; - if(g){ - if(!groups[g]){ - groups[g] = []; - } - groups[g].push(menuItem); - } - }, - - // private - unregisterCheckable : function(menuItem){ - var g = menuItem.group; - if(g){ - groups[g].remove(menuItem); - } - }, - - // private - onCheckChange: function(item, state){ - if(item.group && state){ - var group = groups[item.group], - i = 0, - len = group.length, - current; - - for(; i < len; i++){ - current = group[i]; - if(current != item){ - current.setChecked(false); - } - } - } - }, - - getCheckedItem : function(groupId){ - var g = groups[groupId]; - if(g){ - for(var i = 0, l = g.length; i < l; i++){ - if(g[i].checked){ - return g[i]; - } - } - } - return null; - }, - - setCheckedItem : function(groupId, itemId){ - var g = groups[groupId]; - if(g){ - for(var i = 0, l = g.length; i < l; i++){ - if(g[i].id == itemId){ - g[i].setChecked(true); - } - } - } - return null; - } - }; -}(); -/** - * @class Ext.menu.BaseItem - * @extends Ext.Component - * The base class for all items that render into menus. BaseItem provides default rendering, activated state - * management and base configuration options shared by all menu components. - * @constructor - * Creates a new BaseItem - * @param {Object} config Configuration options - * @xtype menubaseitem - */ -Ext.menu.BaseItem = Ext.extend(Ext.Component, { - /** - * @property parentMenu - * @type Ext.menu.Menu - * The parent Menu of this Item. - */ - /** - * @cfg {Function} handler - * A function that will handle the click event of this menu item (optional). - * The handler is passed the following parameters:

      - *
    • b : Item
      This menu Item.
    • - *
    • e : EventObject
      The click event.
    • - *
    - */ - /** - * @cfg {Object} scope - * The scope (this reference) in which the handler function will be called. - */ - /** - * @cfg {Boolean} canActivate True if this item can be visually activated (defaults to false) - */ - canActivate : false, - /** - * @cfg {String} activeClass The CSS class to use when the item becomes activated (defaults to "x-menu-item-active") - */ - activeClass : "x-menu-item-active", - /** - * @cfg {Boolean} hideOnClick True to hide the containing menu after this item is clicked (defaults to true) - */ - hideOnClick : true, - /** - * @cfg {Number} clickHideDelay Length of time in milliseconds to wait before hiding after a click (defaults to 1) - */ - clickHideDelay : 1, - - // private - ctype : "Ext.menu.BaseItem", - - // private - actionMode : "container", - - initComponent : function(){ - Ext.menu.BaseItem.superclass.initComponent.call(this); - this.addEvents( - /** - * @event click - * Fires when this item is clicked - * @param {Ext.menu.BaseItem} this - * @param {Ext.EventObject} e - */ - 'click', - /** - * @event activate - * Fires when this item is activated - * @param {Ext.menu.BaseItem} this - */ - 'activate', - /** - * @event deactivate - * Fires when this item is deactivated - * @param {Ext.menu.BaseItem} this - */ - 'deactivate' - ); - if(this.handler){ - this.on("click", this.handler, this.scope); - } - }, - - // private - onRender : function(container, position){ - Ext.menu.BaseItem.superclass.onRender.apply(this, arguments); - if(this.ownerCt && this.ownerCt instanceof Ext.menu.Menu){ - this.parentMenu = this.ownerCt; - }else{ - this.container.addClass('x-menu-list-item'); - this.mon(this.el, { - scope: this, - click: this.onClick, - mouseenter: this.activate, - mouseleave: this.deactivate - }); - } - }, - - /** - * Sets the function that will handle click events for this item (equivalent to passing in the {@link #handler} - * config property). If an existing handler is already registered, it will be unregistered for you. - * @param {Function} handler The function that should be called on click - * @param {Object} scope The scope (this reference) in which the handler function is executed. Defaults to this menu item. - */ - setHandler : function(handler, scope){ - if(this.handler){ - this.un("click", this.handler, this.scope); - } - this.on("click", this.handler = handler, this.scope = scope); - }, - - // private - onClick : function(e){ - if(!this.disabled && this.fireEvent("click", this, e) !== false - && (this.parentMenu && this.parentMenu.fireEvent("itemclick", this, e) !== false)){ - this.handleClick(e); - }else{ - e.stopEvent(); - } - }, - - // private - activate : function(){ - if(this.disabled){ - return false; - } - var li = this.container; - li.addClass(this.activeClass); - this.region = li.getRegion().adjust(2, 2, -2, -2); - this.fireEvent("activate", this); - return true; - }, - - // private - deactivate : function(){ - this.container.removeClass(this.activeClass); - this.fireEvent("deactivate", this); - }, - - // private - shouldDeactivate : function(e){ - return !this.region || !this.region.contains(e.getPoint()); - }, - - // private - handleClick : function(e){ - var pm = this.parentMenu; - if(this.hideOnClick){ - if(pm.floating){ - this.clickHideDelayTimer = pm.hide.defer(this.clickHideDelay, pm, [true]); - }else{ - pm.deactivateActive(); - } - } - }, - - beforeDestroy: function(){ - clearTimeout(this.clickHideDelayTimer); - Ext.menu.BaseItem.superclass.beforeDestroy.call(this); - }, - - // private. Do nothing - expandMenu : Ext.emptyFn, - - // private. Do nothing - hideMenu : Ext.emptyFn -}); -Ext.reg('menubaseitem', Ext.menu.BaseItem);/** - * @class Ext.menu.TextItem - * @extends Ext.menu.BaseItem - * Adds a static text string to a menu, usually used as either a heading or group separator. - * @constructor - * Creates a new TextItem - * @param {Object/String} config If config is a string, it is used as the text to display, otherwise it - * is applied as a config object (and should contain a text property). - * @xtype menutextitem - */ -Ext.menu.TextItem = Ext.extend(Ext.menu.BaseItem, { - /** - * @cfg {String} text The text to display for this item (defaults to '') - */ - /** - * @cfg {Boolean} hideOnClick True to hide the containing menu after this item is clicked (defaults to false) - */ - hideOnClick : false, - /** - * @cfg {String} itemCls The default CSS class to use for text items (defaults to "x-menu-text") - */ - itemCls : "x-menu-text", - - constructor : function(config) { - if (typeof config == 'string') { - config = { - text: config - }; - } - Ext.menu.TextItem.superclass.constructor.call(this, config); - }, - - // private - onRender : function() { - var s = document.createElement("span"); - s.className = this.itemCls; - s.innerHTML = this.text; - this.el = s; - Ext.menu.TextItem.superclass.onRender.apply(this, arguments); - } -}); -Ext.reg('menutextitem', Ext.menu.TextItem);/** - * @class Ext.menu.Separator - * @extends Ext.menu.BaseItem - * Adds a separator bar to a menu, used to divide logical groups of menu items. Generally you will - * add one of these by using "-" in you call to add() or in your items config rather than creating one directly. - * @constructor - * @param {Object} config Configuration options - * @xtype menuseparator - */ -Ext.menu.Separator = Ext.extend(Ext.menu.BaseItem, { - /** - * @cfg {String} itemCls The default CSS class to use for separators (defaults to "x-menu-sep") - */ - itemCls : "x-menu-sep", - /** - * @cfg {Boolean} hideOnClick True to hide the containing menu after this item is clicked (defaults to false) - */ - hideOnClick : false, - - /** - * @cfg {String} activeClass - * @hide - */ - activeClass: '', - - // private - onRender : function(li){ - var s = document.createElement("span"); - s.className = this.itemCls; - s.innerHTML = " "; - this.el = s; - li.addClass("x-menu-sep-li"); - Ext.menu.Separator.superclass.onRender.apply(this, arguments); - } -}); -Ext.reg('menuseparator', Ext.menu.Separator);/** - * @class Ext.menu.Item - * @extends Ext.menu.BaseItem - * A base class for all menu items that require menu-related functionality (like sub-menus) and are not static - * display items. Item extends the base functionality of {@link Ext.menu.BaseItem} by adding menu-specific - * activation and click handling. - * @constructor - * Creates a new Item - * @param {Object} config Configuration options - * @xtype menuitem - */ -Ext.menu.Item = Ext.extend(Ext.menu.BaseItem, { - /** - * @property menu - * @type Ext.menu.Menu - * The submenu associated with this Item if one was configured. - */ - /** - * @cfg {Mixed} menu (optional) Either an instance of {@link Ext.menu.Menu} or the config object for an - * {@link Ext.menu.Menu} which acts as the submenu when this item is activated. - */ - /** - * @cfg {String} icon The path to an icon to display in this item (defaults to Ext.BLANK_IMAGE_URL). If - * icon is specified {@link #iconCls} should not be. - */ - /** - * @cfg {String} iconCls A CSS class that specifies a background image that will be used as the icon for - * this item (defaults to ''). If iconCls is specified {@link #icon} should not be. - */ - /** - * @cfg {String} text The text to display in this item (defaults to ''). - */ - /** - * @cfg {String} href The href attribute to use for the underlying anchor link (defaults to '#'). - */ - /** - * @cfg {String} hrefTarget The target attribute to use for the underlying anchor link (defaults to ''). - */ - /** - * @cfg {String} itemCls The default CSS class to use for menu items (defaults to 'x-menu-item') - */ - itemCls : 'x-menu-item', - /** - * @cfg {Boolean} canActivate True if this item can be visually activated (defaults to true) - */ - canActivate : true, - /** - * @cfg {Number} showDelay Length of time in milliseconds to wait before showing this item (defaults to 200) - */ - showDelay: 200, - - /** - * @cfg {String} altText The altText to use for the icon, if it exists. Defaults to ''. - */ - altText: '', - - // doc'd in BaseItem - hideDelay: 200, - - // private - ctype: 'Ext.menu.Item', - - initComponent : function(){ - Ext.menu.Item.superclass.initComponent.call(this); - if(this.menu){ - // If array of items, turn it into an object config so we - // can set the ownerCt property in the config - if (Ext.isArray(this.menu)){ - this.menu = { items: this.menu }; - } - - // An object config will work here, but an instance of a menu - // will have already setup its ref's and have no effect - if (Ext.isObject(this.menu)){ - this.menu.ownerCt = this; - } - - this.menu = Ext.menu.MenuMgr.get(this.menu); - this.menu.ownerCt = undefined; - } - }, - - // private - onRender : function(container, position){ - if (!this.itemTpl) { - this.itemTpl = Ext.menu.Item.prototype.itemTpl = new Ext.XTemplate( - '', - ' target="{hrefTarget}"', - '', - '>', - '{altText}', - '{text}', - '' - ); - } - var a = this.getTemplateArgs(); - this.el = position ? this.itemTpl.insertBefore(position, a, true) : this.itemTpl.append(container, a, true); - this.iconEl = this.el.child('img.x-menu-item-icon'); - this.textEl = this.el.child('.x-menu-item-text'); - if(!this.href) { // if no link defined, prevent the default anchor event - this.mon(this.el, 'click', Ext.emptyFn, null, { preventDefault: true }); - } - Ext.menu.Item.superclass.onRender.call(this, container, position); - }, - - getTemplateArgs: function() { - return { - id: this.id, - cls: this.itemCls + (this.menu ? ' x-menu-item-arrow' : '') + (this.cls ? ' ' + this.cls : ''), - href: this.href || '#', - hrefTarget: this.hrefTarget, - icon: this.icon || Ext.BLANK_IMAGE_URL, - iconCls: this.iconCls || '', - text: this.itemText||this.text||' ', - altText: this.altText || '' - }; - }, - - /** - * Sets the text to display in this menu item - * @param {String} text The text to display - */ - setText : function(text){ - this.text = text||' '; - if(this.rendered){ - this.textEl.update(this.text); - this.parentMenu.layout.doAutoSize(); - } - }, - - /** - * Sets the CSS class to apply to the item's icon element - * @param {String} cls The CSS class to apply - */ - setIconClass : function(cls){ - var oldCls = this.iconCls; - this.iconCls = cls; - if(this.rendered){ - this.iconEl.replaceClass(oldCls, this.iconCls); - } - }, - - //private - beforeDestroy: function(){ - clearTimeout(this.showTimer); - clearTimeout(this.hideTimer); - if (this.menu){ - delete this.menu.ownerCt; - this.menu.destroy(); - } - Ext.menu.Item.superclass.beforeDestroy.call(this); - }, - - // private - handleClick : function(e){ - if(!this.href){ // if no link defined, stop the event automatically - e.stopEvent(); - } - Ext.menu.Item.superclass.handleClick.apply(this, arguments); - }, - - // private - activate : function(autoExpand){ - if(Ext.menu.Item.superclass.activate.apply(this, arguments)){ - this.focus(); - if(autoExpand){ - this.expandMenu(); - } - } - return true; - }, - - // private - shouldDeactivate : function(e){ - if(Ext.menu.Item.superclass.shouldDeactivate.call(this, e)){ - if(this.menu && this.menu.isVisible()){ - return !this.menu.getEl().getRegion().contains(e.getPoint()); - } - return true; - } - return false; - }, - - // private - deactivate : function(){ - Ext.menu.Item.superclass.deactivate.apply(this, arguments); - this.hideMenu(); - }, - - // private - expandMenu : function(autoActivate){ - if(!this.disabled && this.menu){ - clearTimeout(this.hideTimer); - delete this.hideTimer; - if(!this.menu.isVisible() && !this.showTimer){ - this.showTimer = this.deferExpand.defer(this.showDelay, this, [autoActivate]); - }else if (this.menu.isVisible() && autoActivate){ - this.menu.tryActivate(0, 1); - } - } - }, - - // private - deferExpand : function(autoActivate){ - delete this.showTimer; - this.menu.show(this.container, this.parentMenu.subMenuAlign || 'tl-tr?', this.parentMenu); - if(autoActivate){ - this.menu.tryActivate(0, 1); - } - }, - - // private - hideMenu : function(){ - clearTimeout(this.showTimer); - delete this.showTimer; - if(!this.hideTimer && this.menu && this.menu.isVisible()){ - this.hideTimer = this.deferHide.defer(this.hideDelay, this); - } - }, - - // private - deferHide : function(){ - delete this.hideTimer; - if(this.menu.over){ - this.parentMenu.setActiveItem(this, false); - }else{ - this.menu.hide(); - } - } -}); -Ext.reg('menuitem', Ext.menu.Item);/** - * @class Ext.menu.CheckItem - * @extends Ext.menu.Item - * Adds a menu item that contains a checkbox by default, but can also be part of a radio group. - * @constructor - * Creates a new CheckItem - * @param {Object} config Configuration options - * @xtype menucheckitem - */ -Ext.menu.CheckItem = Ext.extend(Ext.menu.Item, { - /** - * @cfg {String} group - * All check items with the same group name will automatically be grouped into a single-select - * radio button group (defaults to '') - */ - /** - * @cfg {String} itemCls The default CSS class to use for check items (defaults to "x-menu-item x-menu-check-item") - */ - itemCls : "x-menu-item x-menu-check-item", - /** - * @cfg {String} groupClass The default CSS class to use for radio group check items (defaults to "x-menu-group-item") - */ - groupClass : "x-menu-group-item", - - /** - * @cfg {Boolean} checked True to initialize this checkbox as checked (defaults to false). Note that - * if this checkbox is part of a radio group (group = true) only the first item in the group that is - * initialized with checked = true will be rendered as checked. - */ - checked: false, - - // private - ctype: "Ext.menu.CheckItem", - - initComponent : function(){ - Ext.menu.CheckItem.superclass.initComponent.call(this); - this.addEvents( - /** - * @event beforecheckchange - * Fires before the checked value is set, providing an opportunity to cancel if needed - * @param {Ext.menu.CheckItem} this - * @param {Boolean} checked The new checked value that will be set - */ - "beforecheckchange" , - /** - * @event checkchange - * Fires after the checked value has been set - * @param {Ext.menu.CheckItem} this - * @param {Boolean} checked The checked value that was set - */ - "checkchange" - ); - /** - * A function that handles the checkchange event. The function is undefined by default, but if an implementation - * is provided, it will be called automatically when the checkchange event fires. - * @param {Ext.menu.CheckItem} this - * @param {Boolean} checked The checked value that was set - * @method checkHandler - */ - if(this.checkHandler){ - this.on('checkchange', this.checkHandler, this.scope); - } - Ext.menu.MenuMgr.registerCheckable(this); - }, - - // private - onRender : function(c){ - Ext.menu.CheckItem.superclass.onRender.apply(this, arguments); - if(this.group){ - this.el.addClass(this.groupClass); - } - if(this.checked){ - this.checked = false; - this.setChecked(true, true); - } - }, - - // private - destroy : function(){ - Ext.menu.MenuMgr.unregisterCheckable(this); - Ext.menu.CheckItem.superclass.destroy.apply(this, arguments); - }, - - /** - * Set the checked state of this item - * @param {Boolean} checked The new checked value - * @param {Boolean} suppressEvent (optional) True to prevent the checkchange event from firing (defaults to false) - */ - setChecked : function(state, suppressEvent){ - var suppress = suppressEvent === true; - if(this.checked != state && (suppress || this.fireEvent("beforecheckchange", this, state) !== false)){ - Ext.menu.MenuMgr.onCheckChange(this, state); - if(this.container){ - this.container[state ? "addClass" : "removeClass"]("x-menu-item-checked"); - } - this.checked = state; - if(!suppress){ - this.fireEvent("checkchange", this, state); - } - } - }, - - // private - handleClick : function(e){ - if(!this.disabled && !(this.checked && this.group)){// disable unselect on radio item - this.setChecked(!this.checked); - } - Ext.menu.CheckItem.superclass.handleClick.apply(this, arguments); - } -}); -Ext.reg('menucheckitem', Ext.menu.CheckItem);/** - * @class Ext.menu.DateMenu - * @extends Ext.menu.Menu - *

    A menu containing an {@link Ext.DatePicker} Component.

    - *

    Notes:

      - *
    • Although not listed here, the constructor for this class - * accepts all of the configuration options of {@link Ext.DatePicker}.
    • - *
    • If subclassing DateMenu, any configuration options for the DatePicker must be - * applied to the initialConfig property of the DateMenu. - * Applying {@link Ext.DatePicker DatePicker} configuration settings to - * this will not affect the DatePicker's configuration.
    • - *
    - * @xtype datemenu - */ - Ext.menu.DateMenu = Ext.extend(Ext.menu.Menu, { - /** - * @cfg {Boolean} enableScrolling - * @hide - */ - enableScrolling : false, - /** - * @cfg {Function} handler - * Optional. A function that will handle the select event of this menu. - * The handler is passed the following parameters:
      - *
    • picker : DatePicker
      The Ext.DatePicker.
    • - *
    • date : Date
      The selected date.
    • - *
    - */ - /** - * @cfg {Object} scope - * The scope (this reference) in which the {@link #handler} - * function will be called. Defaults to this DateMenu instance. - */ - /** - * @cfg {Boolean} hideOnClick - * False to continue showing the menu after a date is selected, defaults to true. - */ - hideOnClick : true, - - /** - * @cfg {String} pickerId - * An id to assign to the underlying date picker. Defaults to null. - */ - pickerId : null, - - /** - * @cfg {Number} maxHeight - * @hide - */ - /** - * @cfg {Number} scrollIncrement - * @hide - */ - /** - * The {@link Ext.DatePicker} instance for this DateMenu - * @property picker - * @type DatePicker - */ - cls : 'x-date-menu', - - /** - * @event click - * @hide - */ - - /** - * @event itemclick - * @hide - */ - - initComponent : function(){ - this.on('beforeshow', this.onBeforeShow, this); - if(this.strict = (Ext.isIE7 && Ext.isStrict)){ - this.on('show', this.onShow, this, {single: true, delay: 20}); - } - Ext.apply(this, { - plain: true, - showSeparator: false, - items: this.picker = new Ext.DatePicker(Ext.applyIf({ - internalRender: this.strict || !Ext.isIE, - ctCls: 'x-menu-date-item', - id: this.pickerId - }, this.initialConfig)) - }); - this.picker.purgeListeners(); - Ext.menu.DateMenu.superclass.initComponent.call(this); - /** - * @event select - * Fires when a date is selected from the {@link #picker Ext.DatePicker} - * @param {DatePicker} picker The {@link #picker Ext.DatePicker} - * @param {Date} date The selected date - */ - this.relayEvents(this.picker, ['select']); - this.on('show', this.picker.focus, this.picker); - this.on('select', this.menuHide, this); - if(this.handler){ - this.on('select', this.handler, this.scope || this); - } - }, - - menuHide : function() { - if(this.hideOnClick){ - this.hide(true); - } - }, - - onBeforeShow : function(){ - if(this.picker){ - this.picker.hideMonthPicker(true); - } - }, - - onShow : function(){ - var el = this.picker.getEl(); - el.setWidth(el.getWidth()); //nasty hack for IE7 strict mode - } - }); - Ext.reg('datemenu', Ext.menu.DateMenu); - /** - * @class Ext.menu.ColorMenu - * @extends Ext.menu.Menu - *

    A menu containing a {@link Ext.ColorPalette} Component.

    - *

    Notes:

      - *
    • Although not listed here, the constructor for this class - * accepts all of the configuration options of {@link Ext.ColorPalette}.
    • - *
    • If subclassing ColorMenu, any configuration options for the ColorPalette must be - * applied to the initialConfig property of the ColorMenu. - * Applying {@link Ext.ColorPalette ColorPalette} configuration settings to - * this will not affect the ColorPalette's configuration.
    • - *
    * - * @xtype colormenu - */ - Ext.menu.ColorMenu = Ext.extend(Ext.menu.Menu, { - /** - * @cfg {Boolean} enableScrolling - * @hide - */ - enableScrolling : false, - /** - * @cfg {Function} handler - * Optional. A function that will handle the select event of this menu. - * The handler is passed the following parameters:
      - *
    • palette : ColorPalette
      The {@link #palette Ext.ColorPalette}.
    • - *
    • color : String
      The 6-digit color hex code (without the # symbol).
    • - *
    - */ - /** - * @cfg {Object} scope - * The scope (this reference) in which the {@link #handler} - * function will be called. Defaults to this ColorMenu instance. - */ - - /** - * @cfg {Boolean} hideOnClick - * False to continue showing the menu after a color is selected, defaults to true. - */ - hideOnClick : true, - - cls : 'x-color-menu', - - /** - * @cfg {String} paletteId - * An id to assign to the underlying color palette. Defaults to null. - */ - paletteId : null, - - /** - * @cfg {Number} maxHeight - * @hide - */ - /** - * @cfg {Number} scrollIncrement - * @hide - */ - /** - * @property palette - * @type ColorPalette - * The {@link Ext.ColorPalette} instance for this ColorMenu - */ - - - /** - * @event click - * @hide - */ - - /** - * @event itemclick - * @hide - */ - - initComponent : function(){ - Ext.apply(this, { - plain: true, - showSeparator: false, - items: this.palette = new Ext.ColorPalette(Ext.applyIf({ - id: this.paletteId - }, this.initialConfig)) - }); - this.palette.purgeListeners(); - Ext.menu.ColorMenu.superclass.initComponent.call(this); - /** - * @event select - * Fires when a color is selected from the {@link #palette Ext.ColorPalette} - * @param {Ext.ColorPalette} palette The {@link #palette Ext.ColorPalette} - * @param {String} color The 6-digit color hex code (without the # symbol) - */ - this.relayEvents(this.palette, ['select']); - this.on('select', this.menuHide, this); - if(this.handler){ - this.on('select', this.handler, this.scope || this); - } - }, - - menuHide : function(){ - if(this.hideOnClick){ - this.hide(true); - } - } -}); -Ext.reg('colormenu', Ext.menu.ColorMenu); -/** - * @class Ext.form.Field - * @extends Ext.BoxComponent - * Base class for form fields that provides default event handling, sizing, value handling and other functionality. - * @constructor - * Creates a new Field - * @param {Object} config Configuration options - * @xtype field - */ -Ext.form.Field = Ext.extend(Ext.BoxComponent, { - /** - *

    The label Element associated with this Field. Only available after this Field has been rendered by a - * {@link form Ext.layout.FormLayout} layout manager.

    - * @type Ext.Element - * @property label - */ - /** - * @cfg {String} inputType The type attribute for input fields -- e.g. radio, text, password, file (defaults - * to 'text'). The types 'file' and 'password' must be used to render those field types currently -- there are - * no separate Ext components for those. Note that if you use inputType:'file', {@link #emptyText} - * is not supported and should be avoided. - */ - /** - * @cfg {Number} tabIndex The tabIndex for this field. Note this only applies to fields that are rendered, - * not those which are built via applyTo (defaults to undefined). - */ - /** - * @cfg {Mixed} value A value to initialize this field with (defaults to undefined). - */ - /** - * @cfg {String} name The field's HTML name attribute (defaults to ''). - * Note: this property must be set if this field is to be automatically included with - * {@link Ext.form.BasicForm#submit form submit()}. - */ - /** - * @cfg {String} cls A custom CSS class to apply to the field's underlying element (defaults to ''). - */ - - /** - * @cfg {String} invalidClass The CSS class to use when marking a field invalid (defaults to 'x-form-invalid') - */ - invalidClass : 'x-form-invalid', - /** - * @cfg {String} invalidText The error text to use when marking a field invalid and no message is provided - * (defaults to 'The value in this field is invalid') - */ - invalidText : 'The value in this field is invalid', - /** - * @cfg {String} focusClass The CSS class to use when the field receives focus (defaults to 'x-form-focus') - */ - focusClass : 'x-form-focus', - /** - * @cfg {Boolean} preventMark - * true to disable {@link #markInvalid marking the field invalid}. - * Defaults to false. - */ - /** - * @cfg {String/Boolean} validationEvent The event that should initiate field validation. Set to false to disable - automatic validation (defaults to 'keyup'). - */ - validationEvent : 'keyup', - /** - * @cfg {Boolean} validateOnBlur Whether the field should validate when it loses focus (defaults to true). - */ - validateOnBlur : true, - /** - * @cfg {Number} validationDelay The length of time in milliseconds after user input begins until validation - * is initiated (defaults to 250) - */ - validationDelay : 250, - /** - * @cfg {String/Object} autoCreate

    A {@link Ext.DomHelper DomHelper} element spec, or true for a default - * element spec. Used to create the {@link Ext.Component#getEl Element} which will encapsulate this Component. - * See {@link Ext.Component#autoEl autoEl} for details. Defaults to:

    - *
    {tag: 'input', type: 'text', size: '20', autocomplete: 'off'}
    - */ - defaultAutoCreate : {tag: 'input', type: 'text', size: '20', autocomplete: 'off'}, - /** - * @cfg {String} fieldClass The default CSS class for the field (defaults to 'x-form-field') - */ - fieldClass : 'x-form-field', - /** - * @cfg {String} msgTarget

    The location where the message text set through {@link #markInvalid} should display. - * Must be one of the following values:

    - *
      - *
    • qtip Display a quick tip containing the message when the user hovers over the field. This is the default. - *
      {@link Ext.QuickTips#init Ext.QuickTips.init} must have been called for this setting to work. - *
    • title Display the message in a default browser title attribute popup.
    • - *
    • under Add a block div beneath the field containing the error message.
    • - *
    • side Add an error icon to the right of the field, displaying the message in a popup on hover.
    • - *
    • [element id] Add the error message directly to the innerHTML of the specified element.
    • - *
    - */ - msgTarget : 'qtip', - /** - * @cfg {String} msgFx Experimental The effect used when displaying a validation message under the field - * (defaults to 'normal'). - */ - msgFx : 'normal', - /** - * @cfg {Boolean} readOnly true to mark the field as readOnly in HTML - * (defaults to false). - *

    Note: this only sets the element's readOnly DOM attribute. - * Setting readOnly=true, for example, will not disable triggering a - * ComboBox or DateField; it gives you the option of forcing the user to choose - * via the trigger without typing in the text box. To hide the trigger use - * {@link Ext.form.TriggerField#hideTrigger hideTrigger}.

    - */ - readOnly : false, - /** - * @cfg {Boolean} disabled True to disable the field (defaults to false). - *

    Be aware that conformant with the HTML specification, - * disabled Fields will not be {@link Ext.form.BasicForm#submit submitted}.

    - */ - disabled : false, - /** - * @cfg {Boolean} submitValue False to clear the name attribute on the field so that it is not submitted during a form post. - * Defaults to true. - */ - submitValue: true, - - // private - isFormField : true, - - // private - msgDisplay: '', - - // private - hasFocus : false, - - // private - initComponent : function(){ - Ext.form.Field.superclass.initComponent.call(this); - this.addEvents( - /** - * @event focus - * Fires when this field receives input focus. - * @param {Ext.form.Field} this - */ - 'focus', - /** - * @event blur - * Fires when this field loses input focus. - * @param {Ext.form.Field} this - */ - 'blur', - /** - * @event specialkey - * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed. - * To handle other keys see {@link Ext.Panel#keys} or {@link Ext.KeyMap}. - * You can check {@link Ext.EventObject#getKey} to determine which key was pressed. - * For example:
    
    -var form = new Ext.form.FormPanel({
    -    ...
    -    items: [{
    -            fieldLabel: 'Field 1',
    -            name: 'field1',
    -            allowBlank: false
    -        },{
    -            fieldLabel: 'Field 2',
    -            name: 'field2',
    -            listeners: {
    -                specialkey: function(field, e){
    -                    // e.HOME, e.END, e.PAGE_UP, e.PAGE_DOWN,
    -                    // e.TAB, e.ESC, arrow keys: e.LEFT, e.RIGHT, e.UP, e.DOWN
    -                    if (e.{@link Ext.EventObject#getKey getKey()} == e.ENTER) {
    -                        var form = field.ownerCt.getForm();
    -                        form.submit();
    -                    }
    -                }
    -            }
    -        }
    -    ],
    -    ...
    -});
    -             * 
    - * @param {Ext.form.Field} this - * @param {Ext.EventObject} e The event object - */ - 'specialkey', - /** - * @event change - * Fires just before the field blurs if the field value has changed. - * @param {Ext.form.Field} this - * @param {Mixed} newValue The new value - * @param {Mixed} oldValue The original value - */ - 'change', - /** - * @event invalid - * Fires after the field has been marked as invalid. - * @param {Ext.form.Field} this - * @param {String} msg The validation message - */ - 'invalid', - /** - * @event valid - * Fires after the field has been validated with no errors. - * @param {Ext.form.Field} this - */ - 'valid' - ); - }, - - /** - * Returns the {@link Ext.form.Field#name name} or {@link Ext.form.ComboBox#hiddenName hiddenName} - * attribute of the field if available. - * @return {String} name The field {@link Ext.form.Field#name name} or {@link Ext.form.ComboBox#hiddenName hiddenName} - */ - getName : function(){ - return this.rendered && this.el.dom.name ? this.el.dom.name : this.name || this.id || ''; - }, - - // private - onRender : function(ct, position){ - if(!this.el){ - var cfg = this.getAutoCreate(); - - if(!cfg.name){ - cfg.name = this.name || this.id; - } - if(this.inputType){ - cfg.type = this.inputType; - } - this.autoEl = cfg; - } - Ext.form.Field.superclass.onRender.call(this, ct, position); - if(this.submitValue === false){ - this.el.dom.removeAttribute('name'); - } - var type = this.el.dom.type; - if(type){ - if(type == 'password'){ - type = 'text'; - } - this.el.addClass('x-form-'+type); - } - if(this.readOnly){ - this.setReadOnly(true); - } - if(this.tabIndex !== undefined){ - this.el.dom.setAttribute('tabIndex', this.tabIndex); - } - - this.el.addClass([this.fieldClass, this.cls]); - }, - - // private - getItemCt : function(){ - return this.itemCt; - }, - - // private - initValue : function(){ - if(this.value !== undefined){ - this.setValue(this.value); - }else if(!Ext.isEmpty(this.el.dom.value) && this.el.dom.value != this.emptyText){ - this.setValue(this.el.dom.value); - } - /** - * The original value of the field as configured in the {@link #value} configuration, or - * as loaded by the last form load operation if the form's {@link Ext.form.BasicForm#trackResetOnLoad trackResetOnLoad} - * setting is true. - * @type mixed - * @property originalValue - */ - this.originalValue = this.getValue(); - }, - - /** - *

    Returns true if the value of this Field has been changed from its original value. - * Will return false if the field is disabled or has not been rendered yet.

    - *

    Note that if the owning {@link Ext.form.BasicForm form} was configured with - * {@link Ext.form.BasicForm}.{@link Ext.form.BasicForm#trackResetOnLoad trackResetOnLoad} - * then the original value is updated when the values are loaded by - * {@link Ext.form.BasicForm}.{@link Ext.form.BasicForm#setValues setValues}.

    - * @return {Boolean} True if this field has been changed from its original value (and - * is not disabled), false otherwise. - */ - isDirty : function() { - if(this.disabled || !this.rendered) { - return false; - } - return String(this.getValue()) !== String(this.originalValue); - }, - - /** - * Sets the read only state of this field. - * @param {Boolean} readOnly Whether the field should be read only. - */ - setReadOnly : function(readOnly){ - if(this.rendered){ - this.el.dom.readOnly = readOnly; - } - this.readOnly = readOnly; - }, - - // private - afterRender : function(){ - Ext.form.Field.superclass.afterRender.call(this); - this.initEvents(); - this.initValue(); - }, - - // private - fireKey : function(e){ - if(e.isSpecialKey()){ - this.fireEvent('specialkey', this, e); - } - }, - - /** - * Resets the current field value to the originally loaded value and clears any validation messages. - * See {@link Ext.form.BasicForm}.{@link Ext.form.BasicForm#trackResetOnLoad trackResetOnLoad} - */ - reset : function(){ - this.setValue(this.originalValue); - this.clearInvalid(); - }, - - // private - initEvents : function(){ - this.mon(this.el, Ext.EventManager.getKeyEvent(), this.fireKey, this); - this.mon(this.el, 'focus', this.onFocus, this); - - // standardise buffer across all browsers + OS-es for consistent event order. - // (the 10ms buffer for Editors fixes a weird FF/Win editor issue when changing OS window focus) - this.mon(this.el, 'blur', this.onBlur, this, this.inEditor ? {buffer:10} : null); - }, - - // private - preFocus: Ext.emptyFn, - - // private - onFocus : function(){ - this.preFocus(); - if(this.focusClass){ - this.el.addClass(this.focusClass); - } - if(!this.hasFocus){ - this.hasFocus = true; - /** - *

    The value that the Field had at the time it was last focused. This is the value that is passed - * to the {@link #change} event which is fired if the value has been changed when the Field is blurred.

    - *

    This will be undefined until the Field has been visited. Compare {@link #originalValue}.

    - * @type mixed - * @property startValue - */ - this.startValue = this.getValue(); - this.fireEvent('focus', this); - } - }, - - // private - beforeBlur : Ext.emptyFn, - - // private - onBlur : function(){ - this.beforeBlur(); - if(this.focusClass){ - this.el.removeClass(this.focusClass); - } - this.hasFocus = false; - if(this.validationEvent !== false && (this.validateOnBlur || this.validationEvent == 'blur')){ - this.validate(); - } - var v = this.getValue(); - if(String(v) !== String(this.startValue)){ - this.fireEvent('change', this, v, this.startValue); - } - this.fireEvent('blur', this); - this.postBlur(); - }, - - // private - postBlur : Ext.emptyFn, - - /** - * Returns whether or not the field value is currently valid by - * {@link #validateValue validating} the {@link #processValue processed value} - * of the field. Note: {@link #disabled} fields are ignored. - * @param {Boolean} preventMark True to disable marking the field invalid - * @return {Boolean} True if the value is valid, else false - */ - isValid : function(preventMark){ - if(this.disabled){ - return true; - } - var restore = this.preventMark; - this.preventMark = preventMark === true; - var v = this.validateValue(this.processValue(this.getRawValue()), preventMark); - this.preventMark = restore; - return v; - }, - - /** - * Validates the field value - * @return {Boolean} True if the value is valid, else false - */ - validate : function(){ - if(this.disabled || this.validateValue(this.processValue(this.getRawValue()))){ - this.clearInvalid(); - return true; - } - return false; - }, - - /** - * This method should only be overridden if necessary to prepare raw values - * for validation (see {@link #validate} and {@link #isValid}). This method - * is expected to return the processed value for the field which will - * be used for validation (see validateValue method). - * @param {Mixed} value - */ - processValue : function(value){ - return value; - }, - - /** - * Uses getErrors to build an array of validation errors. If any errors are found, markInvalid is called - * with the first and false is returned, otherwise true is returned. Previously, subclasses were invited - * to provide an implementation of this to process validations - from 3.2 onwards getErrors should be - * overridden instead. - * @param {Mixed} The current value of the field - * @return {Boolean} True if all validations passed, false if one or more failed - */ - validateValue : function(value) { - //currently, we only show 1 error at a time for a field, so just use the first one - var error = this.getErrors(value)[0]; - - if (error == undefined) { - return true; - } else { - this.markInvalid(error); - return false; - } - }, - - /** - * Runs this field's validators and returns an array of error messages for any validation failures. - * This is called internally during validation and would not usually need to be used manually. - * Each subclass should override or augment the return value to provide their own errors - * @return {Array} All error messages for this field - */ - getErrors: function() { - return []; - }, - - /** - * Gets the active error message for this field. - * @return {String} Returns the active error message on the field, if there is no error, an empty string is returned. - */ - getActiveError : function(){ - return this.activeError || ''; - }, - - /** - *

    Display an error message associated with this field, using {@link #msgTarget} to determine how to - * display the message and applying {@link #invalidClass} to the field's UI element.

    - *

    Note: this method does not cause the Field's {@link #validate} method to return false - * if the value does pass validation. So simply marking a Field as invalid will not prevent - * submission of forms submitted with the {@link Ext.form.Action.Submit#clientValidation} option set.

    - * {@link #isValid invalid}. - * @param {String} msg (optional) The validation message (defaults to {@link #invalidText}) - */ - markInvalid : function(msg){ - //don't set the error icon if we're not rendered or marking is prevented - if (this.rendered && !this.preventMark) { - msg = msg || this.invalidText; - - var mt = this.getMessageHandler(); - if(mt){ - mt.mark(this, msg); - }else if(this.msgTarget){ - this.el.addClass(this.invalidClass); - var t = Ext.getDom(this.msgTarget); - if(t){ - t.innerHTML = msg; - t.style.display = this.msgDisplay; - } - } - } - - this.setActiveError(msg); - }, - - /** - * Clear any invalid styles/messages for this field - */ - clearInvalid : function(){ - //don't remove the error icon if we're not rendered or marking is prevented - if (this.rendered && !this.preventMark) { - this.el.removeClass(this.invalidClass); - var mt = this.getMessageHandler(); - if(mt){ - mt.clear(this); - }else if(this.msgTarget){ - this.el.removeClass(this.invalidClass); - var t = Ext.getDom(this.msgTarget); - if(t){ - t.innerHTML = ''; - t.style.display = 'none'; - } - } - } - - this.unsetActiveError(); - }, - - /** - * Sets the current activeError to the given string. Fires the 'invalid' event. - * This does not set up the error icon, only sets the message and fires the event. To show the error icon, - * use markInvalid instead, which calls this method internally - * @param {String} msg The error message - * @param {Boolean} suppressEvent True to suppress the 'invalid' event from being fired - */ - setActiveError: function(msg, suppressEvent) { - this.activeError = msg; - if (suppressEvent !== true) this.fireEvent('invalid', this, msg); - }, - - /** - * Clears the activeError and fires the 'valid' event. This is called internally by clearInvalid and would not - * usually need to be called manually - * @param {Boolean} suppressEvent True to suppress the 'invalid' event from being fired - */ - unsetActiveError: function(suppressEvent) { - delete this.activeError; - if (suppressEvent !== true) this.fireEvent('valid', this); - }, - - // private - getMessageHandler : function(){ - return Ext.form.MessageTargets[this.msgTarget]; - }, - - // private - getErrorCt : function(){ - return this.el.findParent('.x-form-element', 5, true) || // use form element wrap if available - this.el.findParent('.x-form-field-wrap', 5, true); // else direct field wrap - }, - - // Alignment for 'under' target - alignErrorEl : function(){ - this.errorEl.setWidth(this.getErrorCt().getWidth(true) - 20); - }, - - // Alignment for 'side' target - alignErrorIcon : function(){ - this.errorIcon.alignTo(this.el, 'tl-tr', [2, 0]); - }, - - /** - * Returns the raw data value which may or may not be a valid, defined value. To return a normalized value see {@link #getValue}. - * @return {Mixed} value The field value - */ - getRawValue : function(){ - var v = this.rendered ? this.el.getValue() : Ext.value(this.value, ''); - if(v === this.emptyText){ - v = ''; - } - return v; - }, - - /** - * Returns the normalized data value (undefined or emptyText will be returned as ''). To return the raw value see {@link #getRawValue}. - * @return {Mixed} value The field value - */ - getValue : function(){ - if(!this.rendered) { - return this.value; - } - var v = this.el.getValue(); - if(v === this.emptyText || v === undefined){ - v = ''; - } - return v; - }, - - /** - * Sets the underlying DOM field's value directly, bypassing validation. To set the value with validation see {@link #setValue}. - * @param {Mixed} value The value to set - * @return {Mixed} value The field value that is set - */ - setRawValue : function(v){ - return this.rendered ? (this.el.dom.value = (Ext.isEmpty(v) ? '' : v)) : ''; - }, - - /** - * Sets a data value into the field and validates it. To set the value directly without validation see {@link #setRawValue}. - * @param {Mixed} value The value to set - * @return {Ext.form.Field} this - */ - setValue : function(v){ - this.value = v; - if(this.rendered){ - this.el.dom.value = (Ext.isEmpty(v) ? '' : v); - this.validate(); - } - return this; - }, - - // private, does not work for all fields - append : function(v){ - this.setValue([this.getValue(), v].join('')); - } - - /** - * @cfg {Boolean} autoWidth @hide - */ - /** - * @cfg {Boolean} autoHeight @hide - */ - - /** - * @cfg {String} autoEl @hide - */ -}); - - -Ext.form.MessageTargets = { - 'qtip' : { - mark: function(field, msg){ - field.el.addClass(field.invalidClass); - field.el.dom.qtip = msg; - field.el.dom.qclass = 'x-form-invalid-tip'; - if(Ext.QuickTips){ // fix for floating editors interacting with DND - Ext.QuickTips.enable(); - } - }, - clear: function(field){ - field.el.removeClass(field.invalidClass); - field.el.dom.qtip = ''; - } - }, - 'title' : { - mark: function(field, msg){ - field.el.addClass(field.invalidClass); - field.el.dom.title = msg; - }, - clear: function(field){ - field.el.dom.title = ''; - } - }, - 'under' : { - mark: function(field, msg){ - field.el.addClass(field.invalidClass); - if(!field.errorEl){ - var elp = field.getErrorCt(); - if(!elp){ // field has no container el - field.el.dom.title = msg; - return; - } - field.errorEl = elp.createChild({cls:'x-form-invalid-msg'}); - field.on('resize', field.alignErrorEl, field); - field.on('destroy', function(){ - Ext.destroy(this.errorEl); - }, field); - } - field.alignErrorEl(); - field.errorEl.update(msg); - Ext.form.Field.msgFx[field.msgFx].show(field.errorEl, field); - }, - clear: function(field){ - field.el.removeClass(field.invalidClass); - if(field.errorEl){ - Ext.form.Field.msgFx[field.msgFx].hide(field.errorEl, field); - }else{ - field.el.dom.title = ''; - } - } - }, - 'side' : { - mark: function(field, msg){ - field.el.addClass(field.invalidClass); - if(!field.errorIcon){ - var elp = field.getErrorCt(); - // field has no container el - if(!elp){ - field.el.dom.title = msg; - return; - } - field.errorIcon = elp.createChild({cls:'x-form-invalid-icon'}); - if (field.ownerCt) { - field.ownerCt.on('afterlayout', field.alignErrorIcon, field); - field.ownerCt.on('expand', field.alignErrorIcon, field); - } - field.on('resize', field.alignErrorIcon, field); - field.on('destroy', function(){ - Ext.destroy(this.errorIcon); - }, field); - } - field.alignErrorIcon(); - field.errorIcon.dom.qtip = msg; - field.errorIcon.dom.qclass = 'x-form-invalid-tip'; - field.errorIcon.show(); - }, - clear: function(field){ - field.el.removeClass(field.invalidClass); - if(field.errorIcon){ - field.errorIcon.dom.qtip = ''; - field.errorIcon.hide(); - }else{ - field.el.dom.title = ''; - } - } - } -}; - -// anything other than normal should be considered experimental -Ext.form.Field.msgFx = { - normal : { - show: function(msgEl, f){ - msgEl.setDisplayed('block'); - }, - - hide : function(msgEl, f){ - msgEl.setDisplayed(false).update(''); - } - }, - - slide : { - show: function(msgEl, f){ - msgEl.slideIn('t', {stopFx:true}); - }, - - hide : function(msgEl, f){ - msgEl.slideOut('t', {stopFx:true,useDisplay:true}); - } - }, - - slideRight : { - show: function(msgEl, f){ - msgEl.fixDisplay(); - msgEl.alignTo(f.el, 'tl-tr'); - msgEl.slideIn('l', {stopFx:true}); - }, - - hide : function(msgEl, f){ - msgEl.slideOut('l', {stopFx:true,useDisplay:true}); - } - } -}; -Ext.reg('field', Ext.form.Field); -/** - * @class Ext.form.TextField - * @extends Ext.form.Field - *

    Basic text field. Can be used as a direct replacement for traditional text inputs, - * or as the base class for more sophisticated input controls (like {@link Ext.form.TextArea} - * and {@link Ext.form.ComboBox}).

    - *

    Validation

    - *

    The validation procedure is described in the documentation for {@link #validateValue}.

    - *

    Alter Validation Behavior

    - *

    Validation behavior for each field can be configured:

    - *
      - *
    • {@link Ext.form.TextField#invalidText invalidText} : the default validation message to - * show if any validation step above does not provide a message when invalid
    • - *
    • {@link Ext.form.TextField#maskRe maskRe} : filter out keystrokes before any validation occurs
    • - *
    • {@link Ext.form.TextField#stripCharsRe stripCharsRe} : filter characters after being typed in, - * but before being validated
    • - *
    • {@link Ext.form.Field#invalidClass invalidClass} : alternate style when invalid
    • - *
    • {@link Ext.form.Field#validateOnBlur validateOnBlur}, - * {@link Ext.form.Field#validationDelay validationDelay}, and - * {@link Ext.form.Field#validationEvent validationEvent} : modify how/when validation is triggered
    • - *
    - * - * @constructor Creates a new TextField - * @param {Object} config Configuration options - * - * @xtype textfield - */ -Ext.form.TextField = Ext.extend(Ext.form.Field, { - /** - * @cfg {String} vtypeText A custom error message to display in place of the default message provided - * for the {@link #vtype} currently set for this field (defaults to ''). Note: - * only applies if {@link #vtype} is set, else ignored. - */ - /** - * @cfg {RegExp} stripCharsRe A JavaScript RegExp object used to strip unwanted content from the value - * before validation (defaults to null). - */ - /** - * @cfg {Boolean} grow true if this field should automatically grow and shrink to its content - * (defaults to false) - */ - grow : false, - /** - * @cfg {Number} growMin The minimum width to allow when {@link #grow} = true (defaults - * to 30) - */ - growMin : 30, - /** - * @cfg {Number} growMax The maximum width to allow when {@link #grow} = true (defaults - * to 800) - */ - growMax : 800, - /** - * @cfg {String} vtype A validation type name as defined in {@link Ext.form.VTypes} (defaults to null) - */ - vtype : null, - /** - * @cfg {RegExp} maskRe An input mask regular expression that will be used to filter keystrokes that do - * not match (defaults to null). The maskRe will not operate on any paste events. - */ - maskRe : null, - /** - * @cfg {Boolean} disableKeyFilter Specify true to disable input keystroke filtering (defaults - * to false) - */ - disableKeyFilter : false, - /** - * @cfg {Boolean} allowBlank Specify false to validate that the value's length is > 0 (defaults to - * true) - */ - allowBlank : true, - /** - * @cfg {Number} minLength Minimum input field length required (defaults to 0) - */ - minLength : 0, - /** - * @cfg {Number} maxLength Maximum input field length allowed by validation (defaults to Number.MAX_VALUE). - * This behavior is intended to provide instant feedback to the user by improving usability to allow pasting - * and editing or overtyping and back tracking. To restrict the maximum number of characters that can be - * entered into the field use {@link Ext.form.Field#autoCreate autoCreate} to add - * any attributes you want to a field, for example:
    
    -var myField = new Ext.form.NumberField({
    -    id: 'mobile',
    -    anchor:'90%',
    -    fieldLabel: 'Mobile',
    -    maxLength: 16, // for validation
    -    autoCreate: {tag: 'input', type: 'text', size: '20', autocomplete: 'off', maxlength: '10'}
    -});
    -
    - */ - maxLength : Number.MAX_VALUE, - /** - * @cfg {String} minLengthText Error text to display if the {@link #minLength minimum length} - * validation fails (defaults to 'The minimum length for this field is {minLength}') - */ - minLengthText : 'The minimum length for this field is {0}', - /** - * @cfg {String} maxLengthText Error text to display if the {@link #maxLength maximum length} - * validation fails (defaults to 'The maximum length for this field is {maxLength}') - */ - maxLengthText : 'The maximum length for this field is {0}', - /** - * @cfg {Boolean} selectOnFocus true to automatically select any existing field text when the field - * receives input focus (defaults to false) - */ - selectOnFocus : false, - /** - * @cfg {String} blankText The error text to display if the {@link #allowBlank} validation - * fails (defaults to 'This field is required') - */ - blankText : 'This field is required', - /** - * @cfg {Function} validator - *

    A custom validation function to be called during field validation ({@link #validateValue}) - * (defaults to null). If specified, this function will be called first, allowing the - * developer to override the default validation process.

    - *

    This function will be passed the following Parameters:

    - *
      - *
    • value: Mixed - *
      The current field value
    • - *
    - *

    This function is to Return:

    - *
      - *
    • true: Boolean - *
      true if the value is valid
    • - *
    • msg: String - *
      An error message if the value is invalid
    • - *
    - */ - validator : null, - /** - * @cfg {RegExp} regex A JavaScript RegExp object to be tested against the field value during validation - * (defaults to null). If the test fails, the field will be marked invalid using - * {@link #regexText}. - */ - regex : null, - /** - * @cfg {String} regexText The error text to display if {@link #regex} is used and the - * test fails during validation (defaults to '') - */ - regexText : '', - /** - * @cfg {String} emptyText The default text to place into an empty field (defaults to null). - * Note: that this value will be submitted to the server if this field is enabled and configured - * with a {@link #name}. - */ - emptyText : null, - /** - * @cfg {String} emptyClass The CSS class to apply to an empty field to style the {@link #emptyText} - * (defaults to 'x-form-empty-field'). This class is automatically added and removed as needed - * depending on the current field value. - */ - emptyClass : 'x-form-empty-field', - - /** - * @cfg {Boolean} enableKeyEvents true to enable the proxying of key events for the HTML input - * field (defaults to false) - */ - - initComponent : function(){ - Ext.form.TextField.superclass.initComponent.call(this); - this.addEvents( - /** - * @event autosize - * Fires when the {@link #autoSize} function is triggered. The field may or - * may not have actually changed size according to the default logic, but this event provides - * a hook for the developer to apply additional logic at runtime to resize the field if needed. - * @param {Ext.form.Field} this This text field - * @param {Number} width The new field width - */ - 'autosize', - - /** - * @event keydown - * Keydown input field event. This event only fires if {@link #enableKeyEvents} - * is set to true. - * @param {Ext.form.TextField} this This text field - * @param {Ext.EventObject} e - */ - 'keydown', - /** - * @event keyup - * Keyup input field event. This event only fires if {@link #enableKeyEvents} - * is set to true. - * @param {Ext.form.TextField} this This text field - * @param {Ext.EventObject} e - */ - 'keyup', - /** - * @event keypress - * Keypress input field event. This event only fires if {@link #enableKeyEvents} - * is set to true. - * @param {Ext.form.TextField} this This text field - * @param {Ext.EventObject} e - */ - 'keypress' - ); - }, - - // private - initEvents : function(){ - Ext.form.TextField.superclass.initEvents.call(this); - if(this.validationEvent == 'keyup'){ - this.validationTask = new Ext.util.DelayedTask(this.validate, this); - this.mon(this.el, 'keyup', this.filterValidation, this); - } - else if(this.validationEvent !== false && this.validationEvent != 'blur'){ - this.mon(this.el, this.validationEvent, this.validate, this, {buffer: this.validationDelay}); - } - if(this.selectOnFocus || this.emptyText){ - this.mon(this.el, 'mousedown', this.onMouseDown, this); - - if(this.emptyText){ - this.applyEmptyText(); - } - } - if(this.maskRe || (this.vtype && this.disableKeyFilter !== true && (this.maskRe = Ext.form.VTypes[this.vtype+'Mask']))){ - this.mon(this.el, 'keypress', this.filterKeys, this); - } - if(this.grow){ - this.mon(this.el, 'keyup', this.onKeyUpBuffered, this, {buffer: 50}); - this.mon(this.el, 'click', this.autoSize, this); - } - if(this.enableKeyEvents){ - this.mon(this.el, { - scope: this, - keyup: this.onKeyUp, - keydown: this.onKeyDown, - keypress: this.onKeyPress - }); - } - }, - - onMouseDown: function(e){ - if(!this.hasFocus){ - this.mon(this.el, 'mouseup', Ext.emptyFn, this, { single: true, preventDefault: true }); - } - }, - - processValue : function(value){ - if(this.stripCharsRe){ - var newValue = value.replace(this.stripCharsRe, ''); - if(newValue !== value){ - this.setRawValue(newValue); - return newValue; - } - } - return value; - }, - - filterValidation : function(e){ - if(!e.isNavKeyPress()){ - this.validationTask.delay(this.validationDelay); - } - }, - - //private - onDisable: function(){ - Ext.form.TextField.superclass.onDisable.call(this); - if(Ext.isIE){ - this.el.dom.unselectable = 'on'; - } - }, - - //private - onEnable: function(){ - Ext.form.TextField.superclass.onEnable.call(this); - if(Ext.isIE){ - this.el.dom.unselectable = ''; - } - }, - - // private - onKeyUpBuffered : function(e){ - if(this.doAutoSize(e)){ - this.autoSize(); - } - }, - - // private - doAutoSize : function(e){ - return !e.isNavKeyPress(); - }, - - // private - onKeyUp : function(e){ - this.fireEvent('keyup', this, e); - }, - - // private - onKeyDown : function(e){ - this.fireEvent('keydown', this, e); - }, - - // private - onKeyPress : function(e){ - this.fireEvent('keypress', this, e); - }, - - /** - * Resets the current field value to the originally-loaded value and clears any validation messages. - * Also adds {@link #emptyText} and {@link #emptyClass} if the - * original value was blank. - */ - reset : function(){ - Ext.form.TextField.superclass.reset.call(this); - this.applyEmptyText(); - }, - - applyEmptyText : function(){ - if(this.rendered && this.emptyText && this.getRawValue().length < 1 && !this.hasFocus){ - this.setRawValue(this.emptyText); - this.el.addClass(this.emptyClass); - } - }, - - // private - preFocus : function(){ - var el = this.el, - isEmpty; - if(this.emptyText){ - if(el.dom.value == this.emptyText){ - this.setRawValue(''); - isEmpty = true; - } - el.removeClass(this.emptyClass); - } - if(this.selectOnFocus || isEmpty){ - el.dom.select(); - } - }, - - // private - postBlur : function(){ - this.applyEmptyText(); - }, - - // private - filterKeys : function(e){ - if(e.ctrlKey){ - return; - } - var k = e.getKey(); - if(Ext.isGecko && (e.isNavKeyPress() || k == e.BACKSPACE || (k == e.DELETE && e.button == -1))){ - return; - } - var cc = String.fromCharCode(e.getCharCode()); - if(!Ext.isGecko && e.isSpecialKey() && !cc){ - return; - } - if(!this.maskRe.test(cc)){ - e.stopEvent(); - } - }, - - setValue : function(v){ - if(this.emptyText && this.el && !Ext.isEmpty(v)){ - this.el.removeClass(this.emptyClass); - } - Ext.form.TextField.superclass.setValue.apply(this, arguments); - this.applyEmptyText(); - this.autoSize(); - return this; - }, - - /** - *

    Validates a value according to the field's validation rules and returns an array of errors - * for any failing validations. Validation rules are processed in the following order:

    - *
      - * - *
    • 1. Field specific validator - *
      - *

      A validator offers a way to customize and reuse a validation specification. - * If a field is configured with a {@link #validator} - * function, it will be passed the current field value. The {@link #validator} - * function is expected to return either: - *

        - *
      • Boolean true if the value is valid (validation continues).
      • - *
      • a String to represent the invalid message if invalid (validation halts).
      • - *
      - *
    • - * - *
    • 2. Basic Validation - *
      - *

      If the {@link #validator} has not halted validation, - * basic validation proceeds as follows:

      - * - *
        - * - *
      • {@link #allowBlank} : (Invalid message = - * {@link #emptyText})
        - * Depending on the configuration of {@link #allowBlank}, a - * blank field will cause validation to halt at this step and return - * Boolean true or false accordingly. - *
      • - * - *
      • {@link #minLength} : (Invalid message = - * {@link #minLengthText})
        - * If the passed value does not satisfy the {@link #minLength} - * specified, validation halts. - *
      • - * - *
      • {@link #maxLength} : (Invalid message = - * {@link #maxLengthText})
        - * If the passed value does not satisfy the {@link #maxLength} - * specified, validation halts. - *
      • - * - *
      - *
    • - * - *
    • 3. Preconfigured Validation Types (VTypes) - *
      - *

      If none of the prior validation steps halts validation, a field - * configured with a {@link #vtype} will utilize the - * corresponding {@link Ext.form.VTypes VTypes} validation function. - * If invalid, either the field's {@link #vtypeText} or - * the VTypes vtype Text property will be used for the invalid message. - * Keystrokes on the field will be filtered according to the VTypes - * vtype Mask property.

      - *
    • - * - *
    • 4. Field specific regex test - *
      - *

      If none of the prior validation steps halts validation, a field's - * configured {@link #regex} test will be processed. - * The invalid message for this test is configured with - * {@link #regexText}.

      - *
    • - * - * @param {Mixed} value The value to validate. The processed raw value will be used if nothing is passed - * @return {Array} Array of any validation errors - */ - getErrors: function(value) { - var errors = Ext.form.TextField.superclass.getErrors.apply(this, arguments); - - value = Ext.isDefined(value) ? value : this.processValue(this.getRawValue()); - - if (Ext.isFunction(this.validator)) { - var msg = this.validator(value); - if (msg !== true) { - errors.push(msg); - } - } - - if (value.length < 1 || value === this.emptyText) { - if (this.allowBlank) { - //if value is blank and allowBlank is true, there cannot be any additional errors - return errors; - } else { - errors.push(this.blankText); - } - } - - if (!this.allowBlank && (value.length < 1 || value === this.emptyText)) { // if it's blank - errors.push(this.blankText); - } - - if (value.length < this.minLength) { - errors.push(String.format(this.minLengthText, this.minLength)); - } - - if (value.length > this.maxLength) { - errors.push(String.format(this.maxLengthText, this.maxLength)); - } - - if (this.vtype) { - var vt = Ext.form.VTypes; - if(!vt[this.vtype](value, this)){ - errors.push(this.vtypeText || vt[this.vtype +'Text']); - } - } - - if (this.regex && !this.regex.test(value)) { - errors.push(this.regexText); - } - - return errors; - }, - - /** - * Selects text in this field - * @param {Number} start (optional) The index where the selection should start (defaults to 0) - * @param {Number} end (optional) The index where the selection should end (defaults to the text length) - */ - selectText : function(start, end){ - var v = this.getRawValue(); - var doFocus = false; - if(v.length > 0){ - start = start === undefined ? 0 : start; - end = end === undefined ? v.length : end; - var d = this.el.dom; - if(d.setSelectionRange){ - d.setSelectionRange(start, end); - }else if(d.createTextRange){ - var range = d.createTextRange(); - range.moveStart('character', start); - range.moveEnd('character', end-v.length); - range.select(); - } - doFocus = Ext.isGecko || Ext.isOpera; - }else{ - doFocus = true; - } - if(doFocus){ - this.focus(); - } - }, - - /** - * Automatically grows the field to accomodate the width of the text up to the maximum field width allowed. - * This only takes effect if {@link #grow} = true, and fires the {@link #autosize} event. - */ - autoSize : function(){ - if(!this.grow || !this.rendered){ - return; - } - if(!this.metrics){ - this.metrics = Ext.util.TextMetrics.createInstance(this.el); - } - var el = this.el; - var v = el.dom.value; - var d = document.createElement('div'); - d.appendChild(document.createTextNode(v)); - v = d.innerHTML; - Ext.removeNode(d); - d = null; - v += ' '; - var w = Math.min(this.growMax, Math.max(this.metrics.getWidth(v) + /* add extra padding */ 10, this.growMin)); - this.el.setWidth(w); - this.fireEvent('autosize', this, w); - }, - - onDestroy: function(){ - if(this.validationTask){ - this.validationTask.cancel(); - this.validationTask = null; - } - Ext.form.TextField.superclass.onDestroy.call(this); - } -}); -Ext.reg('textfield', Ext.form.TextField); -/** - * @class Ext.form.TriggerField - * @extends Ext.form.TextField - * Provides a convenient wrapper for TextFields that adds a clickable trigger button (looks like a combobox by default). - * The trigger has no default action, so you must assign a function to implement the trigger click handler by - * overriding {@link #onTriggerClick}. You can create a TriggerField directly, as it renders exactly like a combobox - * for which you can provide a custom implementation. For example: - *
      
      -var trigger = new Ext.form.TriggerField();
      -trigger.onTriggerClick = myTriggerFn;
      -trigger.applyToMarkup('my-field');
      -
      - * - * However, in general you will most likely want to use TriggerField as the base class for a reusable component. - * {@link Ext.form.DateField} and {@link Ext.form.ComboBox} are perfect examples of this. - * - * @constructor - * Create a new TriggerField. - * @param {Object} config Configuration options (valid {@Ext.form.TextField} config options will also be applied - * to the base TextField) - * @xtype trigger - */ -Ext.form.TriggerField = Ext.extend(Ext.form.TextField, { - /** - * @cfg {String} triggerClass - * An additional CSS class used to style the trigger button. The trigger will always get the - * class 'x-form-trigger' by default and triggerClass will be appended if specified. - */ - /** - * @cfg {Mixed} triggerConfig - *

      A {@link Ext.DomHelper DomHelper} config object specifying the structure of the - * trigger element for this Field. (Optional).

      - *

      Specify this when you need a customized element to act as the trigger button for a TriggerField.

      - *

      Note that when using this option, it is the developer's responsibility to ensure correct sizing, positioning - * and appearance of the trigger. Defaults to:

      - *
      {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.triggerClass}
      - */ - /** - * @cfg {String/Object} autoCreate

      A {@link Ext.DomHelper DomHelper} element spec, or true for a default - * element spec. Used to create the {@link Ext.Component#getEl Element} which will encapsulate this Component. - * See {@link Ext.Component#autoEl autoEl} for details. Defaults to:

      - *
      {tag: "input", type: "text", size: "16", autocomplete: "off"}
      - */ - defaultAutoCreate : {tag: "input", type: "text", size: "16", autocomplete: "off"}, - /** - * @cfg {Boolean} hideTrigger true to hide the trigger element and display only the base - * text field (defaults to false) - */ - hideTrigger:false, - /** - * @cfg {Boolean} editable false to prevent the user from typing text directly into the field, - * the field will only respond to a click on the trigger to set the value. (defaults to true). - */ - editable: true, - /** - * @cfg {Boolean} readOnly true to prevent the user from changing the field, and - * hides the trigger. Superceeds the editable and hideTrigger options if the value is true. - * (defaults to false) - */ - readOnly: false, - /** - * @cfg {String} wrapFocusClass The class added to the to the wrap of the trigger element. Defaults to - * x-trigger-wrap-focus. - */ - wrapFocusClass: 'x-trigger-wrap-focus', - /** - * @hide - * @method autoSize - */ - autoSize: Ext.emptyFn, - // private - monitorTab : true, - // private - deferHeight : true, - // private - mimicing : false, - - actionMode: 'wrap', - - defaultTriggerWidth: 17, - - // private - onResize : function(w, h){ - Ext.form.TriggerField.superclass.onResize.call(this, w, h); - var tw = this.getTriggerWidth(); - if(Ext.isNumber(w)){ - this.el.setWidth(w - tw); - } - this.wrap.setWidth(this.el.getWidth() + tw); - }, - - getTriggerWidth: function(){ - var tw = this.trigger.getWidth(); - if(!this.hideTrigger && !this.readOnly && tw === 0){ - tw = this.defaultTriggerWidth; - } - return tw; - }, - - // private - alignErrorIcon : function(){ - if(this.wrap){ - this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]); - } - }, - - // private - onRender : function(ct, position){ - this.doc = Ext.isIE ? Ext.getBody() : Ext.getDoc(); - Ext.form.TriggerField.superclass.onRender.call(this, ct, position); - - this.wrap = this.el.wrap({cls: 'x-form-field-wrap x-form-field-trigger-wrap'}); - this.trigger = this.wrap.createChild(this.triggerConfig || - {tag: "img", src: Ext.BLANK_IMAGE_URL, alt: "", cls: "x-form-trigger " + this.triggerClass}); - this.initTrigger(); - if(!this.width){ - this.wrap.setWidth(this.el.getWidth()+this.trigger.getWidth()); - } - this.resizeEl = this.positionEl = this.wrap; - }, - - getWidth: function() { - return(this.el.getWidth() + this.trigger.getWidth()); - }, - - updateEditState: function(){ - if(this.rendered){ - if (this.readOnly) { - this.el.dom.readOnly = true; - this.el.addClass('x-trigger-noedit'); - this.mun(this.el, 'click', this.onTriggerClick, this); - this.trigger.setDisplayed(false); - } else { - if (!this.editable) { - this.el.dom.readOnly = true; - this.el.addClass('x-trigger-noedit'); - this.mon(this.el, 'click', this.onTriggerClick, this); - } else { - this.el.dom.readOnly = false; - this.el.removeClass('x-trigger-noedit'); - this.mun(this.el, 'click', this.onTriggerClick, this); - } - this.trigger.setDisplayed(!this.hideTrigger); - } - this.onResize(this.width || this.wrap.getWidth()); - } - }, - - /** - * Changes the hidden status of the trigger. - * @param {Boolean} hideTrigger True to hide the trigger, false to show it. - */ - setHideTrigger: function(hideTrigger){ - if(hideTrigger != this.hideTrigger){ - this.hideTrigger = hideTrigger; - this.updateEditState(); - } - }, - - /** - * Allow or prevent the user from directly editing the field text. If false is passed, - * the user will only be able to modify the field using the trigger. Will also add - * a click event to the text field which will call the trigger. This method - * is the runtime equivalent of setting the {@link #editable} config option at config time. - * @param {Boolean} value True to allow the user to directly edit the field text. - */ - setEditable: function(editable){ - if(editable != this.editable){ - this.editable = editable; - this.updateEditState(); - } - }, - - /** - * Setting this to true will supersede settings {@link #editable} and {@link #hideTrigger}. - * Setting this to false will defer back to {@link #editable} and {@link #hideTrigger}. This method - * is the runtime equivalent of setting the {@link #readOnly} config option at config time. - * @param {Boolean} value True to prevent the user changing the field and explicitly - * hide the trigger. - */ - setReadOnly: function(readOnly){ - if(readOnly != this.readOnly){ - this.readOnly = readOnly; - this.updateEditState(); - } - }, - - afterRender : function(){ - Ext.form.TriggerField.superclass.afterRender.call(this); - this.updateEditState(); - }, - - // private - initTrigger : function(){ - this.mon(this.trigger, 'click', this.onTriggerClick, this, {preventDefault:true}); - this.trigger.addClassOnOver('x-form-trigger-over'); - this.trigger.addClassOnClick('x-form-trigger-click'); - }, - - // private - onDestroy : function(){ - Ext.destroy(this.trigger, this.wrap); - if (this.mimicing){ - this.doc.un('mousedown', this.mimicBlur, this); - } - delete this.doc; - Ext.form.TriggerField.superclass.onDestroy.call(this); - }, - - // private - onFocus : function(){ - Ext.form.TriggerField.superclass.onFocus.call(this); - if(!this.mimicing){ - this.wrap.addClass(this.wrapFocusClass); - this.mimicing = true; - this.doc.on('mousedown', this.mimicBlur, this, {delay: 10}); - if(this.monitorTab){ - this.on('specialkey', this.checkTab, this); - } - } - }, - - // private - checkTab : function(me, e){ - if(e.getKey() == e.TAB){ - this.triggerBlur(); - } - }, - - // private - onBlur : Ext.emptyFn, - - // private - mimicBlur : function(e){ - if(!this.isDestroyed && !this.wrap.contains(e.target) && this.validateBlur(e)){ - this.triggerBlur(); - } - }, - - // private - triggerBlur : function(){ - this.mimicing = false; - this.doc.un('mousedown', this.mimicBlur, this); - if(this.monitorTab && this.el){ - this.un('specialkey', this.checkTab, this); - } - Ext.form.TriggerField.superclass.onBlur.call(this); - if(this.wrap){ - this.wrap.removeClass(this.wrapFocusClass); - } - }, - - beforeBlur : Ext.emptyFn, - - // private - // This should be overriden by any subclass that needs to check whether or not the field can be blurred. - validateBlur : function(e){ - return true; - }, - - /** - * The function that should handle the trigger's click event. This method does nothing by default - * until overridden by an implementing function. See Ext.form.ComboBox and Ext.form.DateField for - * sample implementations. - * @method - * @param {EventObject} e - */ - onTriggerClick : Ext.emptyFn - - /** - * @cfg {Boolean} grow @hide - */ - /** - * @cfg {Number} growMin @hide - */ - /** - * @cfg {Number} growMax @hide - */ -}); - -/** - * @class Ext.form.TwinTriggerField - * @extends Ext.form.TriggerField - * TwinTriggerField is not a public class to be used directly. It is meant as an abstract base class - * to be extended by an implementing class. For an example of implementing this class, see the custom - * SearchField implementation here: - * http://extjs.com/deploy/ext/examples/form/custom.html - */ -Ext.form.TwinTriggerField = Ext.extend(Ext.form.TriggerField, { - /** - * @cfg {Mixed} triggerConfig - *

      A {@link Ext.DomHelper DomHelper} config object specifying the structure of the trigger elements - * for this Field. (Optional).

      - *

      Specify this when you need a customized element to contain the two trigger elements for this Field. - * Each trigger element must be marked by the CSS class x-form-trigger (also see - * {@link #trigger1Class} and {@link #trigger2Class}).

      - *

      Note that when using this option, it is the developer's responsibility to ensure correct sizing, - * positioning and appearance of the triggers.

      - */ - /** - * @cfg {String} trigger1Class - * An additional CSS class used to style the trigger button. The trigger will always get the - * class 'x-form-trigger' by default and triggerClass will be appended if specified. - */ - /** - * @cfg {String} trigger2Class - * An additional CSS class used to style the trigger button. The trigger will always get the - * class 'x-form-trigger' by default and triggerClass will be appended if specified. - */ - - initComponent : function(){ - Ext.form.TwinTriggerField.superclass.initComponent.call(this); - - this.triggerConfig = { - tag:'span', cls:'x-form-twin-triggers', cn:[ - {tag: "img", src: Ext.BLANK_IMAGE_URL, alt: "", cls: "x-form-trigger " + this.trigger1Class}, - {tag: "img", src: Ext.BLANK_IMAGE_URL, alt: "", cls: "x-form-trigger " + this.trigger2Class} - ]}; - }, - - getTrigger : function(index){ - return this.triggers[index]; - }, - - afterRender: function(){ - Ext.form.TwinTriggerField.superclass.afterRender.call(this); - var triggers = this.triggers, - i = 0, - len = triggers.length; - - for(; i < len; ++i){ - if(this['hideTrigger' + (i + 1)]){ - triggers[i].hide(); - } - - } - }, - - initTrigger : function(){ - var ts = this.trigger.select('.x-form-trigger', true), - triggerField = this; - - ts.each(function(t, all, index){ - var triggerIndex = 'Trigger'+(index+1); - t.hide = function(){ - var w = triggerField.wrap.getWidth(); - this.dom.style.display = 'none'; - triggerField.el.setWidth(w-triggerField.trigger.getWidth()); - triggerField['hidden' + triggerIndex] = true; - }; - t.show = function(){ - var w = triggerField.wrap.getWidth(); - this.dom.style.display = ''; - triggerField.el.setWidth(w-triggerField.trigger.getWidth()); - triggerField['hidden' + triggerIndex] = false; - }; - this.mon(t, 'click', this['on'+triggerIndex+'Click'], this, {preventDefault:true}); - t.addClassOnOver('x-form-trigger-over'); - t.addClassOnClick('x-form-trigger-click'); - }, this); - this.triggers = ts.elements; - }, - - getTriggerWidth: function(){ - var tw = 0; - Ext.each(this.triggers, function(t, index){ - var triggerIndex = 'Trigger' + (index + 1), - w = t.getWidth(); - if(w === 0 && !this['hidden' + triggerIndex]){ - tw += this.defaultTriggerWidth; - }else{ - tw += w; - } - }, this); - return tw; - }, - - // private - onDestroy : function() { - Ext.destroy(this.triggers); - Ext.form.TwinTriggerField.superclass.onDestroy.call(this); - }, - - /** - * The function that should handle the trigger's click event. This method does nothing by default - * until overridden by an implementing function. See {@link Ext.form.TriggerField#onTriggerClick} - * for additional information. - * @method - * @param {EventObject} e - */ - onTrigger1Click : Ext.emptyFn, - /** - * The function that should handle the trigger's click event. This method does nothing by default - * until overridden by an implementing function. See {@link Ext.form.TriggerField#onTriggerClick} - * for additional information. - * @method - * @param {EventObject} e - */ - onTrigger2Click : Ext.emptyFn -}); -Ext.reg('trigger', Ext.form.TriggerField); -/** - * @class Ext.form.TextArea - * @extends Ext.form.TextField - * Multiline text field. Can be used as a direct replacement for traditional textarea fields, plus adds - * support for auto-sizing. - * @constructor - * Creates a new TextArea - * @param {Object} config Configuration options - * @xtype textarea - */ -Ext.form.TextArea = Ext.extend(Ext.form.TextField, { - /** - * @cfg {Number} growMin The minimum height to allow when {@link Ext.form.TextField#grow grow}=true - * (defaults to 60) - */ - growMin : 60, - /** - * @cfg {Number} growMax The maximum height to allow when {@link Ext.form.TextField#grow grow}=true - * (defaults to 1000) - */ - growMax: 1000, - growAppend : ' \n ', - - enterIsSpecial : false, - - /** - * @cfg {Boolean} preventScrollbars true to prevent scrollbars from appearing regardless of how much text is - * in the field. This option is only relevant when {@link #grow} is true. Equivalent to setting overflow: hidden, defaults to - * false. - */ - preventScrollbars: false, - /** - * @cfg {String/Object} autoCreate

      A {@link Ext.DomHelper DomHelper} element spec, or true for a default - * element spec. Used to create the {@link Ext.Component#getEl Element} which will encapsulate this Component. - * See {@link Ext.Component#autoEl autoEl} for details. Defaults to:

      - *
      {tag: "textarea", style: "width:100px;height:60px;", autocomplete: "off"}
      - */ - - // private - onRender : function(ct, position){ - if(!this.el){ - this.defaultAutoCreate = { - tag: "textarea", - style:"width:100px;height:60px;", - autocomplete: "off" - }; - } - Ext.form.TextArea.superclass.onRender.call(this, ct, position); - if(this.grow){ - this.textSizeEl = Ext.DomHelper.append(document.body, { - tag: "pre", cls: "x-form-grow-sizer" - }); - if(this.preventScrollbars){ - this.el.setStyle("overflow", "hidden"); - } - this.el.setHeight(this.growMin); - } - }, - - onDestroy : function(){ - Ext.removeNode(this.textSizeEl); - Ext.form.TextArea.superclass.onDestroy.call(this); - }, - - fireKey : function(e){ - if(e.isSpecialKey() && (this.enterIsSpecial || (e.getKey() != e.ENTER || e.hasModifier()))){ - this.fireEvent("specialkey", this, e); - } - }, - - // private - doAutoSize : function(e){ - return !e.isNavKeyPress() || e.getKey() == e.ENTER; - }, - - // inherit docs - filterValidation: function(e) { - if(!e.isNavKeyPress() || (!this.enterIsSpecial && e.keyCode == e.ENTER)){ - this.validationTask.delay(this.validationDelay); - } - }, - - /** - * Automatically grows the field to accomodate the height of the text up to the maximum field height allowed. - * This only takes effect if grow = true, and fires the {@link #autosize} event if the height changes. - */ - autoSize: function(){ - if(!this.grow || !this.textSizeEl){ - return; - } - var el = this.el, - v = Ext.util.Format.htmlEncode(el.dom.value), - ts = this.textSizeEl, - h; - - Ext.fly(ts).setWidth(this.el.getWidth()); - if(v.length < 1){ - v = "  "; - }else{ - v += this.growAppend; - if(Ext.isIE){ - v = v.replace(/\n/g, ' 
      '); - } - } - ts.innerHTML = v; - h = Math.min(this.growMax, Math.max(ts.offsetHeight, this.growMin)); - if(h != this.lastHeight){ - this.lastHeight = h; - this.el.setHeight(h); - this.fireEvent("autosize", this, h); - } - } -}); -Ext.reg('textarea', Ext.form.TextArea);/** - * @class Ext.form.NumberField - * @extends Ext.form.TextField - * Numeric text field that provides automatic keystroke filtering and numeric validation. - * @constructor - * Creates a new NumberField - * @param {Object} config Configuration options - * @xtype numberfield - */ -Ext.form.NumberField = Ext.extend(Ext.form.TextField, { - /** - * @cfg {RegExp} stripCharsRe @hide - */ - /** - * @cfg {RegExp} maskRe @hide - */ - /** - * @cfg {String} fieldClass The default CSS class for the field (defaults to "x-form-field x-form-num-field") - */ - fieldClass: "x-form-field x-form-num-field", - - /** - * @cfg {Boolean} allowDecimals False to disallow decimal values (defaults to true) - */ - allowDecimals : true, - - /** - * @cfg {String} decimalSeparator Character(s) to allow as the decimal separator (defaults to '.') - */ - decimalSeparator : ".", - - /** - * @cfg {Number} decimalPrecision The maximum precision to display after the decimal separator (defaults to 2) - */ - decimalPrecision : 2, - - /** - * @cfg {Boolean} allowNegative False to prevent entering a negative sign (defaults to true) - */ - allowNegative : true, - - /** - * @cfg {Number} minValue The minimum allowed value (defaults to Number.NEGATIVE_INFINITY) - */ - minValue : Number.NEGATIVE_INFINITY, - - /** - * @cfg {Number} maxValue The maximum allowed value (defaults to Number.MAX_VALUE) - */ - maxValue : Number.MAX_VALUE, - - /** - * @cfg {String} minText Error text to display if the minimum value validation fails (defaults to "The minimum value for this field is {minValue}") - */ - minText : "The minimum value for this field is {0}", - - /** - * @cfg {String} maxText Error text to display if the maximum value validation fails (defaults to "The maximum value for this field is {maxValue}") - */ - maxText : "The maximum value for this field is {0}", - - /** - * @cfg {String} nanText Error text to display if the value is not a valid number. For example, this can happen - * if a valid character like '.' or '-' is left in the field with no number (defaults to "{value} is not a valid number") - */ - nanText : "{0} is not a valid number", - - /** - * @cfg {String} baseChars The base set of characters to evaluate as valid numbers (defaults to '0123456789'). - */ - baseChars : "0123456789", - - /** - * @cfg {Boolean} autoStripChars True to automatically strip not allowed characters from the field. Defaults to false - */ - autoStripChars: false, - - // private - initEvents : function() { - var allowed = this.baseChars + ''; - if (this.allowDecimals) { - allowed += this.decimalSeparator; - } - if (this.allowNegative) { - allowed += '-'; - } - allowed = Ext.escapeRe(allowed); - this.maskRe = new RegExp('[' + allowed + ']'); - if (this.autoStripChars) { - this.stripCharsRe = new RegExp('[^' + allowed + ']', 'gi'); - } - - Ext.form.NumberField.superclass.initEvents.call(this); - }, - - /** - * Runs all of NumberFields validations and returns an array of any errors. Note that this first - * runs TextField's validations, so the returned array is an amalgamation of all field errors. - * The additional validations run test that the value is a number, and that it is within the - * configured min and max values. - * @param {Mixed} value The value to get errors for (defaults to the current field value) - * @return {Array} All validation errors for this field - */ - getErrors: function(value) { - var errors = Ext.form.NumberField.superclass.getErrors.apply(this, arguments); - - value = Ext.isDefined(value) ? value : this.processValue(this.getRawValue()); - - if (value.length < 1) { // if it's blank and textfield didn't flag it then it's valid - return errors; - } - - value = String(value).replace(this.decimalSeparator, "."); - - if(isNaN(value)){ - errors.push(String.format(this.nanText, value)); - } - - var num = this.parseValue(value); - - if (num < this.minValue) { - errors.push(String.format(this.minText, this.minValue)); - } - - if (num > this.maxValue) { - errors.push(String.format(this.maxText, this.maxValue)); - } - - return errors; - }, - - getValue : function() { - return this.fixPrecision(this.parseValue(Ext.form.NumberField.superclass.getValue.call(this))); - }, - - setValue : function(v) { - v = Ext.isNumber(v) ? v : parseFloat(String(v).replace(this.decimalSeparator, ".")); - v = this.fixPrecision(v); - v = isNaN(v) ? '' : String(v).replace(".", this.decimalSeparator); - return Ext.form.NumberField.superclass.setValue.call(this, v); - }, - - /** - * Replaces any existing {@link #minValue} with the new value. - * @param {Number} value The minimum value - */ - setMinValue : function(value) { - this.minValue = Ext.num(value, Number.NEGATIVE_INFINITY); - }, - - /** - * Replaces any existing {@link #maxValue} with the new value. - * @param {Number} value The maximum value - */ - setMaxValue : function(value) { - this.maxValue = Ext.num(value, Number.MAX_VALUE); - }, - - // private - parseValue : function(value) { - value = parseFloat(String(value).replace(this.decimalSeparator, ".")); - return isNaN(value) ? '' : value; - }, - - /** - * @private - * - */ - fixPrecision : function(value) { - var nan = isNaN(value); - - if (!this.allowDecimals || this.decimalPrecision == -1 || nan || !value) { - return nan ? '' : value; - } - - return parseFloat(parseFloat(value).toFixed(this.decimalPrecision)); - }, - - beforeBlur : function() { - var v = this.parseValue(this.getRawValue()); - - if (!Ext.isEmpty(v)) { - this.setValue(v); - } - } -}); - -Ext.reg('numberfield', Ext.form.NumberField); -/** - * @class Ext.form.DateField - * @extends Ext.form.TriggerField - * Provides a date input field with a {@link Ext.DatePicker} dropdown and automatic date validation. - * @constructor - * Create a new DateField - * @param {Object} config - * @xtype datefield - */ -Ext.form.DateField = Ext.extend(Ext.form.TriggerField, { - /** - * @cfg {String} format - * The default date format string which can be overriden for localization support. The format must be - * valid according to {@link Date#parseDate} (defaults to 'm/d/Y'). - */ - format : "m/d/Y", - /** - * @cfg {String} altFormats - * Multiple date formats separated by "|" to try when parsing a user input value and it - * does not match the defined format (defaults to - * 'm/d/Y|n/j/Y|n/j/y|m/j/y|n/d/y|m/j/Y|n/d/Y|m-d-y|m-d-Y|m/d|m-d|md|mdy|mdY|d|Y-m-d|n-j|n/j'). - */ - altFormats : "m/d/Y|n/j/Y|n/j/y|m/j/y|n/d/y|m/j/Y|n/d/Y|m-d-y|m-d-Y|m/d|m-d|md|mdy|mdY|d|Y-m-d|n-j|n/j", - /** - * @cfg {String} disabledDaysText - * The tooltip to display when the date falls on a disabled day (defaults to 'Disabled') - */ - disabledDaysText : "Disabled", - /** - * @cfg {String} disabledDatesText - * The tooltip text to display when the date falls on a disabled date (defaults to 'Disabled') - */ - disabledDatesText : "Disabled", - /** - * @cfg {String} minText - * The error text to display when the date in the cell is before {@link #minValue} (defaults to - * 'The date in this field must be after {minValue}'). - */ - minText : "The date in this field must be equal to or after {0}", - /** - * @cfg {String} maxText - * The error text to display when the date in the cell is after {@link #maxValue} (defaults to - * 'The date in this field must be before {maxValue}'). - */ - maxText : "The date in this field must be equal to or before {0}", - /** - * @cfg {String} invalidText - * The error text to display when the date in the field is invalid (defaults to - * '{value} is not a valid date - it must be in the format {format}'). - */ - invalidText : "{0} is not a valid date - it must be in the format {1}", - /** - * @cfg {String} triggerClass - * An additional CSS class used to style the trigger button. The trigger will always get the - * class 'x-form-trigger' and triggerClass will be appended if specified - * (defaults to 'x-form-date-trigger' which displays a calendar icon). - */ - triggerClass : 'x-form-date-trigger', - /** - * @cfg {Boolean} showToday - * false to hide the footer area of the DatePicker containing the Today button and disable - * the keyboard handler for spacebar that selects the current date (defaults to true). - */ - showToday : true, - - /** - * @cfg {Number} startDay - * Day index at which the week should begin, 0-based (defaults to 0, which is Sunday) - */ - startDay : 0, - - /** - * @cfg {Date/String} minValue - * The minimum allowed date. Can be either a Javascript date object or a string date in a - * valid format (defaults to null). - */ - /** - * @cfg {Date/String} maxValue - * The maximum allowed date. Can be either a Javascript date object or a string date in a - * valid format (defaults to null). - */ - /** - * @cfg {Array} disabledDays - * An array of days to disable, 0 based (defaults to null). Some examples:
      
      -// disable Sunday and Saturday:
      -disabledDays:  [0, 6]
      -// disable weekdays:
      -disabledDays: [1,2,3,4,5]
      -     * 
      - */ - /** - * @cfg {Array} disabledDates - * An array of "dates" to disable, as strings. These strings will be used to build a dynamic regular - * expression so they are very powerful. Some examples:
      
      -// disable these exact dates:
      -disabledDates: ["03/08/2003", "09/16/2003"]
      -// disable these days for every year:
      -disabledDates: ["03/08", "09/16"]
      -// only match the beginning (useful if you are using short years):
      -disabledDates: ["^03/08"]
      -// disable every day in March 2006:
      -disabledDates: ["03/../2006"]
      -// disable every day in every March:
      -disabledDates: ["^03"]
      -     * 
      - * Note that the format of the dates included in the array should exactly match the {@link #format} config. - * In order to support regular expressions, if you are using a {@link #format date format} that has "." in - * it, you will have to escape the dot when restricting dates. For example: ["03\\.08\\.03"]. - */ - /** - * @cfg {String/Object} autoCreate - * A {@link Ext.DomHelper DomHelper element specification object}, or true for the default element - * specification object:
      
      -     * autoCreate: {tag: "input", type: "text", size: "10", autocomplete: "off"}
      -     * 
      - */ - - // private - defaultAutoCreate : {tag: "input", type: "text", size: "10", autocomplete: "off"}, - - // in the absence of a time value, a default value of 12 noon will be used - // (note: 12 noon was chosen because it steers well clear of all DST timezone changes) - initTime: '12', // 24 hour format - - initTimeFormat: 'H', - - // PUBLIC -- to be documented - safeParse : function(value, format) { - if (Date.formatContainsHourInfo(format)) { - // if parse format contains hour information, no DST adjustment is necessary - return Date.parseDate(value, format); - } else { - // set time to 12 noon, then clear the time - var parsedDate = Date.parseDate(value + ' ' + this.initTime, format + ' ' + this.initTimeFormat); - - if (parsedDate) { - return parsedDate.clearTime(); - } - } - }, - - initComponent : function(){ - Ext.form.DateField.superclass.initComponent.call(this); - - this.addEvents( - /** - * @event select - * Fires when a date is selected via the date picker. - * @param {Ext.form.DateField} this - * @param {Date} date The date that was selected - */ - 'select' - ); - - if(Ext.isString(this.minValue)){ - this.minValue = this.parseDate(this.minValue); - } - if(Ext.isString(this.maxValue)){ - this.maxValue = this.parseDate(this.maxValue); - } - this.disabledDatesRE = null; - this.initDisabledDays(); - }, - - initEvents: function() { - Ext.form.DateField.superclass.initEvents.call(this); - this.keyNav = new Ext.KeyNav(this.el, { - "down": function(e) { - this.onTriggerClick(); - }, - scope: this, - forceKeyDown: true - }); - }, - - - // private - initDisabledDays : function(){ - if(this.disabledDates){ - var dd = this.disabledDates, - len = dd.length - 1, - re = "(?:"; - - Ext.each(dd, function(d, i){ - re += Ext.isDate(d) ? '^' + Ext.escapeRe(d.dateFormat(this.format)) + '$' : dd[i]; - if(i != len){ - re += '|'; - } - }, this); - this.disabledDatesRE = new RegExp(re + ')'); - } - }, - - /** - * Replaces any existing disabled dates with new values and refreshes the DatePicker. - * @param {Array} disabledDates An array of date strings (see the {@link #disabledDates} config - * for details on supported values) used to disable a pattern of dates. - */ - setDisabledDates : function(dd){ - this.disabledDates = dd; - this.initDisabledDays(); - if(this.menu){ - this.menu.picker.setDisabledDates(this.disabledDatesRE); - } - }, - - /** - * Replaces any existing disabled days (by index, 0-6) with new values and refreshes the DatePicker. - * @param {Array} disabledDays An array of disabled day indexes. See the {@link #disabledDays} - * config for details on supported values. - */ - setDisabledDays : function(dd){ - this.disabledDays = dd; - if(this.menu){ - this.menu.picker.setDisabledDays(dd); - } - }, - - /** - * Replaces any existing {@link #minValue} with the new value and refreshes the DatePicker. - * @param {Date} value The minimum date that can be selected - */ - setMinValue : function(dt){ - this.minValue = (Ext.isString(dt) ? this.parseDate(dt) : dt); - if(this.menu){ - this.menu.picker.setMinDate(this.minValue); - } - }, - - /** - * Replaces any existing {@link #maxValue} with the new value and refreshes the DatePicker. - * @param {Date} value The maximum date that can be selected - */ - setMaxValue : function(dt){ - this.maxValue = (Ext.isString(dt) ? this.parseDate(dt) : dt); - if(this.menu){ - this.menu.picker.setMaxDate(this.maxValue); - } - }, - - /** - * Runs all of NumberFields validations and returns an array of any errors. Note that this first - * runs TextField's validations, so the returned array is an amalgamation of all field errors. - * The additional validation checks are testing that the date format is valid, that the chosen - * date is within the min and max date constraints set, that the date chosen is not in the disabledDates - * regex and that the day chosed is not one of the disabledDays. - * @param {Mixed} value The value to get errors for (defaults to the current field value) - * @return {Array} All validation errors for this field - */ - getErrors: function(value) { - var errors = Ext.form.DateField.superclass.getErrors.apply(this, arguments); - - value = this.formatDate(value || this.processValue(this.getRawValue())); - - if (value.length < 1) { // if it's blank and textfield didn't flag it then it's valid - return errors; - } - - var svalue = value; - value = this.parseDate(value); - if (!value) { - errors.push(String.format(this.invalidText, svalue, this.format)); - return errors; - } - - var time = value.getTime(); - if (this.minValue && time < this.minValue.clearTime().getTime()) { - errors.push(String.format(this.minText, this.formatDate(this.minValue))); - } - - if (this.maxValue && time > this.maxValue.clearTime().getTime()) { - errors.push(String.format(this.maxText, this.formatDate(this.maxValue))); - } - - if (this.disabledDays) { - var day = value.getDay(); - - for(var i = 0; i < this.disabledDays.length; i++) { - if (day === this.disabledDays[i]) { - errors.push(this.disabledDaysText); - break; - } - } - } - - var fvalue = this.formatDate(value); - if (this.disabledDatesRE && this.disabledDatesRE.test(fvalue)) { - errors.push(String.format(this.disabledDatesText, fvalue)); - } - - return errors; - }, - - // private - // Provides logic to override the default TriggerField.validateBlur which just returns true - validateBlur : function(){ - return !this.menu || !this.menu.isVisible(); - }, - - /** - * Returns the current date value of the date field. - * @return {Date} The date value - */ - getValue : function(){ - return this.parseDate(Ext.form.DateField.superclass.getValue.call(this)) || ""; - }, - - /** - * Sets the value of the date field. You can pass a date object or any string that can be - * parsed into a valid date, using {@link #format} as the date format, according - * to the same rules as {@link Date#parseDate} (the default format used is "m/d/Y"). - *
      Usage: - *
      
      -//All of these calls set the same date value (May 4, 2006)
      -
      -//Pass a date object:
      -var dt = new Date('5/4/2006');
      -dateField.setValue(dt);
      -
      -//Pass a date string (default format):
      -dateField.setValue('05/04/2006');
      -
      -//Pass a date string (custom format):
      -dateField.format = 'Y-m-d';
      -dateField.setValue('2006-05-04');
      -
      - * @param {String/Date} date The date or valid date string - * @return {Ext.form.Field} this - */ - setValue : function(date){ - return Ext.form.DateField.superclass.setValue.call(this, this.formatDate(this.parseDate(date))); - }, - - // private - parseDate : function(value) { - if(!value || Ext.isDate(value)){ - return value; - } - - var v = this.safeParse(value, this.format), - af = this.altFormats, - afa = this.altFormatsArray; - - if (!v && af) { - afa = afa || af.split("|"); - - for (var i = 0, len = afa.length; i < len && !v; i++) { - v = this.safeParse(value, afa[i]); - } - } - return v; - }, - - // private - onDestroy : function(){ - Ext.destroy(this.menu, this.keyNav); - Ext.form.DateField.superclass.onDestroy.call(this); - }, - - // private - formatDate : function(date){ - return Ext.isDate(date) ? date.dateFormat(this.format) : date; - }, - - /** - * @method onTriggerClick - * @hide - */ - // private - // Implements the default empty TriggerField.onTriggerClick function to display the DatePicker - onTriggerClick : function(){ - if(this.disabled){ - return; - } - if(this.menu == null){ - this.menu = new Ext.menu.DateMenu({ - hideOnClick: false, - focusOnSelect: false - }); - } - this.onFocus(); - Ext.apply(this.menu.picker, { - minDate : this.minValue, - maxDate : this.maxValue, - disabledDatesRE : this.disabledDatesRE, - disabledDatesText : this.disabledDatesText, - disabledDays : this.disabledDays, - disabledDaysText : this.disabledDaysText, - format : this.format, - showToday : this.showToday, - startDay: this.startDay, - minText : String.format(this.minText, this.formatDate(this.minValue)), - maxText : String.format(this.maxText, this.formatDate(this.maxValue)) - }); - this.menu.picker.setValue(this.getValue() || new Date()); - this.menu.show(this.el, "tl-bl?"); - this.menuEvents('on'); - }, - - //private - menuEvents: function(method){ - this.menu[method]('select', this.onSelect, this); - this.menu[method]('hide', this.onMenuHide, this); - this.menu[method]('show', this.onFocus, this); - }, - - onSelect: function(m, d){ - this.setValue(d); - this.fireEvent('select', this, d); - this.menu.hide(); - }, - - onMenuHide: function(){ - this.focus(false, 60); - this.menuEvents('un'); - }, - - // private - beforeBlur : function(){ - var v = this.parseDate(this.getRawValue()); - if(v){ - this.setValue(v); - } - } - - /** - * @cfg {Boolean} grow @hide - */ - /** - * @cfg {Number} growMin @hide - */ - /** - * @cfg {Number} growMax @hide - */ - /** - * @hide - * @method autoSize - */ -}); -Ext.reg('datefield', Ext.form.DateField); -/** - * @class Ext.form.DisplayField - * @extends Ext.form.Field - * A display-only text field which is not validated and not submitted. - * @constructor - * Creates a new DisplayField. - * @param {Object} config Configuration options - * @xtype displayfield - */ -Ext.form.DisplayField = Ext.extend(Ext.form.Field, { - validationEvent : false, - validateOnBlur : false, - defaultAutoCreate : {tag: "div"}, - /** - * @cfg {String} fieldClass The default CSS class for the field (defaults to "x-form-display-field") - */ - fieldClass : "x-form-display-field", - /** - * @cfg {Boolean} htmlEncode false to skip HTML-encoding the text when rendering it (defaults to - * false). This might be useful if you want to include tags in the field's innerHTML rather than - * rendering them as string literals per the default logic. - */ - htmlEncode: false, - - // private - initEvents : Ext.emptyFn, - - isValid : function(){ - return true; - }, - - validate : function(){ - return true; - }, - - getRawValue : function(){ - var v = this.rendered ? this.el.dom.innerHTML : Ext.value(this.value, ''); - if(v === this.emptyText){ - v = ''; - } - if(this.htmlEncode){ - v = Ext.util.Format.htmlDecode(v); - } - return v; - }, - - getValue : function(){ - return this.getRawValue(); - }, - - getName: function() { - return this.name; - }, - - setRawValue : function(v){ - if(this.htmlEncode){ - v = Ext.util.Format.htmlEncode(v); - } - return this.rendered ? (this.el.dom.innerHTML = (Ext.isEmpty(v) ? '' : v)) : (this.value = v); - }, - - setValue : function(v){ - this.setRawValue(v); - return this; - } - /** - * @cfg {String} inputType - * @hide - */ - /** - * @cfg {Boolean} disabled - * @hide - */ - /** - * @cfg {Boolean} readOnly - * @hide - */ - /** - * @cfg {Boolean} validateOnBlur - * @hide - */ - /** - * @cfg {Number} validationDelay - * @hide - */ - /** - * @cfg {String/Boolean} validationEvent - * @hide - */ -}); - -Ext.reg('displayfield', Ext.form.DisplayField); -/** - * @class Ext.form.ComboBox - * @extends Ext.form.TriggerField - *

      A combobox control with support for autocomplete, remote-loading, paging and many other features.

      - *

      A ComboBox works in a similar manner to a traditional HTML <select> field. The difference is - * that to submit the {@link #valueField}, you must specify a {@link #hiddenName} to create a hidden input - * field to hold the value of the valueField. The {@link #displayField} is shown in the text field - * which is named according to the {@link #name}.

      - *

      Events

      - *

      To do something when something in ComboBox is selected, configure the select event:

      
      -var cb = new Ext.form.ComboBox({
      -    // all of your config options
      -    listeners:{
      -         scope: yourScope,
      -         'select': yourFunction
      -    }
      -});
      -
      -// Alternatively, you can assign events after the object is created:
      -var cb = new Ext.form.ComboBox(yourOptions);
      -cb.on('select', yourFunction, yourScope);
      - * 

      - * - *

      ComboBox in Grid

      - *

      If using a ComboBox in an {@link Ext.grid.EditorGridPanel Editor Grid} a {@link Ext.grid.Column#renderer renderer} - * will be needed to show the displayField when the editor is not active. Set up the renderer manually, or implement - * a reusable render, for example:

      
      -// create reusable renderer
      -Ext.util.Format.comboRenderer = function(combo){
      -    return function(value){
      -        var record = combo.findRecord(combo.{@link #valueField}, value);
      -        return record ? record.get(combo.{@link #displayField}) : combo.{@link #valueNotFoundText};
      -    }
      -}
      -
      -// create the combo instance
      -var combo = new Ext.form.ComboBox({
      -    {@link #typeAhead}: true,
      -    {@link #triggerAction}: 'all',
      -    {@link #lazyRender}:true,
      -    {@link #mode}: 'local',
      -    {@link #store}: new Ext.data.ArrayStore({
      -        id: 0,
      -        fields: [
      -            'myId',
      -            'displayText'
      -        ],
      -        data: [[1, 'item1'], [2, 'item2']]
      -    }),
      -    {@link #valueField}: 'myId',
      -    {@link #displayField}: 'displayText'
      -});
      -
      -// snippet of column model used within grid
      -var cm = new Ext.grid.ColumnModel([{
      -       ...
      -    },{
      -       header: "Some Header",
      -       dataIndex: 'whatever',
      -       width: 130,
      -       editor: combo, // specify reference to combo instance
      -       renderer: Ext.util.Format.comboRenderer(combo) // pass combo instance to reusable renderer
      -    },
      -    ...
      -]);
      - * 

      - * - *

      Filtering

      - *

      A ComboBox {@link #doQuery uses filtering itself}, for information about filtering the ComboBox - * store manually see {@link #lastQuery}.

      - * @constructor - * Create a new ComboBox. - * @param {Object} config Configuration options - * @xtype combo - */ -Ext.form.ComboBox = Ext.extend(Ext.form.TriggerField, { - /** - * @cfg {Mixed} transform The id, DOM node or element of an existing HTML SELECT to convert to a ComboBox. - * Note that if you specify this and the combo is going to be in an {@link Ext.form.BasicForm} or - * {@link Ext.form.FormPanel}, you must also set {@link #lazyRender} = true. - */ - /** - * @cfg {Boolean} lazyRender true to prevent the ComboBox from rendering until requested - * (should always be used when rendering into an {@link Ext.Editor} (e.g. {@link Ext.grid.EditorGridPanel Grids}), - * defaults to false). - */ - /** - * @cfg {String/Object} autoCreate

      A {@link Ext.DomHelper DomHelper} element spec, or true for a default - * element spec. Used to create the {@link Ext.Component#getEl Element} which will encapsulate this Component. - * See {@link Ext.Component#autoEl autoEl} for details. Defaults to:

      - *
      {tag: "input", type: "text", size: "24", autocomplete: "off"}
      - */ - /** - * @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to undefined). - * Acceptable values for this property are: - *
        - *
      • any {@link Ext.data.Store Store} subclass
      • - *
      • an Array : Arrays will be converted to a {@link Ext.data.ArrayStore} internally, - * automatically generating {@link Ext.data.Field#name field names} to work with all data components. - *
          - *
        • 1-dimensional array : (e.g., ['Foo','Bar'])
          - * A 1-dimensional array will automatically be expanded (each array item will be used for both the combo - * {@link #valueField} and {@link #displayField})
        • - *
        • 2-dimensional array : (e.g., [['f','Foo'],['b','Bar']])
          - * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo - * {@link #valueField}, while the value at index 1 is assumed to be the combo {@link #displayField}. - *
      - *

      See also {@link #mode}.

      - */ - /** - * @cfg {String} title If supplied, a header element is created containing this text and added into the top of - * the dropdown list (defaults to undefined, with no header element) - */ - - // private - defaultAutoCreate : {tag: "input", type: "text", size: "24", autocomplete: "off"}, - /** - * @cfg {Number} listWidth The width (used as a parameter to {@link Ext.Element#setWidth}) of the dropdown - * list (defaults to the width of the ComboBox field). See also {@link #minListWidth} - */ - /** - * @cfg {String} displayField The underlying {@link Ext.data.Field#name data field name} to bind to this - * ComboBox (defaults to undefined if {@link #mode} = 'remote' or 'field1' if - * {@link #transform transforming a select} or if the {@link #store field name is autogenerated based on - * the store configuration}). - *

      See also {@link #valueField}.

      - *

      Note: if using a ComboBox in an {@link Ext.grid.EditorGridPanel Editor Grid} a - * {@link Ext.grid.Column#renderer renderer} will be needed to show the displayField when the editor is not - * active.

      - */ - /** - * @cfg {String} valueField The underlying {@link Ext.data.Field#name data value name} to bind to this - * ComboBox (defaults to undefined if {@link #mode} = 'remote' or 'field2' if - * {@link #transform transforming a select} or if the {@link #store field name is autogenerated based on - * the store configuration}). - *

      Note: use of a valueField requires the user to make a selection in order for a value to be - * mapped. See also {@link #hiddenName}, {@link #hiddenValue}, and {@link #displayField}.

      - */ - /** - * @cfg {String} hiddenName If specified, a hidden form field with this name is dynamically generated to store the - * field's data value (defaults to the underlying DOM element's name). Required for the combo's value to automatically - * post during a form submission. See also {@link #valueField}. - */ - /** - * @cfg {String} hiddenId If {@link #hiddenName} is specified, hiddenId can also be provided - * to give the hidden field a unique id. The hiddenId and combo {@link Ext.Component#id id} should be - * different, since no two DOM nodes should share the same id. - */ - /** - * @cfg {String} hiddenValue Sets the initial value of the hidden field if {@link #hiddenName} is - * specified to contain the selected {@link #valueField}, from the Store. Defaults to the configured - * {@link Ext.form.Field#value value}. - */ - /** - * @cfg {String} listClass The CSS class to add to the predefined 'x-combo-list' class - * applied the dropdown list element (defaults to ''). - */ - listClass : '', - /** - * @cfg {String} selectedClass CSS class to apply to the selected item in the dropdown list - * (defaults to 'x-combo-selected') - */ - selectedClass : 'x-combo-selected', - /** - * @cfg {String} listEmptyText The empty text to display in the data view if no items are found. - * (defaults to '') - */ - listEmptyText: '', - /** - * @cfg {String} triggerClass An additional CSS class used to style the trigger button. The trigger will always - * get the class 'x-form-trigger' and triggerClass will be appended if specified - * (defaults to 'x-form-arrow-trigger' which displays a downward arrow icon). - */ - triggerClass : 'x-form-arrow-trigger', - /** - * @cfg {Boolean/String} shadow true or "sides" for the default effect, "frame" for - * 4-way shadow, and "drop" for bottom-right - */ - shadow : 'sides', - /** - * @cfg {String/Array} listAlign A valid anchor position value. See {@link Ext.Element#alignTo} for details - * on supported anchor positions and offsets. To specify x/y offsets as well, this value - * may be specified as an Array of {@link Ext.Element#alignTo} method arguments.

      - *
      [ 'tl-bl?', [6,0] ]
      (defaults to 'tl-bl?') - */ - listAlign : 'tl-bl?', - /** - * @cfg {Number} maxHeight The maximum height in pixels of the dropdown list before scrollbars are shown - * (defaults to 300) - */ - maxHeight : 300, - /** - * @cfg {Number} minHeight The minimum height in pixels of the dropdown list when the list is constrained by its - * distance to the viewport edges (defaults to 90) - */ - minHeight : 90, - /** - * @cfg {String} triggerAction The action to execute when the trigger is clicked. - *
        - *
      • 'query' : Default - *

        {@link #doQuery run the query} using the {@link Ext.form.Field#getRawValue raw value}.

      • - *
      • 'all' : - *

        {@link #doQuery run the query} specified by the {@link #allQuery} config option

      • - *
      - *

      See also {@link #queryParam}.

      - */ - triggerAction : 'query', - /** - * @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and - * {@link #typeAhead} activate (defaults to 4 if {@link #mode} = 'remote' or 0 if - * {@link #mode} = 'local', does not apply if - * {@link Ext.form.TriggerField#editable editable} = false). - */ - minChars : 4, - /** - * @cfg {Boolean} autoSelect true to select the first result gathered by the data store (defaults - * to true). A false value would require a manual selection from the dropdown list to set the components value - * unless the value of ({@link #typeAheadDelay}) were true. - */ - autoSelect : true, - /** - * @cfg {Boolean} typeAhead true to populate and autoselect the remainder of the text being - * typed after a configurable delay ({@link #typeAheadDelay}) if it matches a known value (defaults - * to false) - */ - typeAhead : false, - /** - * @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and - * sending the query to filter the dropdown list (defaults to 500 if {@link #mode} = 'remote' - * or 10 if {@link #mode} = 'local') - */ - queryDelay : 500, - /** - * @cfg {Number} pageSize If greater than 0, a {@link Ext.PagingToolbar} is displayed in the - * footer of the dropdown list and the {@link #doQuery filter queries} will execute with page start and - * {@link Ext.PagingToolbar#pageSize limit} parameters. Only applies when {@link #mode} = 'remote' - * (defaults to 0). - */ - pageSize : 0, - /** - * @cfg {Boolean} selectOnFocus true to select any existing text in the field immediately on focus. - * Only applies when {@link Ext.form.TriggerField#editable editable} = true (defaults to - * false). - */ - selectOnFocus : false, - /** - * @cfg {String} queryParam Name of the query ({@link Ext.data.Store#baseParam baseParam} name for the store) - * as it will be passed on the querystring (defaults to 'query') - */ - queryParam : 'query', - /** - * @cfg {String} loadingText The text to display in the dropdown list while data is loading. Only applies - * when {@link #mode} = 'remote' (defaults to 'Loading...') - */ - loadingText : 'Loading...', - /** - * @cfg {Boolean} resizable true to add a resize handle to the bottom of the dropdown list - * (creates an {@link Ext.Resizable} with 'se' {@link Ext.Resizable#pinned pinned} handles). - * Defaults to false. - */ - resizable : false, - /** - * @cfg {Number} handleHeight The height in pixels of the dropdown list resize handle if - * {@link #resizable} = true (defaults to 8) - */ - handleHeight : 8, - /** - * @cfg {String} allQuery The text query to send to the server to return all records for the list - * with no filtering (defaults to '') - */ - allQuery: '', - /** - * @cfg {String} mode Acceptable values are: - *
        - *
      • 'remote' : Default - *

        Automatically loads the {@link #store} the first time the trigger - * is clicked. If you do not want the store to be automatically loaded the first time the trigger is - * clicked, set to 'local' and manually load the store. To force a requery of the store - * every time the trigger is clicked see {@link #lastQuery}.

      • - *
      • 'local' : - *

        ComboBox loads local data

        - *
        
        -var combo = new Ext.form.ComboBox({
        -    renderTo: document.body,
        -    mode: 'local',
        -    store: new Ext.data.ArrayStore({
        -        id: 0,
        -        fields: [
        -            'myId',  // numeric value is the key
        -            'displayText'
        -        ],
        -        data: [[1, 'item1'], [2, 'item2']]  // data is local
        -    }),
        -    valueField: 'myId',
        -    displayField: 'displayText',
        -    triggerAction: 'all'
        -});
        -     * 
      • - *
      - */ - mode: 'remote', - /** - * @cfg {Number} minListWidth The minimum width of the dropdown list in pixels (defaults to 70, will - * be ignored if {@link #listWidth} has a higher value) - */ - minListWidth : 70, - /** - * @cfg {Boolean} forceSelection true to restrict the selected value to one of the values in the list, - * false to allow the user to set arbitrary text into the field (defaults to false) - */ - forceSelection : false, - /** - * @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed - * if {@link #typeAhead} = true (defaults to 250) - */ - typeAheadDelay : 250, - /** - * @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in - * the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this - * default text is used, it means there is no value set and no validation will occur on this field. - */ - - /** - * @cfg {Boolean} lazyInit true to not initialize the list for this combo until the field is focused - * (defaults to true) - */ - lazyInit : true, - - /** - * @cfg {Boolean} clearFilterOnReset true to clear any filters on the store (when in local mode) when reset is called - * (defaults to true) - */ - clearFilterOnReset : true, - - /** - * @cfg {Boolean} submitValue False to clear the name attribute on the field so that it is not submitted during a form post. - * If a hiddenName is specified, setting this to true will cause both the hidden field and the element to be submitted. - * Defaults to undefined. - */ - submitValue: undefined, - - /** - * The value of the match string used to filter the store. Delete this property to force a requery. - * Example use: - *
      
      -var combo = new Ext.form.ComboBox({
      -    ...
      -    mode: 'remote',
      -    ...
      -    listeners: {
      -        // delete the previous query in the beforequery event or set
      -        // combo.lastQuery = null (this will reload the store the next time it expands)
      -        beforequery: function(qe){
      -            delete qe.combo.lastQuery;
      -        }
      -    }
      -});
      -     * 
      - * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used - * configure the combo with lastQuery=''. Example use: - *
      
      -var combo = new Ext.form.ComboBox({
      -    ...
      -    mode: 'local',
      -    triggerAction: 'all',
      -    lastQuery: ''
      -});
      -     * 
      - * @property lastQuery - * @type String - */ - - // private - initComponent : function(){ - Ext.form.ComboBox.superclass.initComponent.call(this); - this.addEvents( - /** - * @event expand - * Fires when the dropdown list is expanded - * @param {Ext.form.ComboBox} combo This combo box - */ - 'expand', - /** - * @event collapse - * Fires when the dropdown list is collapsed - * @param {Ext.form.ComboBox} combo This combo box - */ - 'collapse', - - /** - * @event beforeselect - * Fires before a list item is selected. Return false to cancel the selection. - * @param {Ext.form.ComboBox} combo This combo box - * @param {Ext.data.Record} record The data record returned from the underlying store - * @param {Number} index The index of the selected item in the dropdown list - */ - 'beforeselect', - /** - * @event select - * Fires when a list item is selected - * @param {Ext.form.ComboBox} combo This combo box - * @param {Ext.data.Record} record The data record returned from the underlying store - * @param {Number} index The index of the selected item in the dropdown list - */ - 'select', - /** - * @event beforequery - * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's - * cancel property to true. - * @param {Object} queryEvent An object that has these properties:
        - *
      • combo : Ext.form.ComboBox
        This combo box
      • - *
      • query : String
        The query
      • - *
      • forceAll : Boolean
        True to force "all" query
      • - *
      • cancel : Boolean
        Set to true to cancel the query
      • - *
      - */ - 'beforequery' - ); - if(this.transform){ - var s = Ext.getDom(this.transform); - if(!this.hiddenName){ - this.hiddenName = s.name; - } - if(!this.store){ - this.mode = 'local'; - var d = [], opts = s.options; - for(var i = 0, len = opts.length;i < len; i++){ - var o = opts[i], - value = (o.hasAttribute ? o.hasAttribute('value') : o.getAttributeNode('value').specified) ? o.value : o.text; - if(o.selected && Ext.isEmpty(this.value, true)) { - this.value = value; - } - d.push([value, o.text]); - } - this.store = new Ext.data.ArrayStore({ - idIndex: 0, - fields: ['value', 'text'], - data : d, - autoDestroy: true - }); - this.valueField = 'value'; - this.displayField = 'text'; - } - s.name = Ext.id(); // wipe out the name in case somewhere else they have a reference - if(!this.lazyRender){ - this.target = true; - this.el = Ext.DomHelper.insertBefore(s, this.autoCreate || this.defaultAutoCreate); - this.render(this.el.parentNode, s); - } - Ext.removeNode(s); - } - //auto-configure store from local array data - else if(this.store){ - this.store = Ext.StoreMgr.lookup(this.store); - if(this.store.autoCreated){ - this.displayField = this.valueField = 'field1'; - if(!this.store.expandData){ - this.displayField = 'field2'; - } - this.mode = 'local'; - } - } - - this.selectedIndex = -1; - if(this.mode == 'local'){ - if(!Ext.isDefined(this.initialConfig.queryDelay)){ - this.queryDelay = 10; - } - if(!Ext.isDefined(this.initialConfig.minChars)){ - this.minChars = 0; - } - } - }, - - // private - onRender : function(ct, position){ - if(this.hiddenName && !Ext.isDefined(this.submitValue)){ - this.submitValue = false; - } - Ext.form.ComboBox.superclass.onRender.call(this, ct, position); - if(this.hiddenName){ - this.hiddenField = this.el.insertSibling({tag:'input', type:'hidden', name: this.hiddenName, - id: (this.hiddenId || Ext.id())}, 'before', true); - - } - if(Ext.isGecko){ - this.el.dom.setAttribute('autocomplete', 'off'); - } - - if(!this.lazyInit){ - this.initList(); - }else{ - this.on('focus', this.initList, this, {single: true}); - } - }, - - // private - initValue : function(){ - Ext.form.ComboBox.superclass.initValue.call(this); - if(this.hiddenField){ - this.hiddenField.value = - Ext.value(Ext.isDefined(this.hiddenValue) ? this.hiddenValue : this.value, ''); - } - }, - - getParentZIndex : function(){ - var zindex; - if (this.ownerCt){ - this.findParentBy(function(ct){ - zindex = parseInt(ct.getPositionEl().getStyle('z-index'), 10); - return !!zindex; - }); - } - return zindex; - }, - - getZIndex : function(listParent){ - listParent = listParent || Ext.getDom(this.getListParent() || Ext.getBody()); - var zindex = parseInt(Ext.fly(listParent).getStyle('z-index'), 10); - if(!zindex){ - zindex = this.getParentZIndex(); - } - return (zindex || 12000) + 5; - }, - - // private - initList : function(){ - if(!this.list){ - var cls = 'x-combo-list', - listParent = Ext.getDom(this.getListParent() || Ext.getBody()); - - this.list = new Ext.Layer({ - parentEl: listParent, - shadow: this.shadow, - cls: [cls, this.listClass].join(' '), - constrain:false, - zindex: this.getZIndex(listParent) - }); - - var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth); - this.list.setSize(lw, 0); - this.list.swallowEvent('mousewheel'); - this.assetHeight = 0; - if(this.syncFont !== false){ - this.list.setStyle('font-size', this.el.getStyle('font-size')); - } - if(this.title){ - this.header = this.list.createChild({cls:cls+'-hd', html: this.title}); - this.assetHeight += this.header.getHeight(); - } - - this.innerList = this.list.createChild({cls:cls+'-inner'}); - this.mon(this.innerList, 'mouseover', this.onViewOver, this); - this.mon(this.innerList, 'mousemove', this.onViewMove, this); - this.innerList.setWidth(lw - this.list.getFrameWidth('lr')); - - if(this.pageSize){ - this.footer = this.list.createChild({cls:cls+'-ft'}); - this.pageTb = new Ext.PagingToolbar({ - store: this.store, - pageSize: this.pageSize, - renderTo:this.footer - }); - this.assetHeight += this.footer.getHeight(); - } - - if(!this.tpl){ - /** - * @cfg {String/Ext.XTemplate} tpl

      The template string, or {@link Ext.XTemplate} instance to - * use to display each item in the dropdown list. The dropdown list is displayed in a - * DataView. See {@link #view}.

      - *

      The default template string is:

      
      -                  '<tpl for="."><div class="x-combo-list-item">{' + this.displayField + '}</div></tpl>'
      -                * 
      - *

      Override the default value to create custom UI layouts for items in the list. - * For example:

      
      -                  '<tpl for="."><div ext:qtip="{state}. {nick}" class="x-combo-list-item">{state}</div></tpl>'
      -                * 
      - *

      The template must contain one or more substitution parameters using field - * names from the Combo's {@link #store Store}. In the example above an - *

      ext:qtip
      attribute is added to display other fields from the Store.

      - *

      To preserve the default visual look of list items, add the CSS class name - *

      x-combo-list-item
      to the template's container element.

      - *

      Also see {@link #itemSelector} for additional details.

      - */ - this.tpl = '
      {' + this.displayField + '}
      '; - /** - * @cfg {String} itemSelector - *

      A simple CSS selector (e.g. div.some-class or span:first-child) that will be - * used to determine what nodes the {@link #view Ext.DataView} which handles the dropdown - * display will be working with.

      - *

      Note: this setting is required if a custom XTemplate has been - * specified in {@link #tpl} which assigns a class other than

      'x-combo-list-item'
      - * to dropdown list items - */ - } - - /** - * The {@link Ext.DataView DataView} used to display the ComboBox's options. - * @type Ext.DataView - */ - this.view = new Ext.DataView({ - applyTo: this.innerList, - tpl: this.tpl, - singleSelect: true, - selectedClass: this.selectedClass, - itemSelector: this.itemSelector || '.' + cls + '-item', - emptyText: this.listEmptyText, - deferEmptyText: false - }); - - this.mon(this.view, { - containerclick : this.onViewClick, - click : this.onViewClick, - scope :this - }); - - this.bindStore(this.store, true); - - if(this.resizable){ - this.resizer = new Ext.Resizable(this.list, { - pinned:true, handles:'se' - }); - this.mon(this.resizer, 'resize', function(r, w, h){ - this.maxHeight = h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight; - this.listWidth = w; - this.innerList.setWidth(w - this.list.getFrameWidth('lr')); - this.restrictHeight(); - }, this); - - this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom', this.handleHeight+'px'); - } - } - }, - - /** - *

      Returns the element used to house this ComboBox's pop-up list. Defaults to the document body.

      - * A custom implementation may be provided as a configuration option if the floating list needs to be rendered - * to a different Element. An example might be rendering the list inside a Menu so that clicking - * the list does not hide the Menu:
      
      -var store = new Ext.data.ArrayStore({
      -    autoDestroy: true,
      -    fields: ['initials', 'fullname'],
      -    data : [
      -        ['FF', 'Fred Flintstone'],
      -        ['BR', 'Barney Rubble']
      -    ]
      -});
      -
      -var combo = new Ext.form.ComboBox({
      -    store: store,
      -    displayField: 'fullname',
      -    emptyText: 'Select a name...',
      -    forceSelection: true,
      -    getListParent: function() {
      -        return this.el.up('.x-menu');
      -    },
      -    iconCls: 'no-icon', //use iconCls if placing within menu to shift to right side of menu
      -    mode: 'local',
      -    selectOnFocus: true,
      -    triggerAction: 'all',
      -    typeAhead: true,
      -    width: 135
      -});
      -
      -var menu = new Ext.menu.Menu({
      -    id: 'mainMenu',
      -    items: [
      -        combo // A Field in a Menu
      -    ]
      -});
      -
      - */ - getListParent : function() { - return document.body; - }, - - /** - * Returns the store associated with this combo. - * @return {Ext.data.Store} The store - */ - getStore : function(){ - return this.store; - }, - - // private - bindStore : function(store, initial){ - if(this.store && !initial){ - if(this.store !== store && this.store.autoDestroy){ - this.store.destroy(); - }else{ - this.store.un('beforeload', this.onBeforeLoad, this); - this.store.un('load', this.onLoad, this); - this.store.un('exception', this.collapse, this); - } - if(!store){ - this.store = null; - if(this.view){ - this.view.bindStore(null); - } - if(this.pageTb){ - this.pageTb.bindStore(null); - } - } - } - if(store){ - if(!initial) { - this.lastQuery = null; - if(this.pageTb) { - this.pageTb.bindStore(store); - } - } - - this.store = Ext.StoreMgr.lookup(store); - this.store.on({ - scope: this, - beforeload: this.onBeforeLoad, - load: this.onLoad, - exception: this.collapse - }); - - if(this.view){ - this.view.bindStore(store); - } - } - }, - - reset : function(){ - if(this.clearFilterOnReset && this.mode == 'local'){ - this.store.clearFilter(); - } - Ext.form.ComboBox.superclass.reset.call(this); - }, - - // private - initEvents : function(){ - Ext.form.ComboBox.superclass.initEvents.call(this); - - /** - * @property keyNav - * @type Ext.KeyNav - *

      A {@link Ext.KeyNav KeyNav} object which handles navigation keys for this ComboBox. This performs actions - * based on keystrokes typed when the input field is focused.

      - *

      After the ComboBox has been rendered, you may override existing navigation key functionality, - * or add your own based upon key names as specified in the {@link Ext.KeyNav KeyNav} class.

      - *

      The function is executed in the scope (this reference of the ComboBox. Example:

      
      -myCombo.keyNav.esc = function(e) {  // Override ESC handling function
      -    this.collapse();                // Standard behaviour of Ext's ComboBox.
      -    this.setValue(this.startValue); // We reset to starting value on ESC
      -};
      -myCombo.keyNav.tab = function() {   // Override TAB handling function
      -    this.onViewClick(false);        // Select the currently highlighted row
      -};
      -
      - */ - this.keyNav = new Ext.KeyNav(this.el, { - "up" : function(e){ - this.inKeyMode = true; - this.selectPrev(); - }, - - "down" : function(e){ - if(!this.isExpanded()){ - this.onTriggerClick(); - }else{ - this.inKeyMode = true; - this.selectNext(); - } - }, - - "enter" : function(e){ - this.onViewClick(); - }, - - "esc" : function(e){ - this.collapse(); - }, - - "tab" : function(e){ - if (this.forceSelection === true) { - this.collapse(); - } else { - this.onViewClick(false); - } - return true; - }, - - scope : this, - - doRelay : function(e, h, hname){ - if(hname == 'down' || this.scope.isExpanded()){ - // this MUST be called before ComboBox#fireKey() - var relay = Ext.KeyNav.prototype.doRelay.apply(this, arguments); - if(!Ext.isIE && Ext.EventManager.useKeydown){ - // call Combo#fireKey() for browsers which use keydown event (except IE) - this.scope.fireKey(e); - } - return relay; - } - return true; - }, - - forceKeyDown : true, - defaultEventAction: 'stopEvent' - }); - this.queryDelay = Math.max(this.queryDelay || 10, - this.mode == 'local' ? 10 : 250); - this.dqTask = new Ext.util.DelayedTask(this.initQuery, this); - if(this.typeAhead){ - this.taTask = new Ext.util.DelayedTask(this.onTypeAhead, this); - } - if(!this.enableKeyEvents){ - this.mon(this.el, 'keyup', this.onKeyUp, this); - } - }, - - - // private - onDestroy : function(){ - if (this.dqTask){ - this.dqTask.cancel(); - this.dqTask = null; - } - this.bindStore(null); - Ext.destroy( - this.resizer, - this.view, - this.pageTb, - this.list - ); - Ext.destroyMembers(this, 'hiddenField'); - Ext.form.ComboBox.superclass.onDestroy.call(this); - }, - - // private - fireKey : function(e){ - if (!this.isExpanded()) { - Ext.form.ComboBox.superclass.fireKey.call(this, e); - } - }, - - // private - onResize : function(w, h){ - Ext.form.ComboBox.superclass.onResize.apply(this, arguments); - if(!isNaN(w) && this.isVisible() && this.list){ - this.doResize(w); - }else{ - this.bufferSize = w; - } - }, - - doResize: function(w){ - if(!Ext.isDefined(this.listWidth)){ - var lw = Math.max(w, this.minListWidth); - this.list.setWidth(lw); - this.innerList.setWidth(lw - this.list.getFrameWidth('lr')); - } - }, - - // private - onEnable : function(){ - Ext.form.ComboBox.superclass.onEnable.apply(this, arguments); - if(this.hiddenField){ - this.hiddenField.disabled = false; - } - }, - - // private - onDisable : function(){ - Ext.form.ComboBox.superclass.onDisable.apply(this, arguments); - if(this.hiddenField){ - this.hiddenField.disabled = true; - } - }, - - // private - onBeforeLoad : function(){ - if(!this.hasFocus){ - return; - } - this.innerList.update(this.loadingText ? - '
      '+this.loadingText+'
      ' : ''); - this.restrictHeight(); - this.selectedIndex = -1; - }, - - // private - onLoad : function(){ - if(!this.hasFocus){ - return; - } - if(this.store.getCount() > 0 || this.listEmptyText){ - this.expand(); - this.restrictHeight(); - if(this.lastQuery == this.allQuery){ - if(this.editable){ - this.el.dom.select(); - } - - if(this.autoSelect !== false && !this.selectByValue(this.value, true)){ - this.select(0, true); - } - }else{ - if(this.autoSelect !== false){ - this.selectNext(); - } - if(this.typeAhead && this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE){ - this.taTask.delay(this.typeAheadDelay); - } - } - }else{ - this.collapse(); - } - - }, - - // private - onTypeAhead : function(){ - if(this.store.getCount() > 0){ - var r = this.store.getAt(0); - var newValue = r.data[this.displayField]; - var len = newValue.length; - var selStart = this.getRawValue().length; - if(selStart != len){ - this.setRawValue(newValue); - this.selectText(selStart, newValue.length); - } - } - }, - - // private - assertValue : function(){ - var val = this.getRawValue(), - rec; - - if(this.valueField && Ext.isDefined(this.value)){ - rec = this.findRecord(this.valueField, this.value); - } - if(!rec || rec.get(this.displayField) != val){ - rec = this.findRecord(this.displayField, val); - } - if(!rec && this.forceSelection){ - if(val.length > 0 && val != this.emptyText){ - this.el.dom.value = Ext.value(this.lastSelectionText, ''); - this.applyEmptyText(); - }else{ - this.clearValue(); - } - }else{ - if(rec && this.valueField){ - // onSelect may have already set the value and by doing so - // set the display field properly. Let's not wipe out the - // valueField here by just sending the displayField. - if (this.value == val){ - return; - } - val = rec.get(this.valueField || this.displayField); - } - this.setValue(val); - } - }, - - // private - onSelect : function(record, index){ - if(this.fireEvent('beforeselect', this, record, index) !== false){ - this.setValue(record.data[this.valueField || this.displayField]); - this.collapse(); - this.fireEvent('select', this, record, index); - } - }, - - // inherit docs - getName: function(){ - var hf = this.hiddenField; - return hf && hf.name ? hf.name : this.hiddenName || Ext.form.ComboBox.superclass.getName.call(this); - }, - - /** - * Returns the currently selected field value or empty string if no value is set. - * @return {String} value The selected value - */ - getValue : function(){ - if(this.valueField){ - return Ext.isDefined(this.value) ? this.value : ''; - }else{ - return Ext.form.ComboBox.superclass.getValue.call(this); - } - }, - - /** - * Clears any text/value currently set in the field - */ - clearValue : function(){ - if(this.hiddenField){ - this.hiddenField.value = ''; - } - this.setRawValue(''); - this.lastSelectionText = ''; - this.applyEmptyText(); - this.value = ''; - }, - - /** - * Sets the specified value into the field. If the value finds a match, the corresponding record text - * will be displayed in the field. If the value does not match the data value of an existing item, - * and the valueNotFoundText config option is defined, it will be displayed as the default field text. - * Otherwise the field will be blank (although the value will still be set). - * @param {String} value The value to match - * @return {Ext.form.Field} this - */ - setValue : function(v){ - var text = v; - if(this.valueField){ - var r = this.findRecord(this.valueField, v); - if(r){ - text = r.data[this.displayField]; - }else if(Ext.isDefined(this.valueNotFoundText)){ - text = this.valueNotFoundText; - } - } - this.lastSelectionText = text; - if(this.hiddenField){ - this.hiddenField.value = Ext.value(v, ''); - } - Ext.form.ComboBox.superclass.setValue.call(this, text); - this.value = v; - return this; - }, - - // private - findRecord : function(prop, value){ - var record; - if(this.store.getCount() > 0){ - this.store.each(function(r){ - if(r.data[prop] == value){ - record = r; - return false; - } - }); - } - return record; - }, - - // private - onViewMove : function(e, t){ - this.inKeyMode = false; - }, - - // private - onViewOver : function(e, t){ - if(this.inKeyMode){ // prevent key nav and mouse over conflicts - return; - } - var item = this.view.findItemFromChild(t); - if(item){ - var index = this.view.indexOf(item); - this.select(index, false); - } - }, - - // private - onViewClick : function(doFocus){ - var index = this.view.getSelectedIndexes()[0], - s = this.store, - r = s.getAt(index); - if(r){ - this.onSelect(r, index); - }else { - this.collapse(); - } - if(doFocus !== false){ - this.el.focus(); - } - }, - - - // private - restrictHeight : function(){ - this.innerList.dom.style.height = ''; - var inner = this.innerList.dom, - pad = this.list.getFrameWidth('tb') + (this.resizable ? this.handleHeight : 0) + this.assetHeight, - h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight), - ha = this.getPosition()[1]-Ext.getBody().getScroll().top, - hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height, - space = Math.max(ha, hb, this.minHeight || 0)-this.list.shadowOffset-pad-5; - - h = Math.min(h, space, this.maxHeight); - - this.innerList.setHeight(h); - this.list.beginUpdate(); - this.list.setHeight(h+pad); - this.list.alignTo.apply(this.list, [this.el].concat(this.listAlign)); - this.list.endUpdate(); - }, - - /** - * Returns true if the dropdown list is expanded, else false. - */ - isExpanded : function(){ - return this.list && this.list.isVisible(); - }, - - /** - * Select an item in the dropdown list by its data value. This function does NOT cause the select event to fire. - * The store must be loaded and the list expanded for this function to work, otherwise use setValue. - * @param {String} value The data value of the item to select - * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the - * selected item if it is not currently in view (defaults to true) - * @return {Boolean} True if the value matched an item in the list, else false - */ - selectByValue : function(v, scrollIntoView){ - if(!Ext.isEmpty(v, true)){ - var r = this.findRecord(this.valueField || this.displayField, v); - if(r){ - this.select(this.store.indexOf(r), scrollIntoView); - return true; - } - } - return false; - }, - - /** - * Select an item in the dropdown list by its numeric index in the list. This function does NOT cause the select event to fire. - * The store must be loaded and the list expanded for this function to work, otherwise use setValue. - * @param {Number} index The zero-based index of the list item to select - * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the - * selected item if it is not currently in view (defaults to true) - */ - select : function(index, scrollIntoView){ - this.selectedIndex = index; - this.view.select(index); - if(scrollIntoView !== false){ - var el = this.view.getNode(index); - if(el){ - this.innerList.scrollChildIntoView(el, false); - } - } - - }, - - // private - selectNext : function(){ - var ct = this.store.getCount(); - if(ct > 0){ - if(this.selectedIndex == -1){ - this.select(0); - }else if(this.selectedIndex < ct-1){ - this.select(this.selectedIndex+1); - } - } - }, - - // private - selectPrev : function(){ - var ct = this.store.getCount(); - if(ct > 0){ - if(this.selectedIndex == -1){ - this.select(0); - }else if(this.selectedIndex !== 0){ - this.select(this.selectedIndex-1); - } - } - }, - - // private - onKeyUp : function(e){ - var k = e.getKey(); - if(this.editable !== false && this.readOnly !== true && (k == e.BACKSPACE || !e.isSpecialKey())){ - - this.lastKey = k; - this.dqTask.delay(this.queryDelay); - } - Ext.form.ComboBox.superclass.onKeyUp.call(this, e); - }, - - // private - validateBlur : function(){ - return !this.list || !this.list.isVisible(); - }, - - // private - initQuery : function(){ - this.doQuery(this.getRawValue()); - }, - - // private - beforeBlur : function(){ - this.assertValue(); - }, - - // private - postBlur : function(){ - Ext.form.ComboBox.superclass.postBlur.call(this); - this.collapse(); - this.inKeyMode = false; - }, - - /** - * Execute a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the - * query allowing the query action to be canceled if needed. - * @param {String} query The SQL query to execute - * @param {Boolean} forceAll true to force the query to execute even if there are currently fewer - * characters in the field than the minimum specified by the {@link #minChars} config option. It - * also clears any filter previously saved in the current store (defaults to false) - */ - doQuery : function(q, forceAll){ - q = Ext.isEmpty(q) ? '' : q; - var qe = { - query: q, - forceAll: forceAll, - combo: this, - cancel:false - }; - if(this.fireEvent('beforequery', qe)===false || qe.cancel){ - return false; - } - q = qe.query; - forceAll = qe.forceAll; - if(forceAll === true || (q.length >= this.minChars)){ - if(this.lastQuery !== q){ - this.lastQuery = q; - if(this.mode == 'local'){ - this.selectedIndex = -1; - if(forceAll){ - this.store.clearFilter(); - }else{ - this.store.filter(this.displayField, q); - } - this.onLoad(); - }else{ - this.store.baseParams[this.queryParam] = q; - this.store.load({ - params: this.getParams(q) - }); - this.expand(); - } - }else{ - this.selectedIndex = -1; - this.onLoad(); - } - } - }, - - // private - getParams : function(q){ - var params = {}, - paramNames = this.store.paramNames; - if(this.pageSize){ - params[paramNames.start] = 0; - params[paramNames.limit] = this.pageSize; - } - return params; - }, - - /** - * Hides the dropdown list if it is currently expanded. Fires the {@link #collapse} event on completion. - */ - collapse : function(){ - if(!this.isExpanded()){ - return; - } - this.list.hide(); - Ext.getDoc().un('mousewheel', this.collapseIf, this); - Ext.getDoc().un('mousedown', this.collapseIf, this); - this.fireEvent('collapse', this); - }, - - // private - collapseIf : function(e){ - if(!this.isDestroyed && !e.within(this.wrap) && !e.within(this.list)){ - this.collapse(); - } - }, - - /** - * Expands the dropdown list if it is currently hidden. Fires the {@link #expand} event on completion. - */ - expand : function(){ - if(this.isExpanded() || !this.hasFocus){ - return; - } - - if(this.title || this.pageSize){ - this.assetHeight = 0; - if(this.title){ - this.assetHeight += this.header.getHeight(); - } - if(this.pageSize){ - this.assetHeight += this.footer.getHeight(); - } - } - - if(this.bufferSize){ - this.doResize(this.bufferSize); - delete this.bufferSize; - } - this.list.alignTo.apply(this.list, [this.el].concat(this.listAlign)); - - // zindex can change, re-check it and set it if necessary - this.list.setZIndex(this.getZIndex()); - this.list.show(); - if(Ext.isGecko2){ - this.innerList.setOverflow('auto'); // necessary for FF 2.0/Mac - } - this.mon(Ext.getDoc(), { - scope: this, - mousewheel: this.collapseIf, - mousedown: this.collapseIf - }); - this.fireEvent('expand', this); - }, - - /** - * @method onTriggerClick - * @hide - */ - // private - // Implements the default empty TriggerField.onTriggerClick function - onTriggerClick : function(){ - if(this.readOnly || this.disabled){ - return; - } - if(this.isExpanded()){ - this.collapse(); - this.el.focus(); - }else { - this.onFocus({}); - if(this.triggerAction == 'all') { - this.doQuery(this.allQuery, true); - } else { - this.doQuery(this.getRawValue()); - } - this.el.focus(); - } - } - - /** - * @hide - * @method autoSize - */ - /** - * @cfg {Boolean} grow @hide - */ - /** - * @cfg {Number} growMin @hide - */ - /** - * @cfg {Number} growMax @hide - */ - -}); -Ext.reg('combo', Ext.form.ComboBox); -/** - * @class Ext.form.Checkbox - * @extends Ext.form.Field - * Single checkbox field. Can be used as a direct replacement for traditional checkbox fields. - * @constructor - * Creates a new Checkbox - * @param {Object} config Configuration options - * @xtype checkbox - */ -Ext.form.Checkbox = Ext.extend(Ext.form.Field, { - /** - * @cfg {String} focusClass The CSS class to use when the checkbox receives focus (defaults to undefined) - */ - focusClass : undefined, - /** - * @cfg {String} fieldClass The default CSS class for the checkbox (defaults to 'x-form-field') - */ - fieldClass : 'x-form-field', - /** - * @cfg {Boolean} checked true if the checkbox should render initially checked (defaults to false) - */ - checked : false, - /** - * @cfg {String} boxLabel The text that appears beside the checkbox - */ - boxLabel: ' ', - /** - * @cfg {String/Object} autoCreate A DomHelper element spec, or true for a default element spec (defaults to - * {tag: 'input', type: 'checkbox', autocomplete: 'off'}) - */ - defaultAutoCreate : { tag: 'input', type: 'checkbox', autocomplete: 'off'}, - /** - * @cfg {String} inputValue The value that should go into the generated input element's value attribute - */ - /** - * @cfg {Function} handler A function called when the {@link #checked} value changes (can be used instead of - * handling the check event). The handler is passed the following parameters: - *
        - *
      • checkbox : Ext.form.Checkbox
        The Checkbox being toggled.
      • - *
      • checked : Boolean
        The new checked state of the checkbox.
      • - *
      - */ - /** - * @cfg {Object} scope An object to use as the scope ('this' reference) of the {@link #handler} function - * (defaults to this Checkbox). - */ - - // private - actionMode : 'wrap', - - // private - initComponent : function(){ - Ext.form.Checkbox.superclass.initComponent.call(this); - this.addEvents( - /** - * @event check - * Fires when the checkbox is checked or unchecked. - * @param {Ext.form.Checkbox} this This checkbox - * @param {Boolean} checked The new checked value - */ - 'check' - ); - }, - - // private - onResize : function(){ - Ext.form.Checkbox.superclass.onResize.apply(this, arguments); - if(!this.boxLabel && !this.fieldLabel){ - this.el.alignTo(this.wrap, 'c-c'); - } - }, - - // private - initEvents : function(){ - Ext.form.Checkbox.superclass.initEvents.call(this); - this.mon(this.el, { - scope: this, - click: this.onClick, - change: this.onClick - }); - }, - - /** - * @hide - * Overridden and disabled. The editor element does not support standard valid/invalid marking. - * @method - */ - markInvalid : Ext.emptyFn, - /** - * @hide - * Overridden and disabled. The editor element does not support standard valid/invalid marking. - * @method - */ - clearInvalid : Ext.emptyFn, - - // private - onRender : function(ct, position){ - Ext.form.Checkbox.superclass.onRender.call(this, ct, position); - if(this.inputValue !== undefined){ - this.el.dom.value = this.inputValue; - } - this.wrap = this.el.wrap({cls: 'x-form-check-wrap'}); - if(this.boxLabel){ - this.wrap.createChild({tag: 'label', htmlFor: this.el.id, cls: 'x-form-cb-label', html: this.boxLabel}); - } - if(this.checked){ - this.setValue(true); - }else{ - this.checked = this.el.dom.checked; - } - // Need to repaint for IE, otherwise positioning is broken - if (Ext.isIE && !Ext.isStrict) { - this.wrap.repaint(); - } - this.resizeEl = this.positionEl = this.wrap; - }, - - // private - onDestroy : function(){ - Ext.destroy(this.wrap); - Ext.form.Checkbox.superclass.onDestroy.call(this); - }, - - // private - initValue : function() { - this.originalValue = this.getValue(); - }, - - /** - * Returns the checked state of the checkbox. - * @return {Boolean} True if checked, else false - */ - getValue : function(){ - if(this.rendered){ - return this.el.dom.checked; - } - return this.checked; - }, - - // private - onClick : function(){ - if(this.el.dom.checked != this.checked){ - this.setValue(this.el.dom.checked); - } - }, - - /** - * Sets the checked state of the checkbox, fires the 'check' event, and calls a - * {@link #handler} (if configured). - * @param {Boolean/String} checked The following values will check the checkbox: - * true, 'true', '1', or 'on'. Any other value will uncheck the checkbox. - * @return {Ext.form.Field} this - */ - setValue : function(v){ - var checked = this.checked, - inputVal = this.inputValue; - - if (v === false) { - this.checked = false; - } else { - this.checked = (v === true || v === 'true' || v == '1' || (inputVal ? v == inputVal : String(v).toLowerCase() == 'on')); - } - - if(this.rendered){ - this.el.dom.checked = this.checked; - this.el.dom.defaultChecked = this.checked; - } - if(checked != this.checked){ - this.fireEvent('check', this, this.checked); - if(this.handler){ - this.handler.call(this.scope || this, this, this.checked); - } - } - return this; - } -}); -Ext.reg('checkbox', Ext.form.Checkbox); -/** - * @class Ext.form.CheckboxGroup - * @extends Ext.form.Field - *

      A grouping container for {@link Ext.form.Checkbox} controls.

      - *

      Sample usage:

      - *
      
      -var myCheckboxGroup = new Ext.form.CheckboxGroup({
      -    id:'myGroup',
      -    xtype: 'checkboxgroup',
      -    fieldLabel: 'Single Column',
      -    itemCls: 'x-check-group-alt',
      -    // Put all controls in a single column with width 100%
      -    columns: 1,
      -    items: [
      -        {boxLabel: 'Item 1', name: 'cb-col-1'},
      -        {boxLabel: 'Item 2', name: 'cb-col-2', checked: true},
      -        {boxLabel: 'Item 3', name: 'cb-col-3'}
      -    ]
      -});
      - * 
      - * @constructor - * Creates a new CheckboxGroup - * @param {Object} config Configuration options - * @xtype checkboxgroup - */ -Ext.form.CheckboxGroup = Ext.extend(Ext.form.Field, { - /** - * @cfg {Array} items An Array of {@link Ext.form.Checkbox Checkbox}es or Checkbox config objects - * to arrange in the group. - */ - /** - * @cfg {String/Number/Array} columns Specifies the number of columns to use when displaying grouped - * checkbox/radio controls using automatic layout. This config can take several types of values: - *
      • 'auto' :

        The controls will be rendered one per column on one row and the width - * of each column will be evenly distributed based on the width of the overall field container. This is the default.

      • - *
      • Number :

        If you specific a number (e.g., 3) that number of columns will be - * created and the contained controls will be automatically distributed based on the value of {@link #vertical}.

      • - *
      • Array : Object

        You can also specify an array of column widths, mixing integer - * (fixed width) and float (percentage width) values as needed (e.g., [100, .25, .75]). Any integer values will - * be rendered first, then any float values will be calculated as a percentage of the remaining space. Float - * values do not have to add up to 1 (100%) although if you want the controls to take up the entire field - * container you should do so.

      - */ - columns : 'auto', - /** - * @cfg {Boolean} vertical True to distribute contained controls across columns, completely filling each column - * top to bottom before starting on the next column. The number of controls in each column will be automatically - * calculated to keep columns as even as possible. The default value is false, so that controls will be added - * to columns one at a time, completely filling each row left to right before starting on the next row. - */ - vertical : false, - /** - * @cfg {Boolean} allowBlank False to validate that at least one item in the group is checked (defaults to true). - * If no items are selected at validation time, {@link @blankText} will be used as the error text. - */ - allowBlank : true, - /** - * @cfg {String} blankText Error text to display if the {@link #allowBlank} validation fails (defaults to "You must - * select at least one item in this group") - */ - blankText : "You must select at least one item in this group", - - // private - defaultType : 'checkbox', - - // private - groupCls : 'x-form-check-group', - - // private - initComponent: function(){ - this.addEvents( - /** - * @event change - * Fires when the state of a child checkbox changes. - * @param {Ext.form.CheckboxGroup} this - * @param {Array} checked An array containing the checked boxes. - */ - 'change' - ); - this.on('change', this.validate, this); - Ext.form.CheckboxGroup.superclass.initComponent.call(this); - }, - - // private - onRender : function(ct, position){ - if(!this.el){ - var panelCfg = { - autoEl: { - id: this.id - }, - cls: this.groupCls, - layout: 'column', - renderTo: ct, - bufferResize: false // Default this to false, since it doesn't really have a proper ownerCt. - }; - var colCfg = { - xtype: 'container', - defaultType: this.defaultType, - layout: 'form', - defaults: { - hideLabel: true, - anchor: '100%' - } - }; - - if(this.items[0].items){ - - // The container has standard ColumnLayout configs, so pass them in directly - - Ext.apply(panelCfg, { - layoutConfig: {columns: this.items.length}, - defaults: this.defaults, - items: this.items - }); - for(var i=0, len=this.items.length; i0 && i%rows==0){ - ri++; - } - if(this.items[i].fieldLabel){ - this.items[i].hideLabel = false; - } - cols[ri].items.push(this.items[i]); - }; - }else{ - for(var i=0, len=this.items.length; i
      of each column will be adjusted - * to fit the grid width and prevent horizontal scrolling. If columns are later resized (manually - * or programmatically), the other columns in the grid will not be resized to fit the grid width. - * See {@link #forceFit} also. - */ - autoFill : false, - - /** - * @cfg {Boolean} forceFit - *

      Defaults to false. Specify true to have the column widths re-proportioned - * at all times.

      - *

      The {@link Ext.grid.Column#width initially configured width} of each - * column will be adjusted to fit the grid width and prevent horizontal scrolling. If columns are - * later resized (manually or programmatically), the other columns in the grid will be resized - * to fit the grid width.

      - *

      Columns which are configured with fixed: true are omitted from being resized.

      - *

      See {@link #autoFill}.

      - */ - forceFit : false, - - /** - * @cfg {Array} sortClasses The CSS classes applied to a header when it is sorted. (defaults to ['sort-asc', 'sort-desc']) - */ - sortClasses : ['sort-asc', 'sort-desc'], - - /** - * @cfg {String} sortAscText The text displayed in the 'Sort Ascending' menu item (defaults to 'Sort Ascending') - */ - sortAscText : 'Sort Ascending', - - /** - * @cfg {String} sortDescText The text displayed in the 'Sort Descending' menu item (defaults to 'Sort Descending') - */ - sortDescText : 'Sort Descending', - - /** - * @cfg {String} columnsText The text displayed in the 'Columns' menu item (defaults to 'Columns') - */ - columnsText : 'Columns', - - /** - * @cfg {String} selectedRowClass The CSS class applied to a selected row (defaults to 'x-grid3-row-selected'). An - * example overriding the default styling: -
      
      -    .x-grid3-row-selected {background-color: yellow;}
      -    
      - * Note that this only controls the row, and will not do anything for the text inside it. To style inner - * facets (like text) use something like: -
      
      -    .x-grid3-row-selected .x-grid3-cell-inner {
      -        color: #FFCC00;
      -    }
      -    
      - * @type String - */ - selectedRowClass : 'x-grid3-row-selected', - - // private - borderWidth : 2, - tdClass : 'x-grid3-cell', - hdCls : 'x-grid3-hd', - - - /** - * @cfg {Boolean} markDirty True to show the dirty cell indicator when a cell has been modified. Defaults to true. - */ - markDirty : true, - - /** - * @cfg {Number} cellSelectorDepth The number of levels to search for cells in event delegation (defaults to 4) - */ - cellSelectorDepth : 4, - - /** - * @cfg {Number} rowSelectorDepth The number of levels to search for rows in event delegation (defaults to 10) - */ - rowSelectorDepth : 10, - - /** - * @cfg {Number} rowBodySelectorDepth The number of levels to search for row bodies in event delegation (defaults to 10) - */ - rowBodySelectorDepth : 10, - - /** - * @cfg {String} cellSelector The selector used to find cells internally (defaults to 'td.x-grid3-cell') - */ - cellSelector : 'td.x-grid3-cell', - - /** - * @cfg {String} rowSelector The selector used to find rows internally (defaults to 'div.x-grid3-row') - */ - rowSelector : 'div.x-grid3-row', - - /** - * @cfg {String} rowBodySelector The selector used to find row bodies internally (defaults to 'div.x-grid3-row') - */ - rowBodySelector : 'div.x-grid3-row-body', - - // private - firstRowCls: 'x-grid3-row-first', - lastRowCls: 'x-grid3-row-last', - rowClsRe: /(?:^|\s+)x-grid3-row-(first|last|alt)(?:\s+|$)/g, - - /** - * @cfg {String} headerMenuOpenCls The CSS class to add to the header cell when its menu is visible. Defaults to 'x-grid3-hd-menu-open' - */ - headerMenuOpenCls: 'x-grid3-hd-menu-open', - - /** - * @cfg {String} rowOverCls The CSS class added to each row when it is hovered over. Defaults to 'x-grid3-row-over' - */ - rowOverCls: 'x-grid3-row-over', - - constructor : function(config) { - Ext.apply(this, config); - - // These events are only used internally by the grid components - this.addEvents( - /** - * @event beforerowremoved - * Internal UI Event. Fired before a row is removed. - * @param {Ext.grid.GridView} view - * @param {Number} rowIndex The index of the row to be removed. - * @param {Ext.data.Record} record The Record to be removed - */ - 'beforerowremoved', - - /** - * @event beforerowsinserted - * Internal UI Event. Fired before rows are inserted. - * @param {Ext.grid.GridView} view - * @param {Number} firstRow The index of the first row to be inserted. - * @param {Number} lastRow The index of the last row to be inserted. - */ - 'beforerowsinserted', - - /** - * @event beforerefresh - * Internal UI Event. Fired before the view is refreshed. - * @param {Ext.grid.GridView} view - */ - 'beforerefresh', - - /** - * @event rowremoved - * Internal UI Event. Fired after a row is removed. - * @param {Ext.grid.GridView} view - * @param {Number} rowIndex The index of the row that was removed. - * @param {Ext.data.Record} record The Record that was removed - */ - 'rowremoved', - - /** - * @event rowsinserted - * Internal UI Event. Fired after rows are inserted. - * @param {Ext.grid.GridView} view - * @param {Number} firstRow The index of the first inserted. - * @param {Number} lastRow The index of the last row inserted. - */ - 'rowsinserted', - - /** - * @event rowupdated - * Internal UI Event. Fired after a row has been updated. - * @param {Ext.grid.GridView} view - * @param {Number} firstRow The index of the row updated. - * @param {Ext.data.record} record The Record backing the row updated. - */ - 'rowupdated', - - /** - * @event refresh - * Internal UI Event. Fired after the GridView's body has been refreshed. - * @param {Ext.grid.GridView} view - */ - 'refresh' - ); - - Ext.grid.GridView.superclass.constructor.call(this); - }, - - /* -------------------------------- UI Specific ----------------------------- */ - - /** - * The master template to use when rendering the GridView. Has a default template - * @property Ext.Template - * @type masterTpl - */ - masterTpl: new Ext.Template( - '
      ', - '
      ', - '
      ', - '
      ', - '
      {header}
      ', - '
      ', - '
      ', - '
      ', - '
      ', - '
      {body}
      ', - '', - '
      ', - '
      ', - '
       
      ', - '
       
      ', - '
      ' - ), - - /** - * The template to use when rendering headers. Has a default template - * @property headerTpl - * @type Ext.Template - */ - headerTpl: new Ext.Template( - '', - '', - '{cells}', - '', - '
      ' - ), - - /** - * The template to use when rendering the body. Has a default template - * @property bodyTpl - * @type Ext.Template - */ - bodyTpl: new Ext.Template('{rows}'), - - /** - * The template to use to render each cell. Has a default template - * @property cellTpl - * @type Ext.Template - */ - cellTpl: new Ext.Template( - '', - '
      {value}
      ', - '' - ), - - /** - * @private - * Provides default templates if they are not given for this particular instance. Most of the templates are defined on - * the prototype, the ones defined inside this function are done so because they are based on Grid or GridView configuration - */ - initTemplates : function() { - var templates = this.templates || {}, - template, name, - - headerCellTpl = new Ext.Template( - '', - '
      ', - this.grid.enableHdMenu ? '' : '', - '{value}', - '', - '
      ', - '' - ), - - rowBodyText = [ - '', - '', - '
      {body}
      ', - '', - '' - ].join(""), - - innerText = [ - '', - '', - '{cells}', - this.enableRowBody ? rowBodyText : '', - '', - '
      ' - ].join(""); - - Ext.applyIf(templates, { - hcell : headerCellTpl, - cell : this.cellTpl, - body : this.bodyTpl, - header : this.headerTpl, - master : this.masterTpl, - row : new Ext.Template('
      ' + innerText + '
      '), - rowInner: new Ext.Template(innerText) - }); - - for (name in templates) { - template = templates[name]; - - if (template && Ext.isFunction(template.compile) && !template.compiled) { - template.disableFormats = true; - template.compile(); - } - } - - this.templates = templates; - this.colRe = new RegExp('x-grid3-td-([^\\s]+)', ''); - }, - - /** - * @private - * Each GridView has its own private flyweight, accessed through this method - */ - fly : function(el) { - if (!this._flyweight) { - this._flyweight = new Ext.Element.Flyweight(document.body); - } - this._flyweight.dom = el; - return this._flyweight; - }, - - // private - getEditorParent : function() { - return this.scroller.dom; - }, - - /** - * @private - * Finds and stores references to important elements - */ - initElements : function() { - var Element = Ext.Element, - el = Ext.get(this.grid.getGridEl().dom.firstChild), - mainWrap = new Element(el.child('div.x-grid3-viewport')), - mainHd = new Element(mainWrap.child('div.x-grid3-header')), - scroller = new Element(mainWrap.child('div.x-grid3-scroller')); - - if (this.grid.hideHeaders) { - mainHd.setDisplayed(false); - } - - if (this.forceFit) { - scroller.setStyle('overflow-x', 'hidden'); - } - - /** - * Read-only. The GridView's body Element which encapsulates all rows in the Grid. - * This {@link Ext.Element Element} is only available after the GridPanel has been rendered. - * @type Ext.Element - * @property mainBody - */ - - Ext.apply(this, { - el : el, - mainWrap: mainWrap, - scroller: scroller, - mainHd : mainHd, - innerHd : mainHd.child('div.x-grid3-header-inner').dom, - mainBody: new Element(Element.fly(scroller).child('div.x-grid3-body')), - focusEl : new Element(Element.fly(scroller).child('a')), - - resizeMarker: new Element(el.child('div.x-grid3-resize-marker')), - resizeProxy : new Element(el.child('div.x-grid3-resize-proxy')) - }); - - this.focusEl.swallowEvent('click', true); - }, - - // private - getRows : function() { - return this.hasRows() ? this.mainBody.dom.childNodes : []; - }, - - // finder methods, used with delegation - - // private - findCell : function(el) { - if (!el) { - return false; - } - return this.fly(el).findParent(this.cellSelector, this.cellSelectorDepth); - }, - - /** - *

      Return the index of the grid column which contains the passed HTMLElement.

      - * See also {@link #findRowIndex} - * @param {HTMLElement} el The target element - * @return {Number} The column index, or false if the target element is not within a row of this GridView. - */ - findCellIndex : function(el, requiredCls) { - var cell = this.findCell(el), - hasCls; - - if (cell) { - hasCls = this.fly(cell).hasClass(requiredCls); - if (!requiredCls || hasCls) { - return this.getCellIndex(cell); - } - } - return false; - }, - - // private - getCellIndex : function(el) { - if (el) { - var match = el.className.match(this.colRe); - - if (match && match[1]) { - return this.cm.getIndexById(match[1]); - } - } - return false; - }, - - // private - findHeaderCell : function(el) { - var cell = this.findCell(el); - return cell && this.fly(cell).hasClass(this.hdCls) ? cell : null; - }, - - // private - findHeaderIndex : function(el){ - return this.findCellIndex(el, this.hdCls); - }, - - /** - * Return the HtmlElement representing the grid row which contains the passed element. - * @param {HTMLElement} el The target HTMLElement - * @return {HTMLElement} The row element, or null if the target element is not within a row of this GridView. - */ - findRow : function(el) { - if (!el) { - return false; - } - return this.fly(el).findParent(this.rowSelector, this.rowSelectorDepth); - }, - - /** - * Return the index of the grid row which contains the passed HTMLElement. - * See also {@link #findCellIndex} - * @param {HTMLElement} el The target HTMLElement - * @return {Number} The row index, or false if the target element is not within a row of this GridView. - */ - findRowIndex : function(el) { - var row = this.findRow(el); - return row ? row.rowIndex : false; - }, - - /** - * Return the HtmlElement representing the grid row body which contains the passed element. - * @param {HTMLElement} el The target HTMLElement - * @return {HTMLElement} The row body element, or null if the target element is not within a row body of this GridView. - */ - findRowBody : function(el) { - if (!el) { - return false; - } - - return this.fly(el).findParent(this.rowBodySelector, this.rowBodySelectorDepth); - }, - - // getter methods for fetching elements dynamically in the grid - - /** - * Return the <div> HtmlElement which represents a Grid row for the specified index. - * @param {Number} index The row index - * @return {HtmlElement} The div element. - */ - getRow : function(row) { - return this.getRows()[row]; - }, - - /** - * Returns the grid's <td> HtmlElement at the specified coordinates. - * @param {Number} row The row index in which to find the cell. - * @param {Number} col The column index of the cell. - * @return {HtmlElement} The td at the specified coordinates. - */ - getCell : function(row, col) { - return Ext.fly(this.getRow(row)).query(this.cellSelector)[col]; - }, - - /** - * Return the <td> HtmlElement which represents the Grid's header cell for the specified column index. - * @param {Number} index The column index - * @return {HtmlElement} The td element. - */ - getHeaderCell : function(index) { - return this.mainHd.dom.getElementsByTagName('td')[index]; - }, - - // manipulating elements - - // private - use getRowClass to apply custom row classes - addRowClass : function(rowId, cls) { - var row = this.getRow(rowId); - if (row) { - this.fly(row).addClass(cls); - } - }, - - // private - removeRowClass : function(row, cls) { - var r = this.getRow(row); - if(r){ - this.fly(r).removeClass(cls); - } - }, - - // private - removeRow : function(row) { - Ext.removeNode(this.getRow(row)); - this.syncFocusEl(row); - }, - - // private - removeRows : function(firstRow, lastRow) { - var bd = this.mainBody.dom, - rowIndex; - - for (rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){ - Ext.removeNode(bd.childNodes[firstRow]); - } - - this.syncFocusEl(firstRow); - }, - - /* ----------------------------------- Scrolling functions -------------------------------------------*/ - - // private - getScrollState : function() { - var sb = this.scroller.dom; - - return { - left: sb.scrollLeft, - top : sb.scrollTop - }; - }, - - // private - restoreScroll : function(state) { - var sb = this.scroller.dom; - sb.scrollLeft = state.left; - sb.scrollTop = state.top; - }, - - /** - * Scrolls the grid to the top - */ - scrollToTop : function() { - var dom = this.scroller.dom; - - dom.scrollTop = 0; - dom.scrollLeft = 0; - }, - - // private - syncScroll : function() { - this.syncHeaderScroll(); - var mb = this.scroller.dom; - this.grid.fireEvent('bodyscroll', mb.scrollLeft, mb.scrollTop); - }, - - // private - syncHeaderScroll : function() { - var innerHd = this.innerHd, - scrollLeft = this.scroller.dom.scrollLeft; - - innerHd.scrollLeft = scrollLeft; - innerHd.scrollLeft = scrollLeft; // second time for IE (1/2 time first fails, other browsers ignore) - }, - - /** - * @private - * Ensures the given column has the given icon class - */ - updateSortIcon : function(col, dir) { - var sortClasses = this.sortClasses, - sortClass = sortClasses[dir == "DESC" ? 1 : 0], - headers = this.mainHd.select('td').removeClass(sortClasses); - - headers.item(col).addClass(sortClass); - }, - - /** - * @private - * Updates the size of every column and cell in the grid - */ - updateAllColumnWidths : function() { - var totalWidth = this.getTotalWidth(), - colCount = this.cm.getColumnCount(), - rows = this.getRows(), - rowCount = rows.length, - widths = [], - row, rowFirstChild, trow, i, j; - - for (i = 0; i < colCount; i++) { - widths[i] = this.getColumnWidth(i); - this.getHeaderCell(i).style.width = widths[i]; - } - - this.updateHeaderWidth(); - - for (i = 0; i < rowCount; i++) { - row = rows[i]; - row.style.width = totalWidth; - rowFirstChild = row.firstChild; - - if (rowFirstChild) { - rowFirstChild.style.width = totalWidth; - trow = rowFirstChild.rows[0]; - - for (j = 0; j < colCount; j++) { - trow.childNodes[j].style.width = widths[j]; - } - } - } - - this.onAllColumnWidthsUpdated(widths, totalWidth); - }, - - /** - * @private - * Called after a column's width has been updated, this resizes all of the cells for that column in each row - * @param {Number} column The column index - */ - updateColumnWidth : function(column, width) { - var columnWidth = this.getColumnWidth(column), - totalWidth = this.getTotalWidth(), - headerCell = this.getHeaderCell(column), - nodes = this.getRows(), - nodeCount = nodes.length, - row, i, firstChild; - - this.updateHeaderWidth(); - headerCell.style.width = columnWidth; - - for (i = 0; i < nodeCount; i++) { - row = nodes[i]; - firstChild = row.firstChild; - - row.style.width = totalWidth; - if (firstChild) { - firstChild.style.width = totalWidth; - firstChild.rows[0].childNodes[column].style.width = columnWidth; - } - } - - this.onColumnWidthUpdated(column, columnWidth, totalWidth); - }, - - /** - * @private - * Sets the hidden status of a given column. - * @param {Number} col The column index - * @param {Boolean} hidden True to make the column hidden - */ - updateColumnHidden : function(col, hidden) { - var totalWidth = this.getTotalWidth(), - display = hidden ? 'none' : '', - headerCell = this.getHeaderCell(col), - nodes = this.getRows(), - nodeCount = nodes.length, - row, rowFirstChild, i; - - this.updateHeaderWidth(); - headerCell.style.display = display; - - for (i = 0; i < nodeCount; i++) { - row = nodes[i]; - row.style.width = totalWidth; - rowFirstChild = row.firstChild; - - if (rowFirstChild) { - rowFirstChild.style.width = totalWidth; - rowFirstChild.rows[0].childNodes[col].style.display = display; - } - } - - this.onColumnHiddenUpdated(col, hidden, totalWidth); - delete this.lastViewWidth; //recalc - this.layout(); - }, - - /** - * @private - * Renders all of the rows to a string buffer and returns the string. This is called internally - * by renderRows and performs the actual string building for the rows - it does not inject HTML into the DOM. - * @param {Array} columns The column data acquired from getColumnData. - * @param {Array} records The array of records to render - * @param {Ext.data.Store} store The store to render the rows from - * @param {Number} startRow The index of the first row being rendered. Sometimes we only render a subset of - * the rows so this is used to maintain logic for striping etc - * @param {Number} colCount The total number of columns in the column model - * @param {Boolean} stripe True to stripe the rows - * @return {String} A string containing the HTML for the rendered rows - */ - doRender : function(columns, records, store, startRow, colCount, stripe) { - var templates = this.templates, - cellTemplate = templates.cell, - rowTemplate = templates.row, - last = colCount - 1, - tstyle = 'width:' + this.getTotalWidth() + ';', - // buffers - rowBuffer = [], - colBuffer = [], - rowParams = {tstyle: tstyle}, - meta = {}, - len = records.length, - alt, - column, - record, i, j, rowIndex; - - //build up each row's HTML - for (j = 0; j < len; j++) { - record = records[j]; - colBuffer = []; - - rowIndex = j + startRow; - - //build up each column's HTML - for (i = 0; i < colCount; i++) { - column = columns[i]; - - meta.id = column.id; - meta.css = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : ''); - meta.attr = meta.cellAttr = ''; - meta.style = column.style; - meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store); - - if (Ext.isEmpty(meta.value)) { - meta.value = ' '; - } - - if (this.markDirty && record.dirty && typeof record.modified[column.name] != 'undefined') { - meta.css += ' x-grid3-dirty-cell'; - } - - colBuffer[colBuffer.length] = cellTemplate.apply(meta); - } - - alt = []; - //set up row striping and row dirtiness CSS classes - if (stripe && ((rowIndex + 1) % 2 === 0)) { - alt[0] = 'x-grid3-row-alt'; - } - - if (record.dirty) { - alt[1] = ' x-grid3-dirty-row'; - } - - rowParams.cols = colCount; - - if (this.getRowClass) { - alt[2] = this.getRowClass(record, rowIndex, rowParams, store); - } - - rowParams.alt = alt.join(' '); - rowParams.cells = colBuffer.join(''); - - rowBuffer[rowBuffer.length] = rowTemplate.apply(rowParams); - } - - return rowBuffer.join(''); - }, - - /** - * @private - * Adds CSS classes and rowIndex to each row - * @param {Number} startRow The row to start from (defaults to 0) - */ - processRows : function(startRow, skipStripe) { - if (!this.ds || this.ds.getCount() < 1) { - return; - } - - var rows = this.getRows(), - length = rows.length, - row, i; - - skipStripe = skipStripe || !this.grid.stripeRows; - startRow = startRow || 0; - - for (i = 0; i < length; i++) { - row = rows[i]; - if (row) { - row.rowIndex = i; - if (!skipStripe) { - row.className = row.className.replace(this.rowClsRe, ' '); - if ((i + 1) % 2 === 0){ - row.className += ' x-grid3-row-alt'; - } - } - } - } - - // add first/last-row classes - if (startRow === 0) { - Ext.fly(rows[0]).addClass(this.firstRowCls); - } - - Ext.fly(rows[length - 1]).addClass(this.lastRowCls); - }, - - /** - * @private - */ - afterRender : function() { - if (!this.ds || !this.cm) { - return; - } - - this.mainBody.dom.innerHTML = this.renderBody() || ' '; - this.processRows(0, true); - - if (this.deferEmptyText !== true) { - this.applyEmptyText(); - } - - this.grid.fireEvent('viewready', this.grid); - }, - - /** - * @private - * This is always intended to be called after renderUI. Sets up listeners on the UI elements - * and sets up options like column menus, moving and resizing. - */ - afterRenderUI: function() { - var grid = this.grid; - - this.initElements(); - - // get mousedowns early - Ext.fly(this.innerHd).on('click', this.handleHdDown, this); - - this.mainHd.on({ - scope : this, - mouseover: this.handleHdOver, - mouseout : this.handleHdOut, - mousemove: this.handleHdMove - }); - - this.scroller.on('scroll', this.syncScroll, this); - - if (grid.enableColumnResize !== false) { - this.splitZone = new Ext.grid.GridView.SplitDragZone(grid, this.mainHd.dom); - } - - if (grid.enableColumnMove) { - this.columnDrag = new Ext.grid.GridView.ColumnDragZone(grid, this.innerHd); - this.columnDrop = new Ext.grid.HeaderDropZone(grid, this.mainHd.dom); - } - - if (grid.enableHdMenu !== false) { - this.hmenu = new Ext.menu.Menu({id: grid.id + '-hctx'}); - this.hmenu.add( - {itemId:'asc', text: this.sortAscText, cls: 'xg-hmenu-sort-asc'}, - {itemId:'desc', text: this.sortDescText, cls: 'xg-hmenu-sort-desc'} - ); - - if (grid.enableColumnHide !== false) { - this.colMenu = new Ext.menu.Menu({id:grid.id + '-hcols-menu'}); - this.colMenu.on({ - scope : this, - beforeshow: this.beforeColMenuShow, - itemclick : this.handleHdMenuClick - }); - this.hmenu.add('-', { - itemId:'columns', - hideOnClick: false, - text: this.columnsText, - menu: this.colMenu, - iconCls: 'x-cols-icon' - }); - } - - this.hmenu.on('itemclick', this.handleHdMenuClick, this); - } - - if (grid.trackMouseOver) { - this.mainBody.on({ - scope : this, - mouseover: this.onRowOver, - mouseout : this.onRowOut - }); - } - - if (grid.enableDragDrop || grid.enableDrag) { - this.dragZone = new Ext.grid.GridDragZone(grid, { - ddGroup : grid.ddGroup || 'GridDD' - }); - } - - this.updateHeaderSortState(); - }, - - /** - * @private - * Renders each of the UI elements in turn. This is called internally, once, by this.render. It does not - * render rows from the store, just the surrounding UI elements. - */ - renderUI : function() { - var templates = this.templates; - - return templates.master.apply({ - body : templates.body.apply({rows:' '}), - header: this.renderHeaders(), - ostyle: 'width:' + this.getOffsetWidth() + ';', - bstyle: 'width:' + this.getTotalWidth() + ';' - }); - }, - - // private - processEvent : function(name, e) { - var target = e.getTarget(), - grid = this.grid, - header = this.findHeaderIndex(target), - row, cell, col, body; - - grid.fireEvent(name, e); - - if (header !== false) { - grid.fireEvent('header' + name, grid, header, e); - } else { - row = this.findRowIndex(target); - -// Grid's value-added events must bubble correctly to allow cancelling via returning false: cell->column->row -// We must allow a return of false at any of these levels to cancel the event processing. -// Particularly allowing rowmousedown to be cancellable by prior handlers which need to prevent selection. - if (row !== false) { - cell = this.findCellIndex(target); - if (cell !== false) { - col = grid.colModel.getColumnAt(cell); - if (grid.fireEvent('cell' + name, grid, row, cell, e) !== false) { - if (!col || (col.processEvent && (col.processEvent(name, e, grid, row, cell) !== false))) { - grid.fireEvent('row' + name, grid, row, e); - } - } - } else { - if (grid.fireEvent('row' + name, grid, row, e) !== false) { - (body = this.findRowBody(target)) && grid.fireEvent('rowbody' + name, grid, row, e); - } - } - } else { - grid.fireEvent('container' + name, grid, e); - } - } - }, - - /** - * @private - * Sizes the grid's header and body elements - */ - layout : function(initial) { - if (!this.mainBody) { - return; // not rendered - } - - var grid = this.grid, - gridEl = grid.getGridEl(), - gridSize = gridEl.getSize(true), - gridWidth = gridSize.width, - gridHeight = gridSize.height, - scroller = this.scroller, - scrollStyle, headerHeight, scrollHeight; - - if (gridWidth < 20 || gridHeight < 20) { - return; - } - - if (grid.autoHeight) { - scrollStyle = scroller.dom.style; - scrollStyle.overflow = 'visible'; - - if (Ext.isWebKit) { - scrollStyle.position = 'static'; - } - } else { - this.el.setSize(gridWidth, gridHeight); - - headerHeight = this.mainHd.getHeight(); - scrollHeight = gridHeight - headerHeight; - - scroller.setSize(gridWidth, scrollHeight); - - if (this.innerHd) { - this.innerHd.style.width = (gridWidth) + "px"; - } - } - - if (this.forceFit || (initial === true && this.autoFill)) { - if (this.lastViewWidth != gridWidth) { - this.fitColumns(false, false); - this.lastViewWidth = gridWidth; - } - } else { - this.autoExpand(); - this.syncHeaderScroll(); - } - - this.onLayout(gridWidth, scrollHeight); - }, - - // template functions for subclasses and plugins - // these functions include precalculated values - onLayout : function(vw, vh) { - // do nothing - }, - - onColumnWidthUpdated : function(col, w, tw) { - //template method - }, - - onAllColumnWidthsUpdated : function(ws, tw) { - //template method - }, - - onColumnHiddenUpdated : function(col, hidden, tw) { - // template method - }, - - updateColumnText : function(col, text) { - // template method - }, - - afterMove : function(colIndex) { - // template method - }, - - /* ----------------------------------- Core Specific -------------------------------------------*/ - // private - init : function(grid) { - this.grid = grid; - - this.initTemplates(); - this.initData(grid.store, grid.colModel); - this.initUI(grid); - }, - - // private - getColumnId : function(index){ - return this.cm.getColumnId(index); - }, - - // private - getOffsetWidth : function() { - return (this.cm.getTotalWidth() + this.getScrollOffset()) + 'px'; - }, - - // private - getScrollOffset: function() { - return Ext.num(this.scrollOffset, Ext.getScrollBarWidth()); - }, - - /** - * @private - * Renders the header row using the 'header' template. Does not inject the HTML into the DOM, just - * returns a string. - * @return {String} Rendered header row - */ - renderHeaders : function() { - var colModel = this.cm, - templates = this.templates, - headerTpl = templates.hcell, - properties = {}, - colCount = colModel.getColumnCount(), - last = colCount - 1, - cells = [], - i, cssCls; - - for (i = 0; i < colCount; i++) { - if (i == 0) { - cssCls = 'x-grid3-cell-first '; - } else { - cssCls = i == last ? 'x-grid3-cell-last ' : ''; - } - - properties = { - id : colModel.getColumnId(i), - value : colModel.getColumnHeader(i) || '', - style : this.getColumnStyle(i, true), - css : cssCls, - tooltip: this.getColumnTooltip(i) - }; - - if (colModel.config[i].align == 'right') { - properties.istyle = 'padding-right: 16px;'; - } else { - delete properties.istyle; - } - - cells[i] = headerTpl.apply(properties); - } - - return templates.header.apply({ - cells : cells.join(""), - tstyle: String.format("width: {0};", this.getTotalWidth()) - }); - }, - - /** - * @private - */ - getColumnTooltip : function(i) { - var tooltip = this.cm.getColumnTooltip(i); - if (tooltip) { - if (Ext.QuickTips.isEnabled()) { - return 'ext:qtip="' + tooltip + '"'; - } else { - return 'title="' + tooltip + '"'; - } - } - - return ''; - }, - - // private - beforeUpdate : function() { - this.grid.stopEditing(true); - }, - - /** - * @private - * Re-renders the headers and ensures they are sized correctly - */ - updateHeaders : function() { - this.innerHd.firstChild.innerHTML = this.renderHeaders(); - - this.updateHeaderWidth(false); - }, - - /** - * @private - * Ensures that the header is sized to the total width available to it - * @param {Boolean} updateMain True to update the mainBody's width also (defaults to true) - */ - updateHeaderWidth: function(updateMain) { - var innerHdChild = this.innerHd.firstChild, - totalWidth = this.getTotalWidth(); - - innerHdChild.style.width = this.getOffsetWidth(); - innerHdChild.firstChild.style.width = totalWidth; - - if (updateMain !== false) { - this.mainBody.dom.style.width = totalWidth; - } - }, - - /** - * Focuses the specified row. - * @param {Number} row The row index - */ - focusRow : function(row) { - this.focusCell(row, 0, false); - }, - - /** - * Focuses the specified cell. - * @param {Number} row The row index - * @param {Number} col The column index - */ - focusCell : function(row, col, hscroll) { - this.syncFocusEl(this.ensureVisible(row, col, hscroll)); - - var focusEl = this.focusEl; - - if (Ext.isGecko) { - focusEl.focus(); - } else { - focusEl.focus.defer(1, focusEl); - } - }, - - /** - * @private - * Finds the Elements corresponding to the given row and column indexes - */ - resolveCell : function(row, col, hscroll) { - if (!Ext.isNumber(row)) { - row = row.rowIndex; - } - - if (!this.ds) { - return null; - } - - if (row < 0 || row >= this.ds.getCount()) { - return null; - } - col = (col !== undefined ? col : 0); - - var rowEl = this.getRow(row), - colModel = this.cm, - colCount = colModel.getColumnCount(), - cellEl; - - if (!(hscroll === false && col === 0)) { - while (col < colCount && colModel.isHidden(col)) { - col++; - } - - cellEl = this.getCell(row, col); - } - - return {row: rowEl, cell: cellEl}; - }, - - /** - * @private - * Returns the XY co-ordinates of a given row/cell resolution (see {@link #resolveCell}) - * @return {Array} X and Y coords - */ - getResolvedXY : function(resolved) { - if (!resolved) { - return null; - } - - var cell = resolved.cell, - row = resolved.row; - - if (cell) { - return Ext.fly(cell).getXY(); - } else { - return [this.el.getX(), Ext.fly(row).getY()]; - } - }, - - /** - * @private - * Moves the focus element to the x and y co-ordinates of the given row and column - */ - syncFocusEl : function(row, col, hscroll) { - var xy = row; - - if (!Ext.isArray(xy)) { - row = Math.min(row, Math.max(0, this.getRows().length-1)); - - if (isNaN(row)) { - return; - } - - xy = this.getResolvedXY(this.resolveCell(row, col, hscroll)); - } - - this.focusEl.setXY(xy || this.scroller.getXY()); - }, - - /** - * @private - */ - ensureVisible : function(row, col, hscroll) { - var resolved = this.resolveCell(row, col, hscroll); - - if (!resolved || !resolved.row) { - return null; - } - - var rowEl = resolved.row, - cellEl = resolved.cell, - c = this.scroller.dom, - p = rowEl, - ctop = 0, - stop = this.el.dom; - - while (p && p != stop) { - ctop += p.offsetTop; - p = p.offsetParent; - } - - ctop -= this.mainHd.dom.offsetHeight; - stop = parseInt(c.scrollTop, 10); - - var cbot = ctop + rowEl.offsetHeight, - ch = c.clientHeight, - sbot = stop + ch; - - - if (ctop < stop) { - c.scrollTop = ctop; - } else if(cbot > sbot) { - c.scrollTop = cbot-ch; - } - - if (hscroll !== false) { - var cleft = parseInt(cellEl.offsetLeft, 10), - cright = cleft + cellEl.offsetWidth, - sleft = parseInt(c.scrollLeft, 10), - sright = sleft + c.clientWidth; - - if (cleft < sleft) { - c.scrollLeft = cleft; - } else if(cright > sright) { - c.scrollLeft = cright-c.clientWidth; - } - } - - return this.getResolvedXY(resolved); - }, - - // private - insertRows : function(dm, firstRow, lastRow, isUpdate) { - var last = dm.getCount() - 1; - if( !isUpdate && firstRow === 0 && lastRow >= last) { - this.fireEvent('beforerowsinserted', this, firstRow, lastRow); - this.refresh(); - this.fireEvent('rowsinserted', this, firstRow, lastRow); - } else { - if (!isUpdate) { - this.fireEvent('beforerowsinserted', this, firstRow, lastRow); - } - var html = this.renderRows(firstRow, lastRow), - before = this.getRow(firstRow); - if (before) { - if(firstRow === 0){ - Ext.fly(this.getRow(0)).removeClass(this.firstRowCls); - } - Ext.DomHelper.insertHtml('beforeBegin', before, html); - } else { - var r = this.getRow(last - 1); - if(r){ - Ext.fly(r).removeClass(this.lastRowCls); - } - Ext.DomHelper.insertHtml('beforeEnd', this.mainBody.dom, html); - } - if (!isUpdate) { - this.processRows(firstRow); - this.fireEvent('rowsinserted', this, firstRow, lastRow); - } else if (firstRow === 0 || firstRow >= last) { - //ensure first/last row is kept after an update. - Ext.fly(this.getRow(firstRow)).addClass(firstRow === 0 ? this.firstRowCls : this.lastRowCls); - } - } - this.syncFocusEl(firstRow); - }, - - /** - * @private - * DEPRECATED - this doesn't appear to be called anywhere in the library, remove in 4.0. - */ - deleteRows : function(dm, firstRow, lastRow) { - if (dm.getRowCount() < 1) { - this.refresh(); - } else { - this.fireEvent('beforerowsdeleted', this, firstRow, lastRow); - - this.removeRows(firstRow, lastRow); - - this.processRows(firstRow); - this.fireEvent('rowsdeleted', this, firstRow, lastRow); - } - }, - - /** - * @private - * Builds a CSS string for the given column index - * @param {Number} colIndex The column index - * @param {Boolean} isHeader True if getting the style for the column's header - * @return {String} The CSS string - */ - getColumnStyle : function(colIndex, isHeader) { - var colModel = this.cm, - colConfig = colModel.config, - style = isHeader ? '' : colConfig[colIndex].css || '', - align = colConfig[colIndex].align; - - style += String.format("width: {0};", this.getColumnWidth(colIndex)); - - if (colModel.isHidden(colIndex)) { - style += 'display: none; '; - } - - if (align) { - style += String.format("text-align: {0};", align); - } - - return style; - }, - - /** - * @private - * Returns the width of a given column minus its border width - * @return {Number} The column index - * @return {String|Number} The width in pixels - */ - getColumnWidth : function(column) { - var columnWidth = this.cm.getColumnWidth(column), - borderWidth = this.borderWidth; - - if (Ext.isNumber(columnWidth)) { - if (Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2)) { - return columnWidth + "px"; - } else { - return Math.max(columnWidth - borderWidth, 0) + "px"; - } - } else { - return columnWidth; - } - }, - - /** - * @private - * Returns the total width of all visible columns - * @return {String} - */ - getTotalWidth : function() { - return this.cm.getTotalWidth() + 'px'; - }, - - /** - * @private - * Resizes each column to fit the available grid width. - * TODO: The second argument isn't even used, remove it in 4.0 - * @param {Boolean} preventRefresh True to prevent resizing of each row to the new column sizes (defaults to false) - * @param {null} onlyExpand NOT USED, will be removed in 4.0 - * @param {Number} omitColumn The index of a column to leave at its current width. Defaults to undefined - * @return {Boolean} True if the operation succeeded, false if not or undefined if the grid view is not yet initialized - */ - fitColumns : function(preventRefresh, onlyExpand, omitColumn) { - var grid = this.grid, - colModel = this.cm, - totalColWidth = colModel.getTotalWidth(false), - gridWidth = this.getGridInnerWidth(), - extraWidth = gridWidth - totalColWidth, - columns = [], - extraCol = 0, - width = 0, - colWidth, fraction, i; - - // not initialized, so don't screw up the default widths - if (gridWidth < 20 || extraWidth === 0) { - return false; - } - - var visibleColCount = colModel.getColumnCount(true), - totalColCount = colModel.getColumnCount(false), - adjCount = visibleColCount - (Ext.isNumber(omitColumn) ? 1 : 0); - - if (adjCount === 0) { - adjCount = 1; - omitColumn = undefined; - } - - //FIXME: the algorithm used here is odd and potentially confusing. Includes this for loop and the while after it. - for (i = 0; i < totalColCount; i++) { - if (!colModel.isFixed(i) && i !== omitColumn) { - colWidth = colModel.getColumnWidth(i); - columns.push(i, colWidth); - - if (!colModel.isHidden(i)) { - extraCol = i; - width += colWidth; - } - } - } - - fraction = (gridWidth - colModel.getTotalWidth()) / width; - - while (columns.length) { - colWidth = columns.pop(); - i = columns.pop(); - - colModel.setColumnWidth(i, Math.max(grid.minColumnWidth, Math.floor(colWidth + colWidth * fraction)), true); - } - - //this has been changed above so remeasure now - totalColWidth = colModel.getTotalWidth(false); - - if (totalColWidth > gridWidth) { - var adjustCol = (adjCount == visibleColCount) ? extraCol : omitColumn, - newWidth = Math.max(1, colModel.getColumnWidth(adjustCol) - (totalColWidth - gridWidth)); - - colModel.setColumnWidth(adjustCol, newWidth, true); - } - - if (preventRefresh !== true) { - this.updateAllColumnWidths(); - } - - return true; - }, - - /** - * @private - * Resizes the configured autoExpandColumn to take the available width after the other columns have - * been accounted for - * @param {Boolean} preventUpdate True to prevent the resizing of all rows (defaults to false) - */ - autoExpand : function(preventUpdate) { - var grid = this.grid, - colModel = this.cm, - gridWidth = this.getGridInnerWidth(), - totalColumnWidth = colModel.getTotalWidth(false), - autoExpandColumn = grid.autoExpandColumn; - - if (!this.userResized && autoExpandColumn) { - if (gridWidth != totalColumnWidth) { - //if we are not already using all available width, resize the autoExpandColumn - var colIndex = colModel.getIndexById(autoExpandColumn), - currentWidth = colModel.getColumnWidth(colIndex), - desiredWidth = gridWidth - totalColumnWidth + currentWidth, - newWidth = Math.min(Math.max(desiredWidth, grid.autoExpandMin), grid.autoExpandMax); - - if (currentWidth != newWidth) { - colModel.setColumnWidth(colIndex, newWidth, true); - - if (preventUpdate !== true) { - this.updateColumnWidth(colIndex, newWidth); - } - } - } - } - }, - - /** - * Returns the total internal width available to the grid, taking the scrollbar into account - * @return {Number} The total width - */ - getGridInnerWidth: function() { - return this.grid.getGridEl().getWidth(true) - this.getScrollOffset(); - }, - - /** - * @private - * Returns an array of column configurations - one for each column - * @return {Array} Array of column config objects. This includes the column name, renderer, id style and renderer - */ - getColumnData : function() { - var columns = [], - colModel = this.cm, - colCount = colModel.getColumnCount(), - fields = this.ds.fields, - i, name; - - for (i = 0; i < colCount; i++) { - name = colModel.getDataIndex(i); - - columns[i] = { - name : Ext.isDefined(name) ? name : (fields.get(i) ? fields.get(i).name : undefined), - renderer: colModel.getRenderer(i), - scope : colModel.getRendererScope(i), - id : colModel.getColumnId(i), - style : this.getColumnStyle(i) - }; - } - - return columns; - }, - - /** - * @private - * Renders rows between start and end indexes - * @param {Number} startRow Index of the first row to render - * @param {Number} endRow Index of the last row to render - */ - renderRows : function(startRow, endRow) { - var grid = this.grid, - store = grid.store, - stripe = grid.stripeRows, - colModel = grid.colModel, - colCount = colModel.getColumnCount(), - rowCount = store.getCount(), - records; - - if (rowCount < 1) { - return ''; - } - - startRow = startRow || 0; - endRow = Ext.isDefined(endRow) ? endRow : rowCount - 1; - records = store.getRange(startRow, endRow); - - return this.doRender(this.getColumnData(), records, store, startRow, colCount, stripe); - }, - - // private - renderBody : function(){ - var markup = this.renderRows() || ' '; - return this.templates.body.apply({rows: markup}); - }, - - /** - * @private - * Refreshes a row by re-rendering it. Fires the rowupdated event when done - */ - refreshRow: function(record) { - var store = this.ds, - colCount = this.cm.getColumnCount(), - columns = this.getColumnData(), - last = colCount - 1, - cls = ['x-grid3-row'], - rowParams = { - tstyle: String.format("width: {0};", this.getTotalWidth()) - }, - colBuffer = [], - cellTpl = this.templates.cell, - rowIndex, row, column, meta, css, i; - - if (Ext.isNumber(record)) { - rowIndex = record; - record = store.getAt(rowIndex); - } else { - rowIndex = store.indexOf(record); - } - - //the record could not be found - if (!record || rowIndex < 0) { - return; - } - - //builds each column in this row - for (i = 0; i < colCount; i++) { - column = columns[i]; - - if (i == 0) { - css = 'x-grid3-cell-first'; - } else { - css = (i == last) ? 'x-grid3-cell-last ' : ''; - } - - meta = { - id : column.id, - style : column.style, - css : css, - attr : "", - cellAttr: "" - }; - // Need to set this after, because we pass meta to the renderer - meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store); - - if (Ext.isEmpty(meta.value)) { - meta.value = ' '; - } - - if (this.markDirty && record.dirty && typeof record.modified[column.name] != 'undefined') { - meta.css += ' x-grid3-dirty-cell'; - } - - colBuffer[i] = cellTpl.apply(meta); - } - - row = this.getRow(rowIndex); - row.className = ''; - - if (this.grid.stripeRows && ((rowIndex + 1) % 2 === 0)) { - cls.push('x-grid3-row-alt'); - } - - if (this.getRowClass) { - rowParams.cols = colCount; - cls.push(this.getRowClass(record, rowIndex, rowParams, store)); - } - - this.fly(row).addClass(cls).setStyle(rowParams.tstyle); - rowParams.cells = colBuffer.join(""); - row.innerHTML = this.templates.rowInner.apply(rowParams); - - this.fireEvent('rowupdated', this, rowIndex, record); - }, - - /** - * Refreshs the grid UI - * @param {Boolean} headersToo (optional) True to also refresh the headers - */ - refresh : function(headersToo) { - this.fireEvent('beforerefresh', this); - this.grid.stopEditing(true); - - var result = this.renderBody(); - this.mainBody.update(result).setWidth(this.getTotalWidth()); - if (headersToo === true) { - this.updateHeaders(); - this.updateHeaderSortState(); - } - this.processRows(0, true); - this.layout(); - this.applyEmptyText(); - this.fireEvent('refresh', this); - }, - - /** - * @private - * Displays the configured emptyText if there are currently no rows to display - */ - applyEmptyText : function() { - if (this.emptyText && !this.hasRows()) { - this.mainBody.update('
      ' + this.emptyText + '
      '); - } - }, - - /** - * @private - * Adds sorting classes to the column headers based on the bound store's sortInfo. Fires the 'sortchange' event - * if the sorting has changed since this function was last run. - */ - updateHeaderSortState : function() { - var state = this.ds.getSortState(); - if (!state) { - return; - } - - if (!this.sortState || (this.sortState.field != state.field || this.sortState.direction != state.direction)) { - this.grid.fireEvent('sortchange', this.grid, state); - } - - this.sortState = state; - - var sortColumn = this.cm.findColumnIndex(state.field); - if (sortColumn != -1) { - var sortDir = state.direction; - this.updateSortIcon(sortColumn, sortDir); - } - }, - - /** - * @private - * Removes any sorting indicator classes from the column headers - */ - clearHeaderSortState : function() { - if (!this.sortState) { - return; - } - this.grid.fireEvent('sortchange', this.grid, null); - this.mainHd.select('td').removeClass(this.sortClasses); - delete this.sortState; - }, - - /** - * @private - * Destroys all objects associated with the GridView - */ - destroy : function() { - var me = this, - grid = me.grid, - gridEl = grid.getGridEl(), - dragZone = me.dragZone, - splitZone = me.splitZone, - columnDrag = me.columnDrag, - columnDrop = me.columnDrop, - scrollToTopTask = me.scrollToTopTask, - columnDragData, - columnDragProxy; - - if (scrollToTopTask && scrollToTopTask.cancel) { - scrollToTopTask.cancel(); - } - - Ext.destroyMembers(me, 'colMenu', 'hmenu'); - - me.initData(null, null); - me.purgeListeners(); - - Ext.fly(me.innerHd).un("click", me.handleHdDown, me); - - if (grid.enableColumnMove) { - columnDragData = columnDrag.dragData; - columnDragProxy = columnDrag.proxy; - Ext.destroy( - columnDrag.el, - columnDragProxy.ghost, - columnDragProxy.el, - columnDrop.el, - columnDrop.proxyTop, - columnDrop.proxyBottom, - columnDragData.ddel, - columnDragData.header - ); - - if (columnDragProxy.anim) { - Ext.destroy(columnDragProxy.anim); - } - - delete columnDragProxy.ghost; - delete columnDragData.ddel; - delete columnDragData.header; - columnDrag.destroy(); - - delete Ext.dd.DDM.locationCache[columnDrag.id]; - delete columnDrag._domRef; - - delete columnDrop.proxyTop; - delete columnDrop.proxyBottom; - columnDrop.destroy(); - delete Ext.dd.DDM.locationCache["gridHeader" + gridEl.id]; - delete columnDrop._domRef; - delete Ext.dd.DDM.ids[columnDrop.ddGroup]; - } - - if (splitZone) { // enableColumnResize - splitZone.destroy(); - delete splitZone._domRef; - delete Ext.dd.DDM.ids["gridSplitters" + gridEl.id]; - } - - Ext.fly(me.innerHd).removeAllListeners(); - Ext.removeNode(me.innerHd); - delete me.innerHd; - - Ext.destroy( - me.el, - me.mainWrap, - me.mainHd, - me.scroller, - me.mainBody, - me.focusEl, - me.resizeMarker, - me.resizeProxy, - me.activeHdBtn, - me._flyweight, - dragZone, - splitZone - ); - - delete grid.container; - - if (dragZone) { - dragZone.destroy(); - } - - Ext.dd.DDM.currentTarget = null; - delete Ext.dd.DDM.locationCache[gridEl.id]; - - Ext.EventManager.removeResizeListener(me.onWindowResize, me); - }, - - // private - onDenyColumnHide : function() { - - }, - - // private - render : function() { - if (this.autoFill) { - var ct = this.grid.ownerCt; - - if (ct && ct.getLayout()) { - ct.on('afterlayout', function() { - this.fitColumns(true, true); - this.updateHeaders(); - this.updateHeaderSortState(); - }, this, {single: true}); - } - } else if (this.forceFit) { - this.fitColumns(true, false); - } else if (this.grid.autoExpandColumn) { - this.autoExpand(true); - } - - this.grid.getGridEl().dom.innerHTML = this.renderUI(); - - this.afterRenderUI(); - }, - - /* --------------------------------- Model Events and Handlers --------------------------------*/ - - /** - * @private - * Binds a new Store and ColumnModel to this GridView. Removes any listeners from the old objects (if present) - * and adds listeners to the new ones - * @param {Ext.data.Store} newStore The new Store instance - * @param {Ext.grid.ColumnModel} newColModel The new ColumnModel instance - */ - initData : function(newStore, newColModel) { - var me = this; - - if (me.ds) { - var oldStore = me.ds; - - oldStore.un('add', me.onAdd, me); - oldStore.un('load', me.onLoad, me); - oldStore.un('clear', me.onClear, me); - oldStore.un('remove', me.onRemove, me); - oldStore.un('update', me.onUpdate, me); - oldStore.un('datachanged', me.onDataChange, me); - - if (oldStore !== newStore && oldStore.autoDestroy) { - oldStore.destroy(); - } - } - - if (newStore) { - newStore.on({ - scope : me, - load : me.onLoad, - add : me.onAdd, - remove : me.onRemove, - update : me.onUpdate, - clear : me.onClear, - datachanged: me.onDataChange - }); - } - - if (me.cm) { - var oldColModel = me.cm; - - oldColModel.un('configchange', me.onColConfigChange, me); - oldColModel.un('widthchange', me.onColWidthChange, me); - oldColModel.un('headerchange', me.onHeaderChange, me); - oldColModel.un('hiddenchange', me.onHiddenChange, me); - oldColModel.un('columnmoved', me.onColumnMove, me); - } - - if (newColModel) { - delete me.lastViewWidth; - - newColModel.on({ - scope : me, - configchange: me.onColConfigChange, - widthchange : me.onColWidthChange, - headerchange: me.onHeaderChange, - hiddenchange: me.onHiddenChange, - columnmoved : me.onColumnMove - }); - } - - me.ds = newStore; - me.cm = newColModel; - }, - - // private - onDataChange : function(){ - this.refresh(true); - this.updateHeaderSortState(); - this.syncFocusEl(0); - }, - - // private - onClear : function() { - this.refresh(); - this.syncFocusEl(0); - }, - - // private - onUpdate : function(store, record) { - this.refreshRow(record); - }, - - // private - onAdd : function(store, records, index) { - this.insertRows(store, index, index + (records.length-1)); - }, - - // private - onRemove : function(store, record, index, isUpdate) { - if (isUpdate !== true) { - this.fireEvent('beforerowremoved', this, index, record); - } - - this.removeRow(index); - - if (isUpdate !== true) { - this.processRows(index); - this.applyEmptyText(); - this.fireEvent('rowremoved', this, index, record); - } - }, - - /** - * @private - * Called when a store is loaded, scrolls to the top row - */ - onLoad : function() { - if (Ext.isGecko) { - if (!this.scrollToTopTask) { - this.scrollToTopTask = new Ext.util.DelayedTask(this.scrollToTop, this); - } - this.scrollToTopTask.delay(1); - } else { - this.scrollToTop(); - } - }, - - // private - onColWidthChange : function(cm, col, width) { - this.updateColumnWidth(col, width); - }, - - // private - onHeaderChange : function(cm, col, text) { - this.updateHeaders(); - }, - - // private - onHiddenChange : function(cm, col, hidden) { - this.updateColumnHidden(col, hidden); - }, - - // private - onColumnMove : function(cm, oldIndex, newIndex) { - this.indexMap = null; - this.refresh(true); - this.restoreScroll(this.getScrollState()); - - this.afterMove(newIndex); - this.grid.fireEvent('columnmove', oldIndex, newIndex); - }, - - // private - onColConfigChange : function() { - delete this.lastViewWidth; - this.indexMap = null; - this.refresh(true); - }, - - /* -------------------- UI Events and Handlers ------------------------------ */ - // private - initUI : function(grid) { - grid.on('headerclick', this.onHeaderClick, this); - }, - - // private - initEvents : Ext.emptyFn, - - // private - onHeaderClick : function(g, index) { - if (this.headersDisabled || !this.cm.isSortable(index)) { - return; - } - g.stopEditing(true); - g.store.sort(this.cm.getDataIndex(index)); - }, - - /** - * @private - * Adds the hover class to a row when hovered over - */ - onRowOver : function(e, target) { - var row = this.findRowIndex(target); - - if (row !== false) { - this.addRowClass(row, this.rowOverCls); - } - }, - - /** - * @private - * Removes the hover class from a row on mouseout - */ - onRowOut : function(e, target) { - var row = this.findRowIndex(target); - - if (row !== false && !e.within(this.getRow(row), true)) { - this.removeRowClass(row, this.rowOverCls); - } - }, - - // private - onRowSelect : function(row) { - this.addRowClass(row, this.selectedRowClass); - }, - - // private - onRowDeselect : function(row) { - this.removeRowClass(row, this.selectedRowClass); - }, - - // private - onCellSelect : function(row, col) { - var cell = this.getCell(row, col); - if (cell) { - this.fly(cell).addClass('x-grid3-cell-selected'); - } - }, - - // private - onCellDeselect : function(row, col) { - var cell = this.getCell(row, col); - if (cell) { - this.fly(cell).removeClass('x-grid3-cell-selected'); - } - }, - - // private - handleWheel : function(e) { - e.stopPropagation(); - }, - - /** - * @private - * Called by the SplitDragZone when a drag has been completed. Resizes the columns - */ - onColumnSplitterMoved : function(cellIndex, width) { - this.userResized = true; - this.grid.colModel.setColumnWidth(cellIndex, width, true); - - if (this.forceFit) { - this.fitColumns(true, false, cellIndex); - this.updateAllColumnWidths(); - } else { - this.updateColumnWidth(cellIndex, width); - this.syncHeaderScroll(); - } - - this.grid.fireEvent('columnresize', cellIndex, width); - }, - - /** - * @private - * Click handler for the shared column dropdown menu, called on beforeshow. Builds the menu - * which displays the list of columns for the user to show or hide. - */ - beforeColMenuShow : function() { - var colModel = this.cm, - colCount = colModel.getColumnCount(), - colMenu = this.colMenu, - i; - - colMenu.removeAll(); - - for (i = 0; i < colCount; i++) { - if (colModel.config[i].hideable !== false) { - colMenu.add(new Ext.menu.CheckItem({ - text : colModel.getColumnHeader(i), - itemId : 'col-' + colModel.getColumnId(i), - checked : !colModel.isHidden(i), - disabled : colModel.config[i].hideable === false, - hideOnClick: false - })); - } - } - }, - - /** - * @private - * Attached as the 'itemclick' handler to the header menu and the column show/hide submenu (if available). - * Performs sorting if the sorter buttons were clicked, otherwise hides/shows the column that was clicked. - */ - handleHdMenuClick : function(item) { - var store = this.ds, - dataIndex = this.cm.getDataIndex(this.hdCtxIndex); - - switch (item.getItemId()) { - case 'asc': - store.sort(dataIndex, 'ASC'); - break; - case 'desc': - store.sort(dataIndex, 'DESC'); - break; - default: - this.handleHdMenuClickDefault(item); - } - return true; - }, - - /** - * Called by handleHdMenuClick if any button except a sort ASC/DESC button was clicked. The default implementation provides - * the column hide/show functionality based on the check state of the menu item. A different implementation can be provided - * if needed. - * @param {Ext.menu.BaseItem} item The menu item that was clicked - */ - handleHdMenuClickDefault: function(item) { - var colModel = this.cm, - itemId = item.getItemId(), - index = colModel.getIndexById(itemId.substr(4)); - - if (index != -1) { - if (item.checked && colModel.getColumnsBy(this.isHideableColumn, this).length <= 1) { - this.onDenyColumnHide(); - return; - } - colModel.setHidden(index, item.checked); - } - }, - - /** - * @private - * Called when a header cell is clicked - shows the menu if the click happened over a trigger button - */ - handleHdDown : function(e, target) { - if (Ext.fly(target).hasClass('x-grid3-hd-btn')) { - e.stopEvent(); - - var colModel = this.cm, - header = this.findHeaderCell(target), - index = this.getCellIndex(header), - sortable = colModel.isSortable(index), - menu = this.hmenu, - menuItems = menu.items, - menuCls = this.headerMenuOpenCls; - - this.hdCtxIndex = index; - - Ext.fly(header).addClass(menuCls); - menuItems.get('asc').setDisabled(!sortable); - menuItems.get('desc').setDisabled(!sortable); - - menu.on('hide', function() { - Ext.fly(header).removeClass(menuCls); - }, this, {single:true}); - - menu.show(target, 'tl-bl?'); - } - }, - - /** - * @private - * Attached to the headers' mousemove event. This figures out the CSS cursor to use based on where the mouse is currently - * pointed. If the mouse is currently hovered over the extreme left or extreme right of any header cell and the cell next - * to it is resizable it is given the resize cursor, otherwise the cursor is set to an empty string. - */ - handleHdMove : function(e) { - var header = this.findHeaderCell(this.activeHdRef); - - if (header && !this.headersDisabled) { - var handleWidth = this.splitHandleWidth || 5, - activeRegion = this.activeHdRegion, - headerStyle = header.style, - colModel = this.cm, - cursor = '', - pageX = e.getPageX(); - - if (this.grid.enableColumnResize !== false) { - var activeHeaderIndex = this.activeHdIndex, - previousVisible = this.getPreviousVisible(activeHeaderIndex), - currentResizable = colModel.isResizable(activeHeaderIndex), - previousResizable = previousVisible && colModel.isResizable(previousVisible), - inLeftResizer = pageX - activeRegion.left <= handleWidth, - inRightResizer = activeRegion.right - pageX <= (!this.activeHdBtn ? handleWidth : 2); - - if (inLeftResizer && previousResizable) { - cursor = Ext.isAir ? 'move' : Ext.isWebKit ? 'e-resize' : 'col-resize'; // col-resize not always supported - } else if (inRightResizer && currentResizable) { - cursor = Ext.isAir ? 'move' : Ext.isWebKit ? 'w-resize' : 'col-resize'; - } - } - - headerStyle.cursor = cursor; - } - }, - - /** - * @private - * Returns the index of the nearest currently visible header to the left of the given index. - * @param {Number} index The header index - * @return {Number/undefined} The index of the nearest visible header - */ - getPreviousVisible: function(index) { - while (index > 0) { - if (!this.cm.isHidden(index - 1)) { - return index; - } - index--; - } - return undefined; - }, - - /** - * @private - * Tied to the header element's mouseover event - adds the over class to the header cell if the menu is not disabled - * for that cell - */ - handleHdOver : function(e, target) { - var header = this.findHeaderCell(target); - - if (header && !this.headersDisabled) { - var fly = this.fly(header); - - this.activeHdRef = target; - this.activeHdIndex = this.getCellIndex(header); - this.activeHdRegion = fly.getRegion(); - - if (!this.isMenuDisabled(this.activeHdIndex, fly)) { - fly.addClass('x-grid3-hd-over'); - this.activeHdBtn = fly.child('.x-grid3-hd-btn'); - - if (this.activeHdBtn) { - this.activeHdBtn.dom.style.height = (header.firstChild.offsetHeight - 1) + 'px'; - } - } - } - }, - - /** - * @private - * Tied to the header element's mouseout event. Removes the hover class from the header cell - */ - handleHdOut : function(e, target) { - var header = this.findHeaderCell(target); - - if (header && (!Ext.isIE || !e.within(header, true))) { - this.activeHdRef = null; - this.fly(header).removeClass('x-grid3-hd-over'); - header.style.cursor = ''; - } - }, - - /** - * @private - * Used by {@link #handleHdOver} to determine whether or not to show the header menu class on cell hover - * @param {Number} cellIndex The header cell index - * @param {Ext.Element} el The cell element currently being hovered over - */ - isMenuDisabled: function(cellIndex, el) { - return this.cm.isMenuDisabled(cellIndex); - }, - - /** - * @private - * Returns true if there are any rows rendered into the GridView - * @return {Boolean} True if any rows have been rendered - */ - hasRows : function() { - var fc = this.mainBody.dom.firstChild; - return fc && fc.nodeType == 1 && fc.className != 'x-grid-empty'; - }, - - /** - * @private - */ - isHideableColumn : function(c) { - return !c.hidden; - }, - - /** - * @private - * DEPRECATED - will be removed in Ext JS 5.0 - */ - bind : function(d, c) { - this.initData(d, c); - } -}); - - -// private -// This is a support class used internally by the Grid components -Ext.grid.GridView.SplitDragZone = Ext.extend(Ext.dd.DDProxy, { - - constructor: function(grid, hd){ - this.grid = grid; - this.view = grid.getView(); - this.marker = this.view.resizeMarker; - this.proxy = this.view.resizeProxy; - Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this, hd, - 'gridSplitters' + this.grid.getGridEl().id, { - dragElId : Ext.id(this.proxy.dom), resizeFrame:false - }); - this.scroll = false; - this.hw = this.view.splitHandleWidth || 5; - }, - - b4StartDrag : function(x, y){ - this.dragHeadersDisabled = this.view.headersDisabled; - this.view.headersDisabled = true; - var h = this.view.mainWrap.getHeight(); - this.marker.setHeight(h); - this.marker.show(); - this.marker.alignTo(this.view.getHeaderCell(this.cellIndex), 'tl-tl', [-2, 0]); - this.proxy.setHeight(h); - var w = this.cm.getColumnWidth(this.cellIndex), - minw = Math.max(w-this.grid.minColumnWidth, 0); - this.resetConstraints(); - this.setXConstraint(minw, 1000); - this.setYConstraint(0, 0); - this.minX = x - minw; - this.maxX = x + 1000; - this.startPos = x; - Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y); - }, - - allowHeaderDrag : function(e){ - return true; - }, - - handleMouseDown : function(e){ - var t = this.view.findHeaderCell(e.getTarget()); - if(t && this.allowHeaderDrag(e)){ - var xy = this.view.fly(t).getXY(), - x = xy[0], - exy = e.getXY(), - ex = exy[0], - w = t.offsetWidth, - adjust = false; - - if((ex - x) <= this.hw){ - adjust = -1; - }else if((x+w) - ex <= this.hw){ - adjust = 0; - } - if(adjust !== false){ - this.cm = this.grid.colModel; - var ci = this.view.getCellIndex(t); - if(adjust == -1){ - if (ci + adjust < 0) { - return; - } - while(this.cm.isHidden(ci+adjust)){ - --adjust; - if(ci+adjust < 0){ - return; - } - } - } - this.cellIndex = ci+adjust; - this.split = t.dom; - if(this.cm.isResizable(this.cellIndex) && !this.cm.isFixed(this.cellIndex)){ - Ext.grid.GridView.SplitDragZone.superclass.handleMouseDown.apply(this, arguments); - } - }else if(this.view.columnDrag){ - this.view.columnDrag.callHandleMouseDown(e); - } - } - }, - - endDrag : function(e){ - this.marker.hide(); - var v = this.view, - endX = Math.max(this.minX, e.getPageX()), - diff = endX - this.startPos, - disabled = this.dragHeadersDisabled; - - v.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex)+diff); - setTimeout(function(){ - v.headersDisabled = disabled; - }, 50); - }, - - autoOffset : function(){ - this.setDelta(0,0); - } -}); -/** - * @class Ext.grid.PivotGridView - * @extends Ext.grid.GridView - * Specialised GridView for rendering Pivot Grid components. Config can be passed to the PivotGridView via the PivotGrid constructor's - * viewConfig option: -
      
      -new Ext.grid.PivotGrid({
      -    viewConfig: {
      -        title: 'My Pivot Grid',
      -        getCellCls: function(value) {
      -            return value > 10 'red' : 'green';
      -        }
      -    }
      -});
      -
      - *

      Currently {@link #title} and {@link #getCellCls} are the only configuration options accepted by PivotGridView. All other - * interaction is performed via the {@link Ext.grid.PivotGrid PivotGrid} class.

      - */ -Ext.grid.PivotGridView = Ext.extend(Ext.grid.GridView, { - - /** - * The CSS class added to all group header cells. Defaults to 'grid-hd-group-cell' - * @property colHeaderCellCls - * @type String - */ - colHeaderCellCls: 'grid-hd-group-cell', - - /** - * @cfg {String} title Optional title to be placed in the top left corner of the PivotGrid. Defaults to an empty string. - */ - title: '', - - /** - * @cfg {Function} getCellCls Optional function which should return a CSS class name for each cell value. This is useful when - * color coding cells based on their value. Defaults to undefined. - */ - - /** - * Returns the headers to be rendered at the top of the grid. Should be a 2-dimensional array, where each item specifies the number - * of columns it groups (column in this case refers to normal grid columns). In the example below we have 5 city groups, which are - * each part of a continent supergroup. The colspan for each city group refers to the number of normal grid columns that group spans, - * so in this case the grid would be expected to have a total of 12 columns: -
      
      -[
      -    {
      -        items: [
      -            {header: 'England',   colspan: 5},
      -            {header: 'USA',       colspan: 3}
      -        ]
      -    },
      -    {
      -        items: [
      -            {header: 'London',    colspan: 2},
      -            {header: 'Cambridge', colspan: 3},
      -            {header: 'Palo Alto', colspan: 3}
      -        ]
      -    }
      -]
      -
      - * In the example above we have cities nested under countries. The nesting could be deeper if desired - e.g. Continent -> Country -> - * State -> City, or any other structure. The only constaint is that the same depth must be used throughout the structure. - * @return {Array} A tree structure containing the headers to be rendered. Must include the colspan property at each level, which should - * be the sum of all child nodes beneath this node. - */ - getColumnHeaders: function() { - return this.grid.topAxis.buildHeaders();; - }, - - /** - * Returns the headers to be rendered on the left of the grid. Should be a 2-dimensional array, where each item specifies the number - * of rows it groups. In the example below we have 5 city groups, which are each part of a continent supergroup. The rowspan for each - * city group refers to the number of normal grid columns that group spans, so in this case the grid would be expected to have a - * total of 12 rows: -
      
      -[
      -    {
      -        width: 90,
      -        items: [
      -            {header: 'England',   rowspan: 5},
      -            {header: 'USA',       rowspan: 3}
      -        ]
      -    },
      -    {
      -        width: 50,
      -        items: [
      -            {header: 'London',    rowspan: 2},
      -            {header: 'Cambridge', rowspan: 3},
      -            {header: 'Palo Alto', rowspan: 3}
      -        ]
      -    }
      -]
      -
      - * In the example above we have cities nested under countries. The nesting could be deeper if desired - e.g. Continent -> Country -> - * State -> City, or any other structure. The only constaint is that the same depth must be used throughout the structure. - * @return {Array} A tree structure containing the headers to be rendered. Must include the colspan property at each level, which should - * be the sum of all child nodes beneath this node. - * Each group may specify the width it should be rendered with. - * @return {Array} The row groups - */ - getRowHeaders: function() { - return this.grid.leftAxis.buildHeaders(); - }, - - /** - * @private - * Renders rows between start and end indexes - * @param {Number} startRow Index of the first row to render - * @param {Number} endRow Index of the last row to render - */ - renderRows : function(startRow, endRow) { - var grid = this.grid, - rows = grid.extractData(), - rowCount = rows.length, - templates = this.templates, - renderer = grid.renderer, - hasRenderer = typeof renderer == 'function', - getCellCls = this.getCellCls, - hasGetCellCls = typeof getCellCls == 'function', - cellTemplate = templates.cell, - rowTemplate = templates.row, - rowBuffer = [], - meta = {}, - tstyle = 'width:' + this.getGridInnerWidth() + 'px;', - colBuffer, colCount, column, i, row; - - startRow = startRow || 0; - endRow = Ext.isDefined(endRow) ? endRow : rowCount - 1; - - for (i = 0; i < rowCount; i++) { - row = rows[i]; - colCount = row.length; - colBuffer = []; - - //build up each column's HTML - for (var j = 0; j < colCount; j++) { - - meta.id = i + '-' + j; - meta.css = j === 0 ? 'x-grid3-cell-first ' : (j == (colCount - 1) ? 'x-grid3-cell-last ' : ''); - meta.attr = meta.cellAttr = ''; - meta.value = row[j]; - - if (Ext.isEmpty(meta.value)) { - meta.value = ' '; - } - - if (hasRenderer) { - meta.value = renderer(meta.value); - } - - if (hasGetCellCls) { - meta.css += getCellCls(meta.value) + ' '; - } - - colBuffer[colBuffer.length] = cellTemplate.apply(meta); - } - - rowBuffer[rowBuffer.length] = rowTemplate.apply({ - tstyle: tstyle, - cols : colCount, - cells : colBuffer.join(""), - alt : '' - }); - } - - return rowBuffer.join(""); - }, - - /** - * The master template to use when rendering the GridView. Has a default template - * @property Ext.Template - * @type masterTpl - */ - masterTpl: new Ext.Template( - '
      ', - '
      ', - '
      ', - '
      {title}
      ', - '
      ', - '
      ', - '
      ', - '
      ', - '
      ', - '
      ', - '
      ', - '
      {body}
      ', - '', - '
      ', - '
      ', - '
       
      ', - '
       
      ', - '
      ' - ), - - /** - * @private - * Adds a gcell template to the internal templates object. This is used to render the headers in a multi-level column header. - */ - initTemplates: function() { - Ext.grid.PivotGridView.superclass.initTemplates.apply(this, arguments); - - var templates = this.templates || {}; - if (!templates.gcell) { - templates.gcell = new Ext.XTemplate( - '', - '
      ', - this.grid.enableHdMenu ? '' : '', '{value}', - '
      ', - '' - ); - } - - this.templates = templates; - this.hrowRe = new RegExp("ux-grid-hd-group-row-(\\d+)", ""); - }, - - /** - * @private - * Sets up the reference to the row headers element - */ - initElements: function() { - Ext.grid.PivotGridView.superclass.initElements.apply(this, arguments); - - /** - * @property rowHeadersEl - * @type Ext.Element - * The element containing all row headers - */ - this.rowHeadersEl = new Ext.Element(this.scroller.child('div.x-grid3-row-headers')); - - /** - * @property headerTitleEl - * @type Ext.Element - * The element that contains the optional title (top left section of the pivot grid) - */ - this.headerTitleEl = new Ext.Element(this.mainHd.child('div.x-grid3-header-title')); - }, - - /** - * @private - * Takes row headers into account when calculating total available width - */ - getGridInnerWidth: function() { - var previousWidth = Ext.grid.PivotGridView.superclass.getGridInnerWidth.apply(this, arguments); - - return previousWidth - this.getTotalRowHeaderWidth(); - }, - - /** - * Returns the total width of all row headers as specified by {@link #getRowHeaders} - * @return {Number} The total width - */ - getTotalRowHeaderWidth: function() { - var headers = this.getRowHeaders(), - length = headers.length, - total = 0, - i; - - for (i = 0; i< length; i++) { - total += headers[i].width; - } - - return total; - }, - - /** - * @private - * Returns the total height of all column headers - * @return {Number} The total height - */ - getTotalColumnHeaderHeight: function() { - return this.getColumnHeaders().length * 21; - }, - - /** - * Inherit docs - * @private - * @param {HTMLElement} el - */ - getCellIndex : function(el) { - if (el) { - var match = el.className.match(this.colRe), - data; - - if (match && (data = match[1])) { - return parseInt(data.split('-')[1], 10); - } - } - return false; - }, - - - /** - * @private - * Slight specialisation of the GridView renderUI - just adds the row headers - */ - renderUI : function() { - var templates = this.templates, - innerWidth = this.getGridInnerWidth(); - - return templates.master.apply({ - body : templates.body.apply({rows:' '}), - ostyle: 'width:' + innerWidth + 'px', - bstyle: 'width:' + innerWidth + 'px' - }); - }, - - /** - * @private - * Make sure that the headers and rows are all sized correctly during layout - */ - onLayout: function(width, height) { - Ext.grid.PivotGridView.superclass.onLayout.apply(this, arguments); - - var width = this.getGridInnerWidth(); - - this.resizeColumnHeaders(width); - this.resizeAllRows(width); - }, - - /** - * Refreshs the grid UI - * @param {Boolean} headersToo (optional) True to also refresh the headers - */ - refresh : function(headersToo) { - this.fireEvent('beforerefresh', this); - this.grid.stopEditing(true); - - var result = this.renderBody(); - this.mainBody.update(result).setWidth(this.getGridInnerWidth()); - if (headersToo === true) { - this.updateHeaders(); - this.updateHeaderSortState(); - } - this.processRows(0, true); - this.layout(); - this.applyEmptyText(); - this.fireEvent('refresh', this); - }, - - /** - * @private - * Bypasses GridView's renderHeaders as they are taken care of separately by the PivotAxis instances - */ - renderHeaders: Ext.emptyFn, - - /** - * @private - * Taken care of by PivotAxis - */ - fitColumns: Ext.emptyFn, - - /** - * @private - * Called on layout, ensures that the width of each column header is correct. Omitting this can lead to faulty - * layouts when nested in a container. - * @param {Number} width The new width - */ - resizeColumnHeaders: function(width) { - var topAxis = this.grid.topAxis; - - if (topAxis.rendered) { - topAxis.el.setWidth(width); - } - }, - - /** - * @private - * Sets the row header div to the correct width. Should be called after rendering and reconfiguration of headers - */ - resizeRowHeaders: function() { - var rowHeaderWidth = this.getTotalRowHeaderWidth(), - marginStyle = String.format("margin-left: {0}px;", rowHeaderWidth); - - this.rowHeadersEl.setWidth(rowHeaderWidth); - this.mainBody.applyStyles(marginStyle); - Ext.fly(this.innerHd).applyStyles(marginStyle); - - this.headerTitleEl.setWidth(rowHeaderWidth); - this.headerTitleEl.setHeight(this.getTotalColumnHeaderHeight()); - }, - - /** - * @private - * Resizes all rendered rows to the given width. Usually called by onLayout - * @param {Number} width The new width - */ - resizeAllRows: function(width) { - var rows = this.getRows(), - length = rows.length, - i; - - for (i = 0; i < length; i++) { - Ext.fly(rows[i]).setWidth(width); - Ext.fly(rows[i]).child('table').setWidth(width); - } - }, - - /** - * @private - * Updates the Row Headers, deferring the updating of Column Headers to GridView - */ - updateHeaders: function() { - this.renderGroupRowHeaders(); - this.renderGroupColumnHeaders(); - }, - - /** - * @private - * Renders all row header groups at all levels based on the structure fetched from {@link #getGroupRowHeaders} - */ - renderGroupRowHeaders: function() { - var leftAxis = this.grid.leftAxis; - - this.resizeRowHeaders(); - leftAxis.rendered = false; - leftAxis.render(this.rowHeadersEl); - - this.setTitle(this.title); - }, - - /** - * Sets the title text in the top left segment of the PivotGridView - * @param {String} title The title - */ - setTitle: function(title) { - this.headerTitleEl.child('span').dom.innerHTML = title; - }, - - /** - * @private - * Renders all column header groups at all levels based on the structure fetched from {@link #getColumnHeaders} - */ - renderGroupColumnHeaders: function() { - var topAxis = this.grid.topAxis; - - topAxis.rendered = false; - topAxis.render(this.innerHd.firstChild); - }, - - /** - * @private - * Overridden to test whether the user is hovering over a group cell, in which case we don't show the menu - */ - isMenuDisabled: function(cellIndex, el) { - return true; - } -});/** - * @class Ext.grid.PivotAxis - * @extends Ext.Component - *

      PivotAxis is a class that supports a {@link Ext.grid.PivotGrid}. Each PivotGrid contains two PivotAxis instances - the left - * axis and the top axis. Each PivotAxis defines an ordered set of dimensions, each of which should correspond to a field in a - * Store's Record (see {@link Ext.grid.PivotGrid} documentation for further explanation).

      - *

      Developers should have little interaction with the PivotAxis instances directly as most of their management is performed by - * the PivotGrid. An exception is the dynamic reconfiguration of axes at run time - to achieve this we use PivotAxis's - * {@link #setDimensions} function and refresh the grid:

      -
      
      -var pivotGrid = new Ext.grid.PivotGrid({
      -    //some PivotGrid config here
      -});
      -
      -//change the left axis dimensions
      -pivotGrid.leftAxis.setDimensions([
      -    {
      -        dataIndex: 'person',
      -        direction: 'DESC',
      -        width    : 100
      -    },
      -    {
      -        dataIndex: 'product',
      -        direction: 'ASC',
      -        width    : 80
      -    }
      -]);
      -
      -pivotGrid.view.refresh(true);
      -
      - * This clears the previous dimensions on the axis and redraws the grid with the new dimensions. - */ -Ext.grid.PivotAxis = Ext.extend(Ext.Component, { - /** - * @cfg {String} orientation One of 'vertical' or 'horizontal'. Defaults to horizontal - */ - orientation: 'horizontal', - - /** - * @cfg {Number} defaultHeaderWidth The width to render each row header that does not have a width specified via - {@link #getRowGroupHeaders}. Defaults to 80. - */ - defaultHeaderWidth: 80, - - /** - * @private - * @cfg {Number} paddingWidth The amount of padding used by each cell. - * TODO: From 4.x onwards this can be removed as it won't be needed. For now it is used to account for the differences between - * the content box and border box measurement models - */ - paddingWidth: 7, - - /** - * Updates the dimensions used by this axis - * @param {Array} dimensions The new dimensions - */ - setDimensions: function(dimensions) { - this.dimensions = dimensions; - }, - - /** - * @private - * Builds the html table that contains the dimensions for this axis. This branches internally between vertical - * and horizontal orientations because the table structure is slightly different in each case - */ - onRender: function(ct, position) { - var rows = this.orientation == 'horizontal' - ? this.renderHorizontalRows() - : this.renderVerticalRows(); - - this.el = Ext.DomHelper.overwrite(ct.dom, {tag: 'table', cn: rows}, true); - }, - - /** - * @private - * Specialised renderer for horizontal oriented axes - * @return {Object} The HTML Domspec for a horizontal oriented axis - */ - renderHorizontalRows: function() { - var headers = this.buildHeaders(), - rowCount = headers.length, - rows = [], - cells, cols, colCount, i, j; - - for (i = 0; i < rowCount; i++) { - cells = []; - cols = headers[i].items; - colCount = cols.length; - - for (j = 0; j < colCount; j++) { - cells.push({ - tag: 'td', - html: cols[j].header, - colspan: cols[j].span - }); - } - - rows[i] = { - tag: 'tr', - cn: cells - }; - } - - return rows; - }, - - /** - * @private - * Specialised renderer for vertical oriented axes - * @return {Object} The HTML Domspec for a vertical oriented axis - */ - renderVerticalRows: function() { - var headers = this.buildHeaders(), - colCount = headers.length, - rowCells = [], - rows = [], - rowCount, col, row, colWidth, i, j; - - for (i = 0; i < colCount; i++) { - col = headers[i]; - colWidth = col.width || 80; - rowCount = col.items.length; - - for (j = 0; j < rowCount; j++) { - row = col.items[j]; - - rowCells[row.start] = rowCells[row.start] || []; - rowCells[row.start].push({ - tag : 'td', - html : row.header, - rowspan: row.span, - width : Ext.isBorderBox ? colWidth : colWidth - this.paddingWidth - }); - } - } - - rowCount = rowCells.length; - for (i = 0; i < rowCount; i++) { - rows[i] = { - tag: 'tr', - cn : rowCells[i] - }; - } - - return rows; - }, - - /** - * @private - * Returns the set of all unique tuples based on the bound store and dimension definitions. - * Internally we construct a new, temporary store to make use of the multi-sort capabilities of Store. In - * 4.x this functionality should have been moved to MixedCollection so this step should not be needed. - * @return {Array} All unique tuples - */ - getTuples: function() { - var newStore = new Ext.data.Store({}); - - newStore.data = this.store.data.clone(); - newStore.fields = this.store.fields; - - var sorters = [], - dimensions = this.dimensions, - length = dimensions.length, - i; - - for (i = 0; i < length; i++) { - sorters.push({ - field : dimensions[i].dataIndex, - direction: dimensions[i].direction || 'ASC' - }); - } - - newStore.sort(sorters); - - var records = newStore.data.items, - hashes = [], - tuples = [], - recData, hash, info, data, key; - - length = records.length; - - for (i = 0; i < length; i++) { - info = this.getRecordInfo(records[i]); - data = info.data; - hash = ""; - - for (key in data) { - hash += data[key] + '---'; - } - - if (hashes.indexOf(hash) == -1) { - hashes.push(hash); - tuples.push(info); - } - } - - newStore.destroy(); - - return tuples; - }, - - /** - * @private - */ - getRecordInfo: function(record) { - var dimensions = this.dimensions, - length = dimensions.length, - data = {}, - dimension, dataIndex, i; - - //get an object containing just the data we are interested in based on the configured dimensions - for (i = 0; i < length; i++) { - dimension = dimensions[i]; - dataIndex = dimension.dataIndex; - - data[dataIndex] = record.get(dataIndex); - } - - //creates a specialised matcher function for a given tuple. The returned function will return - //true if the record passed to it matches the dataIndex values of each dimension in this axis - var createMatcherFunction = function(data) { - return function(record) { - for (var dataIndex in data) { - if (record.get(dataIndex) != data[dataIndex]) { - return false; - } - } - - return true; - }; - }; - - return { - data: data, - matcher: createMatcherFunction(data) - }; - }, - - /** - * @private - * Uses the calculated set of tuples to build an array of headers that can be rendered into a table using rowspan or - * colspan. Basically this takes the set of tuples and spans any cells that run into one another, so if we had dimensions - * of Person and Product and several tuples containing different Products for the same Person, those Products would be - * spanned. - * @return {Array} The headers - */ - buildHeaders: function() { - var tuples = this.getTuples(), - rowCount = tuples.length, - dimensions = this.dimensions, - dimension, - colCount = dimensions.length, - headers = [], - tuple, rows, currentHeader, previousHeader, span, start, isLast, changed, i, j; - - for (i = 0; i < colCount; i++) { - dimension = dimensions[i]; - rows = []; - span = 0; - start = 0; - - for (j = 0; j < rowCount; j++) { - tuple = tuples[j]; - isLast = j == (rowCount - 1); - currentHeader = tuple.data[dimension.dataIndex]; - - /* - * 'changed' indicates that we need to create a new cell. This should be true whenever the cell - * above (previousHeader) is different from this cell, or when the cell on the previous dimension - * changed (e.g. if the current dimension is Product and the previous was Person, we need to start - * a new cell if Product is the same but Person changed, so we check the previous dimension and tuple) - */ - changed = previousHeader != undefined && previousHeader != currentHeader; - if (i > 0 && j > 0) { - changed = changed || tuple.data[dimensions[i-1].dataIndex] != tuples[j-1].data[dimensions[i-1].dataIndex]; - } - - if (changed) { - rows.push({ - header: previousHeader, - span : span, - start : start - }); - - start += span; - span = 0; - } - - if (isLast) { - rows.push({ - header: currentHeader, - span : span + 1, - start : start - }); - - start += span; - span = 0; - } - - previousHeader = currentHeader; - span++; - } - - headers.push({ - items: rows, - width: dimension.width || this.defaultHeaderWidth - }); - - previousHeader = undefined; - } - - return headers; - } -}); -// private -// This is a support class used internally by the Grid components -Ext.grid.HeaderDragZone = Ext.extend(Ext.dd.DragZone, { - maxDragWidth: 120, - - constructor : function(grid, hd, hd2){ - this.grid = grid; - this.view = grid.getView(); - this.ddGroup = "gridHeader" + this.grid.getGridEl().id; - Ext.grid.HeaderDragZone.superclass.constructor.call(this, hd); - if(hd2){ - this.setHandleElId(Ext.id(hd)); - this.setOuterHandleElId(Ext.id(hd2)); - } - this.scroll = false; - }, - - getDragData : function(e){ - var t = Ext.lib.Event.getTarget(e), - h = this.view.findHeaderCell(t); - if(h){ - return {ddel: h.firstChild, header:h}; - } - return false; - }, - - onInitDrag : function(e){ - // keep the value here so we can restore it; - this.dragHeadersDisabled = this.view.headersDisabled; - this.view.headersDisabled = true; - var clone = this.dragData.ddel.cloneNode(true); - clone.id = Ext.id(); - clone.style.width = Math.min(this.dragData.header.offsetWidth,this.maxDragWidth) + "px"; - this.proxy.update(clone); - return true; - }, - - afterValidDrop : function(){ - this.completeDrop(); - }, - - afterInvalidDrop : function(){ - this.completeDrop(); - }, - - completeDrop: function(){ - var v = this.view, - disabled = this.dragHeadersDisabled; - setTimeout(function(){ - v.headersDisabled = disabled; - }, 50); - } -}); - -// private -// This is a support class used internally by the Grid components -Ext.grid.HeaderDropZone = Ext.extend(Ext.dd.DropZone, { - proxyOffsets : [-4, -9], - fly: Ext.Element.fly, - - constructor : function(grid, hd, hd2){ - this.grid = grid; - this.view = grid.getView(); - // split the proxies so they don't interfere with mouse events - this.proxyTop = Ext.DomHelper.append(document.body, { - cls:"col-move-top", html:" " - }, true); - this.proxyBottom = Ext.DomHelper.append(document.body, { - cls:"col-move-bottom", html:" " - }, true); - this.proxyTop.hide = this.proxyBottom.hide = function(){ - this.setLeftTop(-100,-100); - this.setStyle("visibility", "hidden"); - }; - this.ddGroup = "gridHeader" + this.grid.getGridEl().id; - // temporarily disabled - //Ext.dd.ScrollManager.register(this.view.scroller.dom); - Ext.grid.HeaderDropZone.superclass.constructor.call(this, grid.getGridEl().dom); - }, - - getTargetFromEvent : function(e){ - var t = Ext.lib.Event.getTarget(e), - cindex = this.view.findCellIndex(t); - if(cindex !== false){ - return this.view.getHeaderCell(cindex); - } - }, - - nextVisible : function(h){ - var v = this.view, cm = this.grid.colModel; - h = h.nextSibling; - while(h){ - if(!cm.isHidden(v.getCellIndex(h))){ - return h; - } - h = h.nextSibling; - } - return null; - }, - - prevVisible : function(h){ - var v = this.view, cm = this.grid.colModel; - h = h.prevSibling; - while(h){ - if(!cm.isHidden(v.getCellIndex(h))){ - return h; - } - h = h.prevSibling; - } - return null; - }, - - positionIndicator : function(h, n, e){ - var x = Ext.lib.Event.getPageX(e), - r = Ext.lib.Dom.getRegion(n.firstChild), - px, - pt, - py = r.top + this.proxyOffsets[1]; - if((r.right - x) <= (r.right-r.left)/2){ - px = r.right+this.view.borderWidth; - pt = "after"; - }else{ - px = r.left; - pt = "before"; - } - - if(this.grid.colModel.isFixed(this.view.getCellIndex(n))){ - return false; - } - - px += this.proxyOffsets[0]; - this.proxyTop.setLeftTop(px, py); - this.proxyTop.show(); - if(!this.bottomOffset){ - this.bottomOffset = this.view.mainHd.getHeight(); - } - this.proxyBottom.setLeftTop(px, py+this.proxyTop.dom.offsetHeight+this.bottomOffset); - this.proxyBottom.show(); - return pt; - }, - - onNodeEnter : function(n, dd, e, data){ - if(data.header != n){ - this.positionIndicator(data.header, n, e); - } - }, - - onNodeOver : function(n, dd, e, data){ - var result = false; - if(data.header != n){ - result = this.positionIndicator(data.header, n, e); - } - if(!result){ - this.proxyTop.hide(); - this.proxyBottom.hide(); - } - return result ? this.dropAllowed : this.dropNotAllowed; - }, - - onNodeOut : function(n, dd, e, data){ - this.proxyTop.hide(); - this.proxyBottom.hide(); - }, - - onNodeDrop : function(n, dd, e, data){ - var h = data.header; - if(h != n){ - var cm = this.grid.colModel, - x = Ext.lib.Event.getPageX(e), - r = Ext.lib.Dom.getRegion(n.firstChild), - pt = (r.right - x) <= ((r.right-r.left)/2) ? "after" : "before", - oldIndex = this.view.getCellIndex(h), - newIndex = this.view.getCellIndex(n); - if(pt == "after"){ - newIndex++; - } - if(oldIndex < newIndex){ - newIndex--; - } - cm.moveColumn(oldIndex, newIndex); - return true; - } - return false; - } -}); - -Ext.grid.GridView.ColumnDragZone = Ext.extend(Ext.grid.HeaderDragZone, { - - constructor : function(grid, hd){ - Ext.grid.GridView.ColumnDragZone.superclass.constructor.call(this, grid, hd, null); - this.proxy.el.addClass('x-grid3-col-dd'); - }, - - handleMouseDown : function(e){ - }, - - callHandleMouseDown : function(e){ - Ext.grid.GridView.ColumnDragZone.superclass.handleMouseDown.call(this, e); - } -});// private -// This is a support class used internally by the Grid components -Ext.grid.SplitDragZone = Ext.extend(Ext.dd.DDProxy, { - fly: Ext.Element.fly, - - constructor : function(grid, hd, hd2){ - this.grid = grid; - this.view = grid.getView(); - this.proxy = this.view.resizeProxy; - Ext.grid.SplitDragZone.superclass.constructor.call(this, hd, - "gridSplitters" + this.grid.getGridEl().id, { - dragElId : Ext.id(this.proxy.dom), resizeFrame:false - }); - this.setHandleElId(Ext.id(hd)); - this.setOuterHandleElId(Ext.id(hd2)); - this.scroll = false; - }, - - b4StartDrag : function(x, y){ - this.view.headersDisabled = true; - this.proxy.setHeight(this.view.mainWrap.getHeight()); - var w = this.cm.getColumnWidth(this.cellIndex); - var minw = Math.max(w-this.grid.minColumnWidth, 0); - this.resetConstraints(); - this.setXConstraint(minw, 1000); - this.setYConstraint(0, 0); - this.minX = x - minw; - this.maxX = x + 1000; - this.startPos = x; - Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y); - }, - - - handleMouseDown : function(e){ - var ev = Ext.EventObject.setEvent(e); - var t = this.fly(ev.getTarget()); - if(t.hasClass("x-grid-split")){ - this.cellIndex = this.view.getCellIndex(t.dom); - this.split = t.dom; - this.cm = this.grid.colModel; - if(this.cm.isResizable(this.cellIndex) && !this.cm.isFixed(this.cellIndex)){ - Ext.grid.SplitDragZone.superclass.handleMouseDown.apply(this, arguments); - } - } - }, - - endDrag : function(e){ - this.view.headersDisabled = false; - var endX = Math.max(this.minX, Ext.lib.Event.getPageX(e)); - var diff = endX - this.startPos; - this.view.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex)+diff); - }, - - autoOffset : function(){ - this.setDelta(0,0); - } -});/** - * @class Ext.grid.GridDragZone - * @extends Ext.dd.DragZone - *

      A customized implementation of a {@link Ext.dd.DragZone DragZone} which provides default implementations of two of the - * template methods of DragZone to enable dragging of the selected rows of a GridPanel.

      - *

      A cooperating {@link Ext.dd.DropZone DropZone} must be created who's template method implementations of - * {@link Ext.dd.DropZone#onNodeEnter onNodeEnter}, {@link Ext.dd.DropZone#onNodeOver onNodeOver}, - * {@link Ext.dd.DropZone#onNodeOut onNodeOut} and {@link Ext.dd.DropZone#onNodeDrop onNodeDrop}

      are able - * to process the {@link #getDragData data} which is provided. - */ -Ext.grid.GridDragZone = function(grid, config){ - this.view = grid.getView(); - Ext.grid.GridDragZone.superclass.constructor.call(this, this.view.mainBody.dom, config); - this.scroll = false; - this.grid = grid; - this.ddel = document.createElement('div'); - this.ddel.className = 'x-grid-dd-wrap'; -}; - -Ext.extend(Ext.grid.GridDragZone, Ext.dd.DragZone, { - ddGroup : "GridDD", - - /** - *

      The provided implementation of the getDragData method which collects the data to be dragged from the GridPanel on mousedown.

      - *

      This data is available for processing in the {@link Ext.dd.DropZone#onNodeEnter onNodeEnter}, {@link Ext.dd.DropZone#onNodeOver onNodeOver}, - * {@link Ext.dd.DropZone#onNodeOut onNodeOut} and {@link Ext.dd.DropZone#onNodeDrop onNodeDrop} methods of a cooperating {@link Ext.dd.DropZone DropZone}.

      - *

      The data object contains the following properties:

        - *
      • grid : Ext.Grid.GridPanel
        The GridPanel from which the data is being dragged.
      • - *
      • ddel : htmlElement
        An htmlElement which provides the "picture" of the data being dragged.
      • - *
      • rowIndex : Number
        The index of the row which receieved the mousedown gesture which triggered the drag.
      • - *
      • selections : Array
        An Array of the selected Records which are being dragged from the GridPanel.
      • - *

      - */ - getDragData : function(e){ - var t = Ext.lib.Event.getTarget(e); - var rowIndex = this.view.findRowIndex(t); - if(rowIndex !== false){ - var sm = this.grid.selModel; - if(!sm.isSelected(rowIndex) || e.hasModifier()){ - sm.handleMouseDown(this.grid, rowIndex, e); - } - return {grid: this.grid, ddel: this.ddel, rowIndex: rowIndex, selections:sm.getSelections()}; - } - return false; - }, - - /** - *

      The provided implementation of the onInitDrag method. Sets the innerHTML of the drag proxy which provides the "picture" - * of the data being dragged.

      - *

      The innerHTML data is found by calling the owning GridPanel's {@link Ext.grid.GridPanel#getDragDropText getDragDropText}.

      - */ - onInitDrag : function(e){ - var data = this.dragData; - this.ddel.innerHTML = this.grid.getDragDropText(); - this.proxy.update(this.ddel); - // fire start drag? - }, - - /** - * An empty immplementation. Implement this to provide behaviour after a repair of an invalid drop. An implementation might highlight - * the selected rows to show that they have not been dragged. - */ - afterRepair : function(){ - this.dragging = false; - }, - - /** - *

      An empty implementation. Implement this to provide coordinates for the drag proxy to slide back to after an invalid drop.

      - *

      Called before a repair of an invalid drop to get the XY to animate to.

      - * @param {EventObject} e The mouse up event - * @return {Array} The xy location (e.g. [100, 200]) - */ - getRepairXY : function(e, data){ - return false; - }, - - onEndDrag : function(data, e){ - // fire end drag? - }, - - onValidDrop : function(dd, e, id){ - // fire drag drop? - this.hideProxy(); - }, - - beforeInvalidDrop : function(e, id){ - - } -}); -/** - * @class Ext.grid.ColumnModel - * @extends Ext.util.Observable - *

      After the data has been read into the client side cache ({@link Ext.data.Store Store}), - * the ColumnModel is used to configure how and what parts of that data will be displayed in the - * vertical slices (columns) of the grid. The Ext.grid.ColumnModel Class is the default implementation - * of a ColumnModel used by implentations of {@link Ext.grid.GridPanel GridPanel}.

      - *

      Data is mapped into the store's records and then indexed into the ColumnModel using the - * {@link Ext.grid.Column#dataIndex dataIndex}:

      - *
      
      -{data source} == mapping ==> {data store} == {@link Ext.grid.Column#dataIndex dataIndex} ==> {ColumnModel}
      - * 
      - *

      Each {@link Ext.grid.Column Column} in the grid's ColumnModel is configured with a - * {@link Ext.grid.Column#dataIndex dataIndex} to specify how the data within - * each record in the store is indexed into the ColumnModel.

      - *

      There are two ways to initialize the ColumnModel class:

      - *

      Initialization Method 1: an Array

      -
      
      - var colModel = new Ext.grid.ColumnModel([
      -    { header: "Ticker", width: 60, sortable: true},
      -    { header: "Company Name", width: 150, sortable: true, id: 'company'},
      -    { header: "Market Cap.", width: 100, sortable: true},
      -    { header: "$ Sales", width: 100, sortable: true, renderer: money},
      -    { header: "Employees", width: 100, sortable: true, resizable: false}
      - ]);
      - 
      - *

      The ColumnModel may be initialized with an Array of {@link Ext.grid.Column} column configuration - * objects to define the initial layout / display of the columns in the Grid. The order of each - * {@link Ext.grid.Column} column configuration object within the specified Array defines the initial - * order of the column display. A Column's display may be initially hidden using the - * {@link Ext.grid.Column#hidden hidden} config property (and then shown using the column - * header menu). Fields that are not included in the ColumnModel will not be displayable at all.

      - *

      How each column in the grid correlates (maps) to the {@link Ext.data.Record} field in the - * {@link Ext.data.Store Store} the column draws its data from is configured through the - * {@link Ext.grid.Column#dataIndex dataIndex}. If the - * {@link Ext.grid.Column#dataIndex dataIndex} is not explicitly defined (as shown in the - * example above) it will use the column configuration's index in the Array as the index.

      - *

      See {@link Ext.grid.Column} for additional configuration options for each column.

      - *

      Initialization Method 2: an Object

      - *

      In order to use configuration options from Ext.grid.ColumnModel, an Object may be used to - * initialize the ColumnModel. The column configuration Array will be specified in the {@link #columns} - * config property. The {@link #defaults} config property can be used to apply defaults - * for all columns, e.g.:

      
      - var colModel = new Ext.grid.ColumnModel({
      -    columns: [
      -        { header: "Ticker", width: 60, menuDisabled: false},
      -        { header: "Company Name", width: 150, id: 'company'},
      -        { header: "Market Cap."},
      -        { header: "$ Sales", renderer: money},
      -        { header: "Employees", resizable: false}
      -    ],
      -    defaults: {
      -        sortable: true,
      -        menuDisabled: true,
      -        width: 100
      -    },
      -    listeners: {
      -        {@link #hiddenchange}: function(cm, colIndex, hidden) {
      -            saveConfig(colIndex, hidden);
      -        }
      -    }
      -});
      - 
      - *

      In both examples above, the ability to apply a CSS class to all cells in a column (including the - * header) is demonstrated through the use of the {@link Ext.grid.Column#id id} config - * option. This column could be styled by including the following css:

      
      - //add this css *after* the core css is loaded
      -.x-grid3-td-company {
      -    color: red; // entire column will have red font
      -}
      -// modify the header row only, adding an icon to the column header
      -.x-grid3-hd-company {
      -    background: transparent
      -        url(../../resources/images/icons/silk/building.png)
      -        no-repeat 3px 3px ! important;
      -        padding-left:20px;
      -}
      - 
      - * Note that the "Company Name" column could be specified as the - * {@link Ext.grid.GridPanel}.{@link Ext.grid.GridPanel#autoExpandColumn autoExpandColumn}. - * @constructor - * @param {Mixed} config Specify either an Array of {@link Ext.grid.Column} configuration objects or specify - * a configuration Object (see introductory section discussion utilizing Initialization Method 2 above). - */ -Ext.grid.ColumnModel = Ext.extend(Ext.util.Observable, { - /** - * @cfg {Number} defaultWidth (optional) The width of columns which have no {@link #width} - * specified (defaults to 100). This property shall preferably be configured through the - * {@link #defaults} config property. - */ - defaultWidth: 100, - - /** - * @cfg {Boolean} defaultSortable (optional) Default sortable of columns which have no - * sortable specified (defaults to false). This property shall preferably be configured - * through the {@link #defaults} config property. - */ - defaultSortable: false, - - /** - * @cfg {Array} columns An Array of object literals. The config options defined by - * {@link Ext.grid.Column} are the options which may appear in the object literal for each - * individual column definition. - */ - - /** - * @cfg {Object} defaults Object literal which will be used to apply {@link Ext.grid.Column} - * configuration options to all {@link #columns}. Configuration options specified with - * individual {@link Ext.grid.Column column} configs will supersede these {@link #defaults}. - */ - - constructor : function(config) { - /** - * An Array of {@link Ext.grid.Column Column definition} objects representing the configuration - * of this ColumnModel. See {@link Ext.grid.Column} for the configuration properties that may - * be specified. - * @property config - * @type Array - */ - if (config.columns) { - Ext.apply(this, config); - this.setConfig(config.columns, true); - } else { - this.setConfig(config, true); - } - - this.addEvents( - /** - * @event widthchange - * Fires when the width of a column is programmaticially changed using - * {@link #setColumnWidth}. - * Note internal resizing suppresses the event from firing. See also - * {@link Ext.grid.GridPanel}.{@link #columnresize}. - * @param {ColumnModel} this - * @param {Number} columnIndex The column index - * @param {Number} newWidth The new width - */ - "widthchange", - - /** - * @event headerchange - * Fires when the text of a header changes. - * @param {ColumnModel} this - * @param {Number} columnIndex The column index - * @param {String} newText The new header text - */ - "headerchange", - - /** - * @event hiddenchange - * Fires when a column is hidden or "unhidden". - * @param {ColumnModel} this - * @param {Number} columnIndex The column index - * @param {Boolean} hidden true if hidden, false otherwise - */ - "hiddenchange", - - /** - * @event columnmoved - * Fires when a column is moved. - * @param {ColumnModel} this - * @param {Number} oldIndex - * @param {Number} newIndex - */ - "columnmoved", - - /** - * @event configchange - * Fires when the configuration is changed - * @param {ColumnModel} this - */ - "configchange" - ); - - Ext.grid.ColumnModel.superclass.constructor.call(this); - }, - - /** - * Returns the id of the column at the specified index. - * @param {Number} index The column index - * @return {String} the id - */ - getColumnId : function(index) { - return this.config[index].id; - }, - - getColumnAt : function(index) { - return this.config[index]; - }, - - /** - *

      Reconfigures this column model according to the passed Array of column definition objects. - * For a description of the individual properties of a column definition object, see the - * Config Options.

      - *

      Causes the {@link #configchange} event to be fired. A {@link Ext.grid.GridPanel GridPanel} - * using this ColumnModel will listen for this event and refresh its UI automatically.

      - * @param {Array} config Array of Column definition objects. - * @param {Boolean} initial Specify true to bypass cleanup which deletes the totalWidth - * and destroys existing editors. - */ - setConfig : function(config, initial) { - var i, c, len; - - if (!initial) { // cleanup - delete this.totalWidth; - - for (i = 0, len = this.config.length; i < len; i++) { - c = this.config[i]; - - if (c.setEditor) { - //check here, in case we have a special column like a CheckboxSelectionModel - c.setEditor(null); - } - } - } - - // backward compatibility - this.defaults = Ext.apply({ - width: this.defaultWidth, - sortable: this.defaultSortable - }, this.defaults); - - this.config = config; - this.lookup = {}; - - for (i = 0, len = config.length; i < len; i++) { - c = Ext.applyIf(config[i], this.defaults); - - // if no id, create one using column's ordinal position - if (Ext.isEmpty(c.id)) { - c.id = i; - } - - if (!c.isColumn) { - var Cls = Ext.grid.Column.types[c.xtype || 'gridcolumn']; - c = new Cls(c); - config[i] = c; - } - - this.lookup[c.id] = c; - } - - if (!initial) { - this.fireEvent('configchange', this); - } - }, - - /** - * Returns the column for a specified id. - * @param {String} id The column id - * @return {Object} the column - */ - getColumnById : function(id) { - return this.lookup[id]; - }, - - /** - * Returns the index for a specified column id. - * @param {String} id The column id - * @return {Number} the index, or -1 if not found - */ - getIndexById : function(id) { - for (var i = 0, len = this.config.length; i < len; i++) { - if (this.config[i].id == id) { - return i; - } - } - return -1; - }, - - /** - * Moves a column from one position to another. - * @param {Number} oldIndex The index of the column to move. - * @param {Number} newIndex The position at which to reinsert the coolumn. - */ - moveColumn : function(oldIndex, newIndex) { - var config = this.config, - c = config[oldIndex]; - - config.splice(oldIndex, 1); - config.splice(newIndex, 0, c); - this.dataMap = null; - this.fireEvent("columnmoved", this, oldIndex, newIndex); - }, - - /** - * Returns the number of columns. - * @param {Boolean} visibleOnly Optional. Pass as true to only include visible columns. - * @return {Number} - */ - getColumnCount : function(visibleOnly) { - var length = this.config.length, - c = 0, - i; - - if (visibleOnly === true) { - for (i = 0; i < length; i++) { - if (!this.isHidden(i)) { - c++; - } - } - - return c; - } - - return length; - }, - - /** - * Returns the column configs that return true by the passed function that is called - * with (columnConfig, index) -
      
      -// returns an array of column config objects for all hidden columns
      -var columns = grid.getColumnModel().getColumnsBy(function(c){
      -  return c.hidden;
      -});
      -
      - * @param {Function} fn A function which, when passed a {@link Ext.grid.Column Column} object, must - * return true if the column is to be included in the returned Array. - * @param {Object} scope (optional) The scope (this reference) in which the function - * is executed. Defaults to this ColumnModel. - * @return {Array} result - */ - getColumnsBy : function(fn, scope) { - var config = this.config, - length = config.length, - result = [], - i, c; - - for (i = 0; i < length; i++){ - c = config[i]; - - if (fn.call(scope || this, c, i) === true) { - result[result.length] = c; - } - } - - return result; - }, - - /** - * Returns true if the specified column is sortable. - * @param {Number} col The column index - * @return {Boolean} - */ - isSortable : function(col) { - return !!this.config[col].sortable; - }, - - /** - * Returns true if the specified column menu is disabled. - * @param {Number} col The column index - * @return {Boolean} - */ - isMenuDisabled : function(col) { - return !!this.config[col].menuDisabled; - }, - - /** - * Returns the rendering (formatting) function defined for the column. - * @param {Number} col The column index. - * @return {Function} The function used to render the cell. See {@link #setRenderer}. - */ - getRenderer : function(col) { - return this.config[col].renderer || Ext.grid.ColumnModel.defaultRenderer; - }, - - getRendererScope : function(col) { - return this.config[col].scope; - }, - - /** - * Sets the rendering (formatting) function for a column. See {@link Ext.util.Format} for some - * default formatting functions. - * @param {Number} col The column index - * @param {Function} fn The function to use to process the cell's raw data - * to return HTML markup for the grid view. The render function is called with - * the following parameters:
        - *
      • value : Object

        The data value for the cell.

      • - *
      • metadata : Object

        An object in which you may set the following attributes:

          - *
        • css : String

          A CSS class name to add to the cell's TD element.

        • - *
        • attr : String

          An HTML attribute definition string to apply to the data container element within the table cell - * (e.g. 'style="color:red;"').

      • - *
      • record : Ext.data.record

        The {@link Ext.data.Record} from which the data was extracted.

      • - *
      • rowIndex : Number

        Row index

      • - *
      • colIndex : Number

        Column index

      • - *
      • store : Ext.data.Store

        The {@link Ext.data.Store} object from which the Record was extracted.

      - */ - setRenderer : function(col, fn) { - this.config[col].renderer = fn; - }, - - /** - * Returns the width for the specified column. - * @param {Number} col The column index - * @return {Number} - */ - getColumnWidth : function(col) { - var width = this.config[col].width; - if(typeof width != 'number'){ - width = this.defaultWidth; - } - return width; - }, - - /** - * Sets the width for a column. - * @param {Number} col The column index - * @param {Number} width The new width - * @param {Boolean} suppressEvent True to suppress firing the {@link #widthchange} - * event. Defaults to false. - */ - setColumnWidth : function(col, width, suppressEvent) { - this.config[col].width = width; - this.totalWidth = null; - - if (!suppressEvent) { - this.fireEvent("widthchange", this, col, width); - } - }, - - /** - * Returns the total width of all columns. - * @param {Boolean} includeHidden True to include hidden column widths - * @return {Number} - */ - getTotalWidth : function(includeHidden) { - if (!this.totalWidth) { - this.totalWidth = 0; - for (var i = 0, len = this.config.length; i < len; i++) { - if (includeHidden || !this.isHidden(i)) { - this.totalWidth += this.getColumnWidth(i); - } - } - } - return this.totalWidth; - }, - - /** - * Returns the header for the specified column. - * @param {Number} col The column index - * @return {String} - */ - getColumnHeader : function(col) { - return this.config[col].header; - }, - - /** - * Sets the header for a column. - * @param {Number} col The column index - * @param {String} header The new header - */ - setColumnHeader : function(col, header) { - this.config[col].header = header; - this.fireEvent("headerchange", this, col, header); - }, - - /** - * Returns the tooltip for the specified column. - * @param {Number} col The column index - * @return {String} - */ - getColumnTooltip : function(col) { - return this.config[col].tooltip; - }, - /** - * Sets the tooltip for a column. - * @param {Number} col The column index - * @param {String} tooltip The new tooltip - */ - setColumnTooltip : function(col, tooltip) { - this.config[col].tooltip = tooltip; - }, - - /** - * Returns the dataIndex for the specified column. -
      
      -// Get field name for the column
      -var fieldName = grid.getColumnModel().getDataIndex(columnIndex);
      -
      - * @param {Number} col The column index - * @return {String} The column's dataIndex - */ - getDataIndex : function(col) { - return this.config[col].dataIndex; - }, - - /** - * Sets the dataIndex for a column. - * @param {Number} col The column index - * @param {String} dataIndex The new dataIndex - */ - setDataIndex : function(col, dataIndex) { - this.config[col].dataIndex = dataIndex; - }, - - /** - * Finds the index of the first matching column for the given dataIndex. - * @param {String} col The dataIndex to find - * @return {Number} The column index, or -1 if no match was found - */ - findColumnIndex : function(dataIndex) { - var c = this.config; - for(var i = 0, len = c.length; i < len; i++){ - if(c[i].dataIndex == dataIndex){ - return i; - } - } - return -1; - }, - - /** - * Returns true if the cell is editable. -
      
      -var store = new Ext.data.Store({...});
      -var colModel = new Ext.grid.ColumnModel({
      -  columns: [...],
      -  isCellEditable: function(col, row) {
      -    var record = store.getAt(row);
      -    if (record.get('readonly')) { // replace with your condition
      -      return false;
      -    }
      -    return Ext.grid.ColumnModel.prototype.isCellEditable.call(this, col, row);
      -  }
      -});
      -var grid = new Ext.grid.GridPanel({
      -  store: store,
      -  colModel: colModel,
      -  ...
      -});
      -
      - * @param {Number} colIndex The column index - * @param {Number} rowIndex The row index - * @return {Boolean} - */ - isCellEditable : function(colIndex, rowIndex) { - var c = this.config[colIndex], - ed = c.editable; - - //force boolean - return !!(ed || (!Ext.isDefined(ed) && c.editor)); - }, - - /** - * Returns the editor defined for the cell/column. - * @param {Number} colIndex The column index - * @param {Number} rowIndex The row index - * @return {Ext.Editor} The {@link Ext.Editor Editor} that was created to wrap - * the {@link Ext.form.Field Field} used to edit the cell. - */ - getCellEditor : function(colIndex, rowIndex) { - return this.config[colIndex].getCellEditor(rowIndex); - }, - - /** - * Sets if a column is editable. - * @param {Number} col The column index - * @param {Boolean} editable True if the column is editable - */ - setEditable : function(col, editable) { - this.config[col].editable = editable; - }, - - /** - * Returns true if the column is {@link Ext.grid.Column#hidden hidden}, - * false otherwise. - * @param {Number} colIndex The column index - * @return {Boolean} - */ - isHidden : function(colIndex) { - return !!this.config[colIndex].hidden; // ensure returns boolean - }, - - /** - * Returns true if the column is {@link Ext.grid.Column#fixed fixed}, - * false otherwise. - * @param {Number} colIndex The column index - * @return {Boolean} - */ - isFixed : function(colIndex) { - return !!this.config[colIndex].fixed; - }, - - /** - * Returns true if the column can be resized - * @return {Boolean} - */ - isResizable : function(colIndex) { - return colIndex >= 0 && this.config[colIndex].resizable !== false && this.config[colIndex].fixed !== true; - }, - - /** - * Sets if a column is hidden. -
      
      -myGrid.getColumnModel().setHidden(0, true); // hide column 0 (0 = the first column).
      -
      - * @param {Number} colIndex The column index - * @param {Boolean} hidden True if the column is hidden - */ - setHidden : function(colIndex, hidden) { - var c = this.config[colIndex]; - if(c.hidden !== hidden){ - c.hidden = hidden; - this.totalWidth = null; - this.fireEvent("hiddenchange", this, colIndex, hidden); - } - }, - - /** - * Sets the editor for a column and destroys the prior editor. - * @param {Number} col The column index - * @param {Object} editor The editor object - */ - setEditor : function(col, editor) { - this.config[col].setEditor(editor); - }, - - /** - * Destroys this column model by purging any event listeners. Destroys and dereferences all Columns. - */ - destroy : function() { - var length = this.config.length, - i = 0; - - for (; i < length; i++){ - this.config[i].destroy(); // Column's destroy encapsulates all cleanup. - } - delete this.config; - delete this.lookup; - this.purgeListeners(); - }, - - /** - * @private - * Setup any saved state for the column, ensures that defaults are applied. - */ - setState : function(col, state) { - state = Ext.applyIf(state, this.defaults); - Ext.apply(this.config[col], state); - } -}); - -// private -Ext.grid.ColumnModel.defaultRenderer = function(value) { - if (typeof value == "string" && value.length < 1) { - return " "; - } - return value; -};/** - * @class Ext.grid.AbstractSelectionModel - * @extends Ext.util.Observable - * Abstract base class for grid SelectionModels. It provides the interface that should be - * implemented by descendant classes. This class should not be directly instantiated. - * @constructor - */ -Ext.grid.AbstractSelectionModel = Ext.extend(Ext.util.Observable, { - /** - * The GridPanel for which this SelectionModel is handling selection. Read-only. - * @type Object - * @property grid - */ - - constructor : function(){ - this.locked = false; - Ext.grid.AbstractSelectionModel.superclass.constructor.call(this); - }, - - /** @ignore Called by the grid automatically. Do not call directly. */ - init : function(grid){ - this.grid = grid; - if(this.lockOnInit){ - delete this.lockOnInit; - this.locked = false; - this.lock(); - } - this.initEvents(); - }, - - /** - * Locks the selections. - */ - lock : function(){ - if(!this.locked){ - this.locked = true; - // If the grid has been set, then the view is already initialized. - var g = this.grid; - if(g){ - g.getView().on({ - scope: this, - beforerefresh: this.sortUnLock, - refresh: this.sortLock - }); - }else{ - this.lockOnInit = true; - } - } - }, - - // set the lock states before and after a view refresh - sortLock : function() { - this.locked = true; - }, - - // set the lock states before and after a view refresh - sortUnLock : function() { - this.locked = false; - }, - - /** - * Unlocks the selections. - */ - unlock : function(){ - if(this.locked){ - this.locked = false; - var g = this.grid, - gv; - - // If the grid has been set, then the view is already initialized. - if(g){ - gv = g.getView(); - gv.un('beforerefresh', this.sortUnLock, this); - gv.un('refresh', this.sortLock, this); - }else{ - delete this.lockOnInit; - } - } - }, - - /** - * Returns true if the selections are locked. - * @return {Boolean} - */ - isLocked : function(){ - return this.locked; - }, - - destroy: function(){ - this.unlock(); - this.purgeListeners(); - } -});/** - * @class Ext.grid.RowSelectionModel - * @extends Ext.grid.AbstractSelectionModel - * The default SelectionModel used by {@link Ext.grid.GridPanel}. - * It supports multiple selections and keyboard selection/navigation. The objects stored - * as selections and returned by {@link #getSelected}, and {@link #getSelections} are - * the {@link Ext.data.Record Record}s which provide the data for the selected rows. - * @constructor - * @param {Object} config - */ -Ext.grid.RowSelectionModel = Ext.extend(Ext.grid.AbstractSelectionModel, { - /** - * @cfg {Boolean} singleSelect - * true to allow selection of only one row at a time (defaults to false - * allowing multiple selections) - */ - singleSelect : false, - - constructor : function(config){ - Ext.apply(this, config); - this.selections = new Ext.util.MixedCollection(false, function(o){ - return o.id; - }); - - this.last = false; - this.lastActive = false; - - this.addEvents( - /** - * @event selectionchange - * Fires when the selection changes - * @param {SelectionModel} this - */ - 'selectionchange', - /** - * @event beforerowselect - * Fires before a row is selected, return false to cancel the selection. - * @param {SelectionModel} this - * @param {Number} rowIndex The index to be selected - * @param {Boolean} keepExisting False if other selections will be cleared - * @param {Record} record The record to be selected - */ - 'beforerowselect', - /** - * @event rowselect - * Fires when a row is selected. - * @param {SelectionModel} this - * @param {Number} rowIndex The selected index - * @param {Ext.data.Record} r The selected record - */ - 'rowselect', - /** - * @event rowdeselect - * Fires when a row is deselected. To prevent deselection - * {@link Ext.grid.AbstractSelectionModel#lock lock the selections}. - * @param {SelectionModel} this - * @param {Number} rowIndex - * @param {Record} record - */ - 'rowdeselect' - ); - Ext.grid.RowSelectionModel.superclass.constructor.call(this); - }, - - /** - * @cfg {Boolean} moveEditorOnEnter - * false to turn off moving the editor to the next row down when the enter key is pressed - * or the next row up when shift + enter keys are pressed. - */ - // private - initEvents : function(){ - - if(!this.grid.enableDragDrop && !this.grid.enableDrag){ - this.grid.on('rowmousedown', this.handleMouseDown, this); - } - - this.rowNav = new Ext.KeyNav(this.grid.getGridEl(), { - up: this.onKeyPress, - down: this.onKeyPress, - scope: this - }); - - this.grid.getView().on({ - scope: this, - refresh: this.onRefresh, - rowupdated: this.onRowUpdated, - rowremoved: this.onRemove - }); - }, - - onKeyPress : function(e, name){ - var up = name == 'up', - method = up ? 'selectPrevious' : 'selectNext', - add = up ? -1 : 1, - last; - if(!e.shiftKey || this.singleSelect){ - this[method](false); - }else if(this.last !== false && this.lastActive !== false){ - last = this.last; - this.selectRange(this.last, this.lastActive + add); - this.grid.getView().focusRow(this.lastActive); - if(last !== false){ - this.last = last; - } - }else{ - this.selectFirstRow(); - } - }, - - // private - onRefresh : function(){ - var ds = this.grid.store, - s = this.getSelections(), - i = 0, - len = s.length, - index, r; - - this.silent = true; - this.clearSelections(true); - for(; i < len; i++){ - r = s[i]; - if((index = ds.indexOfId(r.id)) != -1){ - this.selectRow(index, true); - } - } - if(s.length != this.selections.getCount()){ - this.fireEvent('selectionchange', this); - } - this.silent = false; - }, - - // private - onRemove : function(v, index, r){ - if(this.selections.remove(r) !== false){ - this.fireEvent('selectionchange', this); - } - }, - - // private - onRowUpdated : function(v, index, r){ - if(this.isSelected(r)){ - v.onRowSelect(index); - } - }, - - /** - * Select records. - * @param {Array} records The records to select - * @param {Boolean} keepExisting (optional) true to keep existing selections - */ - selectRecords : function(records, keepExisting){ - if(!keepExisting){ - this.clearSelections(); - } - var ds = this.grid.store, - i = 0, - len = records.length; - for(; i < len; i++){ - this.selectRow(ds.indexOf(records[i]), true); - } - }, - - /** - * Gets the number of selected rows. - * @return {Number} - */ - getCount : function(){ - return this.selections.length; - }, - - /** - * Selects the first row in the grid. - */ - selectFirstRow : function(){ - this.selectRow(0); - }, - - /** - * Select the last row. - * @param {Boolean} keepExisting (optional) true to keep existing selections - */ - selectLastRow : function(keepExisting){ - this.selectRow(this.grid.store.getCount() - 1, keepExisting); - }, - - /** - * Selects the row immediately following the last selected row. - * @param {Boolean} keepExisting (optional) true to keep existing selections - * @return {Boolean} true if there is a next row, else false - */ - selectNext : function(keepExisting){ - if(this.hasNext()){ - this.selectRow(this.last+1, keepExisting); - this.grid.getView().focusRow(this.last); - return true; - } - return false; - }, - - /** - * Selects the row that precedes the last selected row. - * @param {Boolean} keepExisting (optional) true to keep existing selections - * @return {Boolean} true if there is a previous row, else false - */ - selectPrevious : function(keepExisting){ - if(this.hasPrevious()){ - this.selectRow(this.last-1, keepExisting); - this.grid.getView().focusRow(this.last); - return true; - } - return false; - }, - - /** - * Returns true if there is a next record to select - * @return {Boolean} - */ - hasNext : function(){ - return this.last !== false && (this.last+1) < this.grid.store.getCount(); - }, - - /** - * Returns true if there is a previous record to select - * @return {Boolean} - */ - hasPrevious : function(){ - return !!this.last; - }, - - - /** - * Returns the selected records - * @return {Array} Array of selected records - */ - getSelections : function(){ - return [].concat(this.selections.items); - }, - - /** - * Returns the first selected record. - * @return {Record} - */ - getSelected : function(){ - return this.selections.itemAt(0); - }, - - /** - * Calls the passed function with each selection. If the function returns - * false, iteration is stopped and this function returns - * false. Otherwise it returns true. - * @param {Function} fn The function to call upon each iteration. It is passed the selected {@link Ext.data.Record Record}. - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this RowSelectionModel. - * @return {Boolean} true if all selections were iterated - */ - each : function(fn, scope){ - var s = this.getSelections(), - i = 0, - len = s.length; - - for(; i < len; i++){ - if(fn.call(scope || this, s[i], i) === false){ - return false; - } - } - return true; - }, - - /** - * Clears all selections if the selection model - * {@link Ext.grid.AbstractSelectionModel#isLocked is not locked}. - * @param {Boolean} fast (optional) true to bypass the - * conditional checks and events described in {@link #deselectRow}. - */ - clearSelections : function(fast){ - if(this.isLocked()){ - return; - } - if(fast !== true){ - var ds = this.grid.store, - s = this.selections; - s.each(function(r){ - this.deselectRow(ds.indexOfId(r.id)); - }, this); - s.clear(); - }else{ - this.selections.clear(); - } - this.last = false; - }, - - - /** - * Selects all rows if the selection model - * {@link Ext.grid.AbstractSelectionModel#isLocked is not locked}. - */ - selectAll : function(){ - if(this.isLocked()){ - return; - } - this.selections.clear(); - for(var i = 0, len = this.grid.store.getCount(); i < len; i++){ - this.selectRow(i, true); - } - }, - - /** - * Returns true if there is a selection. - * @return {Boolean} - */ - hasSelection : function(){ - return this.selections.length > 0; - }, - - /** - * Returns true if the specified row is selected. - * @param {Number/Record} index The record or index of the record to check - * @return {Boolean} - */ - isSelected : function(index){ - var r = Ext.isNumber(index) ? this.grid.store.getAt(index) : index; - return (r && this.selections.key(r.id) ? true : false); - }, - - /** - * Returns true if the specified record id is selected. - * @param {String} id The id of record to check - * @return {Boolean} - */ - isIdSelected : function(id){ - return (this.selections.key(id) ? true : false); - }, - - // private - handleMouseDown : function(g, rowIndex, e){ - if(e.button !== 0 || this.isLocked()){ - return; - } - var view = this.grid.getView(); - if(e.shiftKey && !this.singleSelect && this.last !== false){ - var last = this.last; - this.selectRange(last, rowIndex, e.ctrlKey); - this.last = last; // reset the last - view.focusRow(rowIndex); - }else{ - var isSelected = this.isSelected(rowIndex); - if(e.ctrlKey && isSelected){ - this.deselectRow(rowIndex); - }else if(!isSelected || this.getCount() > 1){ - this.selectRow(rowIndex, e.ctrlKey || e.shiftKey); - view.focusRow(rowIndex); - } - } - }, - - /** - * Selects multiple rows. - * @param {Array} rows Array of the indexes of the row to select - * @param {Boolean} keepExisting (optional) true to keep - * existing selections (defaults to false) - */ - selectRows : function(rows, keepExisting){ - if(!keepExisting){ - this.clearSelections(); - } - for(var i = 0, len = rows.length; i < len; i++){ - this.selectRow(rows[i], true); - } - }, - - /** - * Selects a range of rows if the selection model - * {@link Ext.grid.AbstractSelectionModel#isLocked is not locked}. - * All rows in between startRow and endRow are also selected. - * @param {Number} startRow The index of the first row in the range - * @param {Number} endRow The index of the last row in the range - * @param {Boolean} keepExisting (optional) True to retain existing selections - */ - selectRange : function(startRow, endRow, keepExisting){ - var i; - if(this.isLocked()){ - return; - } - if(!keepExisting){ - this.clearSelections(); - } - if(startRow <= endRow){ - for(i = startRow; i <= endRow; i++){ - this.selectRow(i, true); - } - }else{ - for(i = startRow; i >= endRow; i--){ - this.selectRow(i, true); - } - } - }, - - /** - * Deselects a range of rows if the selection model - * {@link Ext.grid.AbstractSelectionModel#isLocked is not locked}. - * All rows in between startRow and endRow are also deselected. - * @param {Number} startRow The index of the first row in the range - * @param {Number} endRow The index of the last row in the range - */ - deselectRange : function(startRow, endRow, preventViewNotify){ - if(this.isLocked()){ - return; - } - for(var i = startRow; i <= endRow; i++){ - this.deselectRow(i, preventViewNotify); - } - }, - - /** - * Selects a row. Before selecting a row, checks if the selection model - * {@link Ext.grid.AbstractSelectionModel#isLocked is locked} and fires the - * {@link #beforerowselect} event. If these checks are satisfied the row - * will be selected and followed up by firing the {@link #rowselect} and - * {@link #selectionchange} events. - * @param {Number} row The index of the row to select - * @param {Boolean} keepExisting (optional) true to keep existing selections - * @param {Boolean} preventViewNotify (optional) Specify true to - * prevent notifying the view (disables updating the selected appearance) - */ - selectRow : function(index, keepExisting, preventViewNotify){ - if(this.isLocked() || (index < 0 || index >= this.grid.store.getCount()) || (keepExisting && this.isSelected(index))){ - return; - } - var r = this.grid.store.getAt(index); - if(r && this.fireEvent('beforerowselect', this, index, keepExisting, r) !== false){ - if(!keepExisting || this.singleSelect){ - this.clearSelections(); - } - this.selections.add(r); - this.last = this.lastActive = index; - if(!preventViewNotify){ - this.grid.getView().onRowSelect(index); - } - if(!this.silent){ - this.fireEvent('rowselect', this, index, r); - this.fireEvent('selectionchange', this); - } - } - }, - - /** - * Deselects a row. Before deselecting a row, checks if the selection model - * {@link Ext.grid.AbstractSelectionModel#isLocked is locked}. - * If this check is satisfied the row will be deselected and followed up by - * firing the {@link #rowdeselect} and {@link #selectionchange} events. - * @param {Number} row The index of the row to deselect - * @param {Boolean} preventViewNotify (optional) Specify true to - * prevent notifying the view (disables updating the selected appearance) - */ - deselectRow : function(index, preventViewNotify){ - if(this.isLocked()){ - return; - } - if(this.last == index){ - this.last = false; - } - if(this.lastActive == index){ - this.lastActive = false; - } - var r = this.grid.store.getAt(index); - if(r){ - this.selections.remove(r); - if(!preventViewNotify){ - this.grid.getView().onRowDeselect(index); - } - this.fireEvent('rowdeselect', this, index, r); - this.fireEvent('selectionchange', this); - } - }, - - // private - acceptsNav : function(row, col, cm){ - return !cm.isHidden(col) && cm.isCellEditable(col, row); - }, - - // private - onEditorKey : function(field, e){ - var k = e.getKey(), - newCell, - g = this.grid, - last = g.lastEdit, - ed = g.activeEditor, - shift = e.shiftKey, - ae, last, r, c; - - if(k == e.TAB){ - e.stopEvent(); - ed.completeEdit(); - if(shift){ - newCell = g.walkCells(ed.row, ed.col-1, -1, this.acceptsNav, this); - }else{ - newCell = g.walkCells(ed.row, ed.col+1, 1, this.acceptsNav, this); - } - }else if(k == e.ENTER){ - if(this.moveEditorOnEnter !== false){ - if(shift){ - newCell = g.walkCells(last.row - 1, last.col, -1, this.acceptsNav, this); - }else{ - newCell = g.walkCells(last.row + 1, last.col, 1, this.acceptsNav, this); - } - } - } - if(newCell){ - r = newCell[0]; - c = newCell[1]; - - this.onEditorSelect(r, last.row); - - if(g.isEditor && g.editing){ // *** handle tabbing while editorgrid is in edit mode - ae = g.activeEditor; - if(ae && ae.field.triggerBlur){ - // *** if activeEditor is a TriggerField, explicitly call its triggerBlur() method - ae.field.triggerBlur(); - } - } - g.startEditing(r, c); - } - }, - - onEditorSelect: function(row, lastRow){ - if(lastRow != row){ - this.selectRow(row); // *** highlight newly-selected cell and update selection - } - }, - - destroy : function(){ - Ext.destroy(this.rowNav); - this.rowNav = null; - Ext.grid.RowSelectionModel.superclass.destroy.call(this); - } -}); -/** - * @class Ext.grid.Column - *

      This class encapsulates column configuration data to be used in the initialization of a - * {@link Ext.grid.ColumnModel ColumnModel}.

      - *

      While subclasses are provided to render data in different ways, this class renders a passed - * data field unchanged and is usually used for textual columns.

      - */ -Ext.grid.Column = Ext.extend(Ext.util.Observable, { - /** - * @cfg {Boolean} editable Optional. Defaults to true, enabling the configured - * {@link #editor}. Set to false to initially disable editing on this column. - * The initial configuration may be dynamically altered using - * {@link Ext.grid.ColumnModel}.{@link Ext.grid.ColumnModel#setEditable setEditable()}. - */ - /** - * @cfg {String} id Optional. A name which identifies this column (defaults to the column's initial - * ordinal position.) The id is used to create a CSS class name which is applied to all - * table cells (including headers) in that column (in this context the id does not need to be - * unique). The class name takes the form of
      x-grid3-td-id
      - * Header cells will also receive this class name, but will also have the class
      x-grid3-hd
      - * So, to target header cells, use CSS selectors such as:
      .x-grid3-hd-row .x-grid3-td-id
      - * The {@link Ext.grid.GridPanel#autoExpandColumn} grid config option references the column via this - * unique identifier. - */ - /** - * @cfg {String} header Optional. The header text to be used as innerHTML - * (html tags are accepted) to display in the Grid view. Note: to - * have a clickable header with no text displayed use '&#160;'. - */ - /** - * @cfg {Boolean} groupable Optional. If the grid is being rendered by an {@link Ext.grid.GroupingView}, this option - * may be used to disable the header menu item to group by the column selected. Defaults to true, - * which enables the header menu group option. Set to false to disable (but still show) the - * group option in the header menu for the column. See also {@link #groupName}. - */ - /** - * @cfg {String} groupName Optional. If the grid is being rendered by an {@link Ext.grid.GroupingView}, this option - * may be used to specify the text with which to prefix the group field value in the group header line. - * See also {@link #groupRenderer} and - * {@link Ext.grid.GroupingView}.{@link Ext.grid.GroupingView#showGroupName showGroupName}. - */ - /** - * @cfg {Function} groupRenderer

      Optional. If the grid is being rendered by an {@link Ext.grid.GroupingView}, this option - * may be used to specify the function used to format the grouping field value for display in the group - * {@link #groupName header}. If a groupRenderer is not specified, the configured - * {@link #renderer} will be called; if a {@link #renderer} is also not specified - * the new value of the group field will be used.

      - *

      The called function (either the groupRenderer or {@link #renderer}) will be - * passed the following parameters: - *

        - *
      • v : Object

        The new value of the group field.

      • - *
      • unused : undefined

        Unused parameter.

      • - *
      • r : Ext.data.Record

        The Record providing the data - * for the row which caused group change.

      • - *
      • rowIndex : Number

        The row index of the Record which caused group change.

      • - *
      • colIndex : Number

        The column index of the group field.

      • - *
      • ds : Ext.data.Store

        The Store which is providing the data Model.

      • - *

      - *

      The function should return a string value.

      - */ - /** - * @cfg {String} emptyGroupText Optional. If the grid is being rendered by an {@link Ext.grid.GroupingView}, this option - * may be used to specify the text to display when there is an empty group value. Defaults to the - * {@link Ext.grid.GroupingView}.{@link Ext.grid.GroupingView#emptyGroupText emptyGroupText}. - */ - /** - * @cfg {String} dataIndex

      Required. The name of the field in the - * grid's {@link Ext.data.Store}'s {@link Ext.data.Record} definition from - * which to draw the column's value.

      - */ - /** - * @cfg {Number} width - * Optional. The initial width in pixels of the column. - * The width of each column can also be affected if any of the following are configured: - *
        - *
      • {@link Ext.grid.GridPanel}.{@link Ext.grid.GridPanel#autoExpandColumn autoExpandColumn}
      • - *
      • {@link Ext.grid.GridView}.{@link Ext.grid.GridView#forceFit forceFit} - *
        - *

        By specifying forceFit:true, {@link #fixed non-fixed width} columns will be - * re-proportioned (based on the relative initial widths) to fill the width of the grid so - * that no horizontal scrollbar is shown.

        - *
      • - *
      • {@link Ext.grid.GridView}.{@link Ext.grid.GridView#autoFill autoFill}
      • - *
      • {@link Ext.grid.GridPanel}.{@link Ext.grid.GridPanel#minColumnWidth minColumnWidth}
      • - *

        Note: when the width of each column is determined, a space on the right side - * is reserved for the vertical scrollbar. The - * {@link Ext.grid.GridView}.{@link Ext.grid.GridView#scrollOffset scrollOffset} - * can be modified to reduce or eliminate the reserved offset.

        - */ - /** - * @cfg {Boolean} sortable Optional. true if sorting is to be allowed on this column. - * Defaults to the value of the {@link Ext.grid.ColumnModel#defaultSortable} property. - * Whether local/remote sorting is used is specified in {@link Ext.data.Store#remoteSort}. - */ - /** - * @cfg {Boolean} fixed Optional. true if the column width cannot be changed. Defaults to false. - */ - /** - * @cfg {Boolean} resizable Optional. false to disable column resizing. Defaults to true. - */ - /** - * @cfg {Boolean} menuDisabled Optional. true to disable the column menu. Defaults to false. - */ - /** - * @cfg {Boolean} hidden - * Optional. true to initially hide this column. Defaults to false. - * A hidden column {@link Ext.grid.GridPanel#enableColumnHide may be shown via the header row menu}. - * If a column is never to be shown, simply do not include this column in the Column Model at all. - */ - /** - * @cfg {String} tooltip Optional. A text string to use as the column header's tooltip. If Quicktips - * are enabled, this value will be used as the text of the quick tip, otherwise it will be set as the - * header's HTML title attribute. Defaults to ''. - */ - /** - * @cfg {Mixed} renderer - *

        For an alternative to specifying a renderer see {@link #xtype}

        - *

        Optional. A renderer is an 'interceptor' method which can be used transform data (value, - * appearance, etc.) before it is rendered). This may be specified in either of three ways: - *

          - *
        • A renderer function used to return HTML markup for a cell given the cell's data value.
        • - *
        • A string which references a property name of the {@link Ext.util.Format} class which - * provides a renderer function.
        • - *
        • An object specifying both the renderer function, and its execution scope (this - * reference) e.g.:
          
          -{
          -    fn: this.gridRenderer,
          -    scope: this
          -}
          -
        - * If not specified, the default renderer uses the raw data value.

        - *

        For information about the renderer function (passed parameters, etc.), see - * {@link Ext.grid.ColumnModel#setRenderer}. An example of specifying renderer function inline:

        
        -var companyColumn = {
        -   header: 'Company Name',
        -   dataIndex: 'company',
        -   renderer: function(value, metaData, record, rowIndex, colIndex, store) {
        -      // provide the logic depending on business rules
        -      // name of your own choosing to manipulate the cell depending upon
        -      // the data in the underlying Record object.
        -      if (value == 'whatever') {
        -          //metaData.css : String : A CSS class name to add to the TD element of the cell.
        -          //metaData.attr : String : An html attribute definition string to apply to
        -          //                         the data container element within the table
        -          //                         cell (e.g. 'style="color:red;"').
        -          metaData.css = 'name-of-css-class-you-will-define';
        -      }
        -      return value;
        -   }
        -}
        -     * 
        - * See also {@link #scope}. - */ - /** - * @cfg {String} xtype Optional. A String which references a predefined {@link Ext.grid.Column} subclass - * type which is preconfigured with an appropriate {@link #renderer} to be easily - * configured into a ColumnModel. The predefined {@link Ext.grid.Column} subclass types are: - *
          - *
        • gridcolumn : {@link Ext.grid.Column} (Default)

        • - *
        • booleancolumn : {@link Ext.grid.BooleanColumn}

        • - *
        • numbercolumn : {@link Ext.grid.NumberColumn}

        • - *
        • datecolumn : {@link Ext.grid.DateColumn}

        • - *
        • templatecolumn : {@link Ext.grid.TemplateColumn}

        • - *
        - *

        Configuration properties for the specified xtype may be specified with - * the Column configuration properties, for example:

        - *
        
        -var grid = new Ext.grid.GridPanel({
        -    ...
        -    columns: [{
        -        header: 'Last Updated',
        -        dataIndex: 'lastChange',
        -        width: 85,
        -        sortable: true,
        -        //renderer: Ext.util.Format.dateRenderer('m/d/Y'),
        -        xtype: 'datecolumn', // use xtype instead of renderer
        -        format: 'M/d/Y' // configuration property for {@link Ext.grid.DateColumn}
        -    }, {
        -        ...
        -    }]
        -});
        -     * 
        - */ - /** - * @cfg {Object} scope Optional. The scope (this reference) in which to execute the - * renderer. Defaults to the Column configuration object. - */ - /** - * @cfg {String} align Optional. Set the CSS text-align property of the column. Defaults to undefined. - */ - /** - * @cfg {String} css Optional. An inline style definition string which is applied to all table cells in the column - * (excluding headers). Defaults to undefined. - */ - /** - * @cfg {Boolean} hideable Optional. Specify as false to prevent the user from hiding this column - * (defaults to true). To disallow column hiding globally for all columns in the grid, use - * {@link Ext.grid.GridPanel#enableColumnHide} instead. - */ - /** - * @cfg {Ext.form.Field} editor Optional. The {@link Ext.form.Field} to use when editing values in this column - * if editing is supported by the grid. See {@link #editable} also. - */ - - /** - * @private - * @cfg {Boolean} isColumn - * Used by ColumnModel setConfig method to avoid reprocessing a Column - * if isColumn is not set ColumnModel will recreate a new Ext.grid.Column - * Defaults to true. - */ - isColumn : true, - - constructor : function(config){ - Ext.apply(this, config); - - if(Ext.isString(this.renderer)){ - this.renderer = Ext.util.Format[this.renderer]; - }else if(Ext.isObject(this.renderer)){ - this.scope = this.renderer.scope; - this.renderer = this.renderer.fn; - } - if(!this.scope){ - this.scope = this; - } - - var ed = this.editor; - delete this.editor; - this.setEditor(ed); - this.addEvents( - /** - * @event click - * Fires when this Column is clicked. - * @param {Column} this - * @param {Grid} The owning GridPanel - * @param {Number} rowIndex - * @param {Ext.EventObject} e - */ - 'click', - /** - * @event contextmenu - * Fires when this Column is right clicked. - * @param {Column} this - * @param {Grid} The owning GridPanel - * @param {Number} rowIndex - * @param {Ext.EventObject} e - */ - 'contextmenu', - /** - * @event dblclick - * Fires when this Column is double clicked. - * @param {Column} this - * @param {Grid} The owning GridPanel - * @param {Number} rowIndex - * @param {Ext.EventObject} e - */ - 'dblclick', - /** - * @event mousedown - * Fires when this Column receives a mousedown event. - * @param {Column} this - * @param {Grid} The owning GridPanel - * @param {Number} rowIndex - * @param {Ext.EventObject} e - */ - 'mousedown' - ); - Ext.grid.Column.superclass.constructor.call(this); - }, - - /** - * @private - * Process and refire events routed from the GridView's processEvent method. - * Returns the event handler's status to allow cancelling of GridView's bubbling process. - */ - processEvent : function(name, e, grid, rowIndex, colIndex){ - return this.fireEvent(name, this, grid, rowIndex, e); - }, - - /** - * @private - * Clean up. Remove any Editor. Remove any listeners. - */ - destroy: function() { - if(this.setEditor){ - this.setEditor(null); - } - this.purgeListeners(); - }, - - /** - * Optional. A function which returns displayable data when passed the following parameters: - *
          - *
        • value : Object

          The data value for the cell.

        • - *
        • metadata : Object

          An object in which you may set the following attributes:

            - *
          • css : String

            A CSS class name to add to the cell's TD element.

          • - *
          • attr : String

            An HTML attribute definition string to apply to the data container - * element within the table cell (e.g. 'style="color:red;"').

        • - *
        • record : Ext.data.record

          The {@link Ext.data.Record} from which the data was - * extracted.

        • - *
        • rowIndex : Number

          Row index

        • - *
        • colIndex : Number

          Column index

        • - *
        • store : Ext.data.Store

          The {@link Ext.data.Store} object from which the Record - * was extracted.

        • - *
        - * @property renderer - * @type Function - */ - renderer : function(value){ - return value; - }, - - // private - getEditor: function(rowIndex){ - return this.editable !== false ? this.editor : null; - }, - - /** - * Sets a new editor for this column. - * @param {Ext.Editor/Ext.form.Field} editor The editor to set - */ - setEditor : function(editor){ - var ed = this.editor; - if(ed){ - if(ed.gridEditor){ - ed.gridEditor.destroy(); - delete ed.gridEditor; - }else{ - ed.destroy(); - } - } - this.editor = null; - if(editor){ - //not an instance, create it - if(!editor.isXType){ - editor = Ext.create(editor, 'textfield'); - } - this.editor = editor; - } - }, - - /** - * Returns the {@link Ext.Editor editor} defined for this column that was created to wrap the {@link Ext.form.Field Field} - * used to edit the cell. - * @param {Number} rowIndex The row index - * @return {Ext.Editor} - */ - getCellEditor: function(rowIndex){ - var ed = this.getEditor(rowIndex); - if(ed){ - if(!ed.startEdit){ - if(!ed.gridEditor){ - ed.gridEditor = new Ext.grid.GridEditor(ed); - } - ed = ed.gridEditor; - } - } - return ed; - } -}); - -/** - * @class Ext.grid.BooleanColumn - * @extends Ext.grid.Column - *

        A Column definition class which renders boolean data fields. See the {@link Ext.grid.Column#xtype xtype} - * config option of {@link Ext.grid.Column} for more details.

        - */ -Ext.grid.BooleanColumn = Ext.extend(Ext.grid.Column, { - /** - * @cfg {String} trueText - * The string returned by the renderer when the column value is not falsy (defaults to 'true'). - */ - trueText: 'true', - /** - * @cfg {String} falseText - * The string returned by the renderer when the column value is falsy (but not undefined) (defaults to - * 'false'). - */ - falseText: 'false', - /** - * @cfg {String} undefinedText - * The string returned by the renderer when the column value is undefined (defaults to '&#160;'). - */ - undefinedText: ' ', - - constructor: function(cfg){ - Ext.grid.BooleanColumn.superclass.constructor.call(this, cfg); - var t = this.trueText, f = this.falseText, u = this.undefinedText; - this.renderer = function(v){ - if(v === undefined){ - return u; - } - if(!v || v === 'false'){ - return f; - } - return t; - }; - } -}); - -/** - * @class Ext.grid.NumberColumn - * @extends Ext.grid.Column - *

        A Column definition class which renders a numeric data field according to a {@link #format} string. See the - * {@link Ext.grid.Column#xtype xtype} config option of {@link Ext.grid.Column} for more details.

        - */ -Ext.grid.NumberColumn = Ext.extend(Ext.grid.Column, { - /** - * @cfg {String} format - * A formatting string as used by {@link Ext.util.Format#number} to format a numeric value for this Column - * (defaults to '0,000.00'). - */ - format : '0,000.00', - constructor: function(cfg){ - Ext.grid.NumberColumn.superclass.constructor.call(this, cfg); - this.renderer = Ext.util.Format.numberRenderer(this.format); - } -}); - -/** - * @class Ext.grid.DateColumn - * @extends Ext.grid.Column - *

        A Column definition class which renders a passed date according to the default locale, or a configured - * {@link #format}. See the {@link Ext.grid.Column#xtype xtype} config option of {@link Ext.grid.Column} - * for more details.

        - */ -Ext.grid.DateColumn = Ext.extend(Ext.grid.Column, { - /** - * @cfg {String} format - * A formatting string as used by {@link Date#format} to format a Date for this Column - * (defaults to 'm/d/Y'). - */ - format : 'm/d/Y', - constructor: function(cfg){ - Ext.grid.DateColumn.superclass.constructor.call(this, cfg); - this.renderer = Ext.util.Format.dateRenderer(this.format); - } -}); - -/** - * @class Ext.grid.TemplateColumn - * @extends Ext.grid.Column - *

        A Column definition class which renders a value by processing a {@link Ext.data.Record Record}'s - * {@link Ext.data.Record#data data} using a {@link #tpl configured} {@link Ext.XTemplate XTemplate}. - * See the {@link Ext.grid.Column#xtype xtype} config option of {@link Ext.grid.Column} for more - * details.

        - */ -Ext.grid.TemplateColumn = Ext.extend(Ext.grid.Column, { - /** - * @cfg {String/XTemplate} tpl - * An {@link Ext.XTemplate XTemplate}, or an XTemplate definition string to use to process a - * {@link Ext.data.Record Record}'s {@link Ext.data.Record#data data} to produce a column's rendered value. - */ - constructor: function(cfg){ - Ext.grid.TemplateColumn.superclass.constructor.call(this, cfg); - var tpl = (!Ext.isPrimitive(this.tpl) && this.tpl.compile) ? this.tpl : new Ext.XTemplate(this.tpl); - this.renderer = function(value, p, r){ - return tpl.apply(r.data); - }; - this.tpl = tpl; - } -}); - -/** - * @class Ext.grid.ActionColumn - * @extends Ext.grid.Column - *

        A Grid column type which renders an icon, or a series of icons in a grid cell, and offers a scoped click - * handler for each icon. Example usage:

        -
        
        -new Ext.grid.GridPanel({
        -    store: myStore,
        -    columns: [
        -        {
        -            xtype: 'actioncolumn',
        -            width: 50,
        -            items: [
        -                {
        -                    icon   : 'sell.gif',                // Use a URL in the icon config
        -                    tooltip: 'Sell stock',
        -                    handler: function(grid, rowIndex, colIndex) {
        -                        var rec = store.getAt(rowIndex);
        -                        alert("Sell " + rec.get('company'));
        -                    }
        -                },
        -                {
        -                    getClass: function(v, meta, rec) {  // Or return a class from a function
        -                        if (rec.get('change') < 0) {
        -                            this.items[1].tooltip = 'Do not buy!';
        -                            return 'alert-col';
        -                        } else {
        -                            this.items[1].tooltip = 'Buy stock';
        -                            return 'buy-col';
        -                        }
        -                    },
        -                    handler: function(grid, rowIndex, colIndex) {
        -                        var rec = store.getAt(rowIndex);
        -                        alert("Buy " + rec.get('company'));
        -                    }
        -                }
        -            ]
        -        }
        -        //any other columns here
        -    ]
        -});
        -
        - *

        The action column can be at any index in the columns array, and a grid can have any number of - * action columns.

        - */ -Ext.grid.ActionColumn = Ext.extend(Ext.grid.Column, { - /** - * @cfg {String} icon - * The URL of an image to display as the clickable element in the column. - * Optional - defaults to {@link Ext#BLANK_IMAGE_URL Ext.BLANK_IMAGE_URL}. - */ - /** - * @cfg {String} iconCls - * A CSS class to apply to the icon image. To determine the class dynamically, configure the Column with a {@link #getClass} function. - */ - /** - * @cfg {Function} handler A function called when the icon is clicked. - * The handler is passed the following parameters:
          - *
        • grid : GridPanel
          The owning GridPanel.
        • - *
        • rowIndex : Number
          The row index clicked on.
        • - *
        • colIndex : Number
          The column index clicked on.
        • - *
        • item : Object
          The clicked item (or this Column if multiple - * {@link #items} were not configured).
        • - *
        • e : Event
          The click event.
        • - *
        - */ - /** - * @cfg {Object} scope The scope (this reference) in which the {@link #handler} - * and {@link #getClass} fuctions are executed. Defaults to this Column. - */ - /** - * @cfg {String} tooltip A tooltip message to be displayed on hover. {@link Ext.QuickTips#init Ext.QuickTips} must have - * been initialized. - */ - /** - * @cfg {Boolean} stopSelection Defaults to true. Prevent grid row selection upon mousedown. - */ - /** - * @cfg {Function} getClass A function which returns the CSS class to apply to the icon image. - * The function is passed the following parameters:
          - *
        • v : Object

          The value of the column's configured field (if any).

        • - *
        • metadata : Object

          An object in which you may set the following attributes:

            - *
          • css : String

            A CSS class name to add to the cell's TD element.

          • - *
          • attr : String

            An HTML attribute definition string to apply to the data container element within the table cell - * (e.g. 'style="color:red;"').

          • - *

        • - *
        • r : Ext.data.Record

          The Record providing the data.

        • - *
        • rowIndex : Number

          The row index..

        • - *
        • colIndex : Number

          The column index.

        • - *
        • store : Ext.data.Store

          The Store which is providing the data Model.

        • - *
        - */ - /** - * @cfg {Array} items An Array which may contain multiple icon definitions, each element of which may contain: - *
          - *
        • icon : String
          The url of an image to display as the clickable element - * in the column.
        • - *
        • iconCls : String
          A CSS class to apply to the icon image. - * To determine the class dynamically, configure the item with a getClass function.
        • - *
        • getClass : Function
          A function which returns the CSS class to apply to the icon image. - * The function is passed the following parameters:
            - *
          • v : Object

            The value of the column's configured field (if any).

          • - *
          • metadata : Object

            An object in which you may set the following attributes:

              - *
            • css : String

              A CSS class name to add to the cell's TD element.

            • - *
            • attr : String

              An HTML attribute definition string to apply to the data container element within the table cell - * (e.g. 'style="color:red;"').

            • - *

          • - *
          • r : Ext.data.Record

            The Record providing the data.

          • - *
          • rowIndex : Number

            The row index..

          • - *
          • colIndex : Number

            The column index.

          • - *
          • store : Ext.data.Store

            The Store which is providing the data Model.

          • - *
        • - *
        • handler : Function
          A function called when the icon is clicked.
        • - *
        • scope : Scope
          The scope (this reference) in which the - * handler and getClass functions are executed. Fallback defaults are this Column's - * configured scope, then this Column.
        • - *
        • tooltip : String
          A tooltip message to be displayed on hover. - * {@link Ext.QuickTips#init Ext.QuickTips} must have been initialized.
        • - *
        - */ - header: ' ', - - actionIdRe: /x-action-col-(\d+)/, - - /** - * @cfg {String} altText The alt text to use for the image element. Defaults to ''. - */ - altText: '', - - constructor: function(cfg) { - var me = this, - items = cfg.items || (me.items = [me]), - l = items.length, - i, - item; - - Ext.grid.ActionColumn.superclass.constructor.call(me, cfg); - -// Renderer closure iterates through items creating an element for each and tagging with an identifying -// class name x-action-col-{n} - me.renderer = function(v, meta) { -// Allow a configured renderer to create initial value (And set the other values in the "metadata" argument!) - v = Ext.isFunction(cfg.renderer) ? cfg.renderer.apply(this, arguments)||'' : ''; - - meta.css += ' x-action-col-cell'; - for (i = 0; i < l; i++) { - item = items[i]; - v += '' + (item.altText || me.altText) + ''; - } - return v; - }; - }, - - destroy: function() { - delete this.items; - delete this.renderer; - return Ext.grid.ActionColumn.superclass.destroy.apply(this, arguments); - }, - - /** - * @private - * Process and refire events routed from the GridView's processEvent method. - * Also fires any configured click handlers. By default, cancels the mousedown event to prevent selection. - * Returns the event handler's status to allow cancelling of GridView's bubbling process. - */ - processEvent : function(name, e, grid, rowIndex, colIndex){ - var m = e.getTarget().className.match(this.actionIdRe), - item, fn; - if (m && (item = this.items[parseInt(m[1], 10)])) { - if (name == 'click') { - (fn = item.handler || this.handler) && fn.call(item.scope||this.scope||this, grid, rowIndex, colIndex, item, e); - } else if ((name == 'mousedown') && (item.stopSelection !== false)) { - return false; - } - } - return Ext.grid.ActionColumn.superclass.processEvent.apply(this, arguments); - } -}); - -/* - * @property types - * @type Object - * @member Ext.grid.Column - * @static - *

        An object containing predefined Column classes keyed by a mnemonic code which may be referenced - * by the {@link Ext.grid.ColumnModel#xtype xtype} config option of ColumnModel.

        - *

        This contains the following properties

          - *
        • gridcolumn : {@link Ext.grid.Column Column constructor}
        • - *
        • booleancolumn : {@link Ext.grid.BooleanColumn BooleanColumn constructor}
        • - *
        • numbercolumn : {@link Ext.grid.NumberColumn NumberColumn constructor}
        • - *
        • datecolumn : {@link Ext.grid.DateColumn DateColumn constructor}
        • - *
        • templatecolumn : {@link Ext.grid.TemplateColumn TemplateColumn constructor}
        • - *
        - */ -Ext.grid.Column.types = { - gridcolumn : Ext.grid.Column, - booleancolumn: Ext.grid.BooleanColumn, - numbercolumn: Ext.grid.NumberColumn, - datecolumn: Ext.grid.DateColumn, - templatecolumn: Ext.grid.TemplateColumn, - actioncolumn: Ext.grid.ActionColumn -};/** - * @class Ext.grid.RowNumberer - * This is a utility class that can be passed into a {@link Ext.grid.ColumnModel} as a column config that provides - * an automatic row numbering column. - *
        Usage:
        -
        
        - // This is a typical column config with the first column providing row numbers
        - var colModel = new Ext.grid.ColumnModel([
        -    new Ext.grid.RowNumberer(),
        -    {header: "Name", width: 80, sortable: true},
        -    {header: "Code", width: 50, sortable: true},
        -    {header: "Description", width: 200, sortable: true}
        - ]);
        - 
        - * @constructor - * @param {Object} config The configuration options - */ -Ext.grid.RowNumberer = Ext.extend(Object, { - /** - * @cfg {String} header Any valid text or HTML fragment to display in the header cell for the row - * number column (defaults to ''). - */ - header: "", - /** - * @cfg {Number} width The default width in pixels of the row number column (defaults to 23). - */ - width: 23, - /** - * @cfg {Boolean} sortable True if the row number column is sortable (defaults to false). - * @hide - */ - sortable: false, - - constructor : function(config){ - Ext.apply(this, config); - if(this.rowspan){ - this.renderer = this.renderer.createDelegate(this); - } - }, - - // private - fixed:true, - hideable: false, - menuDisabled:true, - dataIndex: '', - id: 'numberer', - rowspan: undefined, - - // private - renderer : function(v, p, record, rowIndex){ - if(this.rowspan){ - p.cellAttr = 'rowspan="'+this.rowspan+'"'; - } - return rowIndex+1; - } -});/** - * @class Ext.grid.CheckboxSelectionModel - * @extends Ext.grid.RowSelectionModel - * A custom selection model that renders a column of checkboxes that can be toggled to select or deselect rows. - * @constructor - * @param {Object} config The configuration options - */ -Ext.grid.CheckboxSelectionModel = Ext.extend(Ext.grid.RowSelectionModel, { - - /** - * @cfg {Boolean} checkOnly true if rows can only be selected by clicking on the - * checkbox column (defaults to false). - */ - /** - * @cfg {String} header Any valid text or HTML fragment to display in the header cell for the - * checkbox column. Defaults to:
        
        -     * '<div class="x-grid3-hd-checker">&#160;</div>'
        -     * 
        - * The default CSS class of 'x-grid3-hd-checker' displays a checkbox in the header - * and provides support for automatic check all/none behavior on header click. This string - * can be replaced by any valid HTML fragment, including a simple text string (e.g., - * 'Select Rows'), but the automatic check all/none behavior will only work if the - * 'x-grid3-hd-checker' class is supplied. - */ - header : '
         
        ', - /** - * @cfg {Number} width The default width in pixels of the checkbox column (defaults to 20). - */ - width : 20, - /** - * @cfg {Boolean} sortable true if the checkbox column is sortable (defaults to - * false). - */ - sortable : false, - - // private - menuDisabled : true, - fixed : true, - hideable: false, - dataIndex : '', - id : 'checker', - isColumn: true, // So that ColumnModel doesn't feed this through the Column constructor - - constructor : function(){ - Ext.grid.CheckboxSelectionModel.superclass.constructor.apply(this, arguments); - if(this.checkOnly){ - this.handleMouseDown = Ext.emptyFn; - } - }, - - // private - initEvents : function(){ - Ext.grid.CheckboxSelectionModel.superclass.initEvents.call(this); - this.grid.on('render', function(){ - Ext.fly(this.grid.getView().innerHd).on('mousedown', this.onHdMouseDown, this); - }, this); - }, - - /** - * @private - * Process and refire events routed from the GridView's processEvent method. - */ - processEvent : function(name, e, grid, rowIndex, colIndex){ - if (name == 'mousedown') { - this.onMouseDown(e, e.getTarget()); - return false; - } else { - return Ext.grid.Column.prototype.processEvent.apply(this, arguments); - } - }, - - // private - onMouseDown : function(e, t){ - if(e.button === 0 && t.className == 'x-grid3-row-checker'){ // Only fire if left-click - e.stopEvent(); - var row = e.getTarget('.x-grid3-row'); - if(row){ - var index = row.rowIndex; - if(this.isSelected(index)){ - this.deselectRow(index); - }else{ - this.selectRow(index, true); - this.grid.getView().focusRow(index); - } - } - } - }, - - // private - onHdMouseDown : function(e, t) { - if(t.className == 'x-grid3-hd-checker'){ - e.stopEvent(); - var hd = Ext.fly(t.parentNode); - var isChecked = hd.hasClass('x-grid3-hd-checker-on'); - if(isChecked){ - hd.removeClass('x-grid3-hd-checker-on'); - this.clearSelections(); - }else{ - hd.addClass('x-grid3-hd-checker-on'); - this.selectAll(); - } - } - }, - - // private - renderer : function(v, p, record){ - return '
         
        '; - }, - - onEditorSelect: function(row, lastRow){ - if(lastRow != row && !this.checkOnly){ - this.selectRow(row); // *** highlight newly-selected cell and update selection - } - } -});/** - * @class Ext.grid.CellSelectionModel - * @extends Ext.grid.AbstractSelectionModel - * This class provides the basic implementation for single cell selection in a grid. - * The object stored as the selection contains the following properties: - *
          - *
        • cell : see {@link #getSelectedCell} - *
        • record : Ext.data.record The {@link Ext.data.Record Record} - * which provides the data for the row containing the selection
        • - *
        - * @constructor - * @param {Object} config The object containing the configuration of this model. - */ -Ext.grid.CellSelectionModel = Ext.extend(Ext.grid.AbstractSelectionModel, { - - constructor : function(config){ - Ext.apply(this, config); - - this.selection = null; - - this.addEvents( - /** - * @event beforecellselect - * Fires before a cell is selected, return false to cancel the selection. - * @param {SelectionModel} this - * @param {Number} rowIndex The selected row index - * @param {Number} colIndex The selected cell index - */ - "beforecellselect", - /** - * @event cellselect - * Fires when a cell is selected. - * @param {SelectionModel} this - * @param {Number} rowIndex The selected row index - * @param {Number} colIndex The selected cell index - */ - "cellselect", - /** - * @event selectionchange - * Fires when the active selection changes. - * @param {SelectionModel} this - * @param {Object} selection null for no selection or an object with two properties - *
          - *
        • cell : see {@link #getSelectedCell} - *
        • record : Ext.data.record

          The {@link Ext.data.Record Record} - * which provides the data for the row containing the selection

        • - *
        - */ - "selectionchange" - ); - - Ext.grid.CellSelectionModel.superclass.constructor.call(this); - }, - - /** @ignore */ - initEvents : function(){ - this.grid.on('cellmousedown', this.handleMouseDown, this); - this.grid.on(Ext.EventManager.getKeyEvent(), this.handleKeyDown, this); - this.grid.getView().on({ - scope: this, - refresh: this.onViewChange, - rowupdated: this.onRowUpdated, - beforerowremoved: this.clearSelections, - beforerowsinserted: this.clearSelections - }); - if(this.grid.isEditor){ - this.grid.on('beforeedit', this.beforeEdit, this); - } - }, - - //private - beforeEdit : function(e){ - this.select(e.row, e.column, false, true, e.record); - }, - - //private - onRowUpdated : function(v, index, r){ - if(this.selection && this.selection.record == r){ - v.onCellSelect(index, this.selection.cell[1]); - } - }, - - //private - onViewChange : function(){ - this.clearSelections(true); - }, - - /** - * Returns an array containing the row and column indexes of the currently selected cell - * (e.g., [0, 0]), or null if none selected. The array has elements: - *
          - *
        • rowIndex : Number

          The index of the selected row

        • - *
        • cellIndex : Number

          The index of the selected cell. - * Due to possible column reordering, the cellIndex should not be used as an - * index into the Record's data. Instead, use the cellIndex to determine the name - * of the selected cell and use the field name to retrieve the data value from the record:

          
          -// get name
          -var fieldName = grid.getColumnModel().getDataIndex(cellIndex);
          -// get data value based on name
          -var data = record.get(fieldName);
          -     * 

        • - *
        - * @return {Array} An array containing the row and column indexes of the selected cell, or null if none selected. - */ - getSelectedCell : function(){ - return this.selection ? this.selection.cell : null; - }, - - /** - * If anything is selected, clears all selections and fires the selectionchange event. - * @param {Boolean} preventNotify true to prevent the gridview from - * being notified about the change. - */ - clearSelections : function(preventNotify){ - var s = this.selection; - if(s){ - if(preventNotify !== true){ - this.grid.view.onCellDeselect(s.cell[0], s.cell[1]); - } - this.selection = null; - this.fireEvent("selectionchange", this, null); - } - }, - - /** - * Returns true if there is a selection. - * @return {Boolean} - */ - hasSelection : function(){ - return this.selection ? true : false; - }, - - /** @ignore */ - handleMouseDown : function(g, row, cell, e){ - if(e.button !== 0 || this.isLocked()){ - return; - } - this.select(row, cell); - }, - - /** - * Selects a cell. Before selecting a cell, fires the - * {@link #beforecellselect} event. If this check is satisfied the cell - * will be selected and followed up by firing the {@link #cellselect} and - * {@link #selectionchange} events. - * @param {Number} rowIndex The index of the row to select - * @param {Number} colIndex The index of the column to select - * @param {Boolean} preventViewNotify (optional) Specify true to - * prevent notifying the view (disables updating the selected appearance) - * @param {Boolean} preventFocus (optional) Whether to prevent the cell at - * the specified rowIndex / colIndex from being focused. - * @param {Ext.data.Record} r (optional) The record to select - */ - select : function(rowIndex, colIndex, preventViewNotify, preventFocus, /*internal*/ r){ - if(this.fireEvent("beforecellselect", this, rowIndex, colIndex) !== false){ - this.clearSelections(); - r = r || this.grid.store.getAt(rowIndex); - this.selection = { - record : r, - cell : [rowIndex, colIndex] - }; - if(!preventViewNotify){ - var v = this.grid.getView(); - v.onCellSelect(rowIndex, colIndex); - if(preventFocus !== true){ - v.focusCell(rowIndex, colIndex); - } - } - this.fireEvent("cellselect", this, rowIndex, colIndex); - this.fireEvent("selectionchange", this, this.selection); - } - }, - - //private - isSelectable : function(rowIndex, colIndex, cm){ - return !cm.isHidden(colIndex); - }, - - // private - onEditorKey: function(field, e){ - if(e.getKey() == e.TAB){ - this.handleKeyDown(e); - } - }, - - /** @ignore */ - handleKeyDown : function(e){ - if(!e.isNavKeyPress()){ - return; - } - - var k = e.getKey(), - g = this.grid, - s = this.selection, - sm = this, - walk = function(row, col, step){ - return g.walkCells( - row, - col, - step, - g.isEditor && g.editing ? sm.acceptsNav : sm.isSelectable, // *** handle tabbing while editorgrid is in edit mode - sm - ); - }, - cell, newCell, r, c, ae; - - switch(k){ - case e.ESC: - case e.PAGE_UP: - case e.PAGE_DOWN: - // do nothing - break; - default: - // *** call e.stopEvent() only for non ESC, PAGE UP/DOWN KEYS - e.stopEvent(); - break; - } - - if(!s){ - cell = walk(0, 0, 1); // *** use private walk() function defined above - if(cell){ - this.select(cell[0], cell[1]); - } - return; - } - - cell = s.cell; // currently selected cell - r = cell[0]; // current row - c = cell[1]; // current column - - switch(k){ - case e.TAB: - if(e.shiftKey){ - newCell = walk(r, c - 1, -1); - }else{ - newCell = walk(r, c + 1, 1); - } - break; - case e.DOWN: - newCell = walk(r + 1, c, 1); - break; - case e.UP: - newCell = walk(r - 1, c, -1); - break; - case e.RIGHT: - newCell = walk(r, c + 1, 1); - break; - case e.LEFT: - newCell = walk(r, c - 1, -1); - break; - case e.ENTER: - if (g.isEditor && !g.editing) { - g.startEditing(r, c); - return; - } - break; - } - - if(newCell){ - // *** reassign r & c variables to newly-selected cell's row and column - r = newCell[0]; - c = newCell[1]; - - this.select(r, c); // *** highlight newly-selected cell and update selection - - if(g.isEditor && g.editing){ // *** handle tabbing while editorgrid is in edit mode - ae = g.activeEditor; - if(ae && ae.field.triggerBlur){ - // *** if activeEditor is a TriggerField, explicitly call its triggerBlur() method - ae.field.triggerBlur(); - } - g.startEditing(r, c); - } - } - }, - - acceptsNav : function(row, col, cm){ - return !cm.isHidden(col) && cm.isCellEditable(col, row); - } -});/** - * @class Ext.grid.EditorGridPanel - * @extends Ext.grid.GridPanel - *

        This class extends the {@link Ext.grid.GridPanel GridPanel Class} to provide cell editing - * on selected {@link Ext.grid.Column columns}. The editable columns are specified by providing - * an {@link Ext.grid.ColumnModel#editor editor} in the {@link Ext.grid.Column column configuration}.

        - *

        Editability of columns may be controlled programatically by inserting an implementation - * of {@link Ext.grid.ColumnModel#isCellEditable isCellEditable} into the - * {@link Ext.grid.ColumnModel ColumnModel}.

        - *

        Editing is performed on the value of the field specified by the column's - * {@link Ext.grid.ColumnModel#dataIndex dataIndex} in the backing {@link Ext.data.Store Store} - * (so if you are using a {@link Ext.grid.ColumnModel#setRenderer renderer} in order to display - * transformed data, this must be accounted for).

        - *

        If a value-to-description mapping is used to render a column, then a {@link Ext.form.Field#ComboBox ComboBox} - * which uses the same {@link Ext.form.Field#valueField value}-to-{@link Ext.form.Field#displayFieldField description} - * mapping would be an appropriate editor.

        - * If there is a more complex mismatch between the visible data in the grid, and the editable data in - * the {@link Edt.data.Store Store}, then code to transform the data both before and after editing can be - * injected using the {@link #beforeedit} and {@link #afteredit} events. - * @constructor - * @param {Object} config The config object - * @xtype editorgrid - */ -Ext.grid.EditorGridPanel = Ext.extend(Ext.grid.GridPanel, { - /** - * @cfg {Number} clicksToEdit - *

        The number of clicks on a cell required to display the cell's editor (defaults to 2).

        - *

        Setting this option to 'auto' means that mousedown on the selected cell starts - * editing that cell.

        - */ - clicksToEdit: 2, - - /** - * @cfg {Boolean} forceValidation - * True to force validation even if the value is unmodified (defaults to false) - */ - forceValidation: false, - - // private - isEditor : true, - // private - detectEdit: false, - - /** - * @cfg {Boolean} autoEncode - * True to automatically HTML encode and decode values pre and post edit (defaults to false) - */ - autoEncode : false, - - /** - * @cfg {Boolean} trackMouseOver @hide - */ - // private - trackMouseOver: false, // causes very odd FF errors - - // private - initComponent : function(){ - Ext.grid.EditorGridPanel.superclass.initComponent.call(this); - - if(!this.selModel){ - /** - * @cfg {Object} selModel Any subclass of AbstractSelectionModel that will provide the selection model for - * the grid (defaults to {@link Ext.grid.CellSelectionModel} if not specified). - */ - this.selModel = new Ext.grid.CellSelectionModel(); - } - - this.activeEditor = null; - - this.addEvents( - /** - * @event beforeedit - * Fires before cell editing is triggered. The edit event object has the following properties
        - *
          - *
        • grid - This grid
        • - *
        • record - The record being edited
        • - *
        • field - The field name being edited
        • - *
        • value - The value for the field being edited.
        • - *
        • row - The grid row index
        • - *
        • column - The grid column index
        • - *
        • cancel - Set this to true to cancel the edit or return false from your handler.
        • - *
        - * @param {Object} e An edit event (see above for description) - */ - "beforeedit", - /** - * @event afteredit - * Fires after a cell is edited. The edit event object has the following properties
        - *
          - *
        • grid - This grid
        • - *
        • record - The record being edited
        • - *
        • field - The field name being edited
        • - *
        • value - The value being set
        • - *
        • originalValue - The original value for the field, before the edit.
        • - *
        • row - The grid row index
        • - *
        • column - The grid column index
        • - *
        - * - *
        
        -grid.on('afteredit', afterEdit, this );
        -
        -function afterEdit(e) {
        -    // execute an XHR to send/commit data to the server, in callback do (if successful):
        -    e.record.commit();
        -};
        -             * 
        - * @param {Object} e An edit event (see above for description) - */ - "afteredit", - /** - * @event validateedit - * Fires after a cell is edited, but before the value is set in the record. Return false - * to cancel the change. The edit event object has the following properties
        - *
          - *
        • grid - This grid
        • - *
        • record - The record being edited
        • - *
        • field - The field name being edited
        • - *
        • value - The value being set
        • - *
        • originalValue - The original value for the field, before the edit.
        • - *
        • row - The grid row index
        • - *
        • column - The grid column index
        • - *
        • cancel - Set this to true to cancel the edit or return false from your handler.
        • - *
        - * Usage example showing how to remove the red triangle (dirty record indicator) from some - * records (not all). By observing the grid's validateedit event, it can be cancelled if - * the edit occurs on a targeted row (for example) and then setting the field's new value - * in the Record directly: - *
        
        -grid.on('validateedit', function(e) {
        -  var myTargetRow = 6;
        -
        -  if (e.row == myTargetRow) {
        -    e.cancel = true;
        -    e.record.data[e.field] = e.value;
        -  }
        -});
        -             * 
        - * @param {Object} e An edit event (see above for description) - */ - "validateedit" - ); - }, - - // private - initEvents : function(){ - Ext.grid.EditorGridPanel.superclass.initEvents.call(this); - - this.getGridEl().on('mousewheel', this.stopEditing.createDelegate(this, [true]), this); - this.on('columnresize', this.stopEditing, this, [true]); - - if(this.clicksToEdit == 1){ - this.on("cellclick", this.onCellDblClick, this); - }else { - var view = this.getView(); - if(this.clicksToEdit == 'auto' && view.mainBody){ - view.mainBody.on('mousedown', this.onAutoEditClick, this); - } - this.on('celldblclick', this.onCellDblClick, this); - } - }, - - onResize : function(){ - Ext.grid.EditorGridPanel.superclass.onResize.apply(this, arguments); - var ae = this.activeEditor; - if(this.editing && ae){ - ae.realign(true); - } - }, - - // private - onCellDblClick : function(g, row, col){ - this.startEditing(row, col); - }, - - // private - onAutoEditClick : function(e, t){ - if(e.button !== 0){ - return; - } - var row = this.view.findRowIndex(t), - col = this.view.findCellIndex(t); - if(row !== false && col !== false){ - this.stopEditing(); - if(this.selModel.getSelectedCell){ // cell sm - var sc = this.selModel.getSelectedCell(); - if(sc && sc[0] === row && sc[1] === col){ - this.startEditing(row, col); - } - }else{ - if(this.selModel.isSelected(row)){ - this.startEditing(row, col); - } - } - } - }, - - // private - onEditComplete : function(ed, value, startValue){ - this.editing = false; - this.lastActiveEditor = this.activeEditor; - this.activeEditor = null; - - var r = ed.record, - field = this.colModel.getDataIndex(ed.col); - value = this.postEditValue(value, startValue, r, field); - if(this.forceValidation === true || String(value) !== String(startValue)){ - var e = { - grid: this, - record: r, - field: field, - originalValue: startValue, - value: value, - row: ed.row, - column: ed.col, - cancel:false - }; - if(this.fireEvent("validateedit", e) !== false && !e.cancel && String(value) !== String(startValue)){ - r.set(field, e.value); - delete e.cancel; - this.fireEvent("afteredit", e); - } - } - this.view.focusCell(ed.row, ed.col); - }, - - /** - * Starts editing the specified for the specified row/column - * @param {Number} rowIndex - * @param {Number} colIndex - */ - startEditing : function(row, col){ - this.stopEditing(); - if(this.colModel.isCellEditable(col, row)){ - this.view.ensureVisible(row, col, true); - var r = this.store.getAt(row), - field = this.colModel.getDataIndex(col), - e = { - grid: this, - record: r, - field: field, - value: r.data[field], - row: row, - column: col, - cancel:false - }; - if(this.fireEvent("beforeedit", e) !== false && !e.cancel){ - this.editing = true; - var ed = this.colModel.getCellEditor(col, row); - if(!ed){ - return; - } - if(!ed.rendered){ - ed.parentEl = this.view.getEditorParent(ed); - ed.on({ - scope: this, - render: { - fn: function(c){ - c.field.focus(false, true); - }, - single: true, - scope: this - }, - specialkey: function(field, e){ - this.getSelectionModel().onEditorKey(field, e); - }, - complete: this.onEditComplete, - canceledit: this.stopEditing.createDelegate(this, [true]) - }); - } - Ext.apply(ed, { - row : row, - col : col, - record : r - }); - this.lastEdit = { - row: row, - col: col - }; - this.activeEditor = ed; - // Set the selectSameEditor flag if we are reusing the same editor again and - // need to prevent the editor from firing onBlur on itself. - ed.selectSameEditor = (this.activeEditor == this.lastActiveEditor); - var v = this.preEditValue(r, field); - ed.startEdit(this.view.getCell(row, col).firstChild, Ext.isDefined(v) ? v : ''); - - // Clear the selectSameEditor flag - (function(){ - delete ed.selectSameEditor; - }).defer(50); - } - } - }, - - // private - preEditValue : function(r, field){ - var value = r.data[field]; - return this.autoEncode && Ext.isString(value) ? Ext.util.Format.htmlDecode(value) : value; - }, - - // private - postEditValue : function(value, originalValue, r, field){ - return this.autoEncode && Ext.isString(value) ? Ext.util.Format.htmlEncode(value) : value; - }, - - /** - * Stops any active editing - * @param {Boolean} cancel (optional) True to cancel any changes - */ - stopEditing : function(cancel){ - if(this.editing){ - // Store the lastActiveEditor to check if it is changing - var ae = this.lastActiveEditor = this.activeEditor; - if(ae){ - ae[cancel === true ? 'cancelEdit' : 'completeEdit'](); - this.view.focusCell(ae.row, ae.col); - } - this.activeEditor = null; - } - this.editing = false; - } -}); -Ext.reg('editorgrid', Ext.grid.EditorGridPanel);// private -// This is a support class used internally by the Grid components -Ext.grid.GridEditor = function(field, config){ - Ext.grid.GridEditor.superclass.constructor.call(this, field, config); - field.monitorTab = false; -}; - -Ext.extend(Ext.grid.GridEditor, Ext.Editor, { - alignment: "tl-tl", - autoSize: "width", - hideEl : false, - cls: "x-small-editor x-grid-editor", - shim:false, - shadow:false -});/** - * @class Ext.grid.PropertyRecord - * A specific {@link Ext.data.Record} type that represents a name/value pair and is made to work with the - * {@link Ext.grid.PropertyGrid}. Typically, PropertyRecords do not need to be created directly as they can be - * created implicitly by simply using the appropriate data configs either via the {@link Ext.grid.PropertyGrid#source} - * config property or by calling {@link Ext.grid.PropertyGrid#setSource}. However, if the need arises, these records - * can also be created explicitly as shwon below. Example usage: - *
        
        -var rec = new Ext.grid.PropertyRecord({
        -    name: 'Birthday',
        -    value: new Date(Date.parse('05/26/1972'))
        -});
        -// Add record to an already populated grid
        -grid.store.addSorted(rec);
        -
        - * @constructor - * @param {Object} config A data object in the format: {name: [name], value: [value]}. The specified value's type - * will be read automatically by the grid to determine the type of editor to use when displaying it. - */ -Ext.grid.PropertyRecord = Ext.data.Record.create([ - {name:'name',type:'string'}, 'value' -]); - -/** - * @class Ext.grid.PropertyStore - * @extends Ext.util.Observable - * A custom wrapper for the {@link Ext.grid.PropertyGrid}'s {@link Ext.data.Store}. This class handles the mapping - * between the custom data source objects supported by the grid and the {@link Ext.grid.PropertyRecord} format - * required for compatibility with the underlying store. Generally this class should not need to be used directly -- - * the grid's data should be accessed from the underlying store via the {@link #store} property. - * @constructor - * @param {Ext.grid.Grid} grid The grid this store will be bound to - * @param {Object} source The source data config object - */ -Ext.grid.PropertyStore = Ext.extend(Ext.util.Observable, { - - constructor : function(grid, source){ - this.grid = grid; - this.store = new Ext.data.Store({ - recordType : Ext.grid.PropertyRecord - }); - this.store.on('update', this.onUpdate, this); - if(source){ - this.setSource(source); - } - Ext.grid.PropertyStore.superclass.constructor.call(this); - }, - - // protected - should only be called by the grid. Use grid.setSource instead. - setSource : function(o){ - this.source = o; - this.store.removeAll(); - var data = []; - for(var k in o){ - if(this.isEditableValue(o[k])){ - data.push(new Ext.grid.PropertyRecord({name: k, value: o[k]}, k)); - } - } - this.store.loadRecords({records: data}, {}, true); - }, - - // private - onUpdate : function(ds, record, type){ - if(type == Ext.data.Record.EDIT){ - var v = record.data.value; - var oldValue = record.modified.value; - if(this.grid.fireEvent('beforepropertychange', this.source, record.id, v, oldValue) !== false){ - this.source[record.id] = v; - record.commit(); - this.grid.fireEvent('propertychange', this.source, record.id, v, oldValue); - }else{ - record.reject(); - } - } - }, - - // private - getProperty : function(row){ - return this.store.getAt(row); - }, - - // private - isEditableValue: function(val){ - return Ext.isPrimitive(val) || Ext.isDate(val); - }, - - // private - setValue : function(prop, value, create){ - var r = this.getRec(prop); - if(r){ - r.set('value', value); - this.source[prop] = value; - }else if(create){ - // only create if specified. - this.source[prop] = value; - r = new Ext.grid.PropertyRecord({name: prop, value: value}, prop); - this.store.add(r); - - } - }, - - // private - remove : function(prop){ - var r = this.getRec(prop); - if(r){ - this.store.remove(r); - delete this.source[prop]; - } - }, - - // private - getRec : function(prop){ - return this.store.getById(prop); - }, - - // protected - should only be called by the grid. Use grid.getSource instead. - getSource : function(){ - return this.source; - } -}); - -/** - * @class Ext.grid.PropertyColumnModel - * @extends Ext.grid.ColumnModel - * A custom column model for the {@link Ext.grid.PropertyGrid}. Generally it should not need to be used directly. - * @constructor - * @param {Ext.grid.Grid} grid The grid this store will be bound to - * @param {Object} source The source data config object - */ -Ext.grid.PropertyColumnModel = Ext.extend(Ext.grid.ColumnModel, { - // private - strings used for locale support - nameText : 'Name', - valueText : 'Value', - dateFormat : 'm/j/Y', - trueText: 'true', - falseText: 'false', - - constructor : function(grid, store){ - var g = Ext.grid, - f = Ext.form; - - this.grid = grid; - g.PropertyColumnModel.superclass.constructor.call(this, [ - {header: this.nameText, width:50, sortable: true, dataIndex:'name', id: 'name', menuDisabled:true}, - {header: this.valueText, width:50, resizable:false, dataIndex: 'value', id: 'value', menuDisabled:true} - ]); - this.store = store; - - var bfield = new f.Field({ - autoCreate: {tag: 'select', children: [ - {tag: 'option', value: 'true', html: this.trueText}, - {tag: 'option', value: 'false', html: this.falseText} - ]}, - getValue : function(){ - return this.el.dom.value == 'true'; - } - }); - this.editors = { - 'date' : new g.GridEditor(new f.DateField({selectOnFocus:true})), - 'string' : new g.GridEditor(new f.TextField({selectOnFocus:true})), - 'number' : new g.GridEditor(new f.NumberField({selectOnFocus:true, style:'text-align:left;'})), - 'boolean' : new g.GridEditor(bfield, { - autoSize: 'both' - }) - }; - this.renderCellDelegate = this.renderCell.createDelegate(this); - this.renderPropDelegate = this.renderProp.createDelegate(this); - }, - - // private - renderDate : function(dateVal){ - return dateVal.dateFormat(this.dateFormat); - }, - - // private - renderBool : function(bVal){ - return this[bVal ? 'trueText' : 'falseText']; - }, - - // private - isCellEditable : function(colIndex, rowIndex){ - return colIndex == 1; - }, - - // private - getRenderer : function(col){ - return col == 1 ? - this.renderCellDelegate : this.renderPropDelegate; - }, - - // private - renderProp : function(v){ - return this.getPropertyName(v); - }, - - // private - renderCell : function(val, meta, rec){ - var renderer = this.grid.customRenderers[rec.get('name')]; - if(renderer){ - return renderer.apply(this, arguments); - } - var rv = val; - if(Ext.isDate(val)){ - rv = this.renderDate(val); - }else if(typeof val == 'boolean'){ - rv = this.renderBool(val); - } - return Ext.util.Format.htmlEncode(rv); - }, - - // private - getPropertyName : function(name){ - var pn = this.grid.propertyNames; - return pn && pn[name] ? pn[name] : name; - }, - - // private - getCellEditor : function(colIndex, rowIndex){ - var p = this.store.getProperty(rowIndex), - n = p.data.name, - val = p.data.value; - if(this.grid.customEditors[n]){ - return this.grid.customEditors[n]; - } - if(Ext.isDate(val)){ - return this.editors.date; - }else if(typeof val == 'number'){ - return this.editors.number; - }else if(typeof val == 'boolean'){ - return this.editors['boolean']; - }else{ - return this.editors.string; - } - }, - - // inherit docs - destroy : function(){ - Ext.grid.PropertyColumnModel.superclass.destroy.call(this); - this.destroyEditors(this.editors); - this.destroyEditors(this.grid.customEditors); - }, - - destroyEditors: function(editors){ - for(var ed in editors){ - Ext.destroy(editors[ed]); - } - } -}); - -/** - * @class Ext.grid.PropertyGrid - * @extends Ext.grid.EditorGridPanel - * A specialized grid implementation intended to mimic the traditional property grid as typically seen in - * development IDEs. Each row in the grid represents a property of some object, and the data is stored - * as a set of name/value pairs in {@link Ext.grid.PropertyRecord}s. Example usage: - *
        
        -var grid = new Ext.grid.PropertyGrid({
        -    title: 'Properties Grid',
        -    autoHeight: true,
        -    width: 300,
        -    renderTo: 'grid-ct',
        -    source: {
        -        "(name)": "My Object",
        -        "Created": new Date(Date.parse('10/15/2006')),
        -        "Available": false,
        -        "Version": .01,
        -        "Description": "A test object"
        -    }
        -});
        -
        - * @constructor - * @param {Object} config The grid config object - */ -Ext.grid.PropertyGrid = Ext.extend(Ext.grid.EditorGridPanel, { - /** - * @cfg {Object} propertyNames An object containing property name/display name pairs. - * If specified, the display name will be shown in the name column instead of the property name. - */ - /** - * @cfg {Object} source A data object to use as the data source of the grid (see {@link #setSource} for details). - */ - /** - * @cfg {Object} customEditors An object containing name/value pairs of custom editor type definitions that allow - * the grid to support additional types of editable fields. By default, the grid supports strongly-typed editing - * of strings, dates, numbers and booleans using built-in form editors, but any custom type can be supported and - * associated with a custom input control by specifying a custom editor. The name of the editor - * type should correspond with the name of the property that will use the editor. Example usage: - *
        
        -var grid = new Ext.grid.PropertyGrid({
        -    ...
        -    customEditors: {
        -        'Start Time': new Ext.grid.GridEditor(new Ext.form.TimeField({selectOnFocus:true}))
        -    },
        -    source: {
        -        'Start Time': '10:00 AM'
        -    }
        -});
        -
        - */ - /** - * @cfg {Object} source A data object to use as the data source of the grid (see {@link #setSource} for details). - */ - /** - * @cfg {Object} customRenderers An object containing name/value pairs of custom renderer type definitions that allow - * the grid to support custom rendering of fields. By default, the grid supports strongly-typed rendering - * of strings, dates, numbers and booleans using built-in form editors, but any custom type can be supported and - * associated with the type of the value. The name of the renderer type should correspond with the name of the property - * that it will render. Example usage: - *
        
        -var grid = new Ext.grid.PropertyGrid({
        -    ...
        -    customRenderers: {
        -        Available: function(v){
        -            if(v){
        -                return 'Yes';
        -            }else{
        -                return 'No';
        -            }
        -        }
        -    },
        -    source: {
        -        Available: true
        -    }
        -});
        -
        - */ - - // private config overrides - enableColumnMove:false, - stripeRows:false, - trackMouseOver: false, - clicksToEdit:1, - enableHdMenu : false, - viewConfig : { - forceFit:true - }, - - // private - initComponent : function(){ - this.customRenderers = this.customRenderers || {}; - this.customEditors = this.customEditors || {}; - this.lastEditRow = null; - var store = new Ext.grid.PropertyStore(this); - this.propStore = store; - var cm = new Ext.grid.PropertyColumnModel(this, store); - store.store.sort('name', 'ASC'); - this.addEvents( - /** - * @event beforepropertychange - * Fires before a property value changes. Handlers can return false to cancel the property change - * (this will internally call {@link Ext.data.Record#reject} on the property's record). - * @param {Object} source The source data object for the grid (corresponds to the same object passed in - * as the {@link #source} config property). - * @param {String} recordId The record's id in the data store - * @param {Mixed} value The current edited property value - * @param {Mixed} oldValue The original property value prior to editing - */ - 'beforepropertychange', - /** - * @event propertychange - * Fires after a property value has changed. - * @param {Object} source The source data object for the grid (corresponds to the same object passed in - * as the {@link #source} config property). - * @param {String} recordId The record's id in the data store - * @param {Mixed} value The current edited property value - * @param {Mixed} oldValue The original property value prior to editing - */ - 'propertychange' - ); - this.cm = cm; - this.ds = store.store; - Ext.grid.PropertyGrid.superclass.initComponent.call(this); - - this.mon(this.selModel, 'beforecellselect', function(sm, rowIndex, colIndex){ - if(colIndex === 0){ - this.startEditing.defer(200, this, [rowIndex, 1]); - return false; - } - }, this); - }, - - // private - onRender : function(){ - Ext.grid.PropertyGrid.superclass.onRender.apply(this, arguments); - - this.getGridEl().addClass('x-props-grid'); - }, - - // private - afterRender: function(){ - Ext.grid.PropertyGrid.superclass.afterRender.apply(this, arguments); - if(this.source){ - this.setSource(this.source); - } - }, - - /** - * Sets the source data object containing the property data. The data object can contain one or more name/value - * pairs representing all of the properties of an object to display in the grid, and this data will automatically - * be loaded into the grid's {@link #store}. The values should be supplied in the proper data type if needed, - * otherwise string type will be assumed. If the grid already contains data, this method will replace any - * existing data. See also the {@link #source} config value. Example usage: - *
        
        -grid.setSource({
        -    "(name)": "My Object",
        -    "Created": new Date(Date.parse('10/15/2006')),  // date type
        -    "Available": false,  // boolean type
        -    "Version": .01,      // decimal type
        -    "Description": "A test object"
        -});
        -
        - * @param {Object} source The data object - */ - setSource : function(source){ - this.propStore.setSource(source); - }, - - /** - * Gets the source data object containing the property data. See {@link #setSource} for details regarding the - * format of the data object. - * @return {Object} The data object - */ - getSource : function(){ - return this.propStore.getSource(); - }, - - /** - * Sets the value of a property. - * @param {String} prop The name of the property to set - * @param {Mixed} value The value to test - * @param {Boolean} create (Optional) True to create the property if it doesn't already exist. Defaults to false. - */ - setProperty : function(prop, value, create){ - this.propStore.setValue(prop, value, create); - }, - - /** - * Removes a property from the grid. - * @param {String} prop The name of the property to remove - */ - removeProperty : function(prop){ - this.propStore.remove(prop); - } - - /** - * @cfg store - * @hide - */ - /** - * @cfg colModel - * @hide - */ - /** - * @cfg cm - * @hide - */ - /** - * @cfg columns - * @hide - */ -}); -Ext.reg("propertygrid", Ext.grid.PropertyGrid); -/** - * @class Ext.grid.GroupingView - * @extends Ext.grid.GridView - * Adds the ability for single level grouping to the grid. A {@link Ext.data.GroupingStore GroupingStore} - * must be used to enable grouping. Some grouping characteristics may also be configured at the - * {@link Ext.grid.Column Column level}
          - *
        • {@link Ext.grid.Column#emptyGroupText emptyGroupText}
        • - *
        • {@link Ext.grid.Column#groupable groupable}
        • - *
        • {@link Ext.grid.Column#groupName groupName}
        • - *
        • {@link Ext.grid.Column#groupRender groupRender}
        • - *
        - *

        Sample usage:

        - *
        
        -var grid = new Ext.grid.GridPanel({
        -    // A groupingStore is required for a GroupingView
        -    store: new {@link Ext.data.GroupingStore}({
        -        autoDestroy: true,
        -        reader: reader,
        -        data: xg.dummyData,
        -        sortInfo: {field: 'company', direction: 'ASC'},
        -        {@link Ext.data.GroupingStore#groupOnSort groupOnSort}: true,
        -        {@link Ext.data.GroupingStore#remoteGroup remoteGroup}: true,
        -        {@link Ext.data.GroupingStore#groupField groupField}: 'industry'
        -    }),
        -    colModel: new {@link Ext.grid.ColumnModel}({
        -        columns:[
        -            {id:'company',header: 'Company', width: 60, dataIndex: 'company'},
        -            // {@link Ext.grid.Column#groupable groupable}, {@link Ext.grid.Column#groupName groupName}, {@link Ext.grid.Column#groupRender groupRender} are also configurable at column level
        -            {header: 'Price', renderer: Ext.util.Format.usMoney, dataIndex: 'price', {@link Ext.grid.Column#groupable groupable}: false},
        -            {header: 'Change', dataIndex: 'change', renderer: Ext.util.Format.usMoney},
        -            {header: 'Industry', dataIndex: 'industry'},
        -            {header: 'Last Updated', renderer: Ext.util.Format.dateRenderer('m/d/Y'), dataIndex: 'lastChange'}
        -        ],
        -        defaults: {
        -            sortable: true,
        -            menuDisabled: false,
        -            width: 20
        -        }
        -    }),
        -
        -    view: new Ext.grid.GroupingView({
        -        {@link Ext.grid.GridView#forceFit forceFit}: true,
        -        // custom grouping text template to display the number of items per group
        -        {@link #groupTextTpl}: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Items" : "Item"]})'
        -    }),
        -
        -    frame:true,
        -    width: 700,
        -    height: 450,
        -    collapsible: true,
        -    animCollapse: false,
        -    title: 'Grouping Example',
        -    iconCls: 'icon-grid',
        -    renderTo: document.body
        -});
        - * 
        - * @constructor - * @param {Object} config - */ -Ext.grid.GroupingView = Ext.extend(Ext.grid.GridView, { - - /** - * @cfg {String} groupByText Text displayed in the grid header menu for grouping by a column - * (defaults to 'Group By This Field'). - */ - groupByText : 'Group By This Field', - /** - * @cfg {String} showGroupsText Text displayed in the grid header for enabling/disabling grouping - * (defaults to 'Show in Groups'). - */ - showGroupsText : 'Show in Groups', - /** - * @cfg {Boolean} hideGroupedColumn true to hide the column that is currently grouped (defaults to false) - */ - hideGroupedColumn : false, - /** - * @cfg {Boolean} showGroupName If true will display a prefix plus a ': ' before the group field value - * in the group header line. The prefix will consist of the {@link Ext.grid.Column#groupName groupName} - * (or the configured {@link Ext.grid.Column#header header} if not provided) configured in the - * {@link Ext.grid.Column} for each set of grouped rows (defaults to true). - */ - showGroupName : true, - /** - * @cfg {Boolean} startCollapsed true to start all groups collapsed (defaults to false) - */ - startCollapsed : false, - /** - * @cfg {Boolean} enableGrouping false to disable grouping functionality (defaults to true) - */ - enableGrouping : true, - /** - * @cfg {Boolean} enableGroupingMenu true to enable the grouping control in the column menu (defaults to true) - */ - enableGroupingMenu : true, - /** - * @cfg {Boolean} enableNoGroups true to allow the user to turn off grouping (defaults to true) - */ - enableNoGroups : true, - /** - * @cfg {String} emptyGroupText The text to display when there is an empty group value (defaults to '(None)'). - * May also be specified per column, see {@link Ext.grid.Column}.{@link Ext.grid.Column#emptyGroupText emptyGroupText}. - */ - emptyGroupText : '(None)', - /** - * @cfg {Boolean} ignoreAdd true to skip refreshing the view when new rows are added (defaults to false) - */ - ignoreAdd : false, - /** - * @cfg {String} groupTextTpl The template used to render the group header (defaults to '{text}'). - * This is used to format an object which contains the following properties: - *
          - *
        • group : String

          The rendered value of the group field. - * By default this is the unchanged value of the group field. If a {@link Ext.grid.Column#groupRenderer groupRenderer} - * is specified, it is the result of a call to that function.

        • - *
        • gvalue : Object

          The raw value of the group field.

        • - *
        • text : String

          The configured header (as described in {@link #showGroupName}) - * if {@link #showGroupName} is true) plus the rendered group field value.

        • - *
        • groupId : String

          A unique, generated ID which is applied to the - * View Element which contains the group.

        • - *
        • startRow : Number

          The row index of the Record which caused group change.

        • - *
        • rs : Array

          Contains a single element: The Record providing the data - * for the row which caused group change.

        • - *
        • cls : String

          The generated class name string to apply to the group header Element.

        • - *
        • style : String

          The inline style rules to apply to the group header Element.

        • - *

        - * See {@link Ext.XTemplate} for information on how to format data using a template. Possible usage:
        
        -var grid = new Ext.grid.GridPanel({
        -    ...
        -    view: new Ext.grid.GroupingView({
        -        groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Items" : "Item"]})'
        -    }),
        -});
        -     * 
        - */ - groupTextTpl : '{text}', - - /** - * @cfg {String} groupMode Indicates how to construct the group identifier. 'value' constructs the id using - * raw value, 'display' constructs the id using the rendered value. Defaults to 'value'. - */ - groupMode: 'value', - - /** - * @cfg {Function} groupRenderer This property must be configured in the {@link Ext.grid.Column} for - * each column. - */ - - /** - * @cfg {Boolean} cancelEditOnToggle True to cancel any editing when the group header is toggled. Defaults to true. - */ - cancelEditOnToggle: true, - - // private - initTemplates : function(){ - Ext.grid.GroupingView.superclass.initTemplates.call(this); - this.state = {}; - - var sm = this.grid.getSelectionModel(); - sm.on(sm.selectRow ? 'beforerowselect' : 'beforecellselect', - this.onBeforeRowSelect, this); - - if(!this.startGroup){ - this.startGroup = new Ext.XTemplate( - '
        ', - '
        ', this.groupTextTpl ,'
        ', - '
        ' - ); - } - this.startGroup.compile(); - - if (!this.endGroup) { - this.endGroup = '
        '; - } - }, - - // private - findGroup : function(el){ - return Ext.fly(el).up('.x-grid-group', this.mainBody.dom); - }, - - // private - getGroups : function(){ - return this.hasRows() ? this.mainBody.dom.childNodes : []; - }, - - // private - onAdd : function(ds, records, index) { - if (this.canGroup() && !this.ignoreAdd) { - var ss = this.getScrollState(); - this.fireEvent('beforerowsinserted', ds, index, index + (records.length-1)); - this.refresh(); - this.restoreScroll(ss); - this.fireEvent('rowsinserted', ds, index, index + (records.length-1)); - } else if (!this.canGroup()) { - Ext.grid.GroupingView.superclass.onAdd.apply(this, arguments); - } - }, - - // private - onRemove : function(ds, record, index, isUpdate){ - Ext.grid.GroupingView.superclass.onRemove.apply(this, arguments); - var g = document.getElementById(record._groupId); - if(g && g.childNodes[1].childNodes.length < 1){ - Ext.removeNode(g); - } - this.applyEmptyText(); - }, - - // private - refreshRow : function(record){ - if(this.ds.getCount()==1){ - this.refresh(); - }else{ - this.isUpdating = true; - Ext.grid.GroupingView.superclass.refreshRow.apply(this, arguments); - this.isUpdating = false; - } - }, - - // private - beforeMenuShow : function(){ - var item, items = this.hmenu.items, disabled = this.cm.config[this.hdCtxIndex].groupable === false; - if((item = items.get('groupBy'))){ - item.setDisabled(disabled); - } - if((item = items.get('showGroups'))){ - item.setDisabled(disabled); - item.setChecked(this.canGroup(), true); - } - }, - - // private - renderUI : function(){ - var markup = Ext.grid.GroupingView.superclass.renderUI.call(this); - - if(this.enableGroupingMenu && this.hmenu){ - this.hmenu.add('-',{ - itemId:'groupBy', - text: this.groupByText, - handler: this.onGroupByClick, - scope: this, - iconCls:'x-group-by-icon' - }); - if(this.enableNoGroups){ - this.hmenu.add({ - itemId:'showGroups', - text: this.showGroupsText, - checked: true, - checkHandler: this.onShowGroupsClick, - scope: this - }); - } - this.hmenu.on('beforeshow', this.beforeMenuShow, this); - } - return markup; - }, - - processEvent: function(name, e){ - Ext.grid.GroupingView.superclass.processEvent.call(this, name, e); - var hd = e.getTarget('.x-grid-group-hd', this.mainBody); - if(hd){ - // group value is at the end of the string - var field = this.getGroupField(), - prefix = this.getPrefix(field), - groupValue = hd.id.substring(prefix.length), - emptyRe = new RegExp('gp-' + Ext.escapeRe(field) + '--hd'); - - // remove trailing '-hd' - groupValue = groupValue.substr(0, groupValue.length - 3); - - // also need to check for empty groups - if(groupValue || emptyRe.test(hd.id)){ - this.grid.fireEvent('group' + name, this.grid, field, groupValue, e); - } - if(name == 'mousedown' && e.button == 0){ - this.toggleGroup(hd.parentNode); - } - } - - }, - - // private - onGroupByClick : function(){ - var grid = this.grid; - this.enableGrouping = true; - grid.store.groupBy(this.cm.getDataIndex(this.hdCtxIndex)); - grid.fireEvent('groupchange', grid, grid.store.getGroupState()); - this.beforeMenuShow(); // Make sure the checkboxes get properly set when changing groups - this.refresh(); - }, - - // private - onShowGroupsClick : function(mi, checked){ - this.enableGrouping = checked; - if(checked){ - this.onGroupByClick(); - }else{ - this.grid.store.clearGrouping(); - this.grid.fireEvent('groupchange', this, null); - } - }, - - /** - * Toggle the group that contains the specific row. - * @param {Number} rowIndex The row inside the group - * @param {Boolean} expanded (optional) - */ - toggleRowIndex : function(rowIndex, expanded){ - if(!this.canGroup()){ - return; - } - var row = this.getRow(rowIndex); - if(row){ - this.toggleGroup(this.findGroup(row), expanded); - } - }, - - /** - * Toggles the specified group if no value is passed, otherwise sets the expanded state of the group to the value passed. - * @param {String} groupId The groupId assigned to the group (see getGroupId) - * @param {Boolean} expanded (optional) - */ - toggleGroup : function(group, expanded){ - var gel = Ext.get(group), - id = Ext.util.Format.htmlEncode(gel.id); - - expanded = Ext.isDefined(expanded) ? expanded : gel.hasClass('x-grid-group-collapsed'); - if(this.state[id] !== expanded){ - if (this.cancelEditOnToggle !== false) { - this.grid.stopEditing(true); - } - this.state[id] = expanded; - gel[expanded ? 'removeClass' : 'addClass']('x-grid-group-collapsed'); - } - }, - - /** - * Toggles all groups if no value is passed, otherwise sets the expanded state of all groups to the value passed. - * @param {Boolean} expanded (optional) - */ - toggleAllGroups : function(expanded){ - var groups = this.getGroups(); - for(var i = 0, len = groups.length; i < len; i++){ - this.toggleGroup(groups[i], expanded); - } - }, - - /** - * Expands all grouped rows. - */ - expandAllGroups : function(){ - this.toggleAllGroups(true); - }, - - /** - * Collapses all grouped rows. - */ - collapseAllGroups : function(){ - this.toggleAllGroups(false); - }, - - // private - getGroup : function(v, r, groupRenderer, rowIndex, colIndex, ds){ - var column = this.cm.config[colIndex], - g = groupRenderer ? groupRenderer.call(column.scope, v, {}, r, rowIndex, colIndex, ds) : String(v); - if(g === '' || g === ' '){ - g = column.emptyGroupText || this.emptyGroupText; - } - return g; - }, - - // private - getGroupField : function(){ - return this.grid.store.getGroupState(); - }, - - // private - afterRender : function(){ - if(!this.ds || !this.cm){ - return; - } - Ext.grid.GroupingView.superclass.afterRender.call(this); - if(this.grid.deferRowRender){ - this.updateGroupWidths(); - } - }, - - afterRenderUI: function () { - Ext.grid.GroupingView.superclass.afterRenderUI.call(this); - - if (this.enableGroupingMenu && this.hmenu) { - this.hmenu.add('-',{ - itemId:'groupBy', - text: this.groupByText, - handler: this.onGroupByClick, - scope: this, - iconCls:'x-group-by-icon' - }); - - if (this.enableNoGroups) { - this.hmenu.add({ - itemId:'showGroups', - text: this.showGroupsText, - checked: true, - checkHandler: this.onShowGroupsClick, - scope: this - }); - } - - this.hmenu.on('beforeshow', this.beforeMenuShow, this); - } - }, - - // private - renderRows : function(){ - var groupField = this.getGroupField(); - var eg = !!groupField; - // if they turned off grouping and the last grouped field is hidden - if(this.hideGroupedColumn) { - var colIndex = this.cm.findColumnIndex(groupField), - hasLastGroupField = Ext.isDefined(this.lastGroupField); - if(!eg && hasLastGroupField){ - this.mainBody.update(''); - this.cm.setHidden(this.cm.findColumnIndex(this.lastGroupField), false); - delete this.lastGroupField; - }else if (eg && !hasLastGroupField){ - this.lastGroupField = groupField; - this.cm.setHidden(colIndex, true); - }else if (eg && hasLastGroupField && groupField !== this.lastGroupField) { - this.mainBody.update(''); - var oldIndex = this.cm.findColumnIndex(this.lastGroupField); - this.cm.setHidden(oldIndex, false); - this.lastGroupField = groupField; - this.cm.setHidden(colIndex, true); - } - } - return Ext.grid.GroupingView.superclass.renderRows.apply( - this, arguments); - }, - - // private - doRender : function(cs, rs, ds, startRow, colCount, stripe){ - if(rs.length < 1){ - return ''; - } - - if(!this.canGroup() || this.isUpdating){ - return Ext.grid.GroupingView.superclass.doRender.apply(this, arguments); - } - - var groupField = this.getGroupField(), - colIndex = this.cm.findColumnIndex(groupField), - g, - gstyle = 'width:' + this.getTotalWidth() + ';', - cfg = this.cm.config[colIndex], - groupRenderer = cfg.groupRenderer || cfg.renderer, - prefix = this.showGroupName ? (cfg.groupName || cfg.header)+': ' : '', - groups = [], - curGroup, i, len, gid; - - for(i = 0, len = rs.length; i < len; i++){ - var rowIndex = startRow + i, - r = rs[i], - gvalue = r.data[groupField]; - - g = this.getGroup(gvalue, r, groupRenderer, rowIndex, colIndex, ds); - if(!curGroup || curGroup.group != g){ - gid = this.constructId(gvalue, groupField, colIndex); - // if state is defined use it, however state is in terms of expanded - // so negate it, otherwise use the default. - this.state[gid] = !(Ext.isDefined(this.state[gid]) ? !this.state[gid] : this.startCollapsed); - curGroup = { - group: g, - gvalue: gvalue, - text: prefix + g, - groupId: gid, - startRow: rowIndex, - rs: [r], - cls: this.state[gid] ? '' : 'x-grid-group-collapsed', - style: gstyle - }; - groups.push(curGroup); - }else{ - curGroup.rs.push(r); - } - r._groupId = gid; - } - - var buf = []; - for(i = 0, len = groups.length; i < len; i++){ - g = groups[i]; - this.doGroupStart(buf, g, cs, ds, colCount); - buf[buf.length] = Ext.grid.GroupingView.superclass.doRender.call( - this, cs, g.rs, ds, g.startRow, colCount, stripe); - - this.doGroupEnd(buf, g, cs, ds, colCount); - } - return buf.join(''); - }, - - /** - * Dynamically tries to determine the groupId of a specific value - * @param {String} value - * @return {String} The group id - */ - getGroupId : function(value){ - var field = this.getGroupField(); - return this.constructId(value, field, this.cm.findColumnIndex(field)); - }, - - // private - constructId : function(value, field, idx){ - var cfg = this.cm.config[idx], - groupRenderer = cfg.groupRenderer || cfg.renderer, - val = (this.groupMode == 'value') ? value : this.getGroup(value, {data:{}}, groupRenderer, 0, idx, this.ds); - - return this.getPrefix(field) + Ext.util.Format.htmlEncode(val); - }, - - // private - canGroup : function(){ - return this.enableGrouping && !!this.getGroupField(); - }, - - // private - getPrefix: function(field){ - return this.grid.getGridEl().id + '-gp-' + field + '-'; - }, - - // private - doGroupStart : function(buf, g, cs, ds, colCount){ - buf[buf.length] = this.startGroup.apply(g); - }, - - // private - doGroupEnd : function(buf, g, cs, ds, colCount){ - buf[buf.length] = this.endGroup; - }, - - // private - getRows : function(){ - if(!this.canGroup()){ - return Ext.grid.GroupingView.superclass.getRows.call(this); - } - var r = [], - gs = this.getGroups(), - g, - i = 0, - len = gs.length, - j, - jlen; - for(; i < len; ++i){ - g = gs[i].childNodes[1]; - if(g){ - g = g.childNodes; - for(j = 0, jlen = g.length; j < jlen; ++j){ - r[r.length] = g[j]; - } - } - } - return r; - }, - - // private - updateGroupWidths : function(){ - if(!this.canGroup() || !this.hasRows()){ - return; - } - var tw = Math.max(this.cm.getTotalWidth(), this.el.dom.offsetWidth-this.getScrollOffset()) +'px'; - var gs = this.getGroups(); - for(var i = 0, len = gs.length; i < len; i++){ - gs[i].firstChild.style.width = tw; - } - }, - - // private - onColumnWidthUpdated : function(col, w, tw){ - Ext.grid.GroupingView.superclass.onColumnWidthUpdated.call(this, col, w, tw); - this.updateGroupWidths(); - }, - - // private - onAllColumnWidthsUpdated : function(ws, tw){ - Ext.grid.GroupingView.superclass.onAllColumnWidthsUpdated.call(this, ws, tw); - this.updateGroupWidths(); - }, - - // private - onColumnHiddenUpdated : function(col, hidden, tw){ - Ext.grid.GroupingView.superclass.onColumnHiddenUpdated.call(this, col, hidden, tw); - this.updateGroupWidths(); - }, - - // private - onLayout : function(){ - this.updateGroupWidths(); - }, - - // private - onBeforeRowSelect : function(sm, rowIndex){ - this.toggleRowIndex(rowIndex, true); - } -}); -// private -Ext.grid.GroupingView.GROUP_ID = 1000; \ No newline at end of file diff --git a/scm-webapp/src/main/webapp/resources/extjs/ext-all-debug.js b/scm-webapp/src/main/webapp/resources/extjs/ext-all-debug.js deleted file mode 100644 index f878a11418..0000000000 --- a/scm-webapp/src/main/webapp/resources/extjs/ext-all-debug.js +++ /dev/null @@ -1,52079 +0,0 @@ - -(function(){ - -var EXTUTIL = Ext.util, - EACH = Ext.each, - TRUE = true, - FALSE = false; - -EXTUTIL.Observable = function(){ - - var me = this, e = me.events; - if(me.listeners){ - me.on(me.listeners); - delete me.listeners; - } - me.events = e || {}; -}; - -EXTUTIL.Observable.prototype = { - - filterOptRe : /^(?:scope|delay|buffer|single)$/, - - - fireEvent : function(){ - var a = Array.prototype.slice.call(arguments, 0), - ename = a[0].toLowerCase(), - me = this, - ret = TRUE, - ce = me.events[ename], - cc, - q, - c; - if (me.eventsSuspended === TRUE) { - if (q = me.eventQueue) { - q.push(a); - } - } - else if(typeof ce == 'object') { - if (ce.bubble){ - if(ce.fire.apply(ce, a.slice(1)) === FALSE) { - return FALSE; - } - c = me.getBubbleTarget && me.getBubbleTarget(); - if(c && c.enableBubble) { - cc = c.events[ename]; - if(!cc || typeof cc != 'object' || !cc.bubble) { - c.enableBubble(ename); - } - return c.fireEvent.apply(c, a); - } - } - else { - a.shift(); - ret = ce.fire.apply(ce, a); - } - } - return ret; - }, - - - addListener : function(eventName, fn, scope, o){ - var me = this, - e, - oe, - ce; - - if (typeof eventName == 'object') { - o = eventName; - for (e in o) { - oe = o[e]; - if (!me.filterOptRe.test(e)) { - me.addListener(e, oe.fn || oe, oe.scope || o.scope, oe.fn ? oe : o); - } - } - } else { - eventName = eventName.toLowerCase(); - ce = me.events[eventName] || TRUE; - if (typeof ce == 'boolean') { - me.events[eventName] = ce = new EXTUTIL.Event(me, eventName); - } - ce.addListener(fn, scope, typeof o == 'object' ? o : {}); - } - }, - - - removeListener : function(eventName, fn, scope){ - var ce = this.events[eventName.toLowerCase()]; - if (typeof ce == 'object') { - ce.removeListener(fn, scope); - } - }, - - - purgeListeners : function(){ - var events = this.events, - evt, - key; - for(key in events){ - evt = events[key]; - if(typeof evt == 'object'){ - evt.clearListeners(); - } - } - }, - - - addEvents : function(o){ - var me = this; - me.events = me.events || {}; - if (typeof o == 'string') { - var a = arguments, - i = a.length; - while(i--) { - me.events[a[i]] = me.events[a[i]] || TRUE; - } - } else { - Ext.applyIf(me.events, o); - } - }, - - - hasListener : function(eventName){ - var e = this.events[eventName.toLowerCase()]; - return typeof e == 'object' && e.listeners.length > 0; - }, - - - suspendEvents : function(queueSuspended){ - this.eventsSuspended = TRUE; - if(queueSuspended && !this.eventQueue){ - this.eventQueue = []; - } - }, - - - resumeEvents : function(){ - var me = this, - queued = me.eventQueue || []; - me.eventsSuspended = FALSE; - delete me.eventQueue; - EACH(queued, function(e) { - me.fireEvent.apply(me, e); - }); - } -}; - -var OBSERVABLE = EXTUTIL.Observable.prototype; - -OBSERVABLE.on = OBSERVABLE.addListener; - -OBSERVABLE.un = OBSERVABLE.removeListener; - - -EXTUTIL.Observable.releaseCapture = function(o){ - o.fireEvent = OBSERVABLE.fireEvent; -}; - -function createTargeted(h, o, scope){ - return function(){ - if(o.target == arguments[0]){ - h.apply(scope, Array.prototype.slice.call(arguments, 0)); - } - }; -}; - -function createBuffered(h, o, l, scope){ - l.task = new EXTUTIL.DelayedTask(); - return function(){ - l.task.delay(o.buffer, h, scope, Array.prototype.slice.call(arguments, 0)); - }; -}; - -function createSingle(h, e, fn, scope){ - return function(){ - e.removeListener(fn, scope); - return h.apply(scope, arguments); - }; -}; - -function createDelayed(h, o, l, scope){ - return function(){ - var task = new EXTUTIL.DelayedTask(), - args = Array.prototype.slice.call(arguments, 0); - if(!l.tasks) { - l.tasks = []; - } - l.tasks.push(task); - task.delay(o.delay || 10, function(){ - l.tasks.remove(task); - h.apply(scope, args); - }, scope); - }; -}; - -EXTUTIL.Event = function(obj, name){ - this.name = name; - this.obj = obj; - this.listeners = []; -}; - -EXTUTIL.Event.prototype = { - addListener : function(fn, scope, options){ - var me = this, - l; - scope = scope || me.obj; - if(!me.isListening(fn, scope)){ - l = me.createListener(fn, scope, options); - if(me.firing){ - me.listeners = me.listeners.slice(0); - } - me.listeners.push(l); - } - }, - - createListener: function(fn, scope, o){ - o = o || {}; - scope = scope || this.obj; - var l = { - fn: fn, - scope: scope, - options: o - }, h = fn; - if(o.target){ - h = createTargeted(h, o, scope); - } - if(o.delay){ - h = createDelayed(h, o, l, scope); - } - if(o.single){ - h = createSingle(h, this, fn, scope); - } - if(o.buffer){ - h = createBuffered(h, o, l, scope); - } - l.fireFn = h; - return l; - }, - - findListener : function(fn, scope){ - var list = this.listeners, - i = list.length, - l; - - scope = scope || this.obj; - while(i--){ - l = list[i]; - if(l){ - if(l.fn == fn && l.scope == scope){ - return i; - } - } - } - return -1; - }, - - isListening : function(fn, scope){ - return this.findListener(fn, scope) != -1; - }, - - removeListener : function(fn, scope){ - var index, - l, - k, - me = this, - ret = FALSE; - if((index = me.findListener(fn, scope)) != -1){ - if (me.firing) { - me.listeners = me.listeners.slice(0); - } - l = me.listeners[index]; - if(l.task) { - l.task.cancel(); - delete l.task; - } - k = l.tasks && l.tasks.length; - if(k) { - while(k--) { - l.tasks[k].cancel(); - } - delete l.tasks; - } - me.listeners.splice(index, 1); - ret = TRUE; - } - return ret; - }, - - - clearListeners : function(){ - var me = this, - l = me.listeners, - i = l.length; - while(i--) { - me.removeListener(l[i].fn, l[i].scope); - } - }, - - fire : function(){ - var me = this, - listeners = me.listeners, - len = listeners.length, - i = 0, - l; - - if(len > 0){ - me.firing = TRUE; - var args = Array.prototype.slice.call(arguments, 0); - for (; i < len; i++) { - l = listeners[i]; - if(l && l.fireFn.apply(l.scope || me.obj || window, args) === FALSE) { - return (me.firing = FALSE); - } - } - } - me.firing = FALSE; - return TRUE; - } - -}; -})(); - -Ext.DomHelper = function(){ - var tempTableEl = null, - emptyTags = /^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i, - tableRe = /^table|tbody|tr|td$/i, - confRe = /tag|children|cn|html$/i, - tableElRe = /td|tr|tbody/i, - cssRe = /([a-z0-9-]+)\s*:\s*([^;\s]+(?:\s*[^;\s]+)*);?/gi, - endRe = /end/i, - pub, - - afterbegin = 'afterbegin', - afterend = 'afterend', - beforebegin = 'beforebegin', - beforeend = 'beforeend', - ts = '', - te = '
        ', - tbs = ts+'', - tbe = ''+te, - trs = tbs + '', - tre = ''+tbe; - - - function doInsert(el, o, returnElement, pos, sibling, append){ - var newNode = pub.insertHtml(pos, Ext.getDom(el), createHtml(o)); - return returnElement ? Ext.get(newNode, true) : newNode; - } - - - function createHtml(o){ - var b = '', - attr, - val, - key, - cn; - - if(typeof o == "string"){ - b = o; - } else if (Ext.isArray(o)) { - for (var i=0; i < o.length; i++) { - if(o[i]) { - b += createHtml(o[i]); - } - }; - } else { - b += '<' + (o.tag = o.tag || 'div'); - for (attr in o) { - val = o[attr]; - if(!confRe.test(attr)){ - if (typeof val == "object") { - b += ' ' + attr + '="'; - for (key in val) { - b += key + ':' + val[key] + ';'; - }; - b += '"'; - }else{ - b += ' ' + ({cls : 'class', htmlFor : 'for'}[attr] || attr) + '="' + val + '"'; - } - } - }; - - if (emptyTags.test(o.tag)) { - b += '/>'; - } else { - b += '>'; - if ((cn = o.children || o.cn)) { - b += createHtml(cn); - } else if(o.html){ - b += o.html; - } - b += ''; - } - } - return b; - } - - function ieTable(depth, s, h, e){ - tempTableEl.innerHTML = [s, h, e].join(''); - var i = -1, - el = tempTableEl, - ns; - while(++i < depth){ - el = el.firstChild; - } - - if(ns = el.nextSibling){ - var df = document.createDocumentFragment(); - while(el){ - ns = el.nextSibling; - df.appendChild(el); - el = ns; - } - el = df; - } - return el; - } - - - function insertIntoTable(tag, where, el, html) { - var node, - before; - - tempTableEl = tempTableEl || document.createElement('div'); - - if(tag == 'td' && (where == afterbegin || where == beforeend) || - !tableElRe.test(tag) && (where == beforebegin || where == afterend)) { - return; - } - before = where == beforebegin ? el : - where == afterend ? el.nextSibling : - where == afterbegin ? el.firstChild : null; - - if (where == beforebegin || where == afterend) { - el = el.parentNode; - } - - if (tag == 'td' || (tag == 'tr' && (where == beforeend || where == afterbegin))) { - node = ieTable(4, trs, html, tre); - } else if ((tag == 'tbody' && (where == beforeend || where == afterbegin)) || - (tag == 'tr' && (where == beforebegin || where == afterend))) { - node = ieTable(3, tbs, html, tbe); - } else { - node = ieTable(2, ts, html, te); - } - el.insertBefore(node, before); - return node; - } - - - function createContextualFragment(html){ - var div = document.createElement("div"), - fragment = document.createDocumentFragment(), - i = 0, - length, childNodes; - - div.innerHTML = html; - childNodes = div.childNodes; - length = childNodes.length; - - for (; i < length; i++) { - fragment.appendChild(childNodes[i].cloneNode(true)); - } - - return fragment; - } - - pub = { - - markup : function(o){ - return createHtml(o); - }, - - - applyStyles : function(el, styles){ - if (styles) { - var matches; - - el = Ext.fly(el); - if (typeof styles == "function") { - styles = styles.call(); - } - if (typeof styles == "string") { - - cssRe.lastIndex = 0; - while ((matches = cssRe.exec(styles))) { - el.setStyle(matches[1], matches[2]); - } - } else if (typeof styles == "object") { - el.setStyle(styles); - } - } - }, - - insertHtml : function(where, el, html){ - var hash = {}, - hashVal, - range, - rangeEl, - setStart, - frag, - rs; - - where = where.toLowerCase(); - - hash[beforebegin] = ['BeforeBegin', 'previousSibling']; - hash[afterend] = ['AfterEnd', 'nextSibling']; - - if (el.insertAdjacentHTML) { - if(tableRe.test(el.tagName) && (rs = insertIntoTable(el.tagName.toLowerCase(), where, el, html))){ - return rs; - } - - hash[afterbegin] = ['AfterBegin', 'firstChild']; - hash[beforeend] = ['BeforeEnd', 'lastChild']; - if ((hashVal = hash[where])) { - el.insertAdjacentHTML(hashVal[0], html); - return el[hashVal[1]]; - } - } else { - range = el.ownerDocument.createRange(); - setStart = 'setStart' + (endRe.test(where) ? 'After' : 'Before'); - if (hash[where]) { - range[setStart](el); - if (!range.createContextualFragment) { - frag = createContextualFragment(html); - } - else { - frag = range.createContextualFragment(html); - } - el.parentNode.insertBefore(frag, where == beforebegin ? el : el.nextSibling); - return el[(where == beforebegin ? 'previous' : 'next') + 'Sibling']; - } else { - rangeEl = (where == afterbegin ? 'first' : 'last') + 'Child'; - if (el.firstChild) { - range[setStart](el[rangeEl]); - if (!range.createContextualFragment) { - frag = createContextualFragment(html); - } - else { - frag = range.createContextualFragment(html); - } - if(where == afterbegin){ - el.insertBefore(frag, el.firstChild); - }else{ - el.appendChild(frag); - } - } else { - el.innerHTML = html; - } - return el[rangeEl]; - } - } - throw 'Illegal insertion point -> "' + where + '"'; - }, - - - insertBefore : function(el, o, returnElement){ - return doInsert(el, o, returnElement, beforebegin); - }, - - - insertAfter : function(el, o, returnElement){ - return doInsert(el, o, returnElement, afterend, 'nextSibling'); - }, - - - insertFirst : function(el, o, returnElement){ - return doInsert(el, o, returnElement, afterbegin, 'firstChild'); - }, - - - append : function(el, o, returnElement){ - return doInsert(el, o, returnElement, beforeend, '', true); - }, - - - overwrite : function(el, o, returnElement){ - el = Ext.getDom(el); - el.innerHTML = createHtml(o); - return returnElement ? Ext.get(el.firstChild) : el.firstChild; - }, - - createHtml : createHtml - }; - return pub; -}(); - -Ext.Template = function(html){ - var me = this, - a = arguments, - buf = [], - v; - - if (Ext.isArray(html)) { - html = html.join(""); - } else if (a.length > 1) { - for(var i = 0, len = a.length; i < len; i++){ - v = a[i]; - if(typeof v == 'object'){ - Ext.apply(me, v); - } else { - buf.push(v); - } - }; - html = buf.join(''); - } - - - me.html = html; - - if (me.compiled) { - me.compile(); - } -}; -Ext.Template.prototype = { - - re : /\{([\w\-]+)\}/g, - - - - applyTemplate : function(values){ - var me = this; - - return me.compiled ? - me.compiled(values) : - me.html.replace(me.re, function(m, name){ - return values[name] !== undefined ? values[name] : ""; - }); - }, - - - set : function(html, compile){ - var me = this; - me.html = html; - me.compiled = null; - return compile ? me.compile() : me; - }, - - - compile : function(){ - var me = this, - sep = Ext.isGecko ? "+" : ","; - - function fn(m, name){ - name = "values['" + name + "']"; - return "'"+ sep + '(' + name + " == undefined ? '' : " + name + ')' + sep + "'"; - } - - eval("this.compiled = function(values){ return " + (Ext.isGecko ? "'" : "['") + - me.html.replace(/\\/g, '\\\\').replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.re, fn) + - (Ext.isGecko ? "';};" : "'].join('');};")); - return me; - }, - - - insertFirst: function(el, values, returnElement){ - return this.doInsert('afterBegin', el, values, returnElement); - }, - - - insertBefore: function(el, values, returnElement){ - return this.doInsert('beforeBegin', el, values, returnElement); - }, - - - insertAfter : function(el, values, returnElement){ - return this.doInsert('afterEnd', el, values, returnElement); - }, - - - append : function(el, values, returnElement){ - return this.doInsert('beforeEnd', el, values, returnElement); - }, - - doInsert : function(where, el, values, returnEl){ - el = Ext.getDom(el); - var newNode = Ext.DomHelper.insertHtml(where, el, this.applyTemplate(values)); - return returnEl ? Ext.get(newNode, true) : newNode; - }, - - - overwrite : function(el, values, returnElement){ - el = Ext.getDom(el); - el.innerHTML = this.applyTemplate(values); - return returnElement ? Ext.get(el.firstChild, true) : el.firstChild; - } -}; - -Ext.Template.prototype.apply = Ext.Template.prototype.applyTemplate; - - -Ext.Template.from = function(el, config){ - el = Ext.getDom(el); - return new Ext.Template(el.value || el.innerHTML, config || ''); -}; - - -Ext.DomQuery = function(){ - var cache = {}, - simpleCache = {}, - valueCache = {}, - nonSpace = /\S/, - trimRe = /^\s+|\s+$/g, - tplRe = /\{(\d+)\}/g, - modeRe = /^(\s?[\/>+~]\s?|\s|$)/, - tagTokenRe = /^(#)?([\w\-\*]+)/, - nthRe = /(\d*)n\+?(\d*)/, - nthRe2 = /\D/, - - - - isIE = window.ActiveXObject ? true : false, - key = 30803; - - - - eval("var batch = 30803;"); - - - - function child(parent, index){ - var i = 0, - n = parent.firstChild; - while(n){ - if(n.nodeType == 1){ - if(++i == index){ - return n; - } - } - n = n.nextSibling; - } - return null; - } - - - function next(n){ - while((n = n.nextSibling) && n.nodeType != 1); - return n; - } - - - function prev(n){ - while((n = n.previousSibling) && n.nodeType != 1); - return n; - } - - - - function children(parent){ - var n = parent.firstChild, - nodeIndex = -1, - nextNode; - while(n){ - nextNode = n.nextSibling; - - if(n.nodeType == 3 && !nonSpace.test(n.nodeValue)){ - parent.removeChild(n); - }else{ - - n.nodeIndex = ++nodeIndex; - } - n = nextNode; - } - return this; - } - - - - - function byClassName(nodeSet, cls){ - if(!cls){ - return nodeSet; - } - var result = [], ri = -1; - for(var i = 0, ci; ci = nodeSet[i]; i++){ - if((' '+ci.className+' ').indexOf(cls) != -1){ - result[++ri] = ci; - } - } - return result; - }; - - function attrValue(n, attr){ - - if(!n.tagName && typeof n.length != "undefined"){ - n = n[0]; - } - if(!n){ - return null; - } - - if(attr == "for"){ - return n.htmlFor; - } - if(attr == "class" || attr == "className"){ - return n.className; - } - return n.getAttribute(attr) || n[attr]; - - }; - - - - - - function getNodes(ns, mode, tagName){ - var result = [], ri = -1, cs; - if(!ns){ - return result; - } - tagName = tagName || "*"; - - if(typeof ns.getElementsByTagName != "undefined"){ - ns = [ns]; - } - - - - if(!mode){ - for(var i = 0, ni; ni = ns[i]; i++){ - cs = ni.getElementsByTagName(tagName); - for(var j = 0, ci; ci = cs[j]; j++){ - result[++ri] = ci; - } - } - - - } else if(mode == "/" || mode == ">"){ - var utag = tagName.toUpperCase(); - for(var i = 0, ni, cn; ni = ns[i]; i++){ - cn = ni.childNodes; - for(var j = 0, cj; cj = cn[j]; j++){ - if(cj.nodeName == utag || cj.nodeName == tagName || tagName == '*'){ - result[++ri] = cj; - } - } - } - - - }else if(mode == "+"){ - var utag = tagName.toUpperCase(); - for(var i = 0, n; n = ns[i]; i++){ - while((n = n.nextSibling) && n.nodeType != 1); - if(n && (n.nodeName == utag || n.nodeName == tagName || tagName == '*')){ - result[++ri] = n; - } - } - - - }else if(mode == "~"){ - var utag = tagName.toUpperCase(); - for(var i = 0, n; n = ns[i]; i++){ - while((n = n.nextSibling)){ - if (n.nodeName == utag || n.nodeName == tagName || tagName == '*'){ - result[++ri] = n; - } - } - } - } - return result; - } - - function concat(a, b){ - if(b.slice){ - return a.concat(b); - } - for(var i = 0, l = b.length; i < l; i++){ - a[a.length] = b[i]; - } - return a; - } - - function byTag(cs, tagName){ - if(cs.tagName || cs == document){ - cs = [cs]; - } - if(!tagName){ - return cs; - } - var result = [], ri = -1; - tagName = tagName.toLowerCase(); - for(var i = 0, ci; ci = cs[i]; i++){ - if(ci.nodeType == 1 && ci.tagName.toLowerCase() == tagName){ - result[++ri] = ci; - } - } - return result; - } - - function byId(cs, id){ - if(cs.tagName || cs == document){ - cs = [cs]; - } - if(!id){ - return cs; - } - var result = [], ri = -1; - for(var i = 0, ci; ci = cs[i]; i++){ - if(ci && ci.id == id){ - result[++ri] = ci; - return result; - } - } - return result; - } - - - - function byAttribute(cs, attr, value, op, custom){ - var result = [], - ri = -1, - useGetStyle = custom == "{", - fn = Ext.DomQuery.operators[op], - a, - xml, - hasXml; - - for(var i = 0, ci; ci = cs[i]; i++){ - - if(ci.nodeType != 1){ - continue; - } - - if(!hasXml){ - xml = Ext.DomQuery.isXml(ci); - hasXml = true; - } - - - if(!xml){ - if(useGetStyle){ - a = Ext.DomQuery.getStyle(ci, attr); - } else if (attr == "class" || attr == "className"){ - a = ci.className; - } else if (attr == "for"){ - a = ci.htmlFor; - } else if (attr == "href"){ - - - a = ci.getAttribute("href", 2); - } else{ - a = ci.getAttribute(attr); - } - }else{ - a = ci.getAttribute(attr); - } - if((fn && fn(a, value)) || (!fn && a)){ - result[++ri] = ci; - } - } - return result; - } - - function byPseudo(cs, name, value){ - return Ext.DomQuery.pseudos[name](cs, value); - } - - function nodupIEXml(cs){ - var d = ++key, - r; - cs[0].setAttribute("_nodup", d); - r = [cs[0]]; - for(var i = 1, len = cs.length; i < len; i++){ - var c = cs[i]; - if(!c.getAttribute("_nodup") != d){ - c.setAttribute("_nodup", d); - r[r.length] = c; - } - } - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].removeAttribute("_nodup"); - } - return r; - } - - function nodup(cs){ - if(!cs){ - return []; - } - var len = cs.length, c, i, r = cs, cj, ri = -1; - if(!len || typeof cs.nodeType != "undefined" || len == 1){ - return cs; - } - if(isIE && typeof cs[0].selectSingleNode != "undefined"){ - return nodupIEXml(cs); - } - var d = ++key; - cs[0]._nodup = d; - for(i = 1; c = cs[i]; i++){ - if(c._nodup != d){ - c._nodup = d; - }else{ - r = []; - for(var j = 0; j < i; j++){ - r[++ri] = cs[j]; - } - for(j = i+1; cj = cs[j]; j++){ - if(cj._nodup != d){ - cj._nodup = d; - r[++ri] = cj; - } - } - return r; - } - } - return r; - } - - function quickDiffIEXml(c1, c2){ - var d = ++key, - r = []; - for(var i = 0, len = c1.length; i < len; i++){ - c1[i].setAttribute("_qdiff", d); - } - for(var i = 0, len = c2.length; i < len; i++){ - if(c2[i].getAttribute("_qdiff") != d){ - r[r.length] = c2[i]; - } - } - for(var i = 0, len = c1.length; i < len; i++){ - c1[i].removeAttribute("_qdiff"); - } - return r; - } - - function quickDiff(c1, c2){ - var len1 = c1.length, - d = ++key, - r = []; - if(!len1){ - return c2; - } - if(isIE && typeof c1[0].selectSingleNode != "undefined"){ - return quickDiffIEXml(c1, c2); - } - for(var i = 0; i < len1; i++){ - c1[i]._qdiff = d; - } - for(var i = 0, len = c2.length; i < len; i++){ - if(c2[i]._qdiff != d){ - r[r.length] = c2[i]; - } - } - return r; - } - - function quickId(ns, mode, root, id){ - if(ns == root){ - var d = root.ownerDocument || root; - return d.getElementById(id); - } - ns = getNodes(ns, mode, "*"); - return byId(ns, id); - } - - return { - getStyle : function(el, name){ - return Ext.fly(el).getStyle(name); - }, - - compile : function(path, type){ - type = type || "select"; - - - var fn = ["var f = function(root){\n var mode; ++batch; var n = root || document;\n"], - mode, - lastPath, - matchers = Ext.DomQuery.matchers, - matchersLn = matchers.length, - modeMatch, - - lmode = path.match(modeRe); - - if(lmode && lmode[1]){ - fn[fn.length] = 'mode="'+lmode[1].replace(trimRe, "")+'";'; - path = path.replace(lmode[1], ""); - } - - - while(path.substr(0, 1)=="/"){ - path = path.substr(1); - } - - while(path && lastPath != path){ - lastPath = path; - var tokenMatch = path.match(tagTokenRe); - if(type == "select"){ - if(tokenMatch){ - - if(tokenMatch[1] == "#"){ - fn[fn.length] = 'n = quickId(n, mode, root, "'+tokenMatch[2]+'");'; - }else{ - fn[fn.length] = 'n = getNodes(n, mode, "'+tokenMatch[2]+'");'; - } - path = path.replace(tokenMatch[0], ""); - }else if(path.substr(0, 1) != '@'){ - fn[fn.length] = 'n = getNodes(n, mode, "*");'; - } - - }else{ - if(tokenMatch){ - if(tokenMatch[1] == "#"){ - fn[fn.length] = 'n = byId(n, "'+tokenMatch[2]+'");'; - }else{ - fn[fn.length] = 'n = byTag(n, "'+tokenMatch[2]+'");'; - } - path = path.replace(tokenMatch[0], ""); - } - } - while(!(modeMatch = path.match(modeRe))){ - var matched = false; - for(var j = 0; j < matchersLn; j++){ - var t = matchers[j]; - var m = path.match(t.re); - if(m){ - fn[fn.length] = t.select.replace(tplRe, function(x, i){ - return m[i]; - }); - path = path.replace(m[0], ""); - matched = true; - break; - } - } - - if(!matched){ - throw 'Error parsing selector, parsing failed at "' + path + '"'; - } - } - if(modeMatch[1]){ - fn[fn.length] = 'mode="'+modeMatch[1].replace(trimRe, "")+'";'; - path = path.replace(modeMatch[1], ""); - } - } - - fn[fn.length] = "return nodup(n);\n}"; - - - eval(fn.join("")); - return f; - }, - - - jsSelect: function(path, root, type){ - - root = root || document; - - if(typeof root == "string"){ - root = document.getElementById(root); - } - var paths = path.split(","), - results = []; - - - for(var i = 0, len = paths.length; i < len; i++){ - var subPath = paths[i].replace(trimRe, ""); - - if(!cache[subPath]){ - cache[subPath] = Ext.DomQuery.compile(subPath); - if(!cache[subPath]){ - throw subPath + " is not a valid selector"; - } - } - var result = cache[subPath](root); - if(result && result != document){ - results = results.concat(result); - } - } - - - - if(paths.length > 1){ - return nodup(results); - } - return results; - }, - isXml: function(el) { - var docEl = (el ? el.ownerDocument || el : 0).documentElement; - return docEl ? docEl.nodeName !== "HTML" : false; - }, - select : document.querySelectorAll ? function(path, root, type) { - root = root || document; - if (!Ext.DomQuery.isXml(root)) { - try { - var cs = root.querySelectorAll(path); - return Ext.toArray(cs); - } - catch (ex) {} - } - return Ext.DomQuery.jsSelect.call(this, path, root, type); - } : function(path, root, type) { - return Ext.DomQuery.jsSelect.call(this, path, root, type); - }, - - - selectNode : function(path, root){ - return Ext.DomQuery.select(path, root)[0]; - }, - - - selectValue : function(path, root, defaultValue){ - path = path.replace(trimRe, ""); - if(!valueCache[path]){ - valueCache[path] = Ext.DomQuery.compile(path, "select"); - } - var n = valueCache[path](root), v; - n = n[0] ? n[0] : n; - - - - - - if (typeof n.normalize == 'function') n.normalize(); - - v = (n && n.firstChild ? n.firstChild.nodeValue : null); - return ((v === null||v === undefined||v==='') ? defaultValue : v); - }, - - - selectNumber : function(path, root, defaultValue){ - var v = Ext.DomQuery.selectValue(path, root, defaultValue || 0); - return parseFloat(v); - }, - - - is : function(el, ss){ - if(typeof el == "string"){ - el = document.getElementById(el); - } - var isArray = Ext.isArray(el), - result = Ext.DomQuery.filter(isArray ? el : [el], ss); - return isArray ? (result.length == el.length) : (result.length > 0); - }, - - - filter : function(els, ss, nonMatches){ - ss = ss.replace(trimRe, ""); - if(!simpleCache[ss]){ - simpleCache[ss] = Ext.DomQuery.compile(ss, "simple"); - } - var result = simpleCache[ss](els); - return nonMatches ? quickDiff(result, els) : result; - }, - - - matchers : [{ - re: /^\.([\w\-]+)/, - select: 'n = byClassName(n, " {1} ");' - }, { - re: /^\:([\w\-]+)(?:\(((?:[^\s>\/]*|.*?))\))?/, - select: 'n = byPseudo(n, "{1}", "{2}");' - },{ - re: /^(?:([\[\{])(?:@)?([\w\-]+)\s?(?:(=|.=)\s?(["']?)(.*?)\4)?[\]\}])/, - select: 'n = byAttribute(n, "{2}", "{5}", "{3}", "{1}");' - }, { - re: /^#([\w\-]+)/, - select: 'n = byId(n, "{1}");' - },{ - re: /^@([\w\-]+)/, - select: 'return {firstChild:{nodeValue:attrValue(n, "{1}")}};' - } - ], - - /** - * Collection of operator comparison functions. The default operators are =, !=, ^=, $=, *=, %=, |= and ~=. - * New operators can be added as long as the match the format c= where c is any character other than space, > <. - */ - operators : { - "=" : function(a, v){ - return a == v; - }, - "!=" : function(a, v){ - return a != v; - }, - "^=" : function(a, v){ - return a && a.substr(0, v.length) == v; - }, - "$=" : function(a, v){ - return a && a.substr(a.length-v.length) == v; - }, - "*=" : function(a, v){ - return a && a.indexOf(v) !== -1; - }, - "%=" : function(a, v){ - return (a % v) == 0; - }, - "|=" : function(a, v){ - return a && (a == v || a.substr(0, v.length+1) == v+'-'); - }, - "~=" : function(a, v){ - return a && (' '+a+' ').indexOf(' '+v+' ') != -1; - } - }, - - /** - *

        Object hash of "pseudo class" filter functions which are used when filtering selections. Each function is passed - * two parameters:

          - *
        • c : Array
          An Array of DOM elements to filter.
        • - *
        • v : String
          The argument (if any) supplied in the selector.
        • - *
        - *

        A filter function returns an Array of DOM elements which conform to the pseudo class.

        - *

        In addition to the provided pseudo classes listed above such as first-child and nth-child, - * developers may add additional, custom psuedo class filters to select elements according to application-specific requirements.

        - *

        For example, to filter <a> elements to only return links to external resources:

        - *
        -Ext.DomQuery.pseudos.external = function(c, v){
        -    var r = [], ri = -1;
        -    for(var i = 0, ci; ci = c[i]; i++){
        -//      Include in result set only if it's a link to an external resource
        -        if(ci.hostname != location.hostname){
        -            r[++ri] = ci;
        -        }
        -    }
        -    return r;
        -};
        - * Then external links could be gathered with the following statement:
        -var externalLinks = Ext.select("a:external");
        -
        - */ - pseudos : { - "first-child" : function(c){ - var r = [], ri = -1, n; - for(var i = 0, ci; ci = n = c[i]; i++){ - while((n = n.previousSibling) && n.nodeType != 1); - if(!n){ - r[++ri] = ci; - } - } - return r; - }, - - "last-child" : function(c){ - var r = [], ri = -1, n; - for(var i = 0, ci; ci = n = c[i]; i++){ - while((n = n.nextSibling) && n.nodeType != 1); - if(!n){ - r[++ri] = ci; - } - } - return r; - }, - - "nth-child" : function(c, a) { - var r = [], ri = -1, - m = nthRe.exec(a == "even" && "2n" || a == "odd" && "2n+1" || !nthRe2.test(a) && "n+" + a || a), - f = (m[1] || 1) - 0, l = m[2] - 0; - for(var i = 0, n; n = c[i]; i++){ - var pn = n.parentNode; - if (batch != pn._batch) { - var j = 0; - for(var cn = pn.firstChild; cn; cn = cn.nextSibling){ - if(cn.nodeType == 1){ - cn.nodeIndex = ++j; - } - } - pn._batch = batch; - } - if (f == 1) { - if (l == 0 || n.nodeIndex == l){ - r[++ri] = n; - } - } else if ((n.nodeIndex + l) % f == 0){ - r[++ri] = n; - } - } - - return r; - }, - - "only-child" : function(c){ - var r = [], ri = -1;; - for(var i = 0, ci; ci = c[i]; i++){ - if(!prev(ci) && !next(ci)){ - r[++ri] = ci; - } - } - return r; - }, - - "empty" : function(c){ - var r = [], ri = -1; - for(var i = 0, ci; ci = c[i]; i++){ - var cns = ci.childNodes, j = 0, cn, empty = true; - while(cn = cns[j]){ - ++j; - if(cn.nodeType == 1 || cn.nodeType == 3){ - empty = false; - break; - } - } - if(empty){ - r[++ri] = ci; - } - } - return r; - }, - - "contains" : function(c, v){ - var r = [], ri = -1; - for(var i = 0, ci; ci = c[i]; i++){ - if((ci.textContent||ci.innerText||'').indexOf(v) != -1){ - r[++ri] = ci; - } - } - return r; - }, - - "nodeValue" : function(c, v){ - var r = [], ri = -1; - for(var i = 0, ci; ci = c[i]; i++){ - if(ci.firstChild && ci.firstChild.nodeValue == v){ - r[++ri] = ci; - } - } - return r; - }, - - "checked" : function(c){ - var r = [], ri = -1; - for(var i = 0, ci; ci = c[i]; i++){ - if(ci.checked == true){ - r[++ri] = ci; - } - } - return r; - }, - - "not" : function(c, ss){ - return Ext.DomQuery.filter(c, ss, true); - }, - - "any" : function(c, selectors){ - var ss = selectors.split('|'), - r = [], ri = -1, s; - for(var i = 0, ci; ci = c[i]; i++){ - for(var j = 0; s = ss[j]; j++){ - if(Ext.DomQuery.is(ci, s)){ - r[++ri] = ci; - break; - } - } - } - return r; - }, - - "odd" : function(c){ - return this["nth-child"](c, "odd"); - }, - - "even" : function(c){ - return this["nth-child"](c, "even"); - }, - - "nth" : function(c, a){ - return c[a-1] || []; - }, - - "first" : function(c){ - return c[0] || []; - }, - - "last" : function(c){ - return c[c.length-1] || []; - }, - - "has" : function(c, ss){ - var s = Ext.DomQuery.select, - r = [], ri = -1; - for(var i = 0, ci; ci = c[i]; i++){ - if(s(ss, ci).length > 0){ - r[++ri] = ci; - } - } - return r; - }, - - "next" : function(c, ss){ - var is = Ext.DomQuery.is, - r = [], ri = -1; - for(var i = 0, ci; ci = c[i]; i++){ - var n = next(ci); - if(n && is(n, ss)){ - r[++ri] = ci; - } - } - return r; - }, - - "prev" : function(c, ss){ - var is = Ext.DomQuery.is, - r = [], ri = -1; - for(var i = 0, ci; ci = c[i]; i++){ - var n = prev(ci); - if(n && is(n, ss)){ - r[++ri] = ci; - } - } - return r; - } - } - }; -}(); - -/** - * Selects an array of DOM nodes by CSS/XPath selector. Shorthand of {@link Ext.DomQuery#select} - * @param {String} path The selector/xpath query - * @param {Node} root (optional) The start of the query (defaults to document). - * @return {Array} - * @member Ext - * @method query - */ -Ext.query = Ext.DomQuery.select; -/** - * @class Ext.util.DelayedTask - *

        The DelayedTask class provides a convenient way to "buffer" the execution of a method, - * performing setTimeout where a new timeout cancels the old timeout. When called, the - * task will wait the specified time period before executing. If durng that time period, - * the task is called again, the original call will be cancelled. This continues so that - * the function is only called a single time for each iteration.

        - *

        This method is especially useful for things like detecting whether a user has finished - * typing in a text field. An example would be performing validation on a keypress. You can - * use this class to buffer the keypress events for a certain number of milliseconds, and - * perform only if they stop for that amount of time. Usage:

        
        -var task = new Ext.util.DelayedTask(function(){
        -    alert(Ext.getDom('myInputField').value.length);
        -});
        -// Wait 500ms before calling our function. If the user presses another key 
        -// during that 500ms, it will be cancelled and we'll wait another 500ms.
        -Ext.get('myInputField').on('keypress', function(){
        -    task.{@link #delay}(500); 
        -});
        - * 
        - *

        Note that we are using a DelayedTask here to illustrate a point. The configuration - * option buffer for {@link Ext.util.Observable#addListener addListener/on} will - * also setup a delayed task for you to buffer events.

        - * @constructor The parameters to this constructor serve as defaults and are not required. - * @param {Function} fn (optional) The default function to call. - * @param {Object} scope The default scope (The this reference) in which the - * function is called. If not specified, this will refer to the browser window. - * @param {Array} args (optional) The default Array of arguments. - */ -Ext.util.DelayedTask = function(fn, scope, args){ - var me = this, - id, - call = function(){ - clearInterval(id); - id = null; - fn.apply(scope, args || []); - }; - - /** - * Cancels any pending timeout and queues a new one - * @param {Number} delay The milliseconds to delay - * @param {Function} newFn (optional) Overrides function passed to constructor - * @param {Object} newScope (optional) Overrides scope passed to constructor. Remember that if no scope - * is specified, this will refer to the browser window. - * @param {Array} newArgs (optional) Overrides args passed to constructor - */ - me.delay = function(delay, newFn, newScope, newArgs){ - me.cancel(); - fn = newFn || fn; - scope = newScope || scope; - args = newArgs || args; - id = setInterval(call, delay); - }; - - /** - * Cancel the last queued timeout - */ - me.cancel = function(){ - if(id){ - clearInterval(id); - id = null; - } - }; -};/** - * @class Ext.Element - *

        Encapsulates a DOM element, adding simple DOM manipulation facilities, normalizing for browser differences.

        - *

        All instances of this class inherit the methods of {@link Ext.Fx} making visual effects easily available to all DOM elements.

        - *

        Note that the events documented in this class are not Ext events, they encapsulate browser events. To - * access the underlying browser event, see {@link Ext.EventObject#browserEvent}. Some older - * browsers may not support the full range of events. Which events are supported is beyond the control of ExtJs.

        - * Usage:
        -
        
        -// by id
        -var el = Ext.get("my-div");
        -
        -// by DOM element reference
        -var el = Ext.get(myDivElement);
        -
        - * Animations
        - *

        When an element is manipulated, by default there is no animation.

        - *
        
        -var el = Ext.get("my-div");
        -
        -// no animation
        -el.setWidth(100);
        - * 
        - *

        Many of the functions for manipulating an element have an optional "animate" parameter. This - * parameter can be specified as boolean (true) for default animation effects.

        - *
        
        -// default animation
        -el.setWidth(100, true);
        - * 
        - * - *

        To configure the effects, an object literal with animation options to use as the Element animation - * configuration object can also be specified. Note that the supported Element animation configuration - * options are a subset of the {@link Ext.Fx} animation options specific to Fx effects. The supported - * Element animation configuration options are:

        -
        -Option    Default   Description
        ---------- --------  ---------------------------------------------
        -{@link Ext.Fx#duration duration}  .35       The duration of the animation in seconds
        -{@link Ext.Fx#easing easing}    easeOut   The easing method
        -{@link Ext.Fx#callback callback}  none      A function to execute when the anim completes
        -{@link Ext.Fx#scope scope}     this      The scope (this) of the callback function
        -
        - * - *
        
        -// Element animation options object
        -var opt = {
        -    {@link Ext.Fx#duration duration}: 1,
        -    {@link Ext.Fx#easing easing}: 'elasticIn',
        -    {@link Ext.Fx#callback callback}: this.foo,
        -    {@link Ext.Fx#scope scope}: this
        -};
        -// animation with some options set
        -el.setWidth(100, opt);
        - * 
        - *

        The Element animation object being used for the animation will be set on the options - * object as "anim", which allows you to stop or manipulate the animation. Here is an example:

        - *
        
        -// using the "anim" property to get the Anim object
        -if(opt.anim.isAnimated()){
        -    opt.anim.stop();
        -}
        - * 
        - *

        Also see the {@link #animate} method for another animation technique.

        - *

        Composite (Collections of) Elements

        - *

        For working with collections of Elements, see {@link Ext.CompositeElement}

        - * @constructor Create a new Element directly. - * @param {String/HTMLElement} element - * @param {Boolean} forceNew (optional) By default the constructor checks to see if there is already an instance of this element in the cache and if there is it returns the same instance. This will skip that check (useful for extending this class). - */ -(function(){ -var DOC = document; - -Ext.Element = function(element, forceNew){ - var dom = typeof element == "string" ? - DOC.getElementById(element) : element, - id; - - if(!dom) return null; - - id = dom.id; - - if(!forceNew && id && Ext.elCache[id]){ // element object already exists - return Ext.elCache[id].el; - } - - /** - * The DOM element - * @type HTMLElement - */ - this.dom = dom; - - /** - * The DOM element ID - * @type String - */ - this.id = id || Ext.id(dom); -}; - -var DH = Ext.DomHelper, - El = Ext.Element, - EC = Ext.elCache; - -El.prototype = { - /** - * Sets the passed attributes as attributes of this element (a style attribute can be a string, object or function) - * @param {Object} o The object with the attributes - * @param {Boolean} useSet (optional) false to override the default setAttribute to use expandos. - * @return {Ext.Element} this - */ - set : function(o, useSet){ - var el = this.dom, - attr, - val, - useSet = (useSet !== false) && !!el.setAttribute; - - for (attr in o) { - if (o.hasOwnProperty(attr)) { - val = o[attr]; - if (attr == 'style') { - DH.applyStyles(el, val); - } else if (attr == 'cls') { - el.className = val; - } else if (useSet) { - el.setAttribute(attr, val); - } else { - el[attr] = val; - } - } - } - return this; - }, - -// Mouse events - /** - * @event click - * Fires when a mouse click is detected within the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event contextmenu - * Fires when a right click is detected within the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event dblclick - * Fires when a mouse double click is detected within the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event mousedown - * Fires when a mousedown is detected within the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event mouseup - * Fires when a mouseup is detected within the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event mouseover - * Fires when a mouseover is detected within the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event mousemove - * Fires when a mousemove is detected with the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event mouseout - * Fires when a mouseout is detected with the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event mouseenter - * Fires when the mouse enters the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event mouseleave - * Fires when the mouse leaves the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - -// Keyboard events - /** - * @event keypress - * Fires when a keypress is detected within the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event keydown - * Fires when a keydown is detected within the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event keyup - * Fires when a keyup is detected within the element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - - -// HTML frame/object events - /** - * @event load - * Fires when the user agent finishes loading all content within the element. Only supported by window, frames, objects and images. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event unload - * Fires when the user agent removes all content from a window or frame. For elements, it fires when the target element or any of its content has been removed. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event abort - * Fires when an object/image is stopped from loading before completely loaded. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event error - * Fires when an object/image/frame cannot be loaded properly. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event resize - * Fires when a document view is resized. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event scroll - * Fires when a document view is scrolled. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - -// Form events - /** - * @event select - * Fires when a user selects some text in a text field, including input and textarea. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event change - * Fires when a control loses the input focus and its value has been modified since gaining focus. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event submit - * Fires when a form is submitted. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event reset - * Fires when a form is reset. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event focus - * Fires when an element receives focus either via the pointing device or by tab navigation. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event blur - * Fires when an element loses focus either via the pointing device or by tabbing navigation. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - -// User Interface events - /** - * @event DOMFocusIn - * Where supported. Similar to HTML focus event, but can be applied to any focusable element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event DOMFocusOut - * Where supported. Similar to HTML blur event, but can be applied to any focusable element. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event DOMActivate - * Where supported. Fires when an element is activated, for instance, through a mouse click or a keypress. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - -// DOM Mutation events - /** - * @event DOMSubtreeModified - * Where supported. Fires when the subtree is modified. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event DOMNodeInserted - * Where supported. Fires when a node has been added as a child of another node. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event DOMNodeRemoved - * Where supported. Fires when a descendant node of the element is removed. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event DOMNodeRemovedFromDocument - * Where supported. Fires when a node is being removed from a document. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event DOMNodeInsertedIntoDocument - * Where supported. Fires when a node is being inserted into a document. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event DOMAttrModified - * Where supported. Fires when an attribute has been modified. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - /** - * @event DOMCharacterDataModified - * Where supported. Fires when the character data has been modified. - * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event. - * @param {HtmlElement} t The target of the event. - * @param {Object} o The options configuration passed to the {@link #addListener} call. - */ - - /** - * The default unit to append to CSS values where a unit isn't provided (defaults to px). - * @type String - */ - defaultUnit : "px", - - /** - * Returns true if this element matches the passed simple selector (e.g. div.some-class or span:first-child) - * @param {String} selector The simple selector to test - * @return {Boolean} True if this element matches the selector, else false - */ - is : function(simpleSelector){ - return Ext.DomQuery.is(this.dom, simpleSelector); - }, - - /** - * Tries to focus the element. Any exceptions are caught and ignored. - * @param {Number} defer (optional) Milliseconds to defer the focus - * @return {Ext.Element} this - */ - focus : function(defer, /* private */ dom) { - var me = this, - dom = dom || me.dom; - try{ - if(Number(defer)){ - me.focus.defer(defer, null, [null, dom]); - }else{ - dom.focus(); - } - }catch(e){} - return me; - }, - - /** - * Tries to blur the element. Any exceptions are caught and ignored. - * @return {Ext.Element} this - */ - blur : function() { - try{ - this.dom.blur(); - }catch(e){} - return this; - }, - - /** - * Returns the value of the "value" attribute - * @param {Boolean} asNumber true to parse the value as a number - * @return {String/Number} - */ - getValue : function(asNumber){ - var val = this.dom.value; - return asNumber ? parseInt(val, 10) : val; - }, - - /** - * Appends an event handler to this element. The shorthand version {@link #on} is equivalent. - * @param {String} eventName The name of event to handle. - * @param {Function} fn The handler function the event invokes. This function is passed - * the following parameters:
          - *
        • evt : EventObject
          The {@link Ext.EventObject EventObject} describing the event.
        • - *
        • el : HtmlElement
          The DOM element which was the target of the event. - * Note that this may be filtered by using the delegate option.
        • - *
        • o : Object
          The options object from the addListener call.
        • - *
        - * @param {Object} scope (optional) The scope (this reference) in which the handler function is executed. - * If omitted, defaults to this Element.. - * @param {Object} options (optional) An object containing handler configuration properties. - * This may contain any of the following properties:
          - *
        • scope Object :
          The scope (this reference) in which the handler function is executed. - * If omitted, defaults to this Element.
        • - *
        • delegate String:
          A simple selector to filter the target or look for a descendant of the target. See below for additional details.
        • - *
        • stopEvent Boolean:
          True to stop the event. That is stop propagation, and prevent the default action.
        • - *
        • preventDefault Boolean:
          True to prevent the default action
        • - *
        • stopPropagation Boolean:
          True to prevent event propagation
        • - *
        • normalized Boolean:
          False to pass a browser event to the handler function instead of an Ext.EventObject
        • - *
        • target Ext.Element:
          Only call the handler if the event was fired on the target Element, not if the event was bubbled up from a child node.
        • - *
        • delay Number:
          The number of milliseconds to delay the invocation of the handler after the event fires.
        • - *
        • single Boolean:
          True to add a handler to handle just the next firing of the event, and then remove itself.
        • - *
        • buffer Number:
          Causes the handler to be scheduled to run in an {@link Ext.util.DelayedTask} delayed - * by the specified number of milliseconds. If the event fires again within that time, the original - * handler is not invoked, but the new handler is scheduled in its place.
        • - *

        - *

        - * Combining Options
        - * In the following examples, the shorthand form {@link #on} is used rather than the more verbose - * addListener. The two are equivalent. Using the options argument, it is possible to combine different - * types of listeners:
        - *
        - * A delayed, one-time listener that auto stops the event and adds a custom argument (forumId) to the - * options object. The options object is available as the third parameter in the handler function.

        - * Code:
        
        -el.on('click', this.onClick, this, {
        -    single: true,
        -    delay: 100,
        -    stopEvent : true,
        -    forumId: 4
        -});

        - *

        - * Attaching multiple handlers in 1 call
        - * The method also allows for a single argument to be passed which is a config object containing properties - * which specify multiple handlers.

        - *

        - * Code:

        
        -el.on({
        -    'click' : {
        -        fn: this.onClick,
        -        scope: this,
        -        delay: 100
        -    },
        -    'mouseover' : {
        -        fn: this.onMouseOver,
        -        scope: this
        -    },
        -    'mouseout' : {
        -        fn: this.onMouseOut,
        -        scope: this
        -    }
        -});
        - *

        - * Or a shorthand syntax:
        - * Code:

        -el.on({ - 'click' : this.onClick, - 'mouseover' : this.onMouseOver, - 'mouseout' : this.onMouseOut, - scope: this -}); - *

        - *

        delegate

        - *

        This is a configuration option that you can pass along when registering a handler for - * an event to assist with event delegation. Event delegation is a technique that is used to - * reduce memory consumption and prevent exposure to memory-leaks. By registering an event - * for a container element as opposed to each element within a container. By setting this - * configuration option to a simple selector, the target element will be filtered to look for - * a descendant of the target. - * For example:

        
        -// using this markup:
        -<div id='elId'>
        -    <p id='p1'>paragraph one</p>
        -    <p id='p2' class='clickable'>paragraph two</p>
        -    <p id='p3'>paragraph three</p>
        -</div>
        -// utilize event delegation to registering just one handler on the container element:
        -el = Ext.get('elId');
        -el.on(
        -    'click',
        -    function(e,t) {
        -        // handle click
        -        console.info(t.id); // 'p2'
        -    },
        -    this,
        -    {
        -        // filter the target element to be a descendant with the class 'clickable'
        -        delegate: '.clickable'
        -    }
        -);
        -     * 

        - * @return {Ext.Element} this - */ - addListener : function(eventName, fn, scope, options){ - Ext.EventManager.on(this.dom, eventName, fn, scope || this, options); - return this; - }, - - /** - * Removes an event handler from this element. The shorthand version {@link #un} is equivalent. - * Note: if a scope was explicitly specified when {@link #addListener adding} the - * listener, the same scope must be specified here. - * Example: - *
        
        -el.removeListener('click', this.handlerFn);
        -// or
        -el.un('click', this.handlerFn);
        -
        - * @param {String} eventName The name of the event from which to remove the handler. - * @param {Function} fn The handler function to remove. This must be a reference to the function passed into the {@link #addListener} call. - * @param {Object} scope If a scope (this reference) was specified when the listener was added, - * then this must refer to the same object. - * @return {Ext.Element} this - */ - removeListener : function(eventName, fn, scope){ - Ext.EventManager.removeListener(this.dom, eventName, fn, scope || this); - return this; - }, - - /** - * Removes all previous added listeners from this element - * @return {Ext.Element} this - */ - removeAllListeners : function(){ - Ext.EventManager.removeAll(this.dom); - return this; - }, - - /** - * Recursively removes all previous added listeners from this element and its children - * @return {Ext.Element} this - */ - purgeAllListeners : function() { - Ext.EventManager.purgeElement(this, true); - return this; - }, - /** - * @private Test if size has a unit, otherwise appends the default - */ - addUnits : function(size){ - if(size === "" || size == "auto" || size === undefined){ - size = size || ''; - } else if(!isNaN(size) || !unitPattern.test(size)){ - size = size + (this.defaultUnit || 'px'); - } - return size; - }, - - /** - *

        Updates the Same Origin Policy

        - *

        Updating innerHTML of an element will not execute embedded <script> elements. This is a browser restriction.

        - * @param {Mixed} options. Either a sring containing the URL from which to load the HTML, or an {@link Ext.Ajax#request} options object specifying - * exactly how to request the HTML. - * @return {Ext.Element} this - */ - load : function(url, params, cb){ - Ext.Ajax.request(Ext.apply({ - params: params, - url: url.url || url, - callback: cb, - el: this.dom, - indicatorText: url.indicatorText || '' - }, Ext.isObject(url) ? url : {})); - return this; - }, - - - isBorderBox : function(){ - return Ext.isBorderBox || Ext.isForcedBorderBox || noBoxAdjust[(this.dom.tagName || "").toLowerCase()]; - }, - - - remove : function(){ - var me = this, - dom = me.dom; - - if (dom) { - delete me.dom; - Ext.removeNode(dom); - } - }, - - - hover : function(overFn, outFn, scope, options){ - var me = this; - me.on('mouseenter', overFn, scope || me.dom, options); - me.on('mouseleave', outFn, scope || me.dom, options); - return me; - }, - - - contains : function(el){ - return !el ? false : Ext.lib.Dom.isAncestor(this.dom, el.dom ? el.dom : el); - }, - - - getAttributeNS : function(ns, name){ - return this.getAttribute(name, ns); - }, - - - getAttribute: (function(){ - var test = document.createElement('table'), - isBrokenOnTable = false, - hasGetAttribute = 'getAttribute' in test, - unknownRe = /undefined|unknown/; - - if (hasGetAttribute) { - - try { - test.getAttribute('ext:qtip'); - } catch (e) { - isBrokenOnTable = true; - } - - return function(name, ns) { - var el = this.dom, - value; - - if (el.getAttributeNS) { - value = el.getAttributeNS(ns, name) || null; - } - - if (value == null) { - if (ns) { - if (isBrokenOnTable && el.tagName.toUpperCase() == 'TABLE') { - try { - value = el.getAttribute(ns + ':' + name); - } catch (e) { - value = ''; - } - } else { - value = el.getAttribute(ns + ':' + name); - } - } else { - value = el.getAttribute(name) || el[name]; - } - } - return value || ''; - }; - } else { - return function(name, ns) { - var el = this.om, - value, - attribute; - - if (ns) { - attribute = el[ns + ':' + name]; - value = unknownRe.test(typeof attribute) ? undefined : attribute; - } else { - value = el[name]; - } - return value || ''; - }; - } - test = null; - })(), - - - update : function(html) { - if (this.dom) { - this.dom.innerHTML = html; - } - return this; - } -}; - -var ep = El.prototype; - -El.addMethods = function(o){ - Ext.apply(ep, o); -}; - - -ep.on = ep.addListener; - - -ep.un = ep.removeListener; - - -ep.autoBoxAdjust = true; - - -var unitPattern = /\d+(px|em|%|en|ex|pt|in|cm|mm|pc)$/i, - docEl; - - - - -El.get = function(el){ - var ex, - elm, - id; - if(!el){ return null; } - if (typeof el == "string") { - if (!(elm = DOC.getElementById(el))) { - return null; - } - if (EC[el] && EC[el].el) { - ex = EC[el].el; - ex.dom = elm; - } else { - ex = El.addToCache(new El(elm)); - } - return ex; - } else if (el.tagName) { - if(!(id = el.id)){ - id = Ext.id(el); - } - if (EC[id] && EC[id].el) { - ex = EC[id].el; - ex.dom = el; - } else { - ex = El.addToCache(new El(el)); - } - return ex; - } else if (el instanceof El) { - if(el != docEl){ - - - - - if (Ext.isIE && (el.id == undefined || el.id == '')) { - el.dom = el.dom; - } else { - el.dom = DOC.getElementById(el.id) || el.dom; - } - } - return el; - } else if(el.isComposite) { - return el; - } else if(Ext.isArray(el)) { - return El.select(el); - } else if(el == DOC) { - - if(!docEl){ - var f = function(){}; - f.prototype = El.prototype; - docEl = new f(); - docEl.dom = DOC; - } - return docEl; - } - return null; -}; - -El.addToCache = function(el, id){ - id = id || el.id; - EC[id] = { - el: el, - data: {}, - events: {} - }; - return el; -}; - - -El.data = function(el, key, value){ - el = El.get(el); - if (!el) { - return null; - } - var c = EC[el.id].data; - if(arguments.length == 2){ - return c[key]; - }else{ - return (c[key] = value); - } -}; - - - - -function garbageCollect(){ - if(!Ext.enableGarbageCollector){ - clearInterval(El.collectorThreadId); - } else { - var eid, - el, - d, - o; - - for(eid in EC){ - o = EC[eid]; - if(o.skipGC){ - continue; - } - el = o.el; - d = el.dom; - - - - - - - - - - - - - - - - - - if(!d || !d.parentNode || (!d.offsetParent && !DOC.getElementById(eid))){ - if(Ext.enableListenerCollection){ - Ext.EventManager.removeAll(d); - } - delete EC[eid]; - } - } - - if (Ext.isIE) { - var t = {}; - for (eid in EC) { - t[eid] = EC[eid]; - } - EC = Ext.elCache = t; - } - } -} -El.collectorThreadId = setInterval(garbageCollect, 30000); - -var flyFn = function(){}; -flyFn.prototype = El.prototype; - - -El.Flyweight = function(dom){ - this.dom = dom; -}; - -El.Flyweight.prototype = new flyFn(); -El.Flyweight.prototype.isFlyweight = true; -El._flyweights = {}; - - -El.fly = function(el, named){ - var ret = null; - named = named || '_global'; - - if (el = Ext.getDom(el)) { - (El._flyweights[named] = El._flyweights[named] || new El.Flyweight()).dom = el; - ret = El._flyweights[named]; - } - return ret; -}; - - -Ext.get = El.get; - - -Ext.fly = El.fly; - - -var noBoxAdjust = Ext.isStrict ? { - select:1 -} : { - input:1, select:1, textarea:1 -}; -if(Ext.isIE || Ext.isGecko){ - noBoxAdjust['button'] = 1; -} - -})(); - -Ext.Element.addMethods(function(){ - var PARENTNODE = 'parentNode', - NEXTSIBLING = 'nextSibling', - PREVIOUSSIBLING = 'previousSibling', - DQ = Ext.DomQuery, - GET = Ext.get; - - return { - - findParent : function(simpleSelector, maxDepth, returnEl){ - var p = this.dom, - b = document.body, - depth = 0, - stopEl; - if(Ext.isGecko && Object.prototype.toString.call(p) == '[object XULElement]') { - return null; - } - maxDepth = maxDepth || 50; - if (isNaN(maxDepth)) { - stopEl = Ext.getDom(maxDepth); - maxDepth = Number.MAX_VALUE; - } - while(p && p.nodeType == 1 && depth < maxDepth && p != b && p != stopEl){ - if(DQ.is(p, simpleSelector)){ - return returnEl ? GET(p) : p; - } - depth++; - p = p.parentNode; - } - return null; - }, - - - findParentNode : function(simpleSelector, maxDepth, returnEl){ - var p = Ext.fly(this.dom.parentNode, '_internal'); - return p ? p.findParent(simpleSelector, maxDepth, returnEl) : null; - }, - - - up : function(simpleSelector, maxDepth){ - return this.findParentNode(simpleSelector, maxDepth, true); - }, - - - select : function(selector){ - return Ext.Element.select(selector, this.dom); - }, - - - query : function(selector){ - return DQ.select(selector, this.dom); - }, - - - child : function(selector, returnDom){ - var n = DQ.selectNode(selector, this.dom); - return returnDom ? n : GET(n); - }, - - - down : function(selector, returnDom){ - var n = DQ.selectNode(" > " + selector, this.dom); - return returnDom ? n : GET(n); - }, - - - parent : function(selector, returnDom){ - return this.matchNode(PARENTNODE, PARENTNODE, selector, returnDom); - }, - - - next : function(selector, returnDom){ - return this.matchNode(NEXTSIBLING, NEXTSIBLING, selector, returnDom); - }, - - - prev : function(selector, returnDom){ - return this.matchNode(PREVIOUSSIBLING, PREVIOUSSIBLING, selector, returnDom); - }, - - - - first : function(selector, returnDom){ - return this.matchNode(NEXTSIBLING, 'firstChild', selector, returnDom); - }, - - - last : function(selector, returnDom){ - return this.matchNode(PREVIOUSSIBLING, 'lastChild', selector, returnDom); - }, - - matchNode : function(dir, start, selector, returnDom){ - var n = this.dom[start]; - while(n){ - if(n.nodeType == 1 && (!selector || DQ.is(n, selector))){ - return !returnDom ? GET(n) : n; - } - n = n[dir]; - } - return null; - } - }; -}()); -Ext.Element.addMethods( -function() { - var GETDOM = Ext.getDom, - GET = Ext.get, - DH = Ext.DomHelper; - - return { - - appendChild: function(el){ - return GET(el).appendTo(this); - }, - - - appendTo: function(el){ - GETDOM(el).appendChild(this.dom); - return this; - }, - - - insertBefore: function(el){ - (el = GETDOM(el)).parentNode.insertBefore(this.dom, el); - return this; - }, - - - insertAfter: function(el){ - (el = GETDOM(el)).parentNode.insertBefore(this.dom, el.nextSibling); - return this; - }, - - - insertFirst: function(el, returnDom){ - el = el || {}; - if(el.nodeType || el.dom || typeof el == 'string'){ - el = GETDOM(el); - this.dom.insertBefore(el, this.dom.firstChild); - return !returnDom ? GET(el) : el; - }else{ - return this.createChild(el, this.dom.firstChild, returnDom); - } - }, - - - replace: function(el){ - el = GET(el); - this.insertBefore(el); - el.remove(); - return this; - }, - - - replaceWith: function(el){ - var me = this; - - if(el.nodeType || el.dom || typeof el == 'string'){ - el = GETDOM(el); - me.dom.parentNode.insertBefore(el, me.dom); - }else{ - el = DH.insertBefore(me.dom, el); - } - - delete Ext.elCache[me.id]; - Ext.removeNode(me.dom); - me.id = Ext.id(me.dom = el); - Ext.Element.addToCache(me.isFlyweight ? new Ext.Element(me.dom) : me); - return me; - }, - - - createChild: function(config, insertBefore, returnDom){ - config = config || {tag:'div'}; - return insertBefore ? - DH.insertBefore(insertBefore, config, returnDom !== true) : - DH[!this.dom.firstChild ? 'overwrite' : 'append'](this.dom, config, returnDom !== true); - }, - - - wrap: function(config, returnDom){ - var newEl = DH.insertBefore(this.dom, config || {tag: "div"}, !returnDom); - newEl.dom ? newEl.dom.appendChild(this.dom) : newEl.appendChild(this.dom); - return newEl; - }, - - - insertHtml : function(where, html, returnEl){ - var el = DH.insertHtml(where, this.dom, html); - return returnEl ? Ext.get(el) : el; - } - }; -}()); -Ext.Element.addMethods(function(){ - - var supports = Ext.supports, - propCache = {}, - camelRe = /(-[a-z])/gi, - view = document.defaultView, - opacityRe = /alpha\(opacity=(.*)\)/i, - trimRe = /^\s+|\s+$/g, - EL = Ext.Element, - spacesRe = /\s+/, - wordsRe = /\w/g, - PADDING = "padding", - MARGIN = "margin", - BORDER = "border", - LEFT = "-left", - RIGHT = "-right", - TOP = "-top", - BOTTOM = "-bottom", - WIDTH = "-width", - MATH = Math, - HIDDEN = 'hidden', - ISCLIPPED = 'isClipped', - OVERFLOW = 'overflow', - OVERFLOWX = 'overflow-x', - OVERFLOWY = 'overflow-y', - ORIGINALCLIP = 'originalClip', - - borders = {l: BORDER + LEFT + WIDTH, r: BORDER + RIGHT + WIDTH, t: BORDER + TOP + WIDTH, b: BORDER + BOTTOM + WIDTH}, - paddings = {l: PADDING + LEFT, r: PADDING + RIGHT, t: PADDING + TOP, b: PADDING + BOTTOM}, - margins = {l: MARGIN + LEFT, r: MARGIN + RIGHT, t: MARGIN + TOP, b: MARGIN + BOTTOM}, - data = Ext.Element.data; - - - - function camelFn(m, a) { - return a.charAt(1).toUpperCase(); - } - - function chkCache(prop) { - return propCache[prop] || (propCache[prop] = prop == 'float' ? (supports.cssFloat ? 'cssFloat' : 'styleFloat') : prop.replace(camelRe, camelFn)); - } - - return { - - adjustWidth : function(width) { - var me = this; - var isNum = (typeof width == "number"); - if(isNum && me.autoBoxAdjust && !me.isBorderBox()){ - width -= (me.getBorderWidth("lr") + me.getPadding("lr")); - } - return (isNum && width < 0) ? 0 : width; - }, - - - adjustHeight : function(height) { - var me = this; - var isNum = (typeof height == "number"); - if(isNum && me.autoBoxAdjust && !me.isBorderBox()){ - height -= (me.getBorderWidth("tb") + me.getPadding("tb")); - } - return (isNum && height < 0) ? 0 : height; - }, - - - - addClass : function(className){ - var me = this, - i, - len, - v, - cls = []; - - if (!Ext.isArray(className)) { - if (typeof className == 'string' && !this.hasClass(className)) { - me.dom.className += " " + className; - } - } - else { - for (i = 0, len = className.length; i < len; i++) { - v = className[i]; - if (typeof v == 'string' && (' ' + me.dom.className + ' ').indexOf(' ' + v + ' ') == -1) { - cls.push(v); - } - } - if (cls.length) { - me.dom.className += " " + cls.join(" "); - } - } - return me; - }, - - - removeClass : function(className){ - var me = this, - i, - idx, - len, - cls, - elClasses; - if (!Ext.isArray(className)){ - className = [className]; - } - if (me.dom && me.dom.className) { - elClasses = me.dom.className.replace(trimRe, '').split(spacesRe); - for (i = 0, len = className.length; i < len; i++) { - cls = className[i]; - if (typeof cls == 'string') { - cls = cls.replace(trimRe, ''); - idx = elClasses.indexOf(cls); - if (idx != -1) { - elClasses.splice(idx, 1); - } - } - } - me.dom.className = elClasses.join(" "); - } - return me; - }, - - - radioClass : function(className){ - var cn = this.dom.parentNode.childNodes, - v, - i, - len; - className = Ext.isArray(className) ? className : [className]; - for (i = 0, len = cn.length; i < len; i++) { - v = cn[i]; - if (v && v.nodeType == 1) { - Ext.fly(v, '_internal').removeClass(className); - } - }; - return this.addClass(className); - }, - - - toggleClass : function(className){ - return this.hasClass(className) ? this.removeClass(className) : this.addClass(className); - }, - - - hasClass : function(className){ - return className && (' '+this.dom.className+' ').indexOf(' '+className+' ') != -1; - }, - - - replaceClass : function(oldClassName, newClassName){ - return this.removeClass(oldClassName).addClass(newClassName); - }, - - isStyle : function(style, val) { - return this.getStyle(style) == val; - }, - - - getStyle : function(){ - return view && view.getComputedStyle ? - function(prop){ - var el = this.dom, - v, - cs, - out, - display; - - if(el == document){ - return null; - } - prop = chkCache(prop); - out = (v = el.style[prop]) ? v : - (cs = view.getComputedStyle(el, "")) ? cs[prop] : null; - - - - if(prop == 'marginRight' && out != '0px' && !supports.correctRightMargin){ - display = el.style.display; - el.style.display = 'inline-block'; - out = view.getComputedStyle(el, '').marginRight; - el.style.display = display; - } - - if(prop == 'backgroundColor' && out == 'rgba(0, 0, 0, 0)' && !supports.correctTransparentColor){ - out = 'transparent'; - } - return out; - } : - function(prop){ - var el = this.dom, - m, - cs; - - if(el == document) return null; - if (prop == 'opacity') { - if (el.style.filter.match) { - if(m = el.style.filter.match(opacityRe)){ - var fv = parseFloat(m[1]); - if(!isNaN(fv)){ - return fv ? fv / 100 : 0; - } - } - } - return 1; - } - prop = chkCache(prop); - return el.style[prop] || ((cs = el.currentStyle) ? cs[prop] : null); - }; - }(), - - - getColor : function(attr, defaultValue, prefix){ - var v = this.getStyle(attr), - color = (typeof prefix != 'undefined') ? prefix : '#', - h; - - if(!v || (/transparent|inherit/.test(v))) { - return defaultValue; - } - if(/^r/.test(v)){ - Ext.each(v.slice(4, v.length -1).split(','), function(s){ - h = parseInt(s, 10); - color += (h < 16 ? '0' : '') + h.toString(16); - }); - }else{ - v = v.replace('#', ''); - color += v.length == 3 ? v.replace(/^(\w)(\w)(\w)$/, '$1$1$2$2$3$3') : v; - } - return(color.length > 5 ? color.toLowerCase() : defaultValue); - }, - - - setStyle : function(prop, value){ - var tmp, style; - - if (typeof prop != 'object') { - tmp = {}; - tmp[prop] = value; - prop = tmp; - } - for (style in prop) { - value = prop[style]; - style == 'opacity' ? - this.setOpacity(value) : - this.dom.style[chkCache(style)] = value; - } - return this; - }, - - - setOpacity : function(opacity, animate){ - var me = this, - s = me.dom.style; - - if(!animate || !me.anim){ - if(Ext.isIE){ - var opac = opacity < 1 ? 'alpha(opacity=' + opacity * 100 + ')' : '', - val = s.filter.replace(opacityRe, '').replace(trimRe, ''); - - s.zoom = 1; - s.filter = val + (val.length > 0 ? ' ' : '') + opac; - }else{ - s.opacity = opacity; - } - }else{ - me.anim({opacity: {to: opacity}}, me.preanim(arguments, 1), null, .35, 'easeIn'); - } - return me; - }, - - - clearOpacity : function(){ - var style = this.dom.style; - if(Ext.isIE){ - if(!Ext.isEmpty(style.filter)){ - style.filter = style.filter.replace(opacityRe, '').replace(trimRe, ''); - } - }else{ - style.opacity = style['-moz-opacity'] = style['-khtml-opacity'] = ''; - } - return this; - }, - - - getHeight : function(contentHeight){ - var me = this, - dom = me.dom, - hidden = Ext.isIE && me.isStyle('display', 'none'), - h = MATH.max(dom.offsetHeight, hidden ? 0 : dom.clientHeight) || 0; - - h = !contentHeight ? h : h - me.getBorderWidth("tb") - me.getPadding("tb"); - return h < 0 ? 0 : h; - }, - - - getWidth : function(contentWidth){ - var me = this, - dom = me.dom, - hidden = Ext.isIE && me.isStyle('display', 'none'), - w = MATH.max(dom.offsetWidth, hidden ? 0 : dom.clientWidth) || 0; - w = !contentWidth ? w : w - me.getBorderWidth("lr") - me.getPadding("lr"); - return w < 0 ? 0 : w; - }, - - - setWidth : function(width, animate){ - var me = this; - width = me.adjustWidth(width); - !animate || !me.anim ? - me.dom.style.width = me.addUnits(width) : - me.anim({width : {to : width}}, me.preanim(arguments, 1)); - return me; - }, - - - setHeight : function(height, animate){ - var me = this; - height = me.adjustHeight(height); - !animate || !me.anim ? - me.dom.style.height = me.addUnits(height) : - me.anim({height : {to : height}}, me.preanim(arguments, 1)); - return me; - }, - - - getBorderWidth : function(side){ - return this.addStyles(side, borders); - }, - - - getPadding : function(side){ - return this.addStyles(side, paddings); - }, - - - clip : function(){ - var me = this, - dom = me.dom; - - if(!data(dom, ISCLIPPED)){ - data(dom, ISCLIPPED, true); - data(dom, ORIGINALCLIP, { - o: me.getStyle(OVERFLOW), - x: me.getStyle(OVERFLOWX), - y: me.getStyle(OVERFLOWY) - }); - me.setStyle(OVERFLOW, HIDDEN); - me.setStyle(OVERFLOWX, HIDDEN); - me.setStyle(OVERFLOWY, HIDDEN); - } - return me; - }, - - - unclip : function(){ - var me = this, - dom = me.dom; - - if(data(dom, ISCLIPPED)){ - data(dom, ISCLIPPED, false); - var o = data(dom, ORIGINALCLIP); - if(o.o){ - me.setStyle(OVERFLOW, o.o); - } - if(o.x){ - me.setStyle(OVERFLOWX, o.x); - } - if(o.y){ - me.setStyle(OVERFLOWY, o.y); - } - } - return me; - }, - - - addStyles : function(sides, styles){ - var ttlSize = 0, - sidesArr = sides.match(wordsRe), - side, - size, - i, - len = sidesArr.length; - for (i = 0; i < len; i++) { - side = sidesArr[i]; - size = side && parseInt(this.getStyle(styles[side]), 10); - if (size) { - ttlSize += MATH.abs(size); - } - } - return ttlSize; - }, - - margins : margins - }; -}() -); - -(function(){ -var D = Ext.lib.Dom, - LEFT = "left", - RIGHT = "right", - TOP = "top", - BOTTOM = "bottom", - POSITION = "position", - STATIC = "static", - RELATIVE = "relative", - AUTO = "auto", - ZINDEX = "z-index"; - -Ext.Element.addMethods({ - - getX : function(){ - return D.getX(this.dom); - }, - - - getY : function(){ - return D.getY(this.dom); - }, - - - getXY : function(){ - return D.getXY(this.dom); - }, - - - getOffsetsTo : function(el){ - var o = this.getXY(), - e = Ext.fly(el, '_internal').getXY(); - return [o[0]-e[0],o[1]-e[1]]; - }, - - - setX : function(x, animate){ - return this.setXY([x, this.getY()], this.animTest(arguments, animate, 1)); - }, - - - setY : function(y, animate){ - return this.setXY([this.getX(), y], this.animTest(arguments, animate, 1)); - }, - - - setLeft : function(left){ - this.setStyle(LEFT, this.addUnits(left)); - return this; - }, - - - setTop : function(top){ - this.setStyle(TOP, this.addUnits(top)); - return this; - }, - - - setRight : function(right){ - this.setStyle(RIGHT, this.addUnits(right)); - return this; - }, - - - setBottom : function(bottom){ - this.setStyle(BOTTOM, this.addUnits(bottom)); - return this; - }, - - - setXY : function(pos, animate){ - var me = this; - if(!animate || !me.anim){ - D.setXY(me.dom, pos); - }else{ - me.anim({points: {to: pos}}, me.preanim(arguments, 1), 'motion'); - } - return me; - }, - - - setLocation : function(x, y, animate){ - return this.setXY([x, y], this.animTest(arguments, animate, 2)); - }, - - - moveTo : function(x, y, animate){ - return this.setXY([x, y], this.animTest(arguments, animate, 2)); - }, - - - getLeft : function(local){ - return !local ? this.getX() : parseInt(this.getStyle(LEFT), 10) || 0; - }, - - - getRight : function(local){ - var me = this; - return !local ? me.getX() + me.getWidth() : (me.getLeft(true) + me.getWidth()) || 0; - }, - - - getTop : function(local) { - return !local ? this.getY() : parseInt(this.getStyle(TOP), 10) || 0; - }, - - - getBottom : function(local){ - var me = this; - return !local ? me.getY() + me.getHeight() : (me.getTop(true) + me.getHeight()) || 0; - }, - - - position : function(pos, zIndex, x, y){ - var me = this; - - if(!pos && me.isStyle(POSITION, STATIC)){ - me.setStyle(POSITION, RELATIVE); - } else if(pos) { - me.setStyle(POSITION, pos); - } - if(zIndex){ - me.setStyle(ZINDEX, zIndex); - } - if(x || y) me.setXY([x || false, y || false]); - }, - - - clearPositioning : function(value){ - value = value || ''; - this.setStyle({ - left : value, - right : value, - top : value, - bottom : value, - "z-index" : "", - position : STATIC - }); - return this; - }, - - - getPositioning : function(){ - var l = this.getStyle(LEFT); - var t = this.getStyle(TOP); - return { - "position" : this.getStyle(POSITION), - "left" : l, - "right" : l ? "" : this.getStyle(RIGHT), - "top" : t, - "bottom" : t ? "" : this.getStyle(BOTTOM), - "z-index" : this.getStyle(ZINDEX) - }; - }, - - - setPositioning : function(pc){ - var me = this, - style = me.dom.style; - - me.setStyle(pc); - - if(pc.right == AUTO){ - style.right = ""; - } - if(pc.bottom == AUTO){ - style.bottom = ""; - } - - return me; - }, - - - translatePoints : function(x, y){ - y = isNaN(x[1]) ? y : x[1]; - x = isNaN(x[0]) ? x : x[0]; - var me = this, - relative = me.isStyle(POSITION, RELATIVE), - o = me.getXY(), - l = parseInt(me.getStyle(LEFT), 10), - t = parseInt(me.getStyle(TOP), 10); - - l = !isNaN(l) ? l : (relative ? 0 : me.dom.offsetLeft); - t = !isNaN(t) ? t : (relative ? 0 : me.dom.offsetTop); - - return {left: (x - o[0] + l), top: (y - o[1] + t)}; - }, - - animTest : function(args, animate, i) { - return !!animate && this.preanim ? this.preanim(args, i) : false; - } -}); -})(); -Ext.Element.addMethods({ - - isScrollable : function(){ - var dom = this.dom; - return dom.scrollHeight > dom.clientHeight || dom.scrollWidth > dom.clientWidth; - }, - - - scrollTo : function(side, value){ - this.dom["scroll" + (/top/i.test(side) ? "Top" : "Left")] = value; - return this; - }, - - - getScroll : function(){ - var d = this.dom, - doc = document, - body = doc.body, - docElement = doc.documentElement, - l, - t, - ret; - - if(d == doc || d == body){ - if(Ext.isIE && Ext.isStrict){ - l = docElement.scrollLeft; - t = docElement.scrollTop; - }else{ - l = window.pageXOffset; - t = window.pageYOffset; - } - ret = {left: l || (body ? body.scrollLeft : 0), top: t || (body ? body.scrollTop : 0)}; - }else{ - ret = {left: d.scrollLeft, top: d.scrollTop}; - } - return ret; - } -}); - -Ext.Element.VISIBILITY = 1; - -Ext.Element.DISPLAY = 2; - - -Ext.Element.OFFSETS = 3; - - -Ext.Element.ASCLASS = 4; - - -Ext.Element.visibilityCls = 'x-hide-nosize'; - -Ext.Element.addMethods(function(){ - var El = Ext.Element, - OPACITY = "opacity", - VISIBILITY = "visibility", - DISPLAY = "display", - HIDDEN = "hidden", - OFFSETS = "offsets", - ASCLASS = "asclass", - NONE = "none", - NOSIZE = 'nosize', - ORIGINALDISPLAY = 'originalDisplay', - VISMODE = 'visibilityMode', - ISVISIBLE = 'isVisible', - data = El.data, - getDisplay = function(dom){ - var d = data(dom, ORIGINALDISPLAY); - if(d === undefined){ - data(dom, ORIGINALDISPLAY, d = ''); - } - return d; - }, - getVisMode = function(dom){ - var m = data(dom, VISMODE); - if(m === undefined){ - data(dom, VISMODE, m = 1); - } - return m; - }; - - return { - - originalDisplay : "", - visibilityMode : 1, - - - setVisibilityMode : function(visMode){ - data(this.dom, VISMODE, visMode); - return this; - }, - - - animate : function(args, duration, onComplete, easing, animType){ - this.anim(args, {duration: duration, callback: onComplete, easing: easing}, animType); - return this; - }, - - - anim : function(args, opt, animType, defaultDur, defaultEase, cb){ - animType = animType || 'run'; - opt = opt || {}; - var me = this, - anim = Ext.lib.Anim[animType]( - me.dom, - args, - (opt.duration || defaultDur) || .35, - (opt.easing || defaultEase) || 'easeOut', - function(){ - if(cb) cb.call(me); - if(opt.callback) opt.callback.call(opt.scope || me, me, opt); - }, - me - ); - opt.anim = anim; - return anim; - }, - - - preanim : function(a, i){ - return !a[i] ? false : (typeof a[i] == 'object' ? a[i]: {duration: a[i+1], callback: a[i+2], easing: a[i+3]}); - }, - - - isVisible : function() { - var me = this, - dom = me.dom, - visible = data(dom, ISVISIBLE); - - if(typeof visible == 'boolean'){ - return visible; - } - - visible = !me.isStyle(VISIBILITY, HIDDEN) && - !me.isStyle(DISPLAY, NONE) && - !((getVisMode(dom) == El.ASCLASS) && me.hasClass(me.visibilityCls || El.visibilityCls)); - - data(dom, ISVISIBLE, visible); - return visible; - }, - - - setVisible : function(visible, animate){ - var me = this, isDisplay, isVisibility, isOffsets, isNosize, - dom = me.dom, - visMode = getVisMode(dom); - - - - if (typeof animate == 'string'){ - switch (animate) { - case DISPLAY: - visMode = El.DISPLAY; - break; - case VISIBILITY: - visMode = El.VISIBILITY; - break; - case OFFSETS: - visMode = El.OFFSETS; - break; - case NOSIZE: - case ASCLASS: - visMode = El.ASCLASS; - break; - } - me.setVisibilityMode(visMode); - animate = false; - } - - if (!animate || !me.anim) { - if(visMode == El.ASCLASS ){ - - me[visible?'removeClass':'addClass'](me.visibilityCls || El.visibilityCls); - - } else if (visMode == El.DISPLAY){ - - return me.setDisplayed(visible); - - } else if (visMode == El.OFFSETS){ - - if (!visible){ - me.hideModeStyles = { - position: me.getStyle('position'), - top: me.getStyle('top'), - left: me.getStyle('left') - }; - me.applyStyles({position: 'absolute', top: '-10000px', left: '-10000px'}); - } else { - me.applyStyles(me.hideModeStyles || {position: '', top: '', left: ''}); - delete me.hideModeStyles; - } - - }else{ - me.fixDisplay(); - dom.style.visibility = visible ? "visible" : HIDDEN; - } - }else{ - - if(visible){ - me.setOpacity(.01); - me.setVisible(true); - } - me.anim({opacity: { to: (visible?1:0) }}, - me.preanim(arguments, 1), - null, - .35, - 'easeIn', - function(){ - visible || me.setVisible(false).setOpacity(1); - }); - } - data(dom, ISVISIBLE, visible); - return me; - }, - - - - hasMetrics : function(){ - var dom = this.dom; - return this.isVisible() || (getVisMode(dom) == El.VISIBILITY); - }, - - - toggle : function(animate){ - var me = this; - me.setVisible(!me.isVisible(), me.preanim(arguments, 0)); - return me; - }, - - - setDisplayed : function(value) { - if(typeof value == "boolean"){ - value = value ? getDisplay(this.dom) : NONE; - } - this.setStyle(DISPLAY, value); - return this; - }, - - - fixDisplay : function(){ - var me = this; - if(me.isStyle(DISPLAY, NONE)){ - me.setStyle(VISIBILITY, HIDDEN); - me.setStyle(DISPLAY, getDisplay(this.dom)); - if(me.isStyle(DISPLAY, NONE)){ - me.setStyle(DISPLAY, "block"); - } - } - }, - - - hide : function(animate){ - - if (typeof animate == 'string'){ - this.setVisible(false, animate); - return this; - } - this.setVisible(false, this.preanim(arguments, 0)); - return this; - }, - - - show : function(animate){ - - if (typeof animate == 'string'){ - this.setVisible(true, animate); - return this; - } - this.setVisible(true, this.preanim(arguments, 0)); - return this; - } - }; -}());(function(){ - - var NULL = null, - UNDEFINED = undefined, - TRUE = true, - FALSE = false, - SETX = "setX", - SETY = "setY", - SETXY = "setXY", - LEFT = "left", - BOTTOM = "bottom", - TOP = "top", - RIGHT = "right", - HEIGHT = "height", - WIDTH = "width", - POINTS = "points", - HIDDEN = "hidden", - ABSOLUTE = "absolute", - VISIBLE = "visible", - MOTION = "motion", - POSITION = "position", - EASEOUT = "easeOut", - - flyEl = new Ext.Element.Flyweight(), - queues = {}, - getObject = function(o){ - return o || {}; - }, - fly = function(dom){ - flyEl.dom = dom; - flyEl.id = Ext.id(dom); - return flyEl; - }, - - getQueue = function(id){ - if(!queues[id]){ - queues[id] = []; - } - return queues[id]; - }, - setQueue = function(id, value){ - queues[id] = value; - }; - - -Ext.enableFx = TRUE; - - -Ext.Fx = { - - - - switchStatements : function(key, fn, argHash){ - return fn.apply(this, argHash[key]); - }, - - - slideIn : function(anchor, o){ - o = getObject(o); - var me = this, - dom = me.dom, - st = dom.style, - xy, - r, - b, - wrap, - after, - st, - args, - pt, - bw, - bh; - - anchor = anchor || "t"; - - me.queueFx(o, function(){ - xy = fly(dom).getXY(); - - fly(dom).fixDisplay(); - - - r = fly(dom).getFxRestore(); - b = {x: xy[0], y: xy[1], 0: xy[0], 1: xy[1], width: dom.offsetWidth, height: dom.offsetHeight}; - b.right = b.x + b.width; - b.bottom = b.y + b.height; - - - fly(dom).setWidth(b.width).setHeight(b.height); - - - wrap = fly(dom).fxWrap(r.pos, o, HIDDEN); - - st.visibility = VISIBLE; - st.position = ABSOLUTE; - - - function after(){ - fly(dom).fxUnwrap(wrap, r.pos, o); - st.width = r.width; - st.height = r.height; - fly(dom).afterFx(o); - } - - - pt = {to: [b.x, b.y]}; - bw = {to: b.width}; - bh = {to: b.height}; - - function argCalc(wrap, style, ww, wh, sXY, sXYval, s1, s2, w, h, p){ - var ret = {}; - fly(wrap).setWidth(ww).setHeight(wh); - if(fly(wrap)[sXY]){ - fly(wrap)[sXY](sXYval); - } - style[s1] = style[s2] = "0"; - if(w){ - ret.width = w; - } - if(h){ - ret.height = h; - } - if(p){ - ret.points = p; - } - return ret; - }; - - args = fly(dom).switchStatements(anchor.toLowerCase(), argCalc, { - t : [wrap, st, b.width, 0, NULL, NULL, LEFT, BOTTOM, NULL, bh, NULL], - l : [wrap, st, 0, b.height, NULL, NULL, RIGHT, TOP, bw, NULL, NULL], - r : [wrap, st, b.width, b.height, SETX, b.right, LEFT, TOP, NULL, NULL, pt], - b : [wrap, st, b.width, b.height, SETY, b.bottom, LEFT, TOP, NULL, bh, pt], - tl : [wrap, st, 0, 0, NULL, NULL, RIGHT, BOTTOM, bw, bh, pt], - bl : [wrap, st, 0, 0, SETY, b.y + b.height, RIGHT, TOP, bw, bh, pt], - br : [wrap, st, 0, 0, SETXY, [b.right, b.bottom], LEFT, TOP, bw, bh, pt], - tr : [wrap, st, 0, 0, SETX, b.x + b.width, LEFT, BOTTOM, bw, bh, pt] - }); - - st.visibility = VISIBLE; - fly(wrap).show(); - - arguments.callee.anim = fly(wrap).fxanim(args, - o, - MOTION, - .5, - EASEOUT, - after); - }); - return me; - }, - - - slideOut : function(anchor, o){ - o = getObject(o); - var me = this, - dom = me.dom, - st = dom.style, - xy = me.getXY(), - wrap, - r, - b, - a, - zero = {to: 0}; - - anchor = anchor || "t"; - - me.queueFx(o, function(){ - - - r = fly(dom).getFxRestore(); - b = {x: xy[0], y: xy[1], 0: xy[0], 1: xy[1], width: dom.offsetWidth, height: dom.offsetHeight}; - b.right = b.x + b.width; - b.bottom = b.y + b.height; - - - fly(dom).setWidth(b.width).setHeight(b.height); - - - wrap = fly(dom).fxWrap(r.pos, o, VISIBLE); - - st.visibility = VISIBLE; - st.position = ABSOLUTE; - fly(wrap).setWidth(b.width).setHeight(b.height); - - function after(){ - o.useDisplay ? fly(dom).setDisplayed(FALSE) : fly(dom).hide(); - fly(dom).fxUnwrap(wrap, r.pos, o); - st.width = r.width; - st.height = r.height; - fly(dom).afterFx(o); - } - - function argCalc(style, s1, s2, p1, v1, p2, v2, p3, v3){ - var ret = {}; - - style[s1] = style[s2] = "0"; - ret[p1] = v1; - if(p2){ - ret[p2] = v2; - } - if(p3){ - ret[p3] = v3; - } - - return ret; - }; - - a = fly(dom).switchStatements(anchor.toLowerCase(), argCalc, { - t : [st, LEFT, BOTTOM, HEIGHT, zero], - l : [st, RIGHT, TOP, WIDTH, zero], - r : [st, LEFT, TOP, WIDTH, zero, POINTS, {to : [b.right, b.y]}], - b : [st, LEFT, TOP, HEIGHT, zero, POINTS, {to : [b.x, b.bottom]}], - tl : [st, RIGHT, BOTTOM, WIDTH, zero, HEIGHT, zero], - bl : [st, RIGHT, TOP, WIDTH, zero, HEIGHT, zero, POINTS, {to : [b.x, b.bottom]}], - br : [st, LEFT, TOP, WIDTH, zero, HEIGHT, zero, POINTS, {to : [b.x + b.width, b.bottom]}], - tr : [st, LEFT, BOTTOM, WIDTH, zero, HEIGHT, zero, POINTS, {to : [b.right, b.y]}] - }); - - arguments.callee.anim = fly(wrap).fxanim(a, - o, - MOTION, - .5, - EASEOUT, - after); - }); - return me; - }, - - - puff : function(o){ - o = getObject(o); - var me = this, - dom = me.dom, - st = dom.style, - width, - height, - r; - - me.queueFx(o, function(){ - width = fly(dom).getWidth(); - height = fly(dom).getHeight(); - fly(dom).clearOpacity(); - fly(dom).show(); - - - r = fly(dom).getFxRestore(); - - function after(){ - o.useDisplay ? fly(dom).setDisplayed(FALSE) : fly(dom).hide(); - fly(dom).clearOpacity(); - fly(dom).setPositioning(r.pos); - st.width = r.width; - st.height = r.height; - st.fontSize = ''; - fly(dom).afterFx(o); - } - - arguments.callee.anim = fly(dom).fxanim({ - width : {to : fly(dom).adjustWidth(width * 2)}, - height : {to : fly(dom).adjustHeight(height * 2)}, - points : {by : [-width * .5, -height * .5]}, - opacity : {to : 0}, - fontSize: {to : 200, unit: "%"} - }, - o, - MOTION, - .5, - EASEOUT, - after); - }); - return me; - }, - - - switchOff : function(o){ - o = getObject(o); - var me = this, - dom = me.dom, - st = dom.style, - r; - - me.queueFx(o, function(){ - fly(dom).clearOpacity(); - fly(dom).clip(); - - - r = fly(dom).getFxRestore(); - - function after(){ - o.useDisplay ? fly(dom).setDisplayed(FALSE) : fly(dom).hide(); - fly(dom).clearOpacity(); - fly(dom).setPositioning(r.pos); - st.width = r.width; - st.height = r.height; - fly(dom).afterFx(o); - }; - - fly(dom).fxanim({opacity : {to : 0.3}}, - NULL, - NULL, - .1, - NULL, - function(){ - fly(dom).clearOpacity(); - (function(){ - fly(dom).fxanim({ - height : {to : 1}, - points : {by : [0, fly(dom).getHeight() * .5]} - }, - o, - MOTION, - 0.3, - 'easeIn', - after); - }).defer(100); - }); - }); - return me; - }, - - - highlight : function(color, o){ - o = getObject(o); - var me = this, - dom = me.dom, - attr = o.attr || "backgroundColor", - a = {}, - restore; - - me.queueFx(o, function(){ - fly(dom).clearOpacity(); - fly(dom).show(); - - function after(){ - dom.style[attr] = restore; - fly(dom).afterFx(o); - } - restore = dom.style[attr]; - a[attr] = {from: color || "ffff9c", to: o.endColor || fly(dom).getColor(attr) || "ffffff"}; - arguments.callee.anim = fly(dom).fxanim(a, - o, - 'color', - 1, - 'easeIn', - after); - }); - return me; - }, - - - frame : function(color, count, o){ - o = getObject(o); - var me = this, - dom = me.dom, - proxy, - active; - - me.queueFx(o, function(){ - color = color || '#C3DAF9'; - if(color.length == 6){ - color = '#' + color; - } - count = count || 1; - fly(dom).show(); - - var xy = fly(dom).getXY(), - b = {x: xy[0], y: xy[1], 0: xy[0], 1: xy[1], width: dom.offsetWidth, height: dom.offsetHeight}, - queue = function(){ - proxy = fly(document.body || document.documentElement).createChild({ - style:{ - position : ABSOLUTE, - 'z-index': 35000, - border : '0px solid ' + color - } - }); - return proxy.queueFx({}, animFn); - }; - - - arguments.callee.anim = { - isAnimated: true, - stop: function() { - count = 0; - proxy.stopFx(); - } - }; - - function animFn(){ - var scale = Ext.isBorderBox ? 2 : 1; - active = proxy.anim({ - top : {from : b.y, to : b.y - 20}, - left : {from : b.x, to : b.x - 20}, - borderWidth : {from : 0, to : 10}, - opacity : {from : 1, to : 0}, - height : {from : b.height, to : b.height + 20 * scale}, - width : {from : b.width, to : b.width + 20 * scale} - },{ - duration: o.duration || 1, - callback: function() { - proxy.remove(); - --count > 0 ? queue() : fly(dom).afterFx(o); - } - }); - arguments.callee.anim = { - isAnimated: true, - stop: function(){ - active.stop(); - } - }; - }; - queue(); - }); - return me; - }, - - - pause : function(seconds){ - var dom = this.dom, - t; - - this.queueFx({}, function(){ - t = setTimeout(function(){ - fly(dom).afterFx({}); - }, seconds * 1000); - arguments.callee.anim = { - isAnimated: true, - stop: function(){ - clearTimeout(t); - fly(dom).afterFx({}); - } - }; - }); - return this; - }, - - - fadeIn : function(o){ - o = getObject(o); - var me = this, - dom = me.dom, - to = o.endOpacity || 1; - - me.queueFx(o, function(){ - fly(dom).setOpacity(0); - fly(dom).fixDisplay(); - dom.style.visibility = VISIBLE; - arguments.callee.anim = fly(dom).fxanim({opacity:{to:to}}, - o, NULL, .5, EASEOUT, function(){ - if(to == 1){ - fly(dom).clearOpacity(); - } - fly(dom).afterFx(o); - }); - }); - return me; - }, - - - fadeOut : function(o){ - o = getObject(o); - var me = this, - dom = me.dom, - style = dom.style, - to = o.endOpacity || 0; - - me.queueFx(o, function(){ - arguments.callee.anim = fly(dom).fxanim({ - opacity : {to : to}}, - o, - NULL, - .5, - EASEOUT, - function(){ - if(to == 0){ - Ext.Element.data(dom, 'visibilityMode') == Ext.Element.DISPLAY || o.useDisplay ? - style.display = "none" : - style.visibility = HIDDEN; - - fly(dom).clearOpacity(); - } - fly(dom).afterFx(o); - }); - }); - return me; - }, - - - scale : function(w, h, o){ - this.shift(Ext.apply({}, o, { - width: w, - height: h - })); - return this; - }, - - - shift : function(o){ - o = getObject(o); - var dom = this.dom, - a = {}; - - this.queueFx(o, function(){ - for (var prop in o) { - if (o[prop] != UNDEFINED) { - a[prop] = {to : o[prop]}; - } - } - - a.width ? a.width.to = fly(dom).adjustWidth(o.width) : a; - a.height ? a.height.to = fly(dom).adjustWidth(o.height) : a; - - if (a.x || a.y || a.xy) { - a.points = a.xy || - {to : [ a.x ? a.x.to : fly(dom).getX(), - a.y ? a.y.to : fly(dom).getY()]}; - } - - arguments.callee.anim = fly(dom).fxanim(a, - o, - MOTION, - .35, - EASEOUT, - function(){ - fly(dom).afterFx(o); - }); - }); - return this; - }, - - - ghost : function(anchor, o){ - o = getObject(o); - var me = this, - dom = me.dom, - st = dom.style, - a = {opacity: {to: 0}, points: {}}, - pt = a.points, - r, - w, - h; - - anchor = anchor || "b"; - - me.queueFx(o, function(){ - - r = fly(dom).getFxRestore(); - w = fly(dom).getWidth(); - h = fly(dom).getHeight(); - - function after(){ - o.useDisplay ? fly(dom).setDisplayed(FALSE) : fly(dom).hide(); - fly(dom).clearOpacity(); - fly(dom).setPositioning(r.pos); - st.width = r.width; - st.height = r.height; - fly(dom).afterFx(o); - } - - pt.by = fly(dom).switchStatements(anchor.toLowerCase(), function(v1,v2){ return [v1, v2];}, { - t : [0, -h], - l : [-w, 0], - r : [w, 0], - b : [0, h], - tl : [-w, -h], - bl : [-w, h], - br : [w, h], - tr : [w, -h] - }); - - arguments.callee.anim = fly(dom).fxanim(a, - o, - MOTION, - .5, - EASEOUT, after); - }); - return me; - }, - - - syncFx : function(){ - var me = this; - me.fxDefaults = Ext.apply(me.fxDefaults || {}, { - block : FALSE, - concurrent : TRUE, - stopFx : FALSE - }); - return me; - }, - - - sequenceFx : function(){ - var me = this; - me.fxDefaults = Ext.apply(me.fxDefaults || {}, { - block : FALSE, - concurrent : FALSE, - stopFx : FALSE - }); - return me; - }, - - - nextFx : function(){ - var ef = getQueue(this.dom.id)[0]; - if(ef){ - ef.call(this); - } - }, - - - hasActiveFx : function(){ - return getQueue(this.dom.id)[0]; - }, - - - stopFx : function(finish){ - var me = this, - id = me.dom.id; - if(me.hasActiveFx()){ - var cur = getQueue(id)[0]; - if(cur && cur.anim){ - if(cur.anim.isAnimated){ - setQueue(id, [cur]); - cur.anim.stop(finish !== undefined ? finish : TRUE); - }else{ - setQueue(id, []); - } - } - } - return me; - }, - - - beforeFx : function(o){ - if(this.hasActiveFx() && !o.concurrent){ - if(o.stopFx){ - this.stopFx(); - return TRUE; - } - return FALSE; - } - return TRUE; - }, - - - hasFxBlock : function(){ - var q = getQueue(this.dom.id); - return q && q[0] && q[0].block; - }, - - - queueFx : function(o, fn){ - var me = fly(this.dom); - if(!me.hasFxBlock()){ - Ext.applyIf(o, me.fxDefaults); - if(!o.concurrent){ - var run = me.beforeFx(o); - fn.block = o.block; - getQueue(me.dom.id).push(fn); - if(run){ - me.nextFx(); - } - }else{ - fn.call(me); - } - } - return me; - }, - - - fxWrap : function(pos, o, vis){ - var dom = this.dom, - wrap, - wrapXY; - if(!o.wrap || !(wrap = Ext.getDom(o.wrap))){ - if(o.fixPosition){ - wrapXY = fly(dom).getXY(); - } - var div = document.createElement("div"); - div.style.visibility = vis; - wrap = dom.parentNode.insertBefore(div, dom); - fly(wrap).setPositioning(pos); - if(fly(wrap).isStyle(POSITION, "static")){ - fly(wrap).position("relative"); - } - fly(dom).clearPositioning('auto'); - fly(wrap).clip(); - wrap.appendChild(dom); - if(wrapXY){ - fly(wrap).setXY(wrapXY); - } - } - return wrap; - }, - - - fxUnwrap : function(wrap, pos, o){ - var dom = this.dom; - fly(dom).clearPositioning(); - fly(dom).setPositioning(pos); - if(!o.wrap){ - var pn = fly(wrap).dom.parentNode; - pn.insertBefore(dom, wrap); - fly(wrap).remove(); - } - }, - - - getFxRestore : function(){ - var st = this.dom.style; - return {pos: this.getPositioning(), width: st.width, height : st.height}; - }, - - - afterFx : function(o){ - var dom = this.dom, - id = dom.id; - if(o.afterStyle){ - fly(dom).setStyle(o.afterStyle); - } - if(o.afterCls){ - fly(dom).addClass(o.afterCls); - } - if(o.remove == TRUE){ - fly(dom).remove(); - } - if(o.callback){ - o.callback.call(o.scope, fly(dom)); - } - if(!o.concurrent){ - getQueue(id).shift(); - fly(dom).nextFx(); - } - }, - - - fxanim : function(args, opt, animType, defaultDur, defaultEase, cb){ - animType = animType || 'run'; - opt = opt || {}; - var anim = Ext.lib.Anim[animType]( - this.dom, - args, - (opt.duration || defaultDur) || .35, - (opt.easing || defaultEase) || EASEOUT, - cb, - this - ); - opt.anim = anim; - return anim; - } -}; - - -Ext.Fx.resize = Ext.Fx.scale; - - - -Ext.Element.addMethods(Ext.Fx); -})(); - -Ext.CompositeElementLite = function(els, root){ - - this.elements = []; - this.add(els, root); - this.el = new Ext.Element.Flyweight(); -}; - -Ext.CompositeElementLite.prototype = { - isComposite: true, - - - getElement : function(el){ - - var e = this.el; - e.dom = el; - e.id = el.id; - return e; - }, - - - transformElement : function(el){ - return Ext.getDom(el); - }, - - - getCount : function(){ - return this.elements.length; - }, - - add : function(els, root){ - var me = this, - elements = me.elements; - if(!els){ - return this; - } - if(typeof els == "string"){ - els = Ext.Element.selectorFunction(els, root); - }else if(els.isComposite){ - els = els.elements; - }else if(!Ext.isIterable(els)){ - els = [els]; - } - - for(var i = 0, len = els.length; i < len; ++i){ - elements.push(me.transformElement(els[i])); - } - return me; - }, - - invoke : function(fn, args){ - var me = this, - els = me.elements, - len = els.length, - e, - i; - - for(i = 0; i < len; i++) { - e = els[i]; - if(e){ - Ext.Element.prototype[fn].apply(me.getElement(e), args); - } - } - return me; - }, - - item : function(index){ - var me = this, - el = me.elements[index], - out = null; - - if(el){ - out = me.getElement(el); - } - return out; - }, - - - addListener : function(eventName, handler, scope, opt){ - var els = this.elements, - len = els.length, - i, e; - - for(i = 0; i -1){ - replacement = Ext.getDom(replacement); - if(domReplace){ - d = this.elements[index]; - d.parentNode.insertBefore(replacement, d); - Ext.removeNode(d); - } - this.elements.splice(index, 1, replacement); - } - return this; - }, - - - clear : function(){ - this.elements = []; - } -}; - -Ext.CompositeElementLite.prototype.on = Ext.CompositeElementLite.prototype.addListener; - - -Ext.CompositeElementLite.importElementMethods = function() { - var fnName, - ElProto = Ext.Element.prototype, - CelProto = Ext.CompositeElementLite.prototype; - - for (fnName in ElProto) { - if (typeof ElProto[fnName] == 'function'){ - (function(fnName) { - CelProto[fnName] = CelProto[fnName] || function() { - return this.invoke(fnName, arguments); - }; - }).call(CelProto, fnName); - - } - } -}; - -Ext.CompositeElementLite.importElementMethods(); - -if(Ext.DomQuery){ - Ext.Element.selectorFunction = Ext.DomQuery.select; -} - - -Ext.Element.select = function(selector, root){ - var els; - if(typeof selector == "string"){ - els = Ext.Element.selectorFunction(selector, root); - }else if(selector.length !== undefined){ - els = selector; - }else{ - throw "Invalid selector"; - } - return new Ext.CompositeElementLite(els); -}; - -Ext.select = Ext.Element.select; -(function(){ - var BEFOREREQUEST = "beforerequest", - REQUESTCOMPLETE = "requestcomplete", - REQUESTEXCEPTION = "requestexception", - UNDEFINED = undefined, - LOAD = 'load', - POST = 'POST', - GET = 'GET', - WINDOW = window; - - - Ext.data.Connection = function(config){ - Ext.apply(this, config); - this.addEvents( - - BEFOREREQUEST, - - REQUESTCOMPLETE, - - REQUESTEXCEPTION - ); - Ext.data.Connection.superclass.constructor.call(this); - }; - - Ext.extend(Ext.data.Connection, Ext.util.Observable, { - - - - - - timeout : 30000, - - autoAbort:false, - - - disableCaching: true, - - - disableCachingParam: '_dc', - - - request : function(o){ - var me = this; - if(me.fireEvent(BEFOREREQUEST, me, o)){ - if (o.el) { - if(!Ext.isEmpty(o.indicatorText)){ - me.indicatorText = '
        '+o.indicatorText+"
        "; - } - if(me.indicatorText) { - Ext.getDom(o.el).innerHTML = me.indicatorText; - } - o.success = (Ext.isFunction(o.success) ? o.success : function(){}).createInterceptor(function(response) { - Ext.getDom(o.el).innerHTML = response.responseText; - }); - } - - var p = o.params, - url = o.url || me.url, - method, - cb = {success: me.handleResponse, - failure: me.handleFailure, - scope: me, - argument: {options: o}, - timeout : Ext.num(o.timeout, me.timeout) - }, - form, - serForm; - - - if (Ext.isFunction(p)) { - p = p.call(o.scope||WINDOW, o); - } - - p = Ext.urlEncode(me.extraParams, Ext.isObject(p) ? Ext.urlEncode(p) : p); - - if (Ext.isFunction(url)) { - url = url.call(o.scope || WINDOW, o); - } - - if((form = Ext.getDom(o.form))){ - url = url || form.action; - if(o.isUpload || (/multipart\/form-data/i.test(form.getAttribute("enctype")))) { - return me.doFormUpload.call(me, o, p, url); - } - serForm = Ext.lib.Ajax.serializeForm(form); - p = p ? (p + '&' + serForm) : serForm; - } - - method = o.method || me.method || ((p || o.xmlData || o.jsonData) ? POST : GET); - - if(method === GET && (me.disableCaching && o.disableCaching !== false) || o.disableCaching === true){ - var dcp = o.disableCachingParam || me.disableCachingParam; - url = Ext.urlAppend(url, dcp + '=' + (new Date().getTime())); - } - - o.headers = Ext.applyIf(o.headers || {}, me.defaultHeaders || {}); - - if(o.autoAbort === true || me.autoAbort) { - me.abort(); - } - - if((method == GET || o.xmlData || o.jsonData) && p){ - url = Ext.urlAppend(url, p); - p = ''; - } - return (me.transId = Ext.lib.Ajax.request(method, url, cb, p, o)); - }else{ - return o.callback ? o.callback.apply(o.scope, [o,UNDEFINED,UNDEFINED]) : null; - } - }, - - - isLoading : function(transId){ - return transId ? Ext.lib.Ajax.isCallInProgress(transId) : !! this.transId; - }, - - - abort : function(transId){ - if(transId || this.isLoading()){ - Ext.lib.Ajax.abort(transId || this.transId); - } - }, - - - handleResponse : function(response){ - this.transId = false; - var options = response.argument.options; - response.argument = options ? options.argument : null; - this.fireEvent(REQUESTCOMPLETE, this, response, options); - if(options.success){ - options.success.call(options.scope, response, options); - } - if(options.callback){ - options.callback.call(options.scope, options, true, response); - } - }, - - - handleFailure : function(response, e){ - this.transId = false; - var options = response.argument.options; - response.argument = options ? options.argument : null; - this.fireEvent(REQUESTEXCEPTION, this, response, options, e); - if(options.failure){ - options.failure.call(options.scope, response, options); - } - if(options.callback){ - options.callback.call(options.scope, options, false, response); - } - }, - - - doFormUpload : function(o, ps, url){ - var id = Ext.id(), - doc = document, - frame = doc.createElement('iframe'), - form = Ext.getDom(o.form), - hiddens = [], - hd, - encoding = 'multipart/form-data', - buf = { - target: form.target, - method: form.method, - encoding: form.encoding, - enctype: form.enctype, - action: form.action - }; - - - Ext.fly(frame).set({ - id: id, - name: id, - cls: 'x-hidden', - src: Ext.SSL_SECURE_URL - }); - - doc.body.appendChild(frame); - - - if(Ext.isIE){ - document.frames[id].name = id; - } - - - Ext.fly(form).set({ - target: id, - method: POST, - enctype: encoding, - encoding: encoding, - action: url || buf.action - }); - - - Ext.iterate(Ext.urlDecode(ps, false), function(k, v){ - hd = doc.createElement('input'); - Ext.fly(hd).set({ - type: 'hidden', - value: v, - name: k - }); - form.appendChild(hd); - hiddens.push(hd); - }); - - function cb(){ - var me = this, - - r = {responseText : '', - responseXML : null, - argument : o.argument}, - doc, - firstChild; - - try{ - doc = frame.contentWindow.document || frame.contentDocument || WINDOW.frames[id].document; - if(doc){ - if(doc.body){ - if(/textarea/i.test((firstChild = doc.body.firstChild || {}).tagName)){ - r.responseText = firstChild.value; - }else{ - r.responseText = doc.body.innerHTML; - } - } - - r.responseXML = doc.XMLDocument || doc; - } - } - catch(e) {} - - Ext.EventManager.removeListener(frame, LOAD, cb, me); - - me.fireEvent(REQUESTCOMPLETE, me, r, o); - - function runCallback(fn, scope, args){ - if(Ext.isFunction(fn)){ - fn.apply(scope, args); - } - } - - runCallback(o.success, o.scope, [r, o]); - runCallback(o.callback, o.scope, [o, true, r]); - - if(!me.debugUploads){ - setTimeout(function(){Ext.removeNode(frame);}, 100); - } - } - - Ext.EventManager.on(frame, LOAD, cb, this); - form.submit(); - - Ext.fly(form).set(buf); - Ext.each(hiddens, function(h) { - Ext.removeNode(h); - }); - } - }); -})(); - - -Ext.Ajax = new Ext.data.Connection({ - - - - - - - - - - - - - - - - - - autoAbort : false, - - - serializeForm : function(form){ - return Ext.lib.Ajax.serializeForm(form); - } -}); - -Ext.util.JSON = new (function(){ - var useHasOwn = !!{}.hasOwnProperty, - isNative = function() { - var useNative = null; - - return function() { - if (useNative === null) { - useNative = Ext.USE_NATIVE_JSON && window.JSON && JSON.toString() == '[object JSON]'; - } - - return useNative; - }; - }(), - pad = function(n) { - return n < 10 ? "0" + n : n; - }, - doDecode = function(json){ - return json ? eval("(" + json + ")") : ""; - }, - doEncode = function(o){ - if(!Ext.isDefined(o) || o === null){ - return "null"; - }else if(Ext.isArray(o)){ - return encodeArray(o); - }else if(Ext.isDate(o)){ - return Ext.util.JSON.encodeDate(o); - }else if(Ext.isString(o)){ - return encodeString(o); - }else if(typeof o == "number"){ - - return isFinite(o) ? String(o) : "null"; - }else if(Ext.isBoolean(o)){ - return String(o); - }else { - var a = ["{"], b, i, v; - for (i in o) { - - if(!o.getElementsByTagName){ - if(!useHasOwn || o.hasOwnProperty(i)) { - v = o[i]; - switch (typeof v) { - case "undefined": - case "function": - case "unknown": - break; - default: - if(b){ - a.push(','); - } - a.push(doEncode(i), ":", - v === null ? "null" : doEncode(v)); - b = true; - } - } - } - } - a.push("}"); - return a.join(""); - } - }, - m = { - "\b": '\\b', - "\t": '\\t', - "\n": '\\n', - "\f": '\\f', - "\r": '\\r', - '"' : '\\"', - "\\": '\\\\' - }, - encodeString = function(s){ - if (/["\\\x00-\x1f]/.test(s)) { - return '"' + s.replace(/([\x00-\x1f\\"])/g, function(a, b) { - var c = m[b]; - if(c){ - return c; - } - c = b.charCodeAt(); - return "\\u00" + - Math.floor(c / 16).toString(16) + - (c % 16).toString(16); - }) + '"'; - } - return '"' + s + '"'; - }, - encodeArray = function(o){ - var a = ["["], b, i, l = o.length, v; - for (i = 0; i < l; i += 1) { - v = o[i]; - switch (typeof v) { - case "undefined": - case "function": - case "unknown": - break; - default: - if (b) { - a.push(','); - } - a.push(v === null ? "null" : Ext.util.JSON.encode(v)); - b = true; - } - } - a.push("]"); - return a.join(""); - }; - - - this.encodeDate = function(o){ - return '"' + o.getFullYear() + "-" + - pad(o.getMonth() + 1) + "-" + - pad(o.getDate()) + "T" + - pad(o.getHours()) + ":" + - pad(o.getMinutes()) + ":" + - pad(o.getSeconds()) + '"'; - }; - - - this.encode = function() { - var ec; - return function(o) { - if (!ec) { - - ec = isNative() ? JSON.stringify : doEncode; - } - return ec(o); - }; - }(); - - - - this.decode = function() { - var dc; - return function(json) { - if (!dc) { - - dc = isNative() ? JSON.parse : doDecode; - } - return dc(json); - }; - }(); - -})(); - -Ext.encode = Ext.util.JSON.encode; - -Ext.decode = Ext.util.JSON.decode; - -Ext.EventManager = function(){ - var docReadyEvent, - docReadyProcId, - docReadyState = false, - DETECT_NATIVE = Ext.isGecko || Ext.isWebKit || Ext.isSafari, - E = Ext.lib.Event, - D = Ext.lib.Dom, - DOC = document, - WINDOW = window, - DOMCONTENTLOADED = "DOMContentLoaded", - COMPLETE = 'complete', - propRe = /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate)$/, - - specialElCache = []; - - function getId(el){ - var id = false, - i = 0, - len = specialElCache.length, - skip = false, - o; - - if (el) { - if (el.getElementById || el.navigator) { - - for(; i < len; ++i){ - o = specialElCache[i]; - if(o.el === el){ - id = o.id; - break; - } - } - if(!id){ - - id = Ext.id(el); - specialElCache.push({ - id: id, - el: el - }); - skip = true; - } - }else{ - id = Ext.id(el); - } - if(!Ext.elCache[id]){ - Ext.Element.addToCache(new Ext.Element(el), id); - if(skip){ - Ext.elCache[id].skipGC = true; - } - } - } - return id; - } - - - function addListener(el, ename, fn, task, wrap, scope){ - el = Ext.getDom(el); - var id = getId(el), - es = Ext.elCache[id].events, - wfn; - - wfn = E.on(el, ename, wrap); - es[ename] = es[ename] || []; - - - es[ename].push([fn, wrap, scope, wfn, task]); - - - - - - if(el.addEventListener && ename == "mousewheel"){ - var args = ["DOMMouseScroll", wrap, false]; - el.addEventListener.apply(el, args); - Ext.EventManager.addListener(WINDOW, 'unload', function(){ - el.removeEventListener.apply(el, args); - }); - } - - - if(el == DOC && ename == "mousedown"){ - Ext.EventManager.stoppedMouseDownEvent.addListener(wrap); - } - } - - function doScrollChk(){ - - if(window != top){ - return false; - } - - try{ - DOC.documentElement.doScroll('left'); - }catch(e){ - return false; - } - - fireDocReady(); - return true; - } - - function checkReadyState(e){ - - if(Ext.isIE && doScrollChk()){ - return true; - } - if(DOC.readyState == COMPLETE){ - fireDocReady(); - return true; - } - docReadyState || (docReadyProcId = setTimeout(arguments.callee, 2)); - return false; - } - - var styles; - function checkStyleSheets(e){ - styles || (styles = Ext.query('style, link[rel=stylesheet]')); - if(styles.length == DOC.styleSheets.length){ - fireDocReady(); - return true; - } - docReadyState || (docReadyProcId = setTimeout(arguments.callee, 2)); - return false; - } - - function OperaDOMContentLoaded(e){ - DOC.removeEventListener(DOMCONTENTLOADED, arguments.callee, false); - checkStyleSheets(); - } - - function fireDocReady(e){ - if(!docReadyState){ - docReadyState = true; - - if(docReadyProcId){ - clearTimeout(docReadyProcId); - } - if(DETECT_NATIVE) { - DOC.removeEventListener(DOMCONTENTLOADED, fireDocReady, false); - } - if(Ext.isIE && checkReadyState.bindIE){ - DOC.detachEvent('onreadystatechange', checkReadyState); - } - E.un(WINDOW, "load", arguments.callee); - } - if(docReadyEvent && !Ext.isReady){ - Ext.isReady = true; - docReadyEvent.fire(); - docReadyEvent.listeners = []; - } - - } - - function initDocReady(){ - docReadyEvent || (docReadyEvent = new Ext.util.Event()); - if (DETECT_NATIVE) { - DOC.addEventListener(DOMCONTENTLOADED, fireDocReady, false); - } - - if (Ext.isIE){ - - - if(!checkReadyState()){ - checkReadyState.bindIE = true; - DOC.attachEvent('onreadystatechange', checkReadyState); - } - - }else if(Ext.isOpera ){ - - - - (DOC.readyState == COMPLETE && checkStyleSheets()) || - DOC.addEventListener(DOMCONTENTLOADED, OperaDOMContentLoaded, false); - - }else if (Ext.isWebKit){ - - checkReadyState(); - } - - E.on(WINDOW, "load", fireDocReady); - } - - function createTargeted(h, o){ - return function(){ - var args = Ext.toArray(arguments); - if(o.target == Ext.EventObject.setEvent(args[0]).target){ - h.apply(this, args); - } - }; - } - - function createBuffered(h, o, task){ - return function(e){ - - task.delay(o.buffer, h, null, [new Ext.EventObjectImpl(e)]); - }; - } - - function createSingle(h, el, ename, fn, scope){ - return function(e){ - Ext.EventManager.removeListener(el, ename, fn, scope); - h(e); - }; - } - - function createDelayed(h, o, fn){ - return function(e){ - var task = new Ext.util.DelayedTask(h); - if(!fn.tasks) { - fn.tasks = []; - } - fn.tasks.push(task); - task.delay(o.delay || 10, h, null, [new Ext.EventObjectImpl(e)]); - }; - } - - function listen(element, ename, opt, fn, scope){ - var o = (!opt || typeof opt == "boolean") ? {} : opt, - el = Ext.getDom(element), task; - - fn = fn || o.fn; - scope = scope || o.scope; - - if(!el){ - throw "Error listening for \"" + ename + '\". Element "' + element + '" doesn\'t exist.'; - } - function h(e){ - - if(!Ext){ - return; - } - e = Ext.EventObject.setEvent(e); - var t; - if (o.delegate) { - if(!(t = e.getTarget(o.delegate, el))){ - return; - } - } else { - t = e.target; - } - if (o.stopEvent) { - e.stopEvent(); - } - if (o.preventDefault) { - e.preventDefault(); - } - if (o.stopPropagation) { - e.stopPropagation(); - } - if (o.normalized === false) { - e = e.browserEvent; - } - - fn.call(scope || el, e, t, o); - } - if(o.target){ - h = createTargeted(h, o); - } - if(o.delay){ - h = createDelayed(h, o, fn); - } - if(o.single){ - h = createSingle(h, el, ename, fn, scope); - } - if(o.buffer){ - task = new Ext.util.DelayedTask(h); - h = createBuffered(h, o, task); - } - - addListener(el, ename, fn, task, h, scope); - return h; - } - - var pub = { - - addListener : function(element, eventName, fn, scope, options){ - if(typeof eventName == 'object'){ - var o = eventName, e, val; - for(e in o){ - val = o[e]; - if(!propRe.test(e)){ - if(Ext.isFunction(val)){ - - listen(element, e, o, val, o.scope); - }else{ - - listen(element, e, val); - } - } - } - } else { - listen(element, eventName, options, fn, scope); - } - }, - - - removeListener : function(el, eventName, fn, scope){ - el = Ext.getDom(el); - var id = getId(el), - f = el && (Ext.elCache[id].events)[eventName] || [], - wrap, i, l, k, len, fnc; - - for (i = 0, len = f.length; i < len; i++) { - - - if (Ext.isArray(fnc = f[i]) && fnc[0] == fn && (!scope || fnc[2] == scope)) { - if(fnc[4]) { - fnc[4].cancel(); - } - k = fn.tasks && fn.tasks.length; - if(k) { - while(k--) { - fn.tasks[k].cancel(); - } - delete fn.tasks; - } - wrap = fnc[1]; - E.un(el, eventName, E.extAdapter ? fnc[3] : wrap); - - - if(wrap && el.addEventListener && eventName == "mousewheel"){ - el.removeEventListener("DOMMouseScroll", wrap, false); - } - - - if(wrap && el == DOC && eventName == "mousedown"){ - Ext.EventManager.stoppedMouseDownEvent.removeListener(wrap); - } - - f.splice(i, 1); - if (f.length === 0) { - delete Ext.elCache[id].events[eventName]; - } - for (k in Ext.elCache[id].events) { - return false; - } - Ext.elCache[id].events = {}; - return false; - } - } - }, - - - removeAll : function(el){ - el = Ext.getDom(el); - var id = getId(el), - ec = Ext.elCache[id] || {}, - es = ec.events || {}, - f, i, len, ename, fn, k, wrap; - - for(ename in es){ - if(es.hasOwnProperty(ename)){ - f = es[ename]; - - for (i = 0, len = f.length; i < len; i++) { - fn = f[i]; - if(fn[4]) { - fn[4].cancel(); - } - if(fn[0].tasks && (k = fn[0].tasks.length)) { - while(k--) { - fn[0].tasks[k].cancel(); - } - delete fn.tasks; - } - wrap = fn[1]; - E.un(el, ename, E.extAdapter ? fn[3] : wrap); - - - if(el.addEventListener && wrap && ename == "mousewheel"){ - el.removeEventListener("DOMMouseScroll", wrap, false); - } - - - if(wrap && el == DOC && ename == "mousedown"){ - Ext.EventManager.stoppedMouseDownEvent.removeListener(wrap); - } - } - } - } - if (Ext.elCache[id]) { - Ext.elCache[id].events = {}; - } - }, - - getListeners : function(el, eventName) { - el = Ext.getDom(el); - var id = getId(el), - ec = Ext.elCache[id] || {}, - es = ec.events || {}, - results = []; - if (es && es[eventName]) { - return es[eventName]; - } else { - return null; - } - }, - - purgeElement : function(el, recurse, eventName) { - el = Ext.getDom(el); - var id = getId(el), - ec = Ext.elCache[id] || {}, - es = ec.events || {}, - i, f, len; - if (eventName) { - if (es && es.hasOwnProperty(eventName)) { - f = es[eventName]; - for (i = 0, len = f.length; i < len; i++) { - Ext.EventManager.removeListener(el, eventName, f[i][0]); - } - } - } else { - Ext.EventManager.removeAll(el); - } - if (recurse && el && el.childNodes) { - for (i = 0, len = el.childNodes.length; i < len; i++) { - Ext.EventManager.purgeElement(el.childNodes[i], recurse, eventName); - } - } - }, - - _unload : function() { - var el; - for (el in Ext.elCache) { - Ext.EventManager.removeAll(el); - } - delete Ext.elCache; - delete Ext.Element._flyweights; - - - var c, - conn, - tid, - ajax = Ext.lib.Ajax; - (typeof ajax.conn == 'object') ? conn = ajax.conn : conn = {}; - for (tid in conn) { - c = conn[tid]; - if (c) { - ajax.abort({conn: c, tId: tid}); - } - } - }, - - onDocumentReady : function(fn, scope, options){ - if (Ext.isReady) { - docReadyEvent || (docReadyEvent = new Ext.util.Event()); - docReadyEvent.addListener(fn, scope, options); - docReadyEvent.fire(); - docReadyEvent.listeners = []; - } else { - if (!docReadyEvent) { - initDocReady(); - } - options = options || {}; - options.delay = options.delay || 1; - docReadyEvent.addListener(fn, scope, options); - } - }, - - - fireDocReady : fireDocReady - }; - - pub.on = pub.addListener; - - pub.un = pub.removeListener; - - pub.stoppedMouseDownEvent = new Ext.util.Event(); - return pub; -}(); - -Ext.onReady = Ext.EventManager.onDocumentReady; - - - -(function(){ - var initExtCss = function() { - - var bd = document.body || document.getElementsByTagName('body')[0]; - if (!bd) { - return false; - } - - var cls = [' ', - Ext.isIE ? "ext-ie " + (Ext.isIE6 ? 'ext-ie6' : (Ext.isIE7 ? 'ext-ie7' : (Ext.isIE8 ? 'ext-ie8' : 'ext-ie9'))) - : Ext.isGecko ? "ext-gecko " + (Ext.isGecko2 ? 'ext-gecko2' : 'ext-gecko3') - : Ext.isOpera ? "ext-opera" - : Ext.isWebKit ? "ext-webkit" : ""]; - - if (Ext.isSafari) { - cls.push("ext-safari " + (Ext.isSafari2 ? 'ext-safari2' : (Ext.isSafari3 ? 'ext-safari3' : 'ext-safari4'))); - } else if(Ext.isChrome) { - cls.push("ext-chrome"); - } - - if (Ext.isMac) { - cls.push("ext-mac"); - } - if (Ext.isLinux) { - cls.push("ext-linux"); - } - - - if (Ext.isStrict || Ext.isBorderBox) { - var p = bd.parentNode; - if (p) { - if (!Ext.isStrict) { - Ext.fly(p, '_internal').addClass('x-quirks'); - if (Ext.isIE && !Ext.isStrict) { - Ext.isIEQuirks = true; - } - } - Ext.fly(p, '_internal').addClass(((Ext.isStrict && Ext.isIE ) || (!Ext.enableForcedBoxModel && !Ext.isIE)) ? ' ext-strict' : ' ext-border-box'); - } - } - - - if (Ext.enableForcedBoxModel && !Ext.isIE) { - Ext.isForcedBorderBox = true; - cls.push("ext-forced-border-box"); - } - - Ext.fly(bd, '_internal').addClass(cls); - return true; - }; - - if (!initExtCss()) { - Ext.onReady(initExtCss); - } -})(); - - -(function(){ - var supports = Ext.apply(Ext.supports, { - - correctRightMargin: true, - - - correctTransparentColor: true, - - - cssFloat: true - }); - - var supportTests = function(){ - var div = document.createElement('div'), - doc = document, - view, - last; - - div.innerHTML = '
        '; - doc.body.appendChild(div); - last = div.lastChild; - - if((view = doc.defaultView)){ - if(view.getComputedStyle(div.firstChild.firstChild, null).marginRight != '0px'){ - supports.correctRightMargin = false; - } - if(view.getComputedStyle(last, null).backgroundColor != 'transparent'){ - supports.correctTransparentColor = false; - } - } - supports.cssFloat = !!last.style.cssFloat; - doc.body.removeChild(div); - }; - - if (Ext.isReady) { - supportTests(); - } else { - Ext.onReady(supportTests); - } -})(); - - - -Ext.EventObject = function(){ - var E = Ext.lib.Event, - clickRe = /(dbl)?click/, - - safariKeys = { - 3 : 13, - 63234 : 37, - 63235 : 39, - 63232 : 38, - 63233 : 40, - 63276 : 33, - 63277 : 34, - 63272 : 46, - 63273 : 36, - 63275 : 35 - }, - - btnMap = Ext.isIE ? {1:0,4:1,2:2} : {0:0,1:1,2:2}; - - Ext.EventObjectImpl = function(e){ - if(e){ - this.setEvent(e.browserEvent || e); - } - }; - - Ext.EventObjectImpl.prototype = { - - setEvent : function(e){ - var me = this; - if(e == me || (e && e.browserEvent)){ - return e; - } - me.browserEvent = e; - if(e){ - - me.button = e.button ? btnMap[e.button] : (e.which ? e.which - 1 : -1); - if(clickRe.test(e.type) && me.button == -1){ - me.button = 0; - } - me.type = e.type; - me.shiftKey = e.shiftKey; - - me.ctrlKey = e.ctrlKey || e.metaKey || false; - me.altKey = e.altKey; - - me.keyCode = e.keyCode; - me.charCode = e.charCode; - - me.target = E.getTarget(e); - - me.xy = E.getXY(e); - }else{ - me.button = -1; - me.shiftKey = false; - me.ctrlKey = false; - me.altKey = false; - me.keyCode = 0; - me.charCode = 0; - me.target = null; - me.xy = [0, 0]; - } - return me; - }, - - - stopEvent : function(){ - var me = this; - if(me.browserEvent){ - if(me.browserEvent.type == 'mousedown'){ - Ext.EventManager.stoppedMouseDownEvent.fire(me); - } - E.stopEvent(me.browserEvent); - } - }, - - - preventDefault : function(){ - if(this.browserEvent){ - E.preventDefault(this.browserEvent); - } - }, - - - stopPropagation : function(){ - var me = this; - if(me.browserEvent){ - if(me.browserEvent.type == 'mousedown'){ - Ext.EventManager.stoppedMouseDownEvent.fire(me); - } - E.stopPropagation(me.browserEvent); - } - }, - - - getCharCode : function(){ - return this.charCode || this.keyCode; - }, - - - getKey : function(){ - return this.normalizeKey(this.keyCode || this.charCode); - }, - - - normalizeKey: function(k){ - return Ext.isSafari ? (safariKeys[k] || k) : k; - }, - - - getPageX : function(){ - return this.xy[0]; - }, - - - getPageY : function(){ - return this.xy[1]; - }, - - - getXY : function(){ - return this.xy; - }, - - - getTarget : function(selector, maxDepth, returnEl){ - return selector ? Ext.fly(this.target).findParent(selector, maxDepth, returnEl) : (returnEl ? Ext.get(this.target) : this.target); - }, - - - getRelatedTarget : function(){ - return this.browserEvent ? E.getRelatedTarget(this.browserEvent) : null; - }, - - - getWheelDelta : function(){ - var e = this.browserEvent; - var delta = 0; - if(e.wheelDelta){ - delta = e.wheelDelta/120; - }else if(e.detail){ - delta = -e.detail/3; - } - return delta; - }, - - - within : function(el, related, allowEl){ - if(el){ - var t = this[related ? "getRelatedTarget" : "getTarget"](); - return t && ((allowEl ? (t == Ext.getDom(el)) : false) || Ext.fly(el).contains(t)); - } - return false; - } - }; - - return new Ext.EventObjectImpl(); -}(); -Ext.Loader = Ext.apply({}, { - - load: function(fileList, callback, scope, preserveOrder) { - var scope = scope || this, - head = document.getElementsByTagName("head")[0], - fragment = document.createDocumentFragment(), - numFiles = fileList.length, - loadedFiles = 0, - me = this; - - - var loadFileIndex = function(index) { - head.appendChild( - me.buildScriptTag(fileList[index], onFileLoaded) - ); - }; - - - var onFileLoaded = function() { - loadedFiles ++; - - - if (numFiles == loadedFiles && typeof callback == 'function') { - callback.call(scope); - } else { - if (preserveOrder === true) { - loadFileIndex(loadedFiles); - } - } - }; - - if (preserveOrder === true) { - loadFileIndex.call(this, 0); - } else { - - Ext.each(fileList, function(file, index) { - fragment.appendChild( - this.buildScriptTag(file, onFileLoaded) - ); - }, this); - - head.appendChild(fragment); - } - }, - - - buildScriptTag: function(filename, callback) { - var script = document.createElement('script'); - script.type = "text/javascript"; - script.src = filename; - - - if (script.readyState) { - script.onreadystatechange = function() { - if (script.readyState == "loaded" || script.readyState == "complete") { - script.onreadystatechange = null; - callback(); - } - }; - } else { - script.onload = callback; - } - - return script; - } -}); - - -Ext.ns("Ext.grid", "Ext.list", "Ext.dd", "Ext.tree", "Ext.form", "Ext.menu", - "Ext.state", "Ext.layout.boxOverflow", "Ext.app", "Ext.ux", "Ext.chart", "Ext.direct", "Ext.slider"); - - -Ext.apply(Ext, function(){ - var E = Ext, - idSeed = 0, - scrollWidth = null; - - return { - - emptyFn : function(){}, - - - BLANK_IMAGE_URL : Ext.isIE6 || Ext.isIE7 || Ext.isAir ? - 'http:/' + '/www.extjs.com/s.gif' : - 'data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', - - extendX : function(supr, fn){ - return Ext.extend(supr, fn(supr.prototype)); - }, - - - getDoc : function(){ - return Ext.get(document); - }, - - - num : function(v, defaultValue){ - v = Number(Ext.isEmpty(v) || Ext.isArray(v) || typeof v == 'boolean' || (typeof v == 'string' && v.trim().length == 0) ? NaN : v); - return isNaN(v) ? defaultValue : v; - }, - - - value : function(v, defaultValue, allowBlank){ - return Ext.isEmpty(v, allowBlank) ? defaultValue : v; - }, - - - escapeRe : function(s) { - return s.replace(/([-.*+?^${}()|[\]\/\\])/g, "\\$1"); - }, - - sequence : function(o, name, fn, scope){ - o[name] = o[name].createSequence(fn, scope); - }, - - - addBehaviors : function(o){ - if(!Ext.isReady){ - Ext.onReady(function(){ - Ext.addBehaviors(o); - }); - } else { - var cache = {}, - parts, - b, - s; - for (b in o) { - if ((parts = b.split('@'))[1]) { - s = parts[0]; - if(!cache[s]){ - cache[s] = Ext.select(s); - } - cache[s].on(parts[1], o[b]); - } - } - cache = null; - } - }, - - - getScrollBarWidth: function(force){ - if(!Ext.isReady){ - return 0; - } - - if(force === true || scrollWidth === null){ - - var div = Ext.getBody().createChild('
        '), - child = div.child('div', true); - var w1 = child.offsetWidth; - div.setStyle('overflow', (Ext.isWebKit || Ext.isGecko) ? 'auto' : 'scroll'); - var w2 = child.offsetWidth; - div.remove(); - - scrollWidth = w1 - w2 + 2; - } - return scrollWidth; - }, - - - - combine : function(){ - var as = arguments, l = as.length, r = []; - for(var i = 0; i < l; i++){ - var a = as[i]; - if(Ext.isArray(a)){ - r = r.concat(a); - }else if(a.length !== undefined && !a.substr){ - r = r.concat(Array.prototype.slice.call(a, 0)); - }else{ - r.push(a); - } - } - return r; - }, - - - copyTo : function(dest, source, names){ - if(typeof names == 'string'){ - names = names.split(/[,;\s]/); - } - Ext.each(names, function(name){ - if(source.hasOwnProperty(name)){ - dest[name] = source[name]; - } - }, this); - return dest; - }, - - - destroy : function(){ - Ext.each(arguments, function(arg){ - if(arg){ - if(Ext.isArray(arg)){ - this.destroy.apply(this, arg); - }else if(typeof arg.destroy == 'function'){ - arg.destroy(); - }else if(arg.dom){ - arg.remove(); - } - } - }, this); - }, - - - destroyMembers : function(o, arg1, arg2, etc){ - for(var i = 1, a = arguments, len = a.length; i < len; i++) { - Ext.destroy(o[a[i]]); - delete o[a[i]]; - } - }, - - - clean : function(arr){ - var ret = []; - Ext.each(arr, function(v){ - if(!!v){ - ret.push(v); - } - }); - return ret; - }, - - - unique : function(arr){ - var ret = [], - collect = {}; - - Ext.each(arr, function(v) { - if(!collect[v]){ - ret.push(v); - } - collect[v] = true; - }); - return ret; - }, - - - flatten : function(arr){ - var worker = []; - function rFlatten(a) { - Ext.each(a, function(v) { - if(Ext.isArray(v)){ - rFlatten(v); - }else{ - worker.push(v); - } - }); - return worker; - } - return rFlatten(arr); - }, - - - min : function(arr, comp){ - var ret = arr[0]; - comp = comp || function(a,b){ return a < b ? -1 : 1; }; - Ext.each(arr, function(v) { - ret = comp(ret, v) == -1 ? ret : v; - }); - return ret; - }, - - - max : function(arr, comp){ - var ret = arr[0]; - comp = comp || function(a,b){ return a > b ? 1 : -1; }; - Ext.each(arr, function(v) { - ret = comp(ret, v) == 1 ? ret : v; - }); - return ret; - }, - - - mean : function(arr){ - return arr.length > 0 ? Ext.sum(arr) / arr.length : undefined; - }, - - - sum : function(arr){ - var ret = 0; - Ext.each(arr, function(v) { - ret += v; - }); - return ret; - }, - - - partition : function(arr, truth){ - var ret = [[],[]]; - Ext.each(arr, function(v, i, a) { - ret[ (truth && truth(v, i, a)) || (!truth && v) ? 0 : 1].push(v); - }); - return ret; - }, - - - invoke : function(arr, methodName){ - var ret = [], - args = Array.prototype.slice.call(arguments, 2); - Ext.each(arr, function(v,i) { - if (v && typeof v[methodName] == 'function') { - ret.push(v[methodName].apply(v, args)); - } else { - ret.push(undefined); - } - }); - return ret; - }, - - - pluck : function(arr, prop){ - var ret = []; - Ext.each(arr, function(v) { - ret.push( v[prop] ); - }); - return ret; - }, - - - zip : function(){ - var parts = Ext.partition(arguments, function( val ){ return typeof val != 'function'; }), - arrs = parts[0], - fn = parts[1][0], - len = Ext.max(Ext.pluck(arrs, "length")), - ret = []; - - for (var i = 0; i < len; i++) { - ret[i] = []; - if(fn){ - ret[i] = fn.apply(fn, Ext.pluck(arrs, i)); - }else{ - for (var j = 0, aLen = arrs.length; j < aLen; j++){ - ret[i].push( arrs[j][i] ); - } - } - } - return ret; - }, - - - getCmp : function(id){ - return Ext.ComponentMgr.get(id); - }, - - - useShims: E.isIE6 || (E.isMac && E.isGecko2), - - - - type : function(o){ - if(o === undefined || o === null){ - return false; - } - if(o.htmlElement){ - return 'element'; - } - var t = typeof o; - if(t == 'object' && o.nodeName) { - switch(o.nodeType) { - case 1: return 'element'; - case 3: return (/\S/).test(o.nodeValue) ? 'textnode' : 'whitespace'; - } - } - if(t == 'object' || t == 'function') { - switch(o.constructor) { - case Array: return 'array'; - case RegExp: return 'regexp'; - case Date: return 'date'; - } - if(typeof o.length == 'number' && typeof o.item == 'function') { - return 'nodelist'; - } - } - return t; - }, - - intercept : function(o, name, fn, scope){ - o[name] = o[name].createInterceptor(fn, scope); - }, - - - callback : function(cb, scope, args, delay){ - if(typeof cb == 'function'){ - if(delay){ - cb.defer(delay, scope, args || []); - }else{ - cb.apply(scope, args || []); - } - } - } - }; -}()); - - -Ext.apply(Function.prototype, { - - createSequence : function(fcn, scope){ - var method = this; - return (typeof fcn != 'function') ? - this : - function(){ - var retval = method.apply(this || window, arguments); - fcn.apply(scope || this || window, arguments); - return retval; - }; - } -}); - - - -Ext.applyIf(String, { - - - escape : function(string) { - return string.replace(/('|\\)/g, "\\$1"); - }, - - - leftPad : function (val, size, ch) { - var result = String(val); - if(!ch) { - ch = " "; - } - while (result.length < size) { - result = ch + result; - } - return result; - } -}); - - -String.prototype.toggle = function(value, other){ - return this == value ? other : value; -}; - - -String.prototype.trim = function(){ - var re = /^\s+|\s+$/g; - return function(){ return this.replace(re, ""); }; -}(); - - - -Date.prototype.getElapsed = function(date) { - return Math.abs((date || new Date()).getTime()-this.getTime()); -}; - - - -Ext.applyIf(Number.prototype, { - - constrain : function(min, max){ - return Math.min(Math.max(this, min), max); - } -}); -Ext.lib.Dom.getRegion = function(el) { - return Ext.lib.Region.getRegion(el); -}; Ext.lib.Region = function(t, r, b, l) { - var me = this; - me.top = t; - me[1] = t; - me.right = r; - me.bottom = b; - me.left = l; - me[0] = l; - }; - - Ext.lib.Region.prototype = { - contains : function(region) { - var me = this; - return ( region.left >= me.left && - region.right <= me.right && - region.top >= me.top && - region.bottom <= me.bottom ); - - }, - - getArea : function() { - var me = this; - return ( (me.bottom - me.top) * (me.right - me.left) ); - }, - - intersect : function(region) { - var me = this, - t = Math.max(me.top, region.top), - r = Math.min(me.right, region.right), - b = Math.min(me.bottom, region.bottom), - l = Math.max(me.left, region.left); - - if (b >= t && r >= l) { - return new Ext.lib.Region(t, r, b, l); - } - }, - - union : function(region) { - var me = this, - t = Math.min(me.top, region.top), - r = Math.max(me.right, region.right), - b = Math.max(me.bottom, region.bottom), - l = Math.min(me.left, region.left); - - return new Ext.lib.Region(t, r, b, l); - }, - - constrainTo : function(r) { - var me = this; - me.top = me.top.constrain(r.top, r.bottom); - me.bottom = me.bottom.constrain(r.top, r.bottom); - me.left = me.left.constrain(r.left, r.right); - me.right = me.right.constrain(r.left, r.right); - return me; - }, - - adjust : function(t, l, b, r) { - var me = this; - me.top += t; - me.left += l; - me.right += r; - me.bottom += b; - return me; - } - }; - - Ext.lib.Region.getRegion = function(el) { - var p = Ext.lib.Dom.getXY(el), - t = p[1], - r = p[0] + el.offsetWidth, - b = p[1] + el.offsetHeight, - l = p[0]; - - return new Ext.lib.Region(t, r, b, l); - }; Ext.lib.Point = function(x, y) { - if (Ext.isArray(x)) { - y = x[1]; - x = x[0]; - } - var me = this; - me.x = me.right = me.left = me[0] = x; - me.y = me.top = me.bottom = me[1] = y; - }; - - Ext.lib.Point.prototype = new Ext.lib.Region(); - -Ext.apply(Ext.DomHelper, -function(){ - var pub, - afterbegin = 'afterbegin', - afterend = 'afterend', - beforebegin = 'beforebegin', - beforeend = 'beforeend', - confRe = /tag|children|cn|html$/i; - - - function doInsert(el, o, returnElement, pos, sibling, append){ - el = Ext.getDom(el); - var newNode; - if (pub.useDom) { - newNode = createDom(o, null); - if (append) { - el.appendChild(newNode); - } else { - (sibling == 'firstChild' ? el : el.parentNode).insertBefore(newNode, el[sibling] || el); - } - } else { - newNode = Ext.DomHelper.insertHtml(pos, el, Ext.DomHelper.createHtml(o)); - } - return returnElement ? Ext.get(newNode, true) : newNode; - } - - - - function createDom(o, parentNode){ - var el, - doc = document, - useSet, - attr, - val, - cn; - - if (Ext.isArray(o)) { - el = doc.createDocumentFragment(); - for (var i = 0, l = o.length; i < l; i++) { - createDom(o[i], el); - } - } else if (typeof o == 'string') { - el = doc.createTextNode(o); - } else { - el = doc.createElement( o.tag || 'div' ); - useSet = !!el.setAttribute; - for (var attr in o) { - if(!confRe.test(attr)){ - val = o[attr]; - if(attr == 'cls'){ - el.className = val; - }else{ - if(useSet){ - el.setAttribute(attr, val); - }else{ - el[attr] = val; - } - } - } - } - Ext.DomHelper.applyStyles(el, o.style); - - if ((cn = o.children || o.cn)) { - createDom(cn, el); - } else if (o.html) { - el.innerHTML = o.html; - } - } - if(parentNode){ - parentNode.appendChild(el); - } - return el; - } - - pub = { - - createTemplate : function(o){ - var html = Ext.DomHelper.createHtml(o); - return new Ext.Template(html); - }, - - - useDom : false, - - - insertBefore : function(el, o, returnElement){ - return doInsert(el, o, returnElement, beforebegin); - }, - - - insertAfter : function(el, o, returnElement){ - return doInsert(el, o, returnElement, afterend, 'nextSibling'); - }, - - - insertFirst : function(el, o, returnElement){ - return doInsert(el, o, returnElement, afterbegin, 'firstChild'); - }, - - - append: function(el, o, returnElement){ - return doInsert(el, o, returnElement, beforeend, '', true); - }, - - - createDom: createDom - }; - return pub; -}()); - -Ext.apply(Ext.Template.prototype, { - - disableFormats : false, - - - - re : /\{([\w\-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g, - argsRe : /^\s*['"](.*)["']\s*$/, - compileARe : /\\/g, - compileBRe : /(\r\n|\n)/g, - compileCRe : /'/g, - - /** - * Returns an HTML fragment of this template with the specified values applied. - * @param {Object/Array} values The template values. Can be an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}) - * @return {String} The HTML fragment - * @hide repeat doc - */ - applyTemplate : function(values){ - var me = this, - useF = me.disableFormats !== true, - fm = Ext.util.Format, - tpl = me; - - if(me.compiled){ - return me.compiled(values); - } - function fn(m, name, format, args){ - if (format && useF) { - if (format.substr(0, 5) == "this.") { - return tpl.call(format.substr(5), values[name], values); - } else { - if (args) { - // quoted values are required for strings in compiled templates, - // but for non compiled we need to strip them - // quoted reversed for jsmin - var re = me.argsRe; - args = args.split(','); - for(var i = 0, len = args.length; i < len; i++){ - args[i] = args[i].replace(re, "$1"); - } - args = [values[name]].concat(args); - } else { - args = [values[name]]; - } - return fm[format].apply(fm, args); - } - } else { - return values[name] !== undefined ? values[name] : ""; - } - } - return me.html.replace(me.re, fn); - }, - - /** - * Compiles the template into an internal function, eliminating the RegEx overhead. - * @return {Ext.Template} this - * @hide repeat doc - */ - compile : function(){ - var me = this, - fm = Ext.util.Format, - useF = me.disableFormats !== true, - sep = Ext.isGecko ? "+" : ",", - body; - - function fn(m, name, format, args){ - if(format && useF){ - args = args ? ',' + args : ""; - if(format.substr(0, 5) != "this."){ - format = "fm." + format + '('; - }else{ - format = 'this.call("'+ format.substr(5) + '", '; - args = ", values"; - } - }else{ - args= ''; format = "(values['" + name + "'] == undefined ? '' : "; - } - return "'"+ sep + format + "values['" + name + "']" + args + ")"+sep+"'"; - } - - // branched to use + in gecko and [].join() in others - if(Ext.isGecko){ - body = "this.compiled = function(values){ return '" + - me.html.replace(me.compileARe, '\\\\').replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.re, fn) + - "';};"; - }else{ - body = ["this.compiled = function(values){ return ['"]; - body.push(me.html.replace(me.compileARe, '\\\\').replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.re, fn)); - body.push("'].join('');};"); - body = body.join(''); - } - eval(body); - return me; - }, - - // private function used to call members - call : function(fnName, value, allValues){ - return this[fnName](value, allValues); - } -}); -Ext.Template.prototype.apply = Ext.Template.prototype.applyTemplate; -/** - * @class Ext.util.Functions - * @singleton - */ -Ext.util.Functions = { - /** - * Creates an interceptor function. The passed function is called before the original one. If it returns false, - * the original one is not called. The resulting function returns the results of the original function. - * The passed function is called with the parameters of the original function. Example usage: - *
        
        -var sayHi = function(name){
        -    alert('Hi, ' + name);
        -}
        -
        -sayHi('Fred'); // alerts "Hi, Fred"
        -
        -// create a new function that validates input without
        -// directly modifying the original function:
        -var sayHiToFriend = Ext.createInterceptor(sayHi, function(name){
        -    return name == 'Brian';
        -});
        -
        -sayHiToFriend('Fred');  // no alert
        -sayHiToFriend('Brian'); // alerts "Hi, Brian"
        -       
        - * @param {Function} origFn The original function. - * @param {Function} newFn The function to call before the original - * @param {Object} scope (optional) The scope (this reference) in which the passed function is executed. - * If omitted, defaults to the scope in which the original function is called or the browser window. - * @return {Function} The new function - */ - createInterceptor: function(origFn, newFn, scope) { - var method = origFn; - if (!Ext.isFunction(newFn)) { - return origFn; - } - else { - return function() { - var me = this, - args = arguments; - newFn.target = me; - newFn.method = origFn; - return (newFn.apply(scope || me || window, args) !== false) ? - origFn.apply(me || window, args) : - null; - }; - } - }, - - /** - * Creates a delegate (callback) that sets the scope to obj. - * Call directly on any function. Example: Ext.createDelegate(this.myFunction, this, [arg1, arg2]) - * Will create a function that is automatically scoped to obj so that the this variable inside the - * callback points to obj. Example usage: - *
        
        -var sayHi = function(name){
        -    // Note this use of "this.text" here.  This function expects to
        -    // execute within a scope that contains a text property.  In this
        -    // example, the "this" variable is pointing to the btn object that
        -    // was passed in createDelegate below.
        -    alert('Hi, ' + name + '. You clicked the "' + this.text + '" button.');
        -}
        -
        -var btn = new Ext.Button({
        -    text: 'Say Hi',
        -    renderTo: Ext.getBody()
        -});
        -
        -// This callback will execute in the scope of the
        -// button instance. Clicking the button alerts
        -// "Hi, Fred. You clicked the "Say Hi" button."
        -btn.on('click', Ext.createDelegate(sayHi, btn, ['Fred']));
        -       
        - * @param {Function} fn The function to delegate. - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. - * If omitted, defaults to the browser window. - * @param {Array} args (optional) Overrides arguments for the call. (Defaults to the arguments passed by the caller) - * @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding, - * if a number the args are inserted at the specified position - * @return {Function} The new function - */ - createDelegate: function(fn, obj, args, appendArgs) { - if (!Ext.isFunction(fn)) { - return fn; - } - return function() { - var callArgs = args || arguments; - if (appendArgs === true) { - callArgs = Array.prototype.slice.call(arguments, 0); - callArgs = callArgs.concat(args); - } - else if (Ext.isNumber(appendArgs)) { - callArgs = Array.prototype.slice.call(arguments, 0); - // copy arguments first - var applyArgs = [appendArgs, 0].concat(args); - // create method call params - Array.prototype.splice.apply(callArgs, applyArgs); - // splice them in - } - return fn.apply(obj || window, callArgs); - }; - }, - - /** - * Calls this function after the number of millseconds specified, optionally in a specific scope. Example usage: - *
        
        -var sayHi = function(name){
        -    alert('Hi, ' + name);
        -}
        -
        -// executes immediately:
        -sayHi('Fred');
        -
        -// executes after 2 seconds:
        -Ext.defer(sayHi, 2000, this, ['Fred']);
        -
        -// this syntax is sometimes useful for deferring
        -// execution of an anonymous function:
        -Ext.defer(function(){
        -    alert('Anonymous');
        -}, 100);
        -       
        - * @param {Function} fn The function to defer. - * @param {Number} millis The number of milliseconds for the setTimeout call (if less than or equal to 0 the function is executed immediately) - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. - * If omitted, defaults to the browser window. - * @param {Array} args (optional) Overrides arguments for the call. (Defaults to the arguments passed by the caller) - * @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding, - * if a number the args are inserted at the specified position - * @return {Number} The timeout id that can be used with clearTimeout - */ - defer: function(fn, millis, obj, args, appendArgs) { - fn = Ext.util.Functions.createDelegate(fn, obj, args, appendArgs); - if (millis > 0) { - return setTimeout(fn, millis); - } - fn(); - return 0; - }, - - - /** - * Create a combined function call sequence of the original function + the passed function. - * The resulting function returns the results of the original function. - * The passed fcn is called with the parameters of the original function. Example usage: - * - -var sayHi = function(name){ - alert('Hi, ' + name); -} - -sayHi('Fred'); // alerts "Hi, Fred" - -var sayGoodbye = Ext.createSequence(sayHi, function(name){ - alert('Bye, ' + name); -}); - -sayGoodbye('Fred'); // both alerts show - - * @param {Function} origFn The original function. - * @param {Function} newFn The function to sequence - * @param {Object} scope (optional) The scope (this reference) in which the passed function is executed. - * If omitted, defaults to the scope in which the original function is called or the browser window. - * @return {Function} The new function - */ - createSequence: function(origFn, newFn, scope) { - if (!Ext.isFunction(newFn)) { - return origFn; - } - else { - return function() { - var retval = origFn.apply(this || window, arguments); - newFn.apply(scope || this || window, arguments); - return retval; - }; - } - } -}; - -/** - * Shorthand for {@link Ext.util.Functions#defer} - * @param {Function} fn The function to defer. - * @param {Number} millis The number of milliseconds for the setTimeout call (if less than or equal to 0 the function is executed immediately) - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. - * If omitted, defaults to the browser window. - * @param {Array} args (optional) Overrides arguments for the call. (Defaults to the arguments passed by the caller) - * @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding, - * if a number the args are inserted at the specified position - * @return {Number} The timeout id that can be used with clearTimeout - * @member Ext - * @method defer - */ - -Ext.defer = Ext.util.Functions.defer; - -/** - * Shorthand for {@link Ext.util.Functions#createInterceptor} - * @param {Function} origFn The original function. - * @param {Function} newFn The function to call before the original - * @param {Object} scope (optional) The scope (this reference) in which the passed function is executed. - * If omitted, defaults to the scope in which the original function is called or the browser window. - * @return {Function} The new function - * @member Ext - * @method defer - */ - -Ext.createInterceptor = Ext.util.Functions.createInterceptor; - -/** - * Shorthand for {@link Ext.util.Functions#createSequence} - * @param {Function} origFn The original function. - * @param {Function} newFn The function to sequence - * @param {Object} scope (optional) The scope (this reference) in which the passed function is executed. - * If omitted, defaults to the scope in which the original function is called or the browser window. - * @return {Function} The new function - * @member Ext - * @method defer - */ - -Ext.createSequence = Ext.util.Functions.createSequence; - -/** - * Shorthand for {@link Ext.util.Functions#createDelegate} - * @param {Function} fn The function to delegate. - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. - * If omitted, defaults to the browser window. - * @param {Array} args (optional) Overrides arguments for the call. (Defaults to the arguments passed by the caller) - * @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding, - * if a number the args are inserted at the specified position - * @return {Function} The new function - * @member Ext - * @method defer - */ -Ext.createDelegate = Ext.util.Functions.createDelegate; -/** - * @class Ext.util.Observable - */ -Ext.apply(Ext.util.Observable.prototype, function(){ - // this is considered experimental (along with beforeMethod, afterMethod, removeMethodListener?) - // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call - // private - function getMethodEvent(method){ - var e = (this.methodEvents = this.methodEvents || - {})[method], returnValue, v, cancel, obj = this; - - if (!e) { - this.methodEvents[method] = e = {}; - e.originalFn = this[method]; - e.methodName = method; - e.before = []; - e.after = []; - - var makeCall = function(fn, scope, args){ - if((v = fn.apply(scope || obj, args)) !== undefined){ - if (typeof v == 'object') { - if(v.returnValue !== undefined){ - returnValue = v.returnValue; - }else{ - returnValue = v; - } - cancel = !!v.cancel; - } - else - if (v === false) { - cancel = true; - } - else { - returnValue = v; - } - } - }; - - this[method] = function(){ - var args = Array.prototype.slice.call(arguments, 0), - b; - returnValue = v = undefined; - cancel = false; - - for(var i = 0, len = e.before.length; i < len; i++){ - b = e.before[i]; - makeCall(b.fn, b.scope, args); - if (cancel) { - return returnValue; - } - } - - if((v = e.originalFn.apply(obj, args)) !== undefined){ - returnValue = v; - } - - for(var i = 0, len = e.after.length; i < len; i++){ - b = e.after[i]; - makeCall(b.fn, b.scope, args); - if (cancel) { - return returnValue; - } - } - return returnValue; - }; - } - return e; - } - - return { - // these are considered experimental - // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call - // adds an 'interceptor' called before the original method - beforeMethod : function(method, fn, scope){ - getMethodEvent.call(this, method).before.push({ - fn: fn, - scope: scope - }); - }, - - // adds a 'sequence' called after the original method - afterMethod : function(method, fn, scope){ - getMethodEvent.call(this, method).after.push({ - fn: fn, - scope: scope - }); - }, - - removeMethodListener: function(method, fn, scope){ - var e = this.getMethodEvent(method); - for(var i = 0, len = e.before.length; i < len; i++){ - if(e.before[i].fn == fn && e.before[i].scope == scope){ - e.before.splice(i, 1); - return; - } - } - for(var i = 0, len = e.after.length; i < len; i++){ - if(e.after[i].fn == fn && e.after[i].scope == scope){ - e.after.splice(i, 1); - return; - } - } - }, - - /** - * Relays selected events from the specified Observable as if the events were fired by this. - * @param {Object} o The Observable whose events this object is to relay. - * @param {Array} events Array of event names to relay. - */ - relayEvents : function(o, events){ - var me = this; - function createHandler(ename){ - return function(){ - return me.fireEvent.apply(me, [ename].concat(Array.prototype.slice.call(arguments, 0))); - }; - } - for(var i = 0, len = events.length; i < len; i++){ - var ename = events[i]; - me.events[ename] = me.events[ename] || true; - o.on(ename, createHandler(ename), me); - } - }, - - /** - *

        Enables events fired by this Observable to bubble up an owner hierarchy by calling - * this.getBubbleTarget() if present. There is no implementation in the Observable base class.

        - *

        This is commonly used by Ext.Components to bubble events to owner Containers. See {@link Ext.Component.getBubbleTarget}. The default - * implementation in Ext.Component returns the Component's immediate owner. But if a known target is required, this can be overridden to - * access the required target more quickly.

        - *

        Example:

        
        -Ext.override(Ext.form.Field, {
        -    
        -    initComponent : Ext.form.Field.prototype.initComponent.createSequence(function() {
        -        this.enableBubble('change');
        -    }),
        -
        -    
        -    getBubbleTarget : function() {
        -        if (!this.formPanel) {
        -            this.formPanel = this.findParentByType('form');
        -        }
        -        return this.formPanel;
        -    }
        -});
        -
        -var myForm = new Ext.formPanel({
        -    title: 'User Details',
        -    items: [{
        -        ...
        -    }],
        -    listeners: {
        -        change: function() {
        -            
        -            myForm.header.setStyle('color', 'red');
        -        }
        -    }
        -});
        -
        - * @param {String/Array} events The event name to bubble, or an Array of event names. - */ - enableBubble : function(events){ - var me = this; - if(!Ext.isEmpty(events)){ - events = Ext.isArray(events) ? events : Array.prototype.slice.call(arguments, 0); - for(var i = 0, len = events.length; i < len; i++){ - var ename = events[i]; - ename = ename.toLowerCase(); - var ce = me.events[ename] || true; - if (typeof ce == 'boolean') { - ce = new Ext.util.Event(me, ename); - me.events[ename] = ce; - } - ce.bubble = true; - } - } - } - }; -}()); - - - -Ext.util.Observable.capture = function(o, fn, scope){ - o.fireEvent = o.fireEvent.createInterceptor(fn, scope); -}; - - - -Ext.util.Observable.observeClass = function(c, listeners){ - if(c){ - if(!c.fireEvent){ - Ext.apply(c, new Ext.util.Observable()); - Ext.util.Observable.capture(c.prototype, c.fireEvent, c); - } - if(typeof listeners == 'object'){ - c.on(listeners); - } - return c; - } -}; - -Ext.apply(Ext.EventManager, function(){ - var resizeEvent, - resizeTask, - textEvent, - textSize, - D = Ext.lib.Dom, - propRe = /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate)$/, - unload = Ext.EventManager._unload, - curWidth = 0, - curHeight = 0, - - - - useKeydown = Ext.isWebKit ? - Ext.num(navigator.userAgent.match(/AppleWebKit\/(\d+)/)[1]) >= 525 : - !((Ext.isGecko && !Ext.isWindows) || Ext.isOpera); - - return { - _unload: function(){ - Ext.EventManager.un(window, "resize", this.fireWindowResize, this); - unload.call(Ext.EventManager); - }, - - - doResizeEvent: function(){ - var h = D.getViewHeight(), - w = D.getViewWidth(); - - - if(curHeight != h || curWidth != w){ - resizeEvent.fire(curWidth = w, curHeight = h); - } - }, - - - onWindowResize : function(fn, scope, options){ - if(!resizeEvent){ - resizeEvent = new Ext.util.Event(); - resizeTask = new Ext.util.DelayedTask(this.doResizeEvent); - Ext.EventManager.on(window, "resize", this.fireWindowResize, this); - } - resizeEvent.addListener(fn, scope, options); - }, - - - fireWindowResize : function(){ - if(resizeEvent){ - resizeTask.delay(100); - } - }, - - - onTextResize : function(fn, scope, options){ - if(!textEvent){ - textEvent = new Ext.util.Event(); - var textEl = new Ext.Element(document.createElement('div')); - textEl.dom.className = 'x-text-resize'; - textEl.dom.innerHTML = 'X'; - textEl.appendTo(document.body); - textSize = textEl.dom.offsetHeight; - setInterval(function(){ - if(textEl.dom.offsetHeight != textSize){ - textEvent.fire(textSize, textSize = textEl.dom.offsetHeight); - } - }, this.textResizeInterval); - } - textEvent.addListener(fn, scope, options); - }, - - - removeResizeListener : function(fn, scope){ - if(resizeEvent){ - resizeEvent.removeListener(fn, scope); - } - }, - - - fireResize : function(){ - if(resizeEvent){ - resizeEvent.fire(D.getViewWidth(), D.getViewHeight()); - } - }, - - - textResizeInterval : 50, - - - ieDeferSrc : false, - - - getKeyEvent : function(){ - return useKeydown ? 'keydown' : 'keypress'; - }, - - - - useKeydown: useKeydown - }; -}()); - -Ext.EventManager.on = Ext.EventManager.addListener; - - -Ext.apply(Ext.EventObjectImpl.prototype, { - - BACKSPACE: 8, - - TAB: 9, - - NUM_CENTER: 12, - - ENTER: 13, - - RETURN: 13, - - SHIFT: 16, - - CTRL: 17, - CONTROL : 17, - - ALT: 18, - - PAUSE: 19, - - CAPS_LOCK: 20, - - ESC: 27, - - SPACE: 32, - - PAGE_UP: 33, - PAGEUP : 33, - - PAGE_DOWN: 34, - PAGEDOWN : 34, - - END: 35, - - HOME: 36, - - LEFT: 37, - - UP: 38, - - RIGHT: 39, - - DOWN: 40, - - PRINT_SCREEN: 44, - - INSERT: 45, - - DELETE: 46, - - ZERO: 48, - - ONE: 49, - - TWO: 50, - - THREE: 51, - - FOUR: 52, - - FIVE: 53, - - SIX: 54, - - SEVEN: 55, - - EIGHT: 56, - - NINE: 57, - - A: 65, - - B: 66, - - C: 67, - - D: 68, - - E: 69, - - F: 70, - - G: 71, - - H: 72, - - I: 73, - - J: 74, - - K: 75, - - L: 76, - - M: 77, - - N: 78, - - O: 79, - - P: 80, - - Q: 81, - - R: 82, - - S: 83, - - T: 84, - - U: 85, - - V: 86, - - W: 87, - - X: 88, - - Y: 89, - - Z: 90, - - CONTEXT_MENU: 93, - - NUM_ZERO: 96, - - NUM_ONE: 97, - - NUM_TWO: 98, - - NUM_THREE: 99, - - NUM_FOUR: 100, - - NUM_FIVE: 101, - - NUM_SIX: 102, - - NUM_SEVEN: 103, - - NUM_EIGHT: 104, - - NUM_NINE: 105, - - NUM_MULTIPLY: 106, - - NUM_PLUS: 107, - - NUM_MINUS: 109, - - NUM_PERIOD: 110, - - NUM_DIVISION: 111, - - F1: 112, - - F2: 113, - - F3: 114, - - F4: 115, - - F5: 116, - - F6: 117, - - F7: 118, - - F8: 119, - - F9: 120, - - F10: 121, - - F11: 122, - - F12: 123, - - - isNavKeyPress : function(){ - var me = this, - k = this.normalizeKey(me.keyCode); - return (k >= 33 && k <= 40) || - k == me.RETURN || - k == me.TAB || - k == me.ESC; - }, - - isSpecialKey : function(){ - var k = this.normalizeKey(this.keyCode); - return (this.type == 'keypress' && this.ctrlKey) || - this.isNavKeyPress() || - (k == this.BACKSPACE) || - (k >= 16 && k <= 20) || - (k >= 44 && k <= 46); - }, - - getPoint : function(){ - return new Ext.lib.Point(this.xy[0], this.xy[1]); - }, - - - hasModifier : function(){ - return ((this.ctrlKey || this.altKey) || this.shiftKey); - } -}); -Ext.Element.addMethods({ - - swallowEvent : function(eventName, preventDefault) { - var me = this; - function fn(e) { - e.stopPropagation(); - if (preventDefault) { - e.preventDefault(); - } - } - - if (Ext.isArray(eventName)) { - Ext.each(eventName, function(e) { - me.on(e, fn); - }); - return me; - } - me.on(eventName, fn); - return me; - }, - - - relayEvent : function(eventName, observable) { - this.on(eventName, function(e) { - observable.fireEvent(eventName, e); - }); - }, - - - clean : function(forceReclean) { - var me = this, - dom = me.dom, - n = dom.firstChild, - ni = -1; - - if (Ext.Element.data(dom, 'isCleaned') && forceReclean !== true) { - return me; - } - - while (n) { - var nx = n.nextSibling; - if (n.nodeType == 3 && !(/\S/.test(n.nodeValue))) { - dom.removeChild(n); - } else { - n.nodeIndex = ++ni; - } - n = nx; - } - - Ext.Element.data(dom, 'isCleaned', true); - return me; - }, - - - load : function() { - var updateManager = this.getUpdater(); - updateManager.update.apply(updateManager, arguments); - - return this; - }, - - - getUpdater : function() { - return this.updateManager || (this.updateManager = new Ext.Updater(this)); - }, - - - update : function(html, loadScripts, callback) { - if (!this.dom) { - return this; - } - html = html || ""; - - if (loadScripts !== true) { - this.dom.innerHTML = html; - if (typeof callback == 'function') { - callback(); - } - return this; - } - - var id = Ext.id(), - dom = this.dom; - - html += ''; - - Ext.lib.Event.onAvailable(id, function() { - var DOC = document, - hd = DOC.getElementsByTagName("head")[0], - re = /(?:]*)?>)((\n|\r|.)*?)(?:<\/script>)/ig, - srcRe = /\ssrc=([\'\"])(.*?)\1/i, - typeRe = /\stype=([\'\"])(.*?)\1/i, - match, - attrs, - srcMatch, - typeMatch, - el, - s; - - while ((match = re.exec(html))) { - attrs = match[1]; - srcMatch = attrs ? attrs.match(srcRe) : false; - if (srcMatch && srcMatch[2]) { - s = DOC.createElement("script"); - s.src = srcMatch[2]; - typeMatch = attrs.match(typeRe); - if (typeMatch && typeMatch[2]) { - s.type = typeMatch[2]; - } - hd.appendChild(s); - } else if (match[2] && match[2].length > 0) { - if (window.execScript) { - window.execScript(match[2]); - } else { - window.eval(match[2]); - } - } - } - - el = DOC.getElementById(id); - if (el) { - Ext.removeNode(el); - } - - if (typeof callback == 'function') { - callback(); - } - }); - dom.innerHTML = html.replace(/(?:)((\n|\r|.)*?)(?:<\/script>)/ig, ""); - return this; - }, - - - removeAllListeners : function() { - this.removeAnchor(); - Ext.EventManager.removeAll(this.dom); - return this; - }, - - - createProxy : function(config, renderTo, matchBox) { - config = (typeof config == 'object') ? config : {tag : "div", cls: config}; - - var me = this, - proxy = renderTo ? Ext.DomHelper.append(renderTo, config, true) : - Ext.DomHelper.insertBefore(me.dom, config, true); - - if (matchBox && me.setBox && me.getBox) { - proxy.setBox(me.getBox()); - } - return proxy; - } -}); - -Ext.Element.prototype.getUpdateManager = Ext.Element.prototype.getUpdater; - -Ext.Element.addMethods({ - - getAnchorXY : function(anchor, local, s){ - - - anchor = (anchor || "tl").toLowerCase(); - s = s || {}; - - var me = this, - vp = me.dom == document.body || me.dom == document, - w = s.width || vp ? Ext.lib.Dom.getViewWidth() : me.getWidth(), - h = s.height || vp ? Ext.lib.Dom.getViewHeight() : me.getHeight(), - xy, - r = Math.round, - o = me.getXY(), - scroll = me.getScroll(), - extraX = vp ? scroll.left : !local ? o[0] : 0, - extraY = vp ? scroll.top : !local ? o[1] : 0, - hash = { - c : [r(w * 0.5), r(h * 0.5)], - t : [r(w * 0.5), 0], - l : [0, r(h * 0.5)], - r : [w, r(h * 0.5)], - b : [r(w * 0.5), h], - tl : [0, 0], - bl : [0, h], - br : [w, h], - tr : [w, 0] - }; - - xy = hash[anchor]; - return [xy[0] + extraX, xy[1] + extraY]; - }, - - - anchorTo : function(el, alignment, offsets, animate, monitorScroll, callback){ - var me = this, - dom = me.dom, - scroll = !Ext.isEmpty(monitorScroll), - action = function(){ - Ext.fly(dom).alignTo(el, alignment, offsets, animate); - Ext.callback(callback, Ext.fly(dom)); - }, - anchor = this.getAnchor(); - - - this.removeAnchor(); - Ext.apply(anchor, { - fn: action, - scroll: scroll - }); - - Ext.EventManager.onWindowResize(action, null); - - if(scroll){ - Ext.EventManager.on(window, 'scroll', action, null, - {buffer: !isNaN(monitorScroll) ? monitorScroll : 50}); - } - action.call(me); - return me; - }, - - - removeAnchor : function(){ - var me = this, - anchor = this.getAnchor(); - - if(anchor && anchor.fn){ - Ext.EventManager.removeResizeListener(anchor.fn); - if(anchor.scroll){ - Ext.EventManager.un(window, 'scroll', anchor.fn); - } - delete anchor.fn; - } - return me; - }, - - - getAnchor : function(){ - var data = Ext.Element.data, - dom = this.dom; - if (!dom) { - return; - } - var anchor = data(dom, '_anchor'); - - if(!anchor){ - anchor = data(dom, '_anchor', {}); - } - return anchor; - }, - - - getAlignToXY : function(el, p, o){ - el = Ext.get(el); - - if(!el || !el.dom){ - throw "Element.alignToXY with an element that doesn't exist"; - } - - o = o || [0,0]; - p = (!p || p == "?" ? "tl-bl?" : (!(/-/).test(p) && p !== "" ? "tl-" + p : p || "tl-bl")).toLowerCase(); - - var me = this, - d = me.dom, - a1, - a2, - x, - y, - - w, - h, - r, - dw = Ext.lib.Dom.getViewWidth() -10, - dh = Ext.lib.Dom.getViewHeight()-10, - p1y, - p1x, - p2y, - p2x, - swapY, - swapX, - doc = document, - docElement = doc.documentElement, - docBody = doc.body, - scrollX = (docElement.scrollLeft || docBody.scrollLeft || 0)+5, - scrollY = (docElement.scrollTop || docBody.scrollTop || 0)+5, - c = false, - p1 = "", - p2 = "", - m = p.match(/^([a-z]+)-([a-z]+)(\?)?$/); - - if(!m){ - throw "Element.alignTo with an invalid alignment " + p; - } - - p1 = m[1]; - p2 = m[2]; - c = !!m[3]; - - - - a1 = me.getAnchorXY(p1, true); - a2 = el.getAnchorXY(p2, false); - - x = a2[0] - a1[0] + o[0]; - y = a2[1] - a1[1] + o[1]; - - if(c){ - w = me.getWidth(); - h = me.getHeight(); - r = el.getRegion(); - - - - p1y = p1.charAt(0); - p1x = p1.charAt(p1.length-1); - p2y = p2.charAt(0); - p2x = p2.charAt(p2.length-1); - swapY = ((p1y=="t" && p2y=="b") || (p1y=="b" && p2y=="t")); - swapX = ((p1x=="r" && p2x=="l") || (p1x=="l" && p2x=="r")); - - - if (x + w > dw + scrollX) { - x = swapX ? r.left-w : dw+scrollX-w; - } - if (x < scrollX) { - x = swapX ? r.right : scrollX; - } - if (y + h > dh + scrollY) { - y = swapY ? r.top-h : dh+scrollY-h; - } - if (y < scrollY){ - y = swapY ? r.bottom : scrollY; - } - } - return [x,y]; - }, - - - alignTo : function(element, position, offsets, animate){ - var me = this; - return me.setXY(me.getAlignToXY(element, position, offsets), - me.preanim && !!animate ? me.preanim(arguments, 3) : false); - }, - - - adjustForConstraints : function(xy, parent, offsets){ - return this.getConstrainToXY(parent || document, false, offsets, xy) || xy; - }, - - - getConstrainToXY : function(el, local, offsets, proposedXY){ - var os = {top:0, left:0, bottom:0, right: 0}; - - return function(el, local, offsets, proposedXY){ - el = Ext.get(el); - offsets = offsets ? Ext.applyIf(offsets, os) : os; - - var vw, vh, vx = 0, vy = 0; - if(el.dom == document.body || el.dom == document){ - vw =Ext.lib.Dom.getViewWidth(); - vh = Ext.lib.Dom.getViewHeight(); - }else{ - vw = el.dom.clientWidth; - vh = el.dom.clientHeight; - if(!local){ - var vxy = el.getXY(); - vx = vxy[0]; - vy = vxy[1]; - } - } - - var s = el.getScroll(); - - vx += offsets.left + s.left; - vy += offsets.top + s.top; - - vw -= offsets.right; - vh -= offsets.bottom; - - var vr = vx + vw, - vb = vy + vh, - xy = proposedXY || (!local ? this.getXY() : [this.getLeft(true), this.getTop(true)]), - x = xy[0], y = xy[1], - offset = this.getConstrainOffset(), - w = this.dom.offsetWidth + offset, - h = this.dom.offsetHeight + offset; - - - var moved = false; - - - if((x + w) > vr){ - x = vr - w; - moved = true; - } - if((y + h) > vb){ - y = vb - h; - moved = true; - } - - if(x < vx){ - x = vx; - moved = true; - } - if(y < vy){ - y = vy; - moved = true; - } - return moved ? [x, y] : false; - }; - }(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getConstrainOffset : function(){ - return 0; - }, - - - getCenterXY : function(){ - return this.getAlignToXY(document, 'c-c'); - }, - - - center : function(centerIn){ - return this.alignTo(centerIn || document, 'c-c'); - } -}); - -Ext.Element.addMethods({ - - select : function(selector, unique){ - return Ext.Element.select(selector, unique, this.dom); - } -}); -Ext.apply(Ext.Element.prototype, function() { - var GETDOM = Ext.getDom, - GET = Ext.get, - DH = Ext.DomHelper; - - return { - - insertSibling: function(el, where, returnDom){ - var me = this, - rt, - isAfter = (where || 'before').toLowerCase() == 'after', - insertEl; - - if(Ext.isArray(el)){ - insertEl = me; - Ext.each(el, function(e) { - rt = Ext.fly(insertEl, '_internal').insertSibling(e, where, returnDom); - if(isAfter){ - insertEl = rt; - } - }); - return rt; - } - - el = el || {}; - - if(el.nodeType || el.dom){ - rt = me.dom.parentNode.insertBefore(GETDOM(el), isAfter ? me.dom.nextSibling : me.dom); - if (!returnDom) { - rt = GET(rt); - } - }else{ - if (isAfter && !me.dom.nextSibling) { - rt = DH.append(me.dom.parentNode, el, !returnDom); - } else { - rt = DH[isAfter ? 'insertAfter' : 'insertBefore'](me.dom, el, !returnDom); - } - } - return rt; - } - }; -}()); - - -Ext.Element.boxMarkup = '
        '; - -Ext.Element.addMethods(function(){ - var INTERNAL = "_internal", - pxMatch = /(\d+\.?\d+)px/; - return { - - applyStyles : function(style){ - Ext.DomHelper.applyStyles(this.dom, style); - return this; - }, - - - getStyles : function(){ - var ret = {}; - Ext.each(arguments, function(v) { - ret[v] = this.getStyle(v); - }, - this); - return ret; - }, - - - setOverflow : function(v){ - var dom = this.dom; - if(v=='auto' && Ext.isMac && Ext.isGecko2){ - dom.style.overflow = 'hidden'; - (function(){dom.style.overflow = 'auto';}).defer(1); - }else{ - dom.style.overflow = v; - } - }, - - - boxWrap : function(cls){ - cls = cls || 'x-box'; - var el = Ext.get(this.insertHtml("beforeBegin", "
        " + String.format(Ext.Element.boxMarkup, cls) + "
        ")); - Ext.DomQuery.selectNode('.' + cls + '-mc', el.dom).appendChild(this.dom); - return el; - }, - - - setSize : function(width, height, animate){ - var me = this; - if(typeof width == 'object'){ - height = width.height; - width = width.width; - } - width = me.adjustWidth(width); - height = me.adjustHeight(height); - if(!animate || !me.anim){ - me.dom.style.width = me.addUnits(width); - me.dom.style.height = me.addUnits(height); - }else{ - me.anim({width: {to: width}, height: {to: height}}, me.preanim(arguments, 2)); - } - return me; - }, - - - getComputedHeight : function(){ - var me = this, - h = Math.max(me.dom.offsetHeight, me.dom.clientHeight); - if(!h){ - h = parseFloat(me.getStyle('height')) || 0; - if(!me.isBorderBox()){ - h += me.getFrameWidth('tb'); - } - } - return h; - }, - - - getComputedWidth : function(){ - var w = Math.max(this.dom.offsetWidth, this.dom.clientWidth); - if(!w){ - w = parseFloat(this.getStyle('width')) || 0; - if(!this.isBorderBox()){ - w += this.getFrameWidth('lr'); - } - } - return w; - }, - - - getFrameWidth : function(sides, onlyContentBox){ - return onlyContentBox && this.isBorderBox() ? 0 : (this.getPadding(sides) + this.getBorderWidth(sides)); - }, - - - addClassOnOver : function(className){ - this.hover( - function(){ - Ext.fly(this, INTERNAL).addClass(className); - }, - function(){ - Ext.fly(this, INTERNAL).removeClass(className); - } - ); - return this; - }, - - - addClassOnFocus : function(className){ - this.on("focus", function(){ - Ext.fly(this, INTERNAL).addClass(className); - }, this.dom); - this.on("blur", function(){ - Ext.fly(this, INTERNAL).removeClass(className); - }, this.dom); - return this; - }, - - - addClassOnClick : function(className){ - var dom = this.dom; - this.on("mousedown", function(){ - Ext.fly(dom, INTERNAL).addClass(className); - var d = Ext.getDoc(), - fn = function(){ - Ext.fly(dom, INTERNAL).removeClass(className); - d.removeListener("mouseup", fn); - }; - d.on("mouseup", fn); - }); - return this; - }, - - - - getViewSize : function(){ - var doc = document, - d = this.dom, - isDoc = (d == doc || d == doc.body); - - - if (isDoc) { - var extdom = Ext.lib.Dom; - return { - width : extdom.getViewWidth(), - height : extdom.getViewHeight() - }; - - - } else { - return { - width : d.clientWidth, - height : d.clientHeight - }; - } - }, - - - - getStyleSize : function(){ - var me = this, - w, h, - doc = document, - d = this.dom, - isDoc = (d == doc || d == doc.body), - s = d.style; - - - if (isDoc) { - var extdom = Ext.lib.Dom; - return { - width : extdom.getViewWidth(), - height : extdom.getViewHeight() - }; - } - - if(s.width && s.width != 'auto'){ - w = parseFloat(s.width); - if(me.isBorderBox()){ - w -= me.getFrameWidth('lr'); - } - } - - if(s.height && s.height != 'auto'){ - h = parseFloat(s.height); - if(me.isBorderBox()){ - h -= me.getFrameWidth('tb'); - } - } - - return {width: w || me.getWidth(true), height: h || me.getHeight(true)}; - }, - - - getSize : function(contentSize){ - return {width: this.getWidth(contentSize), height: this.getHeight(contentSize)}; - }, - - - repaint : function(){ - var dom = this.dom; - this.addClass("x-repaint"); - setTimeout(function(){ - Ext.fly(dom).removeClass("x-repaint"); - }, 1); - return this; - }, - - - unselectable : function(){ - this.dom.unselectable = "on"; - return this.swallowEvent("selectstart", true). - applyStyles("-moz-user-select:none;-khtml-user-select:none;"). - addClass("x-unselectable"); - }, - - - getMargins : function(side){ - var me = this, - key, - hash = {t:"top", l:"left", r:"right", b: "bottom"}, - o = {}; - - if (!side) { - for (key in me.margins){ - o[hash[key]] = parseFloat(me.getStyle(me.margins[key])) || 0; - } - return o; - } else { - return me.addStyles.call(me, side, me.margins); - } - } - }; -}()); - -Ext.Element.addMethods({ - - setBox : function(box, adjust, animate){ - var me = this, - w = box.width, - h = box.height; - if((adjust && !me.autoBoxAdjust) && !me.isBorderBox()){ - w -= (me.getBorderWidth("lr") + me.getPadding("lr")); - h -= (me.getBorderWidth("tb") + me.getPadding("tb")); - } - me.setBounds(box.x, box.y, w, h, me.animTest.call(me, arguments, animate, 2)); - return me; - }, - - - getBox : function(contentBox, local) { - var me = this, - xy, - left, - top, - getBorderWidth = me.getBorderWidth, - getPadding = me.getPadding, - l, - r, - t, - b; - if(!local){ - xy = me.getXY(); - }else{ - left = parseInt(me.getStyle("left"), 10) || 0; - top = parseInt(me.getStyle("top"), 10) || 0; - xy = [left, top]; - } - var el = me.dom, w = el.offsetWidth, h = el.offsetHeight, bx; - if(!contentBox){ - bx = {x: xy[0], y: xy[1], 0: xy[0], 1: xy[1], width: w, height: h}; - }else{ - l = getBorderWidth.call(me, "l") + getPadding.call(me, "l"); - r = getBorderWidth.call(me, "r") + getPadding.call(me, "r"); - t = getBorderWidth.call(me, "t") + getPadding.call(me, "t"); - b = getBorderWidth.call(me, "b") + getPadding.call(me, "b"); - bx = {x: xy[0]+l, y: xy[1]+t, 0: xy[0]+l, 1: xy[1]+t, width: w-(l+r), height: h-(t+b)}; - } - bx.right = bx.x + bx.width; - bx.bottom = bx.y + bx.height; - return bx; - }, - - - move : function(direction, distance, animate){ - var me = this, - xy = me.getXY(), - x = xy[0], - y = xy[1], - left = [x - distance, y], - right = [x + distance, y], - top = [x, y - distance], - bottom = [x, y + distance], - hash = { - l : left, - left : left, - r : right, - right : right, - t : top, - top : top, - up : top, - b : bottom, - bottom : bottom, - down : bottom - }; - - direction = direction.toLowerCase(); - me.moveTo(hash[direction][0], hash[direction][1], me.animTest.call(me, arguments, animate, 2)); - }, - - - setLeftTop : function(left, top){ - var me = this, - style = me.dom.style; - style.left = me.addUnits(left); - style.top = me.addUnits(top); - return me; - }, - - - getRegion : function(){ - return Ext.lib.Dom.getRegion(this.dom); - }, - - - setBounds : function(x, y, width, height, animate){ - var me = this; - if (!animate || !me.anim) { - me.setSize(width, height); - me.setLocation(x, y); - } else { - me.anim({points: {to: [x, y]}, - width: {to: me.adjustWidth(width)}, - height: {to: me.adjustHeight(height)}}, - me.preanim(arguments, 4), - 'motion'); - } - return me; - }, - - - setRegion : function(region, animate) { - return this.setBounds(region.left, region.top, region.right-region.left, region.bottom-region.top, this.animTest.call(this, arguments, animate, 1)); - } -}); -Ext.Element.addMethods({ - - scrollTo : function(side, value, animate) { - - var top = /top/i.test(side), - me = this, - dom = me.dom, - prop; - if (!animate || !me.anim) { - - prop = 'scroll' + (top ? 'Top' : 'Left'); - dom[prop] = value; - } - else { - - prop = 'scroll' + (top ? 'Left' : 'Top'); - me.anim({scroll: {to: top ? [dom[prop], value] : [value, dom[prop]]}}, me.preanim(arguments, 2), 'scroll'); - } - return me; - }, - - - scrollIntoView : function(container, hscroll) { - var c = Ext.getDom(container) || Ext.getBody().dom, - el = this.dom, - o = this.getOffsetsTo(c), - l = o[0] + c.scrollLeft, - t = o[1] + c.scrollTop, - b = t + el.offsetHeight, - r = l + el.offsetWidth, - ch = c.clientHeight, - ct = parseInt(c.scrollTop, 10), - cl = parseInt(c.scrollLeft, 10), - cb = ct + ch, - cr = cl + c.clientWidth; - - if (el.offsetHeight > ch || t < ct) { - c.scrollTop = t; - } - else if (b > cb) { - c.scrollTop = b-ch; - } - - c.scrollTop = c.scrollTop; - - if (hscroll !== false) { - if (el.offsetWidth > c.clientWidth || l < cl) { - c.scrollLeft = l; - } - else if (r > cr) { - c.scrollLeft = r - c.clientWidth; - } - c.scrollLeft = c.scrollLeft; - } - return this; - }, - - - scrollChildIntoView : function(child, hscroll) { - Ext.fly(child, '_scrollChildIntoView').scrollIntoView(this, hscroll); - }, - - - scroll : function(direction, distance, animate) { - if (!this.isScrollable()) { - return false; - } - var el = this.dom, - l = el.scrollLeft, t = el.scrollTop, - w = el.scrollWidth, h = el.scrollHeight, - cw = el.clientWidth, ch = el.clientHeight, - scrolled = false, v, - hash = { - l: Math.min(l + distance, w-cw), - r: v = Math.max(l - distance, 0), - t: Math.max(t - distance, 0), - b: Math.min(t + distance, h-ch) - }; - hash.d = hash.b; - hash.u = hash.t; - - direction = direction.substr(0, 1); - if ((v = hash[direction]) > -1) { - scrolled = true; - this.scrollTo(direction == 'l' || direction == 'r' ? 'left' : 'top', v, this.preanim(arguments, 2)); - } - return scrolled; - } -}); -Ext.Element.addMethods( - function() { - var VISIBILITY = "visibility", - DISPLAY = "display", - HIDDEN = "hidden", - NONE = "none", - XMASKED = "x-masked", - XMASKEDRELATIVE = "x-masked-relative", - data = Ext.Element.data; - - return { - - isVisible : function(deep) { - var vis = !this.isStyle(VISIBILITY, HIDDEN) && !this.isStyle(DISPLAY, NONE), - p = this.dom.parentNode; - - if (deep !== true || !vis) { - return vis; - } - - while (p && !(/^body/i.test(p.tagName))) { - if (!Ext.fly(p, '_isVisible').isVisible()) { - return false; - } - p = p.parentNode; - } - return true; - }, - - - isDisplayed : function() { - return !this.isStyle(DISPLAY, NONE); - }, - - - enableDisplayMode : function(display) { - this.setVisibilityMode(Ext.Element.DISPLAY); - - if (!Ext.isEmpty(display)) { - data(this.dom, 'originalDisplay', display); - } - - return this; - }, - - - mask : function(msg, msgCls) { - var me = this, - dom = me.dom, - dh = Ext.DomHelper, - EXTELMASKMSG = "ext-el-mask-msg", - el, - mask; - - if (!/^body/i.test(dom.tagName) && me.getStyle('position') == 'static') { - me.addClass(XMASKEDRELATIVE); - } - if (el = data(dom, 'maskMsg')) { - el.remove(); - } - if (el = data(dom, 'mask')) { - el.remove(); - } - - mask = dh.append(dom, {cls : "ext-el-mask"}, true); - data(dom, 'mask', mask); - - me.addClass(XMASKED); - mask.setDisplayed(true); - - if (typeof msg == 'string') { - var mm = dh.append(dom, {cls : EXTELMASKMSG, cn:{tag:'div'}}, true); - data(dom, 'maskMsg', mm); - mm.dom.className = msgCls ? EXTELMASKMSG + " " + msgCls : EXTELMASKMSG; - mm.dom.firstChild.innerHTML = msg; - mm.setDisplayed(true); - mm.center(me); - } - - - if (Ext.isIE && !(Ext.isIE7 && Ext.isStrict) && me.getStyle('height') == 'auto') { - mask.setSize(undefined, me.getHeight()); - } - - return mask; - }, - - - unmask : function() { - var me = this, - dom = me.dom, - mask = data(dom, 'mask'), - maskMsg = data(dom, 'maskMsg'); - - if (mask) { - if (maskMsg) { - maskMsg.remove(); - data(dom, 'maskMsg', undefined); - } - - mask.remove(); - data(dom, 'mask', undefined); - me.removeClass([XMASKED, XMASKEDRELATIVE]); - } - }, - - - isMasked : function() { - var m = data(this.dom, 'mask'); - return m && m.isVisible(); - }, - - - createShim : function() { - var el = document.createElement('iframe'), - shim; - - el.frameBorder = '0'; - el.className = 'ext-shim'; - el.src = Ext.SSL_SECURE_URL; - shim = Ext.get(this.dom.parentNode.insertBefore(el, this.dom)); - shim.autoBoxAdjust = false; - return shim; - } - }; - }() -); -Ext.Element.addMethods({ - - addKeyListener : function(key, fn, scope){ - var config; - if(typeof key != 'object' || Ext.isArray(key)){ - config = { - key: key, - fn: fn, - scope: scope - }; - }else{ - config = { - key : key.key, - shift : key.shift, - ctrl : key.ctrl, - alt : key.alt, - fn: fn, - scope: scope - }; - } - return new Ext.KeyMap(this, config); - }, - - - addKeyMap : function(config){ - return new Ext.KeyMap(this, config); - } -}); - - - -Ext.CompositeElementLite.importElementMethods(); -Ext.apply(Ext.CompositeElementLite.prototype, { - addElements : function(els, root){ - if(!els){ - return this; - } - if(typeof els == "string"){ - els = Ext.Element.selectorFunction(els, root); - } - var yels = this.elements; - Ext.each(els, function(e) { - yels.push(Ext.get(e)); - }); - return this; - }, - - - first : function(){ - return this.item(0); - }, - - - last : function(){ - return this.item(this.getCount()-1); - }, - - - contains : function(el){ - return this.indexOf(el) != -1; - }, - - - removeElement : function(keys, removeDom){ - var me = this, - els = this.elements, - el; - Ext.each(keys, function(val){ - if ((el = (els[val] || els[val = me.indexOf(val)]))) { - if(removeDom){ - if(el.dom){ - el.remove(); - }else{ - Ext.removeNode(el); - } - } - els.splice(val, 1); - } - }); - return this; - } -}); - -Ext.CompositeElement = Ext.extend(Ext.CompositeElementLite, { - - constructor : function(els, root){ - this.elements = []; - this.add(els, root); - }, - - - getElement : function(el){ - - return el; - }, - - - transformElement : function(el){ - return Ext.get(el); - } - - - - - - -}); - - -Ext.Element.select = function(selector, unique, root){ - var els; - if(typeof selector == "string"){ - els = Ext.Element.selectorFunction(selector, root); - }else if(selector.length !== undefined){ - els = selector; - }else{ - throw "Invalid selector"; - } - - return (unique === true) ? new Ext.CompositeElement(els) : new Ext.CompositeElementLite(els); -}; - - -Ext.select = Ext.Element.select; -Ext.UpdateManager = Ext.Updater = Ext.extend(Ext.util.Observable, -function() { - var BEFOREUPDATE = "beforeupdate", - UPDATE = "update", - FAILURE = "failure"; - - - function processSuccess(response){ - var me = this; - me.transaction = null; - if (response.argument.form && response.argument.reset) { - try { - response.argument.form.reset(); - } catch(e){} - } - if (me.loadScripts) { - me.renderer.render(me.el, response, me, - updateComplete.createDelegate(me, [response])); - } else { - me.renderer.render(me.el, response, me); - updateComplete.call(me, response); - } - } - - - function updateComplete(response, type, success){ - this.fireEvent(type || UPDATE, this.el, response); - if(Ext.isFunction(response.argument.callback)){ - response.argument.callback.call(response.argument.scope, this.el, Ext.isEmpty(success) ? true : false, response, response.argument.options); - } - } - - - function processFailure(response){ - updateComplete.call(this, response, FAILURE, !!(this.transaction = null)); - } - - return { - constructor: function(el, forceNew){ - var me = this; - el = Ext.get(el); - if(!forceNew && el.updateManager){ - return el.updateManager; - } - - me.el = el; - - me.defaultUrl = null; - - me.addEvents( - - BEFOREUPDATE, - - UPDATE, - - FAILURE - ); - - Ext.apply(me, Ext.Updater.defaults); - - - - - - - - - me.transaction = null; - - me.refreshDelegate = me.refresh.createDelegate(me); - - me.updateDelegate = me.update.createDelegate(me); - - me.formUpdateDelegate = (me.formUpdate || function(){}).createDelegate(me); - - - me.renderer = me.renderer || me.getDefaultRenderer(); - - Ext.Updater.superclass.constructor.call(me); - }, - - - setRenderer : function(renderer){ - this.renderer = renderer; - }, - - - getRenderer : function(){ - return this.renderer; - }, - - - getDefaultRenderer: function() { - return new Ext.Updater.BasicRenderer(); - }, - - - setDefaultUrl : function(defaultUrl){ - this.defaultUrl = defaultUrl; - }, - - - getEl : function(){ - return this.el; - }, - - - update : function(url, params, callback, discardUrl){ - var me = this, - cfg, - callerScope; - - if(me.fireEvent(BEFOREUPDATE, me.el, url, params) !== false){ - if(Ext.isObject(url)){ - cfg = url; - url = cfg.url; - params = params || cfg.params; - callback = callback || cfg.callback; - discardUrl = discardUrl || cfg.discardUrl; - callerScope = cfg.scope; - if(!Ext.isEmpty(cfg.nocache)){me.disableCaching = cfg.nocache;}; - if(!Ext.isEmpty(cfg.text)){me.indicatorText = '
        '+cfg.text+"
        ";}; - if(!Ext.isEmpty(cfg.scripts)){me.loadScripts = cfg.scripts;}; - if(!Ext.isEmpty(cfg.timeout)){me.timeout = cfg.timeout;}; - } - me.showLoading(); - - if(!discardUrl){ - me.defaultUrl = url; - } - if(Ext.isFunction(url)){ - url = url.call(me); - } - - var o = Ext.apply({}, { - url : url, - params: (Ext.isFunction(params) && callerScope) ? params.createDelegate(callerScope) : params, - success: processSuccess, - failure: processFailure, - scope: me, - callback: undefined, - timeout: (me.timeout*1000), - disableCaching: me.disableCaching, - argument: { - "options": cfg, - "url": url, - "form": null, - "callback": callback, - "scope": callerScope || window, - "params": params - } - }, cfg); - - me.transaction = Ext.Ajax.request(o); - } - }, - - - formUpdate : function(form, url, reset, callback){ - var me = this; - if(me.fireEvent(BEFOREUPDATE, me.el, form, url) !== false){ - if(Ext.isFunction(url)){ - url = url.call(me); - } - form = Ext.getDom(form); - me.transaction = Ext.Ajax.request({ - form: form, - url:url, - success: processSuccess, - failure: processFailure, - scope: me, - timeout: (me.timeout*1000), - argument: { - "url": url, - "form": form, - "callback": callback, - "reset": reset - } - }); - me.showLoading.defer(1, me); - } - }, - - - startAutoRefresh : function(interval, url, params, callback, refreshNow){ - var me = this; - if(refreshNow){ - me.update(url || me.defaultUrl, params, callback, true); - } - if(me.autoRefreshProcId){ - clearInterval(me.autoRefreshProcId); - } - me.autoRefreshProcId = setInterval(me.update.createDelegate(me, [url || me.defaultUrl, params, callback, true]), interval * 1000); - }, - - - stopAutoRefresh : function(){ - if(this.autoRefreshProcId){ - clearInterval(this.autoRefreshProcId); - delete this.autoRefreshProcId; - } - }, - - - isAutoRefreshing : function(){ - return !!this.autoRefreshProcId; - }, - - - showLoading : function(){ - if(this.showLoadIndicator){ - this.el.dom.innerHTML = this.indicatorText; - } - }, - - - abort : function(){ - if(this.transaction){ - Ext.Ajax.abort(this.transaction); - } - }, - - - isUpdating : function(){ - return this.transaction ? Ext.Ajax.isLoading(this.transaction) : false; - }, - - - refresh : function(callback){ - if(this.defaultUrl){ - this.update(this.defaultUrl, null, callback, true); - } - } - }; -}()); - - -Ext.Updater.defaults = { - - timeout : 30, - - disableCaching : false, - - showLoadIndicator : true, - - indicatorText : '
        Loading...
        ', - - loadScripts : false, - - sslBlankUrl : Ext.SSL_SECURE_URL -}; - - - -Ext.Updater.updateElement = function(el, url, params, options){ - var um = Ext.get(el).getUpdater(); - Ext.apply(um, options); - um.update(url, params, options ? options.callback : null); -}; - - -Ext.Updater.BasicRenderer = function(){}; - -Ext.Updater.BasicRenderer.prototype = { - - render : function(el, response, updateManager, callback){ - el.update(response.responseText, updateManager.loadScripts, callback); - } -}; - - - -(function() { - - -Date.useStrict = false; - - - - - -function xf(format) { - var args = Array.prototype.slice.call(arguments, 1); - return format.replace(/\{(\d+)\}/g, function(m, i) { - return args[i]; - }); -} - - - -Date.formatCodeToRegex = function(character, currentGroup) { - - var p = Date.parseCodes[character]; - - if (p) { - p = typeof p == 'function'? p() : p; - Date.parseCodes[character] = p; - } - - return p ? Ext.applyIf({ - c: p.c ? xf(p.c, currentGroup || "{0}") : p.c - }, p) : { - g:0, - c:null, - s:Ext.escapeRe(character) - }; -}; - - -var $f = Date.formatCodeToRegex; - -Ext.apply(Date, { - - parseFunctions: { - "M$": function(input, strict) { - - - var re = new RegExp('\\/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\/'); - var r = (input || '').match(re); - return r? new Date(((r[1] || '') + r[2]) * 1) : null; - } - }, - parseRegexes: [], - - - formatFunctions: { - "M$": function() { - - return '\\/Date(' + this.getTime() + ')\\/'; - } - }, - - y2kYear : 50, - - - MILLI : "ms", - - - SECOND : "s", - - - MINUTE : "mi", - - - HOUR : "h", - - - DAY : "d", - - - MONTH : "mo", - - - YEAR : "y", - - - defaults: {}, - - - dayNames : [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday" - ], - - - monthNames : [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December" - ], - - - monthNumbers : { - Jan:0, - Feb:1, - Mar:2, - Apr:3, - May:4, - Jun:5, - Jul:6, - Aug:7, - Sep:8, - Oct:9, - Nov:10, - Dec:11 - }, - - - getShortMonthName : function(month) { - return Date.monthNames[month].substring(0, 3); - }, - - - getShortDayName : function(day) { - return Date.dayNames[day].substring(0, 3); - }, - - - getMonthNumber : function(name) { - - return Date.monthNumbers[name.substring(0, 1).toUpperCase() + name.substring(1, 3).toLowerCase()]; - }, - - - formatContainsHourInfo : (function(){ - var stripEscapeRe = /(\\.)/g, - hourInfoRe = /([gGhHisucUOPZ]|M\$)/; - return function(format){ - return hourInfoRe.test(format.replace(stripEscapeRe, '')); - }; - })(), - - - formatCodes : { - d: "String.leftPad(this.getDate(), 2, '0')", - D: "Date.getShortDayName(this.getDay())", - j: "this.getDate()", - l: "Date.dayNames[this.getDay()]", - N: "(this.getDay() ? this.getDay() : 7)", - S: "this.getSuffix()", - w: "this.getDay()", - z: "this.getDayOfYear()", - W: "String.leftPad(this.getWeekOfYear(), 2, '0')", - F: "Date.monthNames[this.getMonth()]", - m: "String.leftPad(this.getMonth() + 1, 2, '0')", - M: "Date.getShortMonthName(this.getMonth())", - n: "(this.getMonth() + 1)", - t: "this.getDaysInMonth()", - L: "(this.isLeapYear() ? 1 : 0)", - o: "(this.getFullYear() + (this.getWeekOfYear() == 1 && this.getMonth() > 0 ? +1 : (this.getWeekOfYear() >= 52 && this.getMonth() < 11 ? -1 : 0)))", - Y: "String.leftPad(this.getFullYear(), 4, '0')", - y: "('' + this.getFullYear()).substring(2, 4)", - a: "(this.getHours() < 12 ? 'am' : 'pm')", - A: "(this.getHours() < 12 ? 'AM' : 'PM')", - g: "((this.getHours() % 12) ? this.getHours() % 12 : 12)", - G: "this.getHours()", - h: "String.leftPad((this.getHours() % 12) ? this.getHours() % 12 : 12, 2, '0')", - H: "String.leftPad(this.getHours(), 2, '0')", - i: "String.leftPad(this.getMinutes(), 2, '0')", - s: "String.leftPad(this.getSeconds(), 2, '0')", - u: "String.leftPad(this.getMilliseconds(), 3, '0')", - O: "this.getGMTOffset()", - P: "this.getGMTOffset(true)", - T: "this.getTimezone()", - Z: "(this.getTimezoneOffset() * -60)", - - c: function() { - for (var c = "Y-m-dTH:i:sP", code = [], i = 0, l = c.length; i < l; ++i) { - var e = c.charAt(i); - code.push(e == "T" ? "'T'" : Date.getFormatCode(e)); - } - return code.join(" + "); - }, - - - U: "Math.round(this.getTime() / 1000)" - }, - - - isValid : function(y, m, d, h, i, s, ms) { - - h = h || 0; - i = i || 0; - s = s || 0; - ms = ms || 0; - - - var dt = new Date(y < 100 ? 100 : y, m - 1, d, h, i, s, ms).add(Date.YEAR, y < 100 ? y - 100 : 0); - - return y == dt.getFullYear() && - m == dt.getMonth() + 1 && - d == dt.getDate() && - h == dt.getHours() && - i == dt.getMinutes() && - s == dt.getSeconds() && - ms == dt.getMilliseconds(); - }, - - - parseDate : function(input, format, strict) { - var p = Date.parseFunctions; - if (p[format] == null) { - Date.createParser(format); - } - return p[format](input, Ext.isDefined(strict) ? strict : Date.useStrict); - }, - - - getFormatCode : function(character) { - var f = Date.formatCodes[character]; - - if (f) { - f = typeof f == 'function'? f() : f; - Date.formatCodes[character] = f; - } - - - return f || ("'" + String.escape(character) + "'"); - }, - - - createFormat : function(format) { - var code = [], - special = false, - ch = ''; - - for (var i = 0; i < format.length; ++i) { - ch = format.charAt(i); - if (!special && ch == "\\") { - special = true; - } else if (special) { - special = false; - code.push("'" + String.escape(ch) + "'"); - } else { - code.push(Date.getFormatCode(ch)); - } - } - Date.formatFunctions[format] = new Function("return " + code.join('+')); - }, - - - createParser : function() { - var code = [ - "var dt, y, m, d, h, i, s, ms, o, z, zz, u, v,", - "def = Date.defaults,", - "results = String(input).match(Date.parseRegexes[{0}]);", - - "if(results){", - "{1}", - - "if(u != null){", - "v = new Date(u * 1000);", - "}else{", - - - - "dt = (new Date()).clearTime();", - - - "y = Ext.num(y, Ext.num(def.y, dt.getFullYear()));", - "m = Ext.num(m, Ext.num(def.m - 1, dt.getMonth()));", - "d = Ext.num(d, Ext.num(def.d, dt.getDate()));", - - - "h = Ext.num(h, Ext.num(def.h, dt.getHours()));", - "i = Ext.num(i, Ext.num(def.i, dt.getMinutes()));", - "s = Ext.num(s, Ext.num(def.s, dt.getSeconds()));", - "ms = Ext.num(ms, Ext.num(def.ms, dt.getMilliseconds()));", - - "if(z >= 0 && y >= 0){", - - - - - - "v = new Date(y < 100 ? 100 : y, 0, 1, h, i, s, ms).add(Date.YEAR, y < 100 ? y - 100 : 0);", - - - "v = !strict? v : (strict === true && (z <= 364 || (v.isLeapYear() && z <= 365))? v.add(Date.DAY, z) : null);", - "}else if(strict === true && !Date.isValid(y, m + 1, d, h, i, s, ms)){", - "v = null;", - "}else{", - - - "v = new Date(y < 100 ? 100 : y, m, d, h, i, s, ms).add(Date.YEAR, y < 100 ? y - 100 : 0);", - "}", - "}", - "}", - - "if(v){", - - "if(zz != null){", - - "v = v.add(Date.SECOND, -v.getTimezoneOffset() * 60 - zz);", - "}else if(o){", - - "v = v.add(Date.MINUTE, -v.getTimezoneOffset() + (sn == '+'? -1 : 1) * (hr * 60 + mn));", - "}", - "}", - - "return v;" - ].join('\n'); - - return function(format) { - var regexNum = Date.parseRegexes.length, - currentGroup = 1, - calc = [], - regex = [], - special = false, - ch = "", - i = 0, - obj, - last; - - for (; i < format.length; ++i) { - ch = format.charAt(i); - if (!special && ch == "\\") { - special = true; - } else if (special) { - special = false; - regex.push(String.escape(ch)); - } else { - obj = $f(ch, currentGroup); - currentGroup += obj.g; - regex.push(obj.s); - if (obj.g && obj.c) { - if (obj.calcLast) { - last = obj.c; - } else { - calc.push(obj.c); - } - } - } - } - - if (last) { - calc.push(last); - } - - Date.parseRegexes[regexNum] = new RegExp("^" + regex.join('') + "$", 'i'); - Date.parseFunctions[format] = new Function("input", "strict", xf(code, regexNum, calc.join(''))); - }; - }(), - - - parseCodes : { - - d: { - g:1, - c:"d = parseInt(results[{0}], 10);\n", - s:"(\\d{2})" - }, - j: { - g:1, - c:"d = parseInt(results[{0}], 10);\n", - s:"(\\d{1,2})" - }, - D: function() { - for (var a = [], i = 0; i < 7; a.push(Date.getShortDayName(i)), ++i); - return { - g:0, - c:null, - s:"(?:" + a.join("|") +")" - }; - }, - l: function() { - return { - g:0, - c:null, - s:"(?:" + Date.dayNames.join("|") + ")" - }; - }, - N: { - g:0, - c:null, - s:"[1-7]" - }, - S: { - g:0, - c:null, - s:"(?:st|nd|rd|th)" - }, - w: { - g:0, - c:null, - s:"[0-6]" - }, - z: { - g:1, - c:"z = parseInt(results[{0}], 10);\n", - s:"(\\d{1,3})" - }, - W: { - g:0, - c:null, - s:"(?:\\d{2})" - }, - F: function() { - return { - g:1, - c:"m = parseInt(Date.getMonthNumber(results[{0}]), 10);\n", - s:"(" + Date.monthNames.join("|") + ")" - }; - }, - M: function() { - for (var a = [], i = 0; i < 12; a.push(Date.getShortMonthName(i)), ++i); - return Ext.applyIf({ - s:"(" + a.join("|") + ")" - }, $f("F")); - }, - m: { - g:1, - c:"m = parseInt(results[{0}], 10) - 1;\n", - s:"(\\d{2})" - }, - n: { - g:1, - c:"m = parseInt(results[{0}], 10) - 1;\n", - s:"(\\d{1,2})" - }, - t: { - g:0, - c:null, - s:"(?:\\d{2})" - }, - L: { - g:0, - c:null, - s:"(?:1|0)" - }, - o: function() { - return $f("Y"); - }, - Y: { - g:1, - c:"y = parseInt(results[{0}], 10);\n", - s:"(\\d{4})" - }, - y: { - g:1, - c:"var ty = parseInt(results[{0}], 10);\n" - + "y = ty > Date.y2kYear ? 1900 + ty : 2000 + ty;\n", - s:"(\\d{1,2})" - }, - - a: function(){ - return $f("A"); - }, - A: { - - calcLast: true, - g:1, - c:"if (/(am)/i.test(results[{0}])) {\n" - + "if (!h || h == 12) { h = 0; }\n" - + "} else { if (!h || h < 12) { h = (h || 0) + 12; }}", - s:"(AM|PM|am|pm)" - }, - g: function() { - return $f("G"); - }, - G: { - g:1, - c:"h = parseInt(results[{0}], 10);\n", - s:"(\\d{1,2})" - }, - h: function() { - return $f("H"); - }, - H: { - g:1, - c:"h = parseInt(results[{0}], 10);\n", - s:"(\\d{2})" - }, - i: { - g:1, - c:"i = parseInt(results[{0}], 10);\n", - s:"(\\d{2})" - }, - s: { - g:1, - c:"s = parseInt(results[{0}], 10);\n", - s:"(\\d{2})" - }, - u: { - g:1, - c:"ms = results[{0}]; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n", - s:"(\\d+)" - }, - O: { - g:1, - c:[ - "o = results[{0}];", - "var sn = o.substring(0,1),", - "hr = o.substring(1,3)*1 + Math.floor(o.substring(3,5) / 60),", - "mn = o.substring(3,5) % 60;", - "o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + String.leftPad(hr, 2, '0') + String.leftPad(mn, 2, '0')) : null;\n" - ].join("\n"), - s: "([+\-]\\d{4})" - }, - P: { - g:1, - c:[ - "o = results[{0}];", - "var sn = o.substring(0,1),", - "hr = o.substring(1,3)*1 + Math.floor(o.substring(4,6) / 60),", - "mn = o.substring(4,6) % 60;", - "o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + String.leftPad(hr, 2, '0') + String.leftPad(mn, 2, '0')) : null;\n" - ].join("\n"), - s: "([+\-]\\d{2}:\\d{2})" - }, - T: { - g:0, - c:null, - s:"[A-Z]{1,4}" - }, - Z: { - g:1, - c:"zz = results[{0}] * 1;\n" - + "zz = (-43200 <= zz && zz <= 50400)? zz : null;\n", - s:"([+\-]?\\d{1,5})" - }, - c: function() { - var calc = [], - arr = [ - $f("Y", 1), - $f("m", 2), - $f("d", 3), - $f("h", 4), - $f("i", 5), - $f("s", 6), - {c:"ms = results[7] || '0'; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n"}, - {c:[ - "if(results[8]) {", - "if(results[8] == 'Z'){", - "zz = 0;", - "}else if (results[8].indexOf(':') > -1){", - $f("P", 8).c, - "}else{", - $f("O", 8).c, - "}", - "}" - ].join('\n')} - ]; - - for (var i = 0, l = arr.length; i < l; ++i) { - calc.push(arr[i].c); - } - - return { - g:1, - c:calc.join(""), - s:[ - arr[0].s, - "(?:", "-", arr[1].s, - "(?:", "-", arr[2].s, - "(?:", - "(?:T| )?", - arr[3].s, ":", arr[4].s, - "(?::", arr[5].s, ")?", - "(?:(?:\\.|,)(\\d+))?", - "(Z|(?:[-+]\\d{2}(?::)?\\d{2}))?", - ")?", - ")?", - ")?" - ].join("") - }; - }, - U: { - g:1, - c:"u = parseInt(results[{0}], 10);\n", - s:"(-?\\d+)" - } - } -}); - -}()); - -Ext.apply(Date.prototype, { - - dateFormat : function(format) { - if (Date.formatFunctions[format] == null) { - Date.createFormat(format); - } - return Date.formatFunctions[format].call(this); - }, - - - getTimezone : function() { - - - - - - - - - - - - - return this.toString().replace(/^.* (?:\((.*)\)|([A-Z]{1,4})(?:[\-+][0-9]{4})?(?: -?\d+)?)$/, "$1$2").replace(/[^A-Z]/g, ""); - }, - - - getGMTOffset : function(colon) { - return (this.getTimezoneOffset() > 0 ? "-" : "+") - + String.leftPad(Math.floor(Math.abs(this.getTimezoneOffset()) / 60), 2, "0") - + (colon ? ":" : "") - + String.leftPad(Math.abs(this.getTimezoneOffset() % 60), 2, "0"); - }, - - - getDayOfYear: function() { - var num = 0, - d = this.clone(), - m = this.getMonth(), - i; - - for (i = 0, d.setDate(1), d.setMonth(0); i < m; d.setMonth(++i)) { - num += d.getDaysInMonth(); - } - return num + this.getDate() - 1; - }, - - - getWeekOfYear : function() { - - var ms1d = 864e5, - ms7d = 7 * ms1d; - - return function() { - var DC3 = Date.UTC(this.getFullYear(), this.getMonth(), this.getDate() + 3) / ms1d, - AWN = Math.floor(DC3 / 7), - Wyr = new Date(AWN * ms7d).getUTCFullYear(); - - return AWN - Math.floor(Date.UTC(Wyr, 0, 7) / ms7d) + 1; - }; - }(), - - - isLeapYear : function() { - var year = this.getFullYear(); - return !!((year & 3) == 0 && (year % 100 || (year % 400 == 0 && year))); - }, - - - getFirstDayOfMonth : function() { - var day = (this.getDay() - (this.getDate() - 1)) % 7; - return (day < 0) ? (day + 7) : day; - }, - - - getLastDayOfMonth : function() { - return this.getLastDateOfMonth().getDay(); - }, - - - - getFirstDateOfMonth : function() { - return new Date(this.getFullYear(), this.getMonth(), 1); - }, - - - getLastDateOfMonth : function() { - return new Date(this.getFullYear(), this.getMonth(), this.getDaysInMonth()); - }, - - - getDaysInMonth: function() { - var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - - return function() { - var m = this.getMonth(); - - return m == 1 && this.isLeapYear() ? 29 : daysInMonth[m]; - }; - }(), - - - getSuffix : function() { - switch (this.getDate()) { - case 1: - case 21: - case 31: - return "st"; - case 2: - case 22: - return "nd"; - case 3: - case 23: - return "rd"; - default: - return "th"; - } - }, - - - clone : function() { - return new Date(this.getTime()); - }, - - - isDST : function() { - - - return new Date(this.getFullYear(), 0, 1).getTimezoneOffset() != this.getTimezoneOffset(); - }, - - - clearTime : function(clone) { - if (clone) { - return this.clone().clearTime(); - } - - - var d = this.getDate(); - - - this.setHours(0); - this.setMinutes(0); - this.setSeconds(0); - this.setMilliseconds(0); - - if (this.getDate() != d) { - - - - - for (var hr = 1, c = this.add(Date.HOUR, hr); c.getDate() != d; hr++, c = this.add(Date.HOUR, hr)); - - this.setDate(d); - this.setHours(c.getHours()); - } - - return this; - }, - - - add : function(interval, value) { - var d = this.clone(); - if (!interval || value === 0) return d; - - switch(interval.toLowerCase()) { - case Date.MILLI: - d.setMilliseconds(this.getMilliseconds() + value); - break; - case Date.SECOND: - d.setSeconds(this.getSeconds() + value); - break; - case Date.MINUTE: - d.setMinutes(this.getMinutes() + value); - break; - case Date.HOUR: - d.setHours(this.getHours() + value); - break; - case Date.DAY: - d.setDate(this.getDate() + value); - break; - case Date.MONTH: - var day = this.getDate(); - if (day > 28) { - day = Math.min(day, this.getFirstDateOfMonth().add('mo', value).getLastDateOfMonth().getDate()); - } - d.setDate(day); - d.setMonth(this.getMonth() + value); - break; - case Date.YEAR: - d.setFullYear(this.getFullYear() + value); - break; - } - return d; - }, - - - between : function(start, end) { - var t = this.getTime(); - return start.getTime() <= t && t <= end.getTime(); - } -}); - - - -Date.prototype.format = Date.prototype.dateFormat; - - - -if (Ext.isSafari && (navigator.userAgent.match(/WebKit\/(\d+)/)[1] || NaN) < 420) { - Ext.apply(Date.prototype, { - _xMonth : Date.prototype.setMonth, - _xDate : Date.prototype.setDate, - - - - setMonth : function(num) { - if (num <= -1) { - var n = Math.ceil(-num), - back_year = Math.ceil(n / 12), - month = (n % 12) ? 12 - n % 12 : 0; - - this.setFullYear(this.getFullYear() - back_year); - - return this._xMonth(month); - } else { - return this._xMonth(num); - } - }, - - - - - setDate : function(d) { - - - return this.setTime(this.getTime() - (this.getDate() - d) * 864e5); - } - }); -} - - - - - -Ext.util.MixedCollection = function(allowFunctions, keyFn){ - this.items = []; - this.map = {}; - this.keys = []; - this.length = 0; - this.addEvents( - - 'clear', - - 'add', - - 'replace', - - 'remove', - 'sort' - ); - this.allowFunctions = allowFunctions === true; - if(keyFn){ - this.getKey = keyFn; - } - Ext.util.MixedCollection.superclass.constructor.call(this); -}; - -Ext.extend(Ext.util.MixedCollection, Ext.util.Observable, { - - - allowFunctions : false, - - - add : function(key, o){ - if(arguments.length == 1){ - o = arguments[0]; - key = this.getKey(o); - } - if(typeof key != 'undefined' && key !== null){ - var old = this.map[key]; - if(typeof old != 'undefined'){ - return this.replace(key, o); - } - this.map[key] = o; - } - this.length++; - this.items.push(o); - this.keys.push(key); - this.fireEvent('add', this.length-1, o, key); - return o; - }, - - - getKey : function(o){ - return o.id; - }, - - - replace : function(key, o){ - if(arguments.length == 1){ - o = arguments[0]; - key = this.getKey(o); - } - var old = this.map[key]; - if(typeof key == 'undefined' || key === null || typeof old == 'undefined'){ - return this.add(key, o); - } - var index = this.indexOfKey(key); - this.items[index] = o; - this.map[key] = o; - this.fireEvent('replace', key, old, o); - return o; - }, - - - addAll : function(objs){ - if(arguments.length > 1 || Ext.isArray(objs)){ - var args = arguments.length > 1 ? arguments : objs; - for(var i = 0, len = args.length; i < len; i++){ - this.add(args[i]); - } - }else{ - for(var key in objs){ - if(this.allowFunctions || typeof objs[key] != 'function'){ - this.add(key, objs[key]); - } - } - } - }, - - - each : function(fn, scope){ - var items = [].concat(this.items); - for(var i = 0, len = items.length; i < len; i++){ - if(fn.call(scope || items[i], items[i], i, len) === false){ - break; - } - } - }, - - - eachKey : function(fn, scope){ - for(var i = 0, len = this.keys.length; i < len; i++){ - fn.call(scope || window, this.keys[i], this.items[i], i, len); - } - }, - - - find : function(fn, scope){ - for(var i = 0, len = this.items.length; i < len; i++){ - if(fn.call(scope || window, this.items[i], this.keys[i])){ - return this.items[i]; - } - } - return null; - }, - - - insert : function(index, key, o){ - if(arguments.length == 2){ - o = arguments[1]; - key = this.getKey(o); - } - if(this.containsKey(key)){ - this.suspendEvents(); - this.removeKey(key); - this.resumeEvents(); - } - if(index >= this.length){ - return this.add(key, o); - } - this.length++; - this.items.splice(index, 0, o); - if(typeof key != 'undefined' && key !== null){ - this.map[key] = o; - } - this.keys.splice(index, 0, key); - this.fireEvent('add', index, o, key); - return o; - }, - - - remove : function(o){ - return this.removeAt(this.indexOf(o)); - }, - - - removeAt : function(index){ - if(index < this.length && index >= 0){ - this.length--; - var o = this.items[index]; - this.items.splice(index, 1); - var key = this.keys[index]; - if(typeof key != 'undefined'){ - delete this.map[key]; - } - this.keys.splice(index, 1); - this.fireEvent('remove', o, key); - return o; - } - return false; - }, - - - removeKey : function(key){ - return this.removeAt(this.indexOfKey(key)); - }, - - - getCount : function(){ - return this.length; - }, - - - indexOf : function(o){ - return this.items.indexOf(o); - }, - - - indexOfKey : function(key){ - return this.keys.indexOf(key); - }, - - - item : function(key){ - var mk = this.map[key], - item = mk !== undefined ? mk : (typeof key == 'number') ? this.items[key] : undefined; - return typeof item != 'function' || this.allowFunctions ? item : null; - }, - - - itemAt : function(index){ - return this.items[index]; - }, - - - key : function(key){ - return this.map[key]; - }, - - - contains : function(o){ - return this.indexOf(o) != -1; - }, - - - containsKey : function(key){ - return typeof this.map[key] != 'undefined'; - }, - - - clear : function(){ - this.length = 0; - this.items = []; - this.keys = []; - this.map = {}; - this.fireEvent('clear'); - }, - - - first : function(){ - return this.items[0]; - }, - - - last : function(){ - return this.items[this.length-1]; - }, - - - _sort : function(property, dir, fn){ - var i, len, - dsc = String(dir).toUpperCase() == 'DESC' ? -1 : 1, - - - c = [], - keys = this.keys, - items = this.items; - - - fn = fn || function(a, b) { - return a - b; - }; - - - for(i = 0, len = items.length; i < len; i++){ - c[c.length] = { - key : keys[i], - value: items[i], - index: i - }; - } - - - c.sort(function(a, b){ - var v = fn(a[property], b[property]) * dsc; - if(v === 0){ - v = (a.index < b.index ? -1 : 1); - } - return v; - }); - - - for(i = 0, len = c.length; i < len; i++){ - items[i] = c[i].value; - keys[i] = c[i].key; - } - - this.fireEvent('sort', this); - }, - - - sort : function(dir, fn){ - this._sort('value', dir, fn); - }, - - - reorder: function(mapping) { - this.suspendEvents(); - - var items = this.items, - index = 0, - length = items.length, - order = [], - remaining = [], - oldIndex; - - - for (oldIndex in mapping) { - order[mapping[oldIndex]] = items[oldIndex]; - } - - for (index = 0; index < length; index++) { - if (mapping[index] == undefined) { - remaining.push(items[index]); - } - } - - for (index = 0; index < length; index++) { - if (order[index] == undefined) { - order[index] = remaining.shift(); - } - } - - this.clear(); - this.addAll(order); - - this.resumeEvents(); - this.fireEvent('sort', this); - }, - - - keySort : function(dir, fn){ - this._sort('key', dir, fn || function(a, b){ - var v1 = String(a).toUpperCase(), v2 = String(b).toUpperCase(); - return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0); - }); - }, - - - getRange : function(start, end){ - var items = this.items; - if(items.length < 1){ - return []; - } - start = start || 0; - end = Math.min(typeof end == 'undefined' ? this.length-1 : end, this.length-1); - var i, r = []; - if(start <= end){ - for(i = start; i <= end; i++) { - r[r.length] = items[i]; - } - }else{ - for(i = start; i >= end; i--) { - r[r.length] = items[i]; - } - } - return r; - }, - - - filter : function(property, value, anyMatch, caseSensitive){ - if(Ext.isEmpty(value, false)){ - return this.clone(); - } - value = this.createValueMatcher(value, anyMatch, caseSensitive); - return this.filterBy(function(o){ - return o && value.test(o[property]); - }); - }, - - - filterBy : function(fn, scope){ - var r = new Ext.util.MixedCollection(); - r.getKey = this.getKey; - var k = this.keys, it = this.items; - for(var i = 0, len = it.length; i < len; i++){ - if(fn.call(scope||this, it[i], k[i])){ - r.add(k[i], it[i]); - } - } - return r; - }, - - - findIndex : function(property, value, start, anyMatch, caseSensitive){ - if(Ext.isEmpty(value, false)){ - return -1; - } - value = this.createValueMatcher(value, anyMatch, caseSensitive); - return this.findIndexBy(function(o){ - return o && value.test(o[property]); - }, null, start); - }, - - - findIndexBy : function(fn, scope, start){ - var k = this.keys, it = this.items; - for(var i = (start||0), len = it.length; i < len; i++){ - if(fn.call(scope||this, it[i], k[i])){ - return i; - } - } - return -1; - }, - - - createValueMatcher : function(value, anyMatch, caseSensitive, exactMatch) { - if (!value.exec) { - var er = Ext.escapeRe; - value = String(value); - - if (anyMatch === true) { - value = er(value); - } else { - value = '^' + er(value); - if (exactMatch === true) { - value += '$'; - } - } - value = new RegExp(value, caseSensitive ? '' : 'i'); - } - return value; - }, - - - clone : function(){ - var r = new Ext.util.MixedCollection(); - var k = this.keys, it = this.items; - for(var i = 0, len = it.length; i < len; i++){ - r.add(k[i], it[i]); - } - r.getKey = this.getKey; - return r; - } -}); - -Ext.util.MixedCollection.prototype.get = Ext.util.MixedCollection.prototype.item; - -Ext.AbstractManager = Ext.extend(Object, { - typeName: 'type', - - constructor: function(config) { - Ext.apply(this, config || {}); - - - this.all = new Ext.util.MixedCollection(); - - this.types = {}; - }, - - - get : function(id){ - return this.all.get(id); - }, - - - register: function(item) { - this.all.add(item); - }, - - - unregister: function(item) { - this.all.remove(item); - }, - - - registerType : function(type, cls){ - this.types[type] = cls; - cls[this.typeName] = type; - }, - - - isRegistered : function(type){ - return this.types[type] !== undefined; - }, - - - create: function(config, defaultType) { - var type = config[this.typeName] || config.type || defaultType, - Constructor = this.types[type]; - - if (Constructor == undefined) { - throw new Error(String.format("The '{0}' type has not been registered with this manager", type)); - } - - return new Constructor(config); - }, - - - onAvailable : function(id, fn, scope){ - var all = this.all; - - all.on("add", function(index, o){ - if (o.id == id) { - fn.call(scope || o, o); - all.un("add", fn, scope); - } - }); - } -}); -Ext.util.Format = function() { - var trimRe = /^\s+|\s+$/g, - stripTagsRE = /<\/?[^>]+>/gi, - stripScriptsRe = /(?:)((\n|\r|.)*?)(?:<\/script>)/ig, - nl2brRe = /\r?\n/g; - - return { - - ellipsis : function(value, len, word) { - if (value && value.length > len) { - if (word) { - var vs = value.substr(0, len - 2), - index = Math.max(vs.lastIndexOf(' '), vs.lastIndexOf('.'), vs.lastIndexOf('!'), vs.lastIndexOf('?')); - if (index == -1 || index < (len - 15)) { - return value.substr(0, len - 3) + "..."; - } else { - return vs.substr(0, index) + "..."; - } - } else { - return value.substr(0, len - 3) + "..."; - } - } - return value; - }, - - - undef : function(value) { - return value !== undefined ? value : ""; - }, - - - defaultValue : function(value, defaultValue) { - return value !== undefined && value !== '' ? value : defaultValue; - }, - - - htmlEncode : function(value) { - return !value ? value : String(value).replace(/&/g, "&").replace(/>/g, ">").replace(/").replace(/</g, "<").replace(/"/g, '"').replace(/&/g, "&"); - }, - - - trim : function(value) { - return String(value).replace(trimRe, ""); - }, - - - substr : function(value, start, length) { - return String(value).substr(start, length); - }, - - - lowercase : function(value) { - return String(value).toLowerCase(); - }, - - - uppercase : function(value) { - return String(value).toUpperCase(); - }, - - - capitalize : function(value) { - return !value ? value : value.charAt(0).toUpperCase() + value.substr(1).toLowerCase(); - }, - - - call : function(value, fn) { - if (arguments.length > 2) { - var args = Array.prototype.slice.call(arguments, 2); - args.unshift(value); - return eval(fn).apply(window, args); - } else { - return eval(fn).call(window, value); - } - }, - - - usMoney : function(v) { - v = (Math.round((v-0)*100))/100; - v = (v == Math.floor(v)) ? v + ".00" : ((v*10 == Math.floor(v*10)) ? v + "0" : v); - v = String(v); - var ps = v.split('.'), - whole = ps[0], - sub = ps[1] ? '.'+ ps[1] : '.00', - r = /(\d+)(\d{3})/; - while (r.test(whole)) { - whole = whole.replace(r, '$1' + ',' + '$2'); - } - v = whole + sub; - if (v.charAt(0) == '-') { - return '-$' + v.substr(1); - } - return "$" + v; - }, - - - date : function(v, format) { - if (!v) { - return ""; - } - if (!Ext.isDate(v)) { - v = new Date(Date.parse(v)); - } - return v.dateFormat(format || "m/d/Y"); - }, - - - dateRenderer : function(format) { - return function(v) { - return Ext.util.Format.date(v, format); - }; - }, - - - stripTags : function(v) { - return !v ? v : String(v).replace(stripTagsRE, ""); - }, - - - stripScripts : function(v) { - return !v ? v : String(v).replace(stripScriptsRe, ""); - }, - - - fileSize : function(size) { - if (size < 1024) { - return size + " bytes"; - } else if (size < 1048576) { - return (Math.round(((size*10) / 1024))/10) + " KB"; - } else { - return (Math.round(((size*10) / 1048576))/10) + " MB"; - } - }, - - - math : function(){ - var fns = {}; - - return function(v, a){ - if (!fns[a]) { - fns[a] = new Function('v', 'return v ' + a + ';'); - } - return fns[a](v); - }; - }(), - - - round : function(value, precision) { - var result = Number(value); - if (typeof precision == 'number') { - precision = Math.pow(10, precision); - result = Math.round(value * precision) / precision; - } - return result; - }, - - - number: function(v, format) { - if (!format) { - return v; - } - v = Ext.num(v, NaN); - if (isNaN(v)) { - return ''; - } - var comma = ',', - dec = '.', - i18n = false, - neg = v < 0; - - v = Math.abs(v); - if (format.substr(format.length - 2) == '/i') { - format = format.substr(0, format.length - 2); - i18n = true; - comma = '.'; - dec = ','; - } - - var hasComma = format.indexOf(comma) != -1, - psplit = (i18n ? format.replace(/[^\d\,]/g, '') : format.replace(/[^\d\.]/g, '')).split(dec); - - if (1 < psplit.length) { - v = v.toFixed(psplit[1].length); - } else if(2 < psplit.length) { - throw ('NumberFormatException: invalid format, formats should have no more than 1 period: ' + format); - } else { - v = v.toFixed(0); - } - - var fnum = v.toString(); - - psplit = fnum.split('.'); - - if (hasComma) { - var cnum = psplit[0], - parr = [], - j = cnum.length, - m = Math.floor(j / 3), - n = cnum.length % 3 || 3, - i; - - for (i = 0; i < j; i += n) { - if (i != 0) { - n = 3; - } - - parr[parr.length] = cnum.substr(i, n); - m -= 1; - } - fnum = parr.join(comma); - if (psplit[1]) { - fnum += dec + psplit[1]; - } - } else { - if (psplit[1]) { - fnum = psplit[0] + dec + psplit[1]; - } - } - - return (neg ? '-' : '') + format.replace(/[\d,?\.?]+/, fnum); - }, - - - numberRenderer : function(format) { - return function(v) { - return Ext.util.Format.number(v, format); - }; - }, - - - plural : function(v, s, p) { - return v +' ' + (v == 1 ? s : (p ? p : s+'s')); - }, - - - nl2br : function(v) { - return Ext.isEmpty(v) ? '' : v.replace(nl2brRe, '
        '); - } - }; -}(); - -Ext.XTemplate = function(){ - Ext.XTemplate.superclass.constructor.apply(this, arguments); - - var me = this, - s = me.html, - re = /]*>((?:(?=([^<]+))\2|<(?!tpl\b[^>]*>))*?)<\/tpl>/, - nameRe = /^]*?for="(.*?)"/, - ifRe = /^]*?if="(.*?)"/, - execRe = /^]*?exec="(.*?)"/, - m, - id = 0, - tpls = [], - VALUES = 'values', - PARENT = 'parent', - XINDEX = 'xindex', - XCOUNT = 'xcount', - RETURN = 'return ', - WITHVALUES = 'with(values){ '; - - s = ['', s, ''].join(''); - - while((m = s.match(re))){ - var m2 = m[0].match(nameRe), - m3 = m[0].match(ifRe), - m4 = m[0].match(execRe), - exp = null, - fn = null, - exec = null, - name = m2 && m2[1] ? m2[1] : ''; - - if (m3) { - exp = m3 && m3[1] ? m3[1] : null; - if(exp){ - fn = new Function(VALUES, PARENT, XINDEX, XCOUNT, WITHVALUES + RETURN +(Ext.util.Format.htmlDecode(exp))+'; }'); - } - } - if (m4) { - exp = m4 && m4[1] ? m4[1] : null; - if(exp){ - exec = new Function(VALUES, PARENT, XINDEX, XCOUNT, WITHVALUES +(Ext.util.Format.htmlDecode(exp))+'; }'); - } - } - if(name){ - switch(name){ - case '.': name = new Function(VALUES, PARENT, WITHVALUES + RETURN + VALUES + '; }'); break; - case '..': name = new Function(VALUES, PARENT, WITHVALUES + RETURN + PARENT + '; }'); break; - default: name = new Function(VALUES, PARENT, WITHVALUES + RETURN + name + '; }'); - } - } - tpls.push({ - id: id, - target: name, - exec: exec, - test: fn, - body: m[1]||'' - }); - s = s.replace(m[0], '{xtpl'+ id + '}'); - ++id; - } - for(var i = tpls.length-1; i >= 0; --i){ - me.compileTpl(tpls[i]); - } - me.master = tpls[tpls.length-1]; - me.tpls = tpls; -}; -Ext.extend(Ext.XTemplate, Ext.Template, { - - re : /\{([\w\-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\\]\s?[\d\.\+\-\*\\\(\)]+)?\}/g, - - codeRe : /\{\[((?:\\\]|.|\n)*?)\]\}/g, - - - applySubTemplate : function(id, values, parent, xindex, xcount){ - var me = this, - len, - t = me.tpls[id], - vs, - buf = []; - if ((t.test && !t.test.call(me, values, parent, xindex, xcount)) || - (t.exec && t.exec.call(me, values, parent, xindex, xcount))) { - return ''; - } - vs = t.target ? t.target.call(me, values, parent) : values; - len = vs.length; - parent = t.target ? values : parent; - if(t.target && Ext.isArray(vs)){ - for(var i = 0, len = vs.length; i < len; i++){ - buf[buf.length] = t.compiled.call(me, vs[i], parent, i+1, len); - } - return buf.join(''); - } - return t.compiled.call(me, vs, parent, xindex, xcount); - }, - - - compileTpl : function(tpl){ - var fm = Ext.util.Format, - useF = this.disableFormats !== true, - sep = Ext.isGecko ? "+" : ",", - body; - - function fn(m, name, format, args, math){ - if(name.substr(0, 4) == 'xtpl'){ - return "'"+ sep +'this.applySubTemplate('+name.substr(4)+', values, parent, xindex, xcount)'+sep+"'"; - } - var v; - if(name === '.'){ - v = 'values'; - }else if(name === '#'){ - v = 'xindex'; - }else if(name.indexOf('.') != -1){ - v = name; - }else{ - v = "values['" + name + "']"; - } - if(math){ - v = '(' + v + math + ')'; - } - if (format && useF) { - args = args ? ',' + args : ""; - if(format.substr(0, 5) != "this."){ - format = "fm." + format + '('; - }else{ - format = 'this.call("'+ format.substr(5) + '", '; - args = ", values"; - } - } else { - args= ''; format = "("+v+" === undefined ? '' : "; - } - return "'"+ sep + format + v + args + ")"+sep+"'"; - } - - function codeFn(m, code){ - - return "'" + sep + '(' + code.replace(/\\'/g, "'") + ')' + sep + "'"; - } - - - if(Ext.isGecko){ - body = "tpl.compiled = function(values, parent, xindex, xcount){ return '" + - tpl.body.replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.re, fn).replace(this.codeRe, codeFn) + - "';};"; - }else{ - body = ["tpl.compiled = function(values, parent, xindex, xcount){ return ['"]; - body.push(tpl.body.replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.re, fn).replace(this.codeRe, codeFn)); - body.push("'].join('');};"); - body = body.join(''); - } - eval(body); - return this; - }, - - - applyTemplate : function(values){ - return this.master.compiled.call(this, values, {}, 1, 1); - }, - - - compile : function(){return this;} - - - - - -}); - -Ext.XTemplate.prototype.apply = Ext.XTemplate.prototype.applyTemplate; - - -Ext.XTemplate.from = function(el){ - el = Ext.getDom(el); - return new Ext.XTemplate(el.value || el.innerHTML); -}; - -Ext.util.CSS = function(){ - var rules = null; - var doc = document; - - var camelRe = /(-[a-z])/gi; - var camelFn = function(m, a){ return a.charAt(1).toUpperCase(); }; - - return { - - createStyleSheet : function(cssText, id){ - var ss; - var head = doc.getElementsByTagName("head")[0]; - var rules = doc.createElement("style"); - rules.setAttribute("type", "text/css"); - if(id){ - rules.setAttribute("id", id); - } - if(Ext.isIE){ - head.appendChild(rules); - ss = rules.styleSheet; - ss.cssText = cssText; - }else{ - try{ - rules.appendChild(doc.createTextNode(cssText)); - }catch(e){ - rules.cssText = cssText; - } - head.appendChild(rules); - ss = rules.styleSheet ? rules.styleSheet : (rules.sheet || doc.styleSheets[doc.styleSheets.length-1]); - } - this.cacheStyleSheet(ss); - return ss; - }, - - - removeStyleSheet : function(id){ - var existing = doc.getElementById(id); - if(existing){ - existing.parentNode.removeChild(existing); - } - }, - - - swapStyleSheet : function(id, url){ - this.removeStyleSheet(id); - var ss = doc.createElement("link"); - ss.setAttribute("rel", "stylesheet"); - ss.setAttribute("type", "text/css"); - ss.setAttribute("id", id); - ss.setAttribute("href", url); - doc.getElementsByTagName("head")[0].appendChild(ss); - }, - - - refreshCache : function(){ - return this.getRules(true); - }, - - - cacheStyleSheet : function(ss){ - if(!rules){ - rules = {}; - } - try{ - var ssRules = ss.cssRules || ss.rules; - for(var j = ssRules.length-1; j >= 0; --j){ - rules[ssRules[j].selectorText.toLowerCase()] = ssRules[j]; - } - }catch(e){} - }, - - - getRules : function(refreshCache){ - if(rules === null || refreshCache){ - rules = {}; - var ds = doc.styleSheets; - for(var i =0, len = ds.length; i < len; i++){ - try{ - this.cacheStyleSheet(ds[i]); - }catch(e){} - } - } - return rules; - }, - - - getRule : function(selector, refreshCache){ - var rs = this.getRules(refreshCache); - if(!Ext.isArray(selector)){ - return rs[selector.toLowerCase()]; - } - for(var i = 0; i < selector.length; i++){ - if(rs[selector[i]]){ - return rs[selector[i].toLowerCase()]; - } - } - return null; - }, - - - - updateRule : function(selector, property, value){ - if(!Ext.isArray(selector)){ - var rule = this.getRule(selector); - if(rule){ - rule.style[property.replace(camelRe, camelFn)] = value; - return true; - } - }else{ - for(var i = 0; i < selector.length; i++){ - if(this.updateRule(selector[i], property, value)){ - return true; - } - } - } - return false; - } - }; -}(); -Ext.util.ClickRepeater = Ext.extend(Ext.util.Observable, { - - constructor : function(el, config){ - this.el = Ext.get(el); - this.el.unselectable(); - - Ext.apply(this, config); - - this.addEvents( - - "mousedown", - - "click", - - "mouseup" - ); - - if(!this.disabled){ - this.disabled = true; - this.enable(); - } - - - if(this.handler){ - this.on("click", this.handler, this.scope || this); - } - - Ext.util.ClickRepeater.superclass.constructor.call(this); - }, - - interval : 20, - delay: 250, - preventDefault : true, - stopDefault : false, - timer : 0, - - - enable: function(){ - if(this.disabled){ - this.el.on('mousedown', this.handleMouseDown, this); - if (Ext.isIE){ - this.el.on('dblclick', this.handleDblClick, this); - } - if(this.preventDefault || this.stopDefault){ - this.el.on('click', this.eventOptions, this); - } - } - this.disabled = false; - }, - - - disable: function( force){ - if(force || !this.disabled){ - clearTimeout(this.timer); - if(this.pressClass){ - this.el.removeClass(this.pressClass); - } - Ext.getDoc().un('mouseup', this.handleMouseUp, this); - this.el.removeAllListeners(); - } - this.disabled = true; - }, - - - setDisabled: function(disabled){ - this[disabled ? 'disable' : 'enable'](); - }, - - eventOptions: function(e){ - if(this.preventDefault){ - e.preventDefault(); - } - if(this.stopDefault){ - e.stopEvent(); - } - }, - - - destroy : function() { - this.disable(true); - Ext.destroy(this.el); - this.purgeListeners(); - }, - - handleDblClick : function(e){ - clearTimeout(this.timer); - this.el.blur(); - - this.fireEvent("mousedown", this, e); - this.fireEvent("click", this, e); - }, - - - handleMouseDown : function(e){ - clearTimeout(this.timer); - this.el.blur(); - if(this.pressClass){ - this.el.addClass(this.pressClass); - } - this.mousedownTime = new Date(); - - Ext.getDoc().on("mouseup", this.handleMouseUp, this); - this.el.on("mouseout", this.handleMouseOut, this); - - this.fireEvent("mousedown", this, e); - this.fireEvent("click", this, e); - - - if (this.accelerate) { - this.delay = 400; - } - this.timer = this.click.defer(this.delay || this.interval, this, [e]); - }, - - - click : function(e){ - this.fireEvent("click", this, e); - this.timer = this.click.defer(this.accelerate ? - this.easeOutExpo(this.mousedownTime.getElapsed(), - 400, - -390, - 12000) : - this.interval, this, [e]); - }, - - easeOutExpo : function (t, b, c, d) { - return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; - }, - - - handleMouseOut : function(){ - clearTimeout(this.timer); - if(this.pressClass){ - this.el.removeClass(this.pressClass); - } - this.el.on("mouseover", this.handleMouseReturn, this); - }, - - - handleMouseReturn : function(){ - this.el.un("mouseover", this.handleMouseReturn, this); - if(this.pressClass){ - this.el.addClass(this.pressClass); - } - this.click(); - }, - - - handleMouseUp : function(e){ - clearTimeout(this.timer); - this.el.un("mouseover", this.handleMouseReturn, this); - this.el.un("mouseout", this.handleMouseOut, this); - Ext.getDoc().un("mouseup", this.handleMouseUp, this); - this.el.removeClass(this.pressClass); - this.fireEvent("mouseup", this, e); - } -}); -Ext.KeyNav = function(el, config){ - this.el = Ext.get(el); - Ext.apply(this, config); - if(!this.disabled){ - this.disabled = true; - this.enable(); - } -}; - -Ext.KeyNav.prototype = { - - disabled : false, - - defaultEventAction: "stopEvent", - - forceKeyDown : false, - - - relay : function(e){ - var k = e.getKey(), - h = this.keyToHandler[k]; - if(h && this[h]){ - if(this.doRelay(e, this[h], h) !== true){ - e[this.defaultEventAction](); - } - } - }, - - - doRelay : function(e, h, hname){ - return h.call(this.scope || this, e, hname); - }, - - - enter : false, - left : false, - right : false, - up : false, - down : false, - tab : false, - esc : false, - pageUp : false, - pageDown : false, - del : false, - home : false, - end : false, - space : false, - - - keyToHandler : { - 37 : "left", - 39 : "right", - 38 : "up", - 40 : "down", - 33 : "pageUp", - 34 : "pageDown", - 46 : "del", - 36 : "home", - 35 : "end", - 13 : "enter", - 27 : "esc", - 9 : "tab", - 32 : "space" - }, - - stopKeyUp: function(e) { - var k = e.getKey(); - - if (k >= 37 && k <= 40) { - - - e.stopEvent(); - } - }, - - - destroy: function(){ - this.disable(); - }, - - - enable: function() { - if (this.disabled) { - if (Ext.isSafari2) { - - this.el.on('keyup', this.stopKeyUp, this); - } - - this.el.on(this.isKeydown()? 'keydown' : 'keypress', this.relay, this); - this.disabled = false; - } - }, - - - disable: function() { - if (!this.disabled) { - if (Ext.isSafari2) { - - this.el.un('keyup', this.stopKeyUp, this); - } - - this.el.un(this.isKeydown()? 'keydown' : 'keypress', this.relay, this); - this.disabled = true; - } - }, - - - setDisabled : function(disabled){ - this[disabled ? "disable" : "enable"](); - }, - - - isKeydown: function(){ - return this.forceKeyDown || Ext.EventManager.useKeydown; - } -}; - -Ext.KeyMap = function(el, config, eventName){ - this.el = Ext.get(el); - this.eventName = eventName || "keydown"; - this.bindings = []; - if(config){ - this.addBinding(config); - } - this.enable(); -}; - -Ext.KeyMap.prototype = { - - stopEvent : false, - - - addBinding : function(config){ - if(Ext.isArray(config)){ - Ext.each(config, function(c){ - this.addBinding(c); - }, this); - return; - } - var keyCode = config.key, - fn = config.fn || config.handler, - scope = config.scope; - - if (config.stopEvent) { - this.stopEvent = config.stopEvent; - } - - if(typeof keyCode == "string"){ - var ks = []; - var keyString = keyCode.toUpperCase(); - for(var j = 0, len = keyString.length; j < len; j++){ - ks.push(keyString.charCodeAt(j)); - } - keyCode = ks; - } - var keyArray = Ext.isArray(keyCode); - - var handler = function(e){ - if(this.checkModifiers(config, e)){ - var k = e.getKey(); - if(keyArray){ - for(var i = 0, len = keyCode.length; i < len; i++){ - if(keyCode[i] == k){ - if(this.stopEvent){ - e.stopEvent(); - } - fn.call(scope || window, k, e); - return; - } - } - }else{ - if(k == keyCode){ - if(this.stopEvent){ - e.stopEvent(); - } - fn.call(scope || window, k, e); - } - } - } - }; - this.bindings.push(handler); - }, - - - checkModifiers: function(config, e){ - var val, key, keys = ['shift', 'ctrl', 'alt']; - for (var i = 0, len = keys.length; i < len; ++i){ - key = keys[i]; - val = config[key]; - if(!(val === undefined || (val === e[key + 'Key']))){ - return false; - } - } - return true; - }, - - - on : function(key, fn, scope){ - var keyCode, shift, ctrl, alt; - if(typeof key == "object" && !Ext.isArray(key)){ - keyCode = key.key; - shift = key.shift; - ctrl = key.ctrl; - alt = key.alt; - }else{ - keyCode = key; - } - this.addBinding({ - key: keyCode, - shift: shift, - ctrl: ctrl, - alt: alt, - fn: fn, - scope: scope - }); - }, - - - handleKeyDown : function(e){ - if(this.enabled){ - var b = this.bindings; - for(var i = 0, len = b.length; i < len; i++){ - b[i].call(this, e); - } - } - }, - - - isEnabled : function(){ - return this.enabled; - }, - - - enable: function(){ - if(!this.enabled){ - this.el.on(this.eventName, this.handleKeyDown, this); - this.enabled = true; - } - }, - - - disable: function(){ - if(this.enabled){ - this.el.removeListener(this.eventName, this.handleKeyDown, this); - this.enabled = false; - } - }, - - - setDisabled : function(disabled){ - this[disabled ? "disable" : "enable"](); - } -}; -Ext.util.TextMetrics = function(){ - var shared; - return { - - measure : function(el, text, fixedWidth){ - if(!shared){ - shared = Ext.util.TextMetrics.Instance(el, fixedWidth); - } - shared.bind(el); - shared.setFixedWidth(fixedWidth || 'auto'); - return shared.getSize(text); - }, - - - createInstance : function(el, fixedWidth){ - return Ext.util.TextMetrics.Instance(el, fixedWidth); - } - }; -}(); - -Ext.util.TextMetrics.Instance = function(bindTo, fixedWidth){ - var ml = new Ext.Element(document.createElement('div')); - document.body.appendChild(ml.dom); - ml.position('absolute'); - ml.setLeftTop(-1000, -1000); - ml.hide(); - - if(fixedWidth){ - ml.setWidth(fixedWidth); - } - - var instance = { - - getSize : function(text){ - ml.update(text); - var s = ml.getSize(); - ml.update(''); - return s; - }, - - - bind : function(el){ - ml.setStyle( - Ext.fly(el).getStyles('font-size','font-style', 'font-weight', 'font-family','line-height', 'text-transform', 'letter-spacing') - ); - }, - - - setFixedWidth : function(width){ - ml.setWidth(width); - }, - - - getWidth : function(text){ - ml.dom.style.width = 'auto'; - return this.getSize(text).width; - }, - - - getHeight : function(text){ - return this.getSize(text).height; - } - }; - - instance.bind(bindTo); - - return instance; -}; - -Ext.Element.addMethods({ - - getTextWidth : function(text, min, max){ - return (Ext.util.TextMetrics.measure(this.dom, Ext.value(text, this.dom.innerHTML, true)).width).constrain(min || 0, max || 1000000); - } -}); - -Ext.util.Cookies = { - - set : function(name, value){ - var argv = arguments; - var argc = arguments.length; - var expires = (argc > 2) ? argv[2] : null; - var path = (argc > 3) ? argv[3] : '/'; - var domain = (argc > 4) ? argv[4] : null; - var secure = (argc > 5) ? argv[5] : false; - document.cookie = name + "=" + escape(value) + ((expires === null) ? "" : ("; expires=" + expires.toGMTString())) + ((path === null) ? "" : ("; path=" + path)) + ((domain === null) ? "" : ("; domain=" + domain)) + ((secure === true) ? "; secure" : ""); - }, - - - get : function(name){ - var arg = name + "="; - var alen = arg.length; - var clen = document.cookie.length; - var i = 0; - var j = 0; - while(i < clen){ - j = i + alen; - if(document.cookie.substring(i, j) == arg){ - return Ext.util.Cookies.getCookieVal(j); - } - i = document.cookie.indexOf(" ", i) + 1; - if(i === 0){ - break; - } - } - return null; - }, - - - clear : function(name){ - if(Ext.util.Cookies.get(name)){ - document.cookie = name + "=" + "; expires=Thu, 01-Jan-70 00:00:01 GMT"; - } - }, - - getCookieVal : function(offset){ - var endstr = document.cookie.indexOf(";", offset); - if(endstr == -1){ - endstr = document.cookie.length; - } - return unescape(document.cookie.substring(offset, endstr)); - } -}; -Ext.handleError = function(e) { - throw e; -}; - - -Ext.Error = function(message) { - - this.message = (this.lang[message]) ? this.lang[message] : message; -}; - -Ext.Error.prototype = new Error(); -Ext.apply(Ext.Error.prototype, { - - lang: {}, - - name: 'Ext.Error', - - getName : function() { - return this.name; - }, - - getMessage : function() { - return this.message; - }, - - toJson : function() { - return Ext.encode(this); - } -}); - -Ext.ComponentMgr = function(){ - var all = new Ext.util.MixedCollection(); - var types = {}; - var ptypes = {}; - - return { - - register : function(c){ - all.add(c); - }, - - - unregister : function(c){ - all.remove(c); - }, - - - get : function(id){ - return all.get(id); - }, - - - onAvailable : function(id, fn, scope){ - all.on("add", function(index, o){ - if(o.id == id){ - fn.call(scope || o, o); - all.un("add", fn, scope); - } - }); - }, - - - all : all, - - - types : types, - - - ptypes: ptypes, - - - isRegistered : function(xtype){ - return types[xtype] !== undefined; - }, - - - isPluginRegistered : function(ptype){ - return ptypes[ptype] !== undefined; - }, - - - registerType : function(xtype, cls){ - types[xtype] = cls; - cls.xtype = xtype; - }, - - - create : function(config, defaultType){ - return config.render ? config : new types[config.xtype || defaultType](config); - }, - - - registerPlugin : function(ptype, cls){ - ptypes[ptype] = cls; - cls.ptype = ptype; - }, - - - createPlugin : function(config, defaultType){ - var PluginCls = ptypes[config.ptype || defaultType]; - if (PluginCls.init) { - return PluginCls; - } else { - return new PluginCls(config); - } - } - }; -}(); - - -Ext.reg = Ext.ComponentMgr.registerType; - -Ext.preg = Ext.ComponentMgr.registerPlugin; - -Ext.create = Ext.ComponentMgr.create; -Ext.Component = function(config){ - config = config || {}; - if(config.initialConfig){ - if(config.isAction){ - this.baseAction = config; - } - config = config.initialConfig; - }else if(config.tagName || config.dom || Ext.isString(config)){ - config = {applyTo: config, id: config.id || config}; - } - - - this.initialConfig = config; - - Ext.apply(this, config); - this.addEvents( - - 'added', - - 'disable', - - 'enable', - - 'beforeshow', - - 'show', - - 'beforehide', - - 'hide', - - 'removed', - - 'beforerender', - - 'render', - - 'afterrender', - - 'beforedestroy', - - 'destroy', - - 'beforestaterestore', - - 'staterestore', - - 'beforestatesave', - - 'statesave' - ); - this.getId(); - Ext.ComponentMgr.register(this); - Ext.Component.superclass.constructor.call(this); - - if(this.baseAction){ - this.baseAction.addComponent(this); - } - - this.initComponent(); - - if(this.plugins){ - if(Ext.isArray(this.plugins)){ - for(var i = 0, len = this.plugins.length; i < len; i++){ - this.plugins[i] = this.initPlugin(this.plugins[i]); - } - }else{ - this.plugins = this.initPlugin(this.plugins); - } - } - - if(this.stateful !== false){ - this.initState(); - } - - if(this.applyTo){ - this.applyToMarkup(this.applyTo); - delete this.applyTo; - }else if(this.renderTo){ - this.render(this.renderTo); - delete this.renderTo; - } -}; - - -Ext.Component.AUTO_ID = 1000; - -Ext.extend(Ext.Component, Ext.util.Observable, { - - - - - - - - - - - - - - - - - - disabled : false, - - hidden : false, - - - - - - - - autoEl : 'div', - - - disabledClass : 'x-item-disabled', - - allowDomMove : true, - - autoShow : false, - - hideMode : 'display', - - hideParent : false, - - - - - - rendered : false, - - - - - - - - tplWriteMode : 'overwrite', - - - - - bubbleEvents: [], - - - - ctype : 'Ext.Component', - - - actionMode : 'el', - - - getActionEl : function(){ - return this[this.actionMode]; - }, - - initPlugin : function(p){ - if(p.ptype && !Ext.isFunction(p.init)){ - p = Ext.ComponentMgr.createPlugin(p); - }else if(Ext.isString(p)){ - p = Ext.ComponentMgr.createPlugin({ - ptype: p - }); - } - p.init(this); - return p; - }, - - - initComponent : function(){ - - if(this.listeners){ - this.on(this.listeners); - delete this.listeners; - } - this.enableBubble(this.bubbleEvents); - }, - - - render : function(container, position){ - if(!this.rendered && this.fireEvent('beforerender', this) !== false){ - if(!container && this.el){ - this.el = Ext.get(this.el); - container = this.el.dom.parentNode; - this.allowDomMove = false; - } - this.container = Ext.get(container); - if(this.ctCls){ - this.container.addClass(this.ctCls); - } - this.rendered = true; - if(position !== undefined){ - if(Ext.isNumber(position)){ - position = this.container.dom.childNodes[position]; - }else{ - position = Ext.getDom(position); - } - } - this.onRender(this.container, position || null); - if(this.autoShow){ - this.el.removeClass(['x-hidden','x-hide-' + this.hideMode]); - } - if(this.cls){ - this.el.addClass(this.cls); - delete this.cls; - } - if(this.style){ - this.el.applyStyles(this.style); - delete this.style; - } - if(this.overCls){ - this.el.addClassOnOver(this.overCls); - } - this.fireEvent('render', this); - - - - - var contentTarget = this.getContentTarget(); - if (this.html){ - contentTarget.update(Ext.DomHelper.markup(this.html)); - delete this.html; - } - if (this.contentEl){ - var ce = Ext.getDom(this.contentEl); - Ext.fly(ce).removeClass(['x-hidden', 'x-hide-display']); - contentTarget.appendChild(ce); - } - if (this.tpl) { - if (!this.tpl.compile) { - this.tpl = new Ext.XTemplate(this.tpl); - } - if (this.data) { - this.tpl[this.tplWriteMode](contentTarget, this.data); - delete this.data; - } - } - this.afterRender(this.container); - - - if(this.hidden){ - - this.doHide(); - } - if(this.disabled){ - - this.disable(true); - } - - if(this.stateful !== false){ - this.initStateEvents(); - } - this.fireEvent('afterrender', this); - } - return this; - }, - - - - update: function(htmlOrData, loadScripts, cb) { - var contentTarget = this.getContentTarget(); - if (this.tpl && typeof htmlOrData !== "string") { - this.tpl[this.tplWriteMode](contentTarget, htmlOrData || {}); - } else { - var html = Ext.isObject(htmlOrData) ? Ext.DomHelper.markup(htmlOrData) : htmlOrData; - contentTarget.update(html, loadScripts, cb); - } - }, - - - - onAdded : function(container, pos) { - this.ownerCt = container; - this.initRef(); - this.fireEvent('added', this, container, pos); - }, - - - onRemoved : function() { - this.removeRef(); - this.fireEvent('removed', this, this.ownerCt); - delete this.ownerCt; - }, - - - initRef : function() { - - if(this.ref && !this.refOwner){ - var levels = this.ref.split('/'), - last = levels.length, - i = 0, - t = this; - - while(t && i < last){ - t = t.ownerCt; - ++i; - } - if(t){ - t[this.refName = levels[--i]] = this; - - this.refOwner = t; - } - } - }, - - removeRef : function() { - if (this.refOwner && this.refName) { - delete this.refOwner[this.refName]; - delete this.refOwner; - } - }, - - - initState : function(){ - if(Ext.state.Manager){ - var id = this.getStateId(); - if(id){ - var state = Ext.state.Manager.get(id); - if(state){ - if(this.fireEvent('beforestaterestore', this, state) !== false){ - this.applyState(Ext.apply({}, state)); - this.fireEvent('staterestore', this, state); - } - } - } - } - }, - - - getStateId : function(){ - return this.stateId || ((/^(ext-comp-|ext-gen)/).test(String(this.id)) ? null : this.id); - }, - - - initStateEvents : function(){ - if(this.stateEvents){ - for(var i = 0, e; e = this.stateEvents[i]; i++){ - this.on(e, this.saveState, this, {delay:100}); - } - } - }, - - - applyState : function(state){ - if(state){ - Ext.apply(this, state); - } - }, - - - getState : function(){ - return null; - }, - - - saveState : function(){ - if(Ext.state.Manager && this.stateful !== false){ - var id = this.getStateId(); - if(id){ - var state = this.getState(); - if(this.fireEvent('beforestatesave', this, state) !== false){ - Ext.state.Manager.set(id, state); - this.fireEvent('statesave', this, state); - } - } - } - }, - - - applyToMarkup : function(el){ - this.allowDomMove = false; - this.el = Ext.get(el); - this.render(this.el.dom.parentNode); - }, - - - addClass : function(cls){ - if(this.el){ - this.el.addClass(cls); - }else{ - this.cls = this.cls ? this.cls + ' ' + cls : cls; - } - return this; - }, - - - removeClass : function(cls){ - if(this.el){ - this.el.removeClass(cls); - }else if(this.cls){ - this.cls = this.cls.split(' ').remove(cls).join(' '); - } - return this; - }, - - - - onRender : function(ct, position){ - if(!this.el && this.autoEl){ - if(Ext.isString(this.autoEl)){ - this.el = document.createElement(this.autoEl); - }else{ - var div = document.createElement('div'); - Ext.DomHelper.overwrite(div, this.autoEl); - this.el = div.firstChild; - } - if (!this.el.id) { - this.el.id = this.getId(); - } - } - if(this.el){ - this.el = Ext.get(this.el); - if(this.allowDomMove !== false){ - ct.dom.insertBefore(this.el.dom, position); - if (div) { - Ext.removeNode(div); - div = null; - } - } - } - }, - - - getAutoCreate : function(){ - var cfg = Ext.isObject(this.autoCreate) ? - this.autoCreate : Ext.apply({}, this.defaultAutoCreate); - if(this.id && !cfg.id){ - cfg.id = this.id; - } - return cfg; - }, - - - afterRender : Ext.emptyFn, - - - destroy : function(){ - if(!this.isDestroyed){ - if(this.fireEvent('beforedestroy', this) !== false){ - this.destroying = true; - this.beforeDestroy(); - if(this.ownerCt && this.ownerCt.remove){ - this.ownerCt.remove(this, false); - } - if(this.rendered){ - this.el.remove(); - if(this.actionMode == 'container' || this.removeMode == 'container'){ - this.container.remove(); - } - } - - if(this.focusTask && this.focusTask.cancel){ - this.focusTask.cancel(); - } - this.onDestroy(); - Ext.ComponentMgr.unregister(this); - this.fireEvent('destroy', this); - this.purgeListeners(); - this.destroying = false; - this.isDestroyed = true; - } - } - }, - - deleteMembers : function(){ - var args = arguments; - for(var i = 0, len = args.length; i < len; ++i){ - delete this[args[i]]; - } - }, - - - beforeDestroy : Ext.emptyFn, - - - onDestroy : Ext.emptyFn, - - - getEl : function(){ - return this.el; - }, - - - getContentTarget : function(){ - return this.el; - }, - - - getId : function(){ - return this.id || (this.id = 'ext-comp-' + (++Ext.Component.AUTO_ID)); - }, - - - getItemId : function(){ - return this.itemId || this.getId(); - }, - - - focus : function(selectText, delay){ - if(delay){ - this.focusTask = new Ext.util.DelayedTask(this.focus, this, [selectText, false]); - this.focusTask.delay(Ext.isNumber(delay) ? delay : 10); - return this; - } - if(this.rendered && !this.isDestroyed){ - this.el.focus(); - if(selectText === true){ - this.el.dom.select(); - } - } - return this; - }, - - - blur : function(){ - if(this.rendered){ - this.el.blur(); - } - return this; - }, - - - disable : function( silent){ - if(this.rendered){ - this.onDisable(); - } - this.disabled = true; - if(silent !== true){ - this.fireEvent('disable', this); - } - return this; - }, - - - onDisable : function(){ - this.getActionEl().addClass(this.disabledClass); - this.el.dom.disabled = true; - }, - - - enable : function(){ - if(this.rendered){ - this.onEnable(); - } - this.disabled = false; - this.fireEvent('enable', this); - return this; - }, - - - onEnable : function(){ - this.getActionEl().removeClass(this.disabledClass); - this.el.dom.disabled = false; - }, - - - setDisabled : function(disabled){ - return this[disabled ? 'disable' : 'enable'](); - }, - - - show : function(){ - if(this.fireEvent('beforeshow', this) !== false){ - this.hidden = false; - if(this.autoRender){ - this.render(Ext.isBoolean(this.autoRender) ? Ext.getBody() : this.autoRender); - } - if(this.rendered){ - this.onShow(); - } - this.fireEvent('show', this); - } - return this; - }, - - - onShow : function(){ - this.getVisibilityEl().removeClass('x-hide-' + this.hideMode); - }, - - - hide : function(){ - if(this.fireEvent('beforehide', this) !== false){ - this.doHide(); - this.fireEvent('hide', this); - } - return this; - }, - - - doHide: function(){ - this.hidden = true; - if(this.rendered){ - this.onHide(); - } - }, - - - onHide : function(){ - this.getVisibilityEl().addClass('x-hide-' + this.hideMode); - }, - - - getVisibilityEl : function(){ - return this.hideParent ? this.container : this.getActionEl(); - }, - - - setVisible : function(visible){ - return this[visible ? 'show' : 'hide'](); - }, - - - isVisible : function(){ - return this.rendered && this.getVisibilityEl().isVisible(); - }, - - - cloneConfig : function(overrides){ - overrides = overrides || {}; - var id = overrides.id || Ext.id(); - var cfg = Ext.applyIf(overrides, this.initialConfig); - cfg.id = id; - return new this.constructor(cfg); - }, - - - getXType : function(){ - return this.constructor.xtype; - }, - - - isXType : function(xtype, shallow){ - - if (Ext.isFunction(xtype)){ - xtype = xtype.xtype; - }else if (Ext.isObject(xtype)){ - xtype = xtype.constructor.xtype; - } - - return !shallow ? ('/' + this.getXTypes() + '/').indexOf('/' + xtype + '/') != -1 : this.constructor.xtype == xtype; - }, - - - getXTypes : function(){ - var tc = this.constructor; - if(!tc.xtypes){ - var c = [], sc = this; - while(sc && sc.constructor.xtype){ - c.unshift(sc.constructor.xtype); - sc = sc.constructor.superclass; - } - tc.xtypeChain = c; - tc.xtypes = c.join('/'); - } - return tc.xtypes; - }, - - - findParentBy : function(fn) { - for (var p = this.ownerCt; (p != null) && !fn(p, this); p = p.ownerCt); - return p || null; - }, - - - findParentByType : function(xtype, shallow){ - return this.findParentBy(function(c){ - return c.isXType(xtype, shallow); - }); - }, - - - bubble : function(fn, scope, args){ - var p = this; - while(p){ - if(fn.apply(scope || p, args || [p]) === false){ - break; - } - p = p.ownerCt; - } - return this; - }, - - - getPositionEl : function(){ - return this.positionEl || this.el; - }, - - - purgeListeners : function(){ - Ext.Component.superclass.purgeListeners.call(this); - if(this.mons){ - this.on('beforedestroy', this.clearMons, this, {single: true}); - } - }, - - - clearMons : function(){ - Ext.each(this.mons, function(m){ - m.item.un(m.ename, m.fn, m.scope); - }, this); - this.mons = []; - }, - - - createMons: function(){ - if(!this.mons){ - this.mons = []; - this.on('beforedestroy', this.clearMons, this, {single: true}); - } - }, - - - mon : function(item, ename, fn, scope, opt){ - this.createMons(); - if(Ext.isObject(ename)){ - var propRe = /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate)$/; - - var o = ename; - for(var e in o){ - if(propRe.test(e)){ - continue; - } - if(Ext.isFunction(o[e])){ - - this.mons.push({ - item: item, ename: e, fn: o[e], scope: o.scope - }); - item.on(e, o[e], o.scope, o); - }else{ - - this.mons.push({ - item: item, ename: e, fn: o[e], scope: o.scope - }); - item.on(e, o[e]); - } - } - return; - } - - this.mons.push({ - item: item, ename: ename, fn: fn, scope: scope - }); - item.on(ename, fn, scope, opt); - }, - - - mun : function(item, ename, fn, scope){ - var found, mon; - this.createMons(); - for(var i = 0, len = this.mons.length; i < len; ++i){ - mon = this.mons[i]; - if(item === mon.item && ename == mon.ename && fn === mon.fn && scope === mon.scope){ - this.mons.splice(i, 1); - item.un(ename, fn, scope); - found = true; - break; - } - } - return found; - }, - - - nextSibling : function(){ - if(this.ownerCt){ - var index = this.ownerCt.items.indexOf(this); - if(index != -1 && index+1 < this.ownerCt.items.getCount()){ - return this.ownerCt.items.itemAt(index+1); - } - } - return null; - }, - - - previousSibling : function(){ - if(this.ownerCt){ - var index = this.ownerCt.items.indexOf(this); - if(index > 0){ - return this.ownerCt.items.itemAt(index-1); - } - } - return null; - }, - - - getBubbleTarget : function(){ - return this.ownerCt; - } -}); - -Ext.reg('component', Ext.Component); - -Ext.Action = Ext.extend(Object, { - - - - - - - - - constructor : function(config){ - this.initialConfig = config; - this.itemId = config.itemId = (config.itemId || config.id || Ext.id()); - this.items = []; - }, - - - isAction : true, - - - setText : function(text){ - this.initialConfig.text = text; - this.callEach('setText', [text]); - }, - - - getText : function(){ - return this.initialConfig.text; - }, - - - setIconClass : function(cls){ - this.initialConfig.iconCls = cls; - this.callEach('setIconClass', [cls]); - }, - - - getIconClass : function(){ - return this.initialConfig.iconCls; - }, - - - setDisabled : function(v){ - this.initialConfig.disabled = v; - this.callEach('setDisabled', [v]); - }, - - - enable : function(){ - this.setDisabled(false); - }, - - - disable : function(){ - this.setDisabled(true); - }, - - - isDisabled : function(){ - return this.initialConfig.disabled; - }, - - - setHidden : function(v){ - this.initialConfig.hidden = v; - this.callEach('setVisible', [!v]); - }, - - - show : function(){ - this.setHidden(false); - }, - - - hide : function(){ - this.setHidden(true); - }, - - - isHidden : function(){ - return this.initialConfig.hidden; - }, - - - setHandler : function(fn, scope){ - this.initialConfig.handler = fn; - this.initialConfig.scope = scope; - this.callEach('setHandler', [fn, scope]); - }, - - - each : function(fn, scope){ - Ext.each(this.items, fn, scope); - }, - - - callEach : function(fnName, args){ - var cs = this.items; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i][fnName].apply(cs[i], args); - } - }, - - - addComponent : function(comp){ - this.items.push(comp); - comp.on('destroy', this.removeComponent, this); - }, - - - removeComponent : function(comp){ - this.items.remove(comp); - }, - - - execute : function(){ - this.initialConfig.handler.apply(this.initialConfig.scope || window, arguments); - } -}); - -(function(){ -Ext.Layer = function(config, existingEl){ - config = config || {}; - var dh = Ext.DomHelper, - cp = config.parentEl, pel = cp ? Ext.getDom(cp) : document.body; - - if (existingEl) { - this.dom = Ext.getDom(existingEl); - } - if(!this.dom){ - var o = config.dh || {tag: 'div', cls: 'x-layer'}; - this.dom = dh.append(pel, o); - } - if(config.cls){ - this.addClass(config.cls); - } - this.constrain = config.constrain !== false; - this.setVisibilityMode(Ext.Element.VISIBILITY); - if(config.id){ - this.id = this.dom.id = config.id; - }else{ - this.id = Ext.id(this.dom); - } - this.zindex = config.zindex || this.getZIndex(); - this.position('absolute', this.zindex); - if(config.shadow){ - this.shadowOffset = config.shadowOffset || 4; - this.shadow = new Ext.Shadow({ - offset : this.shadowOffset, - mode : config.shadow - }); - }else{ - this.shadowOffset = 0; - } - this.useShim = config.shim !== false && Ext.useShims; - this.useDisplay = config.useDisplay; - this.hide(); -}; - -var supr = Ext.Element.prototype; - - -var shims = []; - -Ext.extend(Ext.Layer, Ext.Element, { - - getZIndex : function(){ - return this.zindex || parseInt((this.getShim() || this).getStyle('z-index'), 10) || 11000; - }, - - getShim : function(){ - if(!this.useShim){ - return null; - } - if(this.shim){ - return this.shim; - } - var shim = shims.shift(); - if(!shim){ - shim = this.createShim(); - shim.enableDisplayMode('block'); - shim.dom.style.display = 'none'; - shim.dom.style.visibility = 'visible'; - } - var pn = this.dom.parentNode; - if(shim.dom.parentNode != pn){ - pn.insertBefore(shim.dom, this.dom); - } - shim.setStyle('z-index', this.getZIndex()-2); - this.shim = shim; - return shim; - }, - - hideShim : function(){ - if(this.shim){ - this.shim.setDisplayed(false); - shims.push(this.shim); - delete this.shim; - } - }, - - disableShadow : function(){ - if(this.shadow){ - this.shadowDisabled = true; - this.shadow.hide(); - this.lastShadowOffset = this.shadowOffset; - this.shadowOffset = 0; - } - }, - - enableShadow : function(show){ - if(this.shadow){ - this.shadowDisabled = false; - if(Ext.isDefined(this.lastShadowOffset)) { - this.shadowOffset = this.lastShadowOffset; - delete this.lastShadowOffset; - } - if(show){ - this.sync(true); - } - } - }, - - - - - sync : function(doShow){ - var shadow = this.shadow; - if(!this.updating && this.isVisible() && (shadow || this.useShim)){ - var shim = this.getShim(), - w = this.getWidth(), - h = this.getHeight(), - l = this.getLeft(true), - t = this.getTop(true); - - if(shadow && !this.shadowDisabled){ - if(doShow && !shadow.isVisible()){ - shadow.show(this); - }else{ - shadow.realign(l, t, w, h); - } - if(shim){ - if(doShow){ - shim.show(); - } - - var shadowAdj = shadow.el.getXY(), shimStyle = shim.dom.style, - shadowSize = shadow.el.getSize(); - shimStyle.left = (shadowAdj[0])+'px'; - shimStyle.top = (shadowAdj[1])+'px'; - shimStyle.width = (shadowSize.width)+'px'; - shimStyle.height = (shadowSize.height)+'px'; - } - }else if(shim){ - if(doShow){ - shim.show(); - } - shim.setSize(w, h); - shim.setLeftTop(l, t); - } - } - }, - - - destroy : function(){ - this.hideShim(); - if(this.shadow){ - this.shadow.hide(); - } - this.removeAllListeners(); - Ext.removeNode(this.dom); - delete this.dom; - }, - - remove : function(){ - this.destroy(); - }, - - - beginUpdate : function(){ - this.updating = true; - }, - - - endUpdate : function(){ - this.updating = false; - this.sync(true); - }, - - - hideUnders : function(negOffset){ - if(this.shadow){ - this.shadow.hide(); - } - this.hideShim(); - }, - - - constrainXY : function(){ - if(this.constrain){ - var vw = Ext.lib.Dom.getViewWidth(), - vh = Ext.lib.Dom.getViewHeight(); - var s = Ext.getDoc().getScroll(); - - var xy = this.getXY(); - var x = xy[0], y = xy[1]; - var so = this.shadowOffset; - var w = this.dom.offsetWidth+so, h = this.dom.offsetHeight+so; - - var moved = false; - - if((x + w) > vw+s.left){ - x = vw - w - so; - moved = true; - } - if((y + h) > vh+s.top){ - y = vh - h - so; - moved = true; - } - - if(x < s.left){ - x = s.left; - moved = true; - } - if(y < s.top){ - y = s.top; - moved = true; - } - if(moved){ - if(this.avoidY){ - var ay = this.avoidY; - if(y <= ay && (y+h) >= ay){ - y = ay-h-5; - } - } - xy = [x, y]; - this.storeXY(xy); - supr.setXY.call(this, xy); - this.sync(); - } - } - return this; - }, - - getConstrainOffset : function(){ - return this.shadowOffset; - }, - - isVisible : function(){ - return this.visible; - }, - - - showAction : function(){ - this.visible = true; - if(this.useDisplay === true){ - this.setDisplayed(''); - }else if(this.lastXY){ - supr.setXY.call(this, this.lastXY); - }else if(this.lastLT){ - supr.setLeftTop.call(this, this.lastLT[0], this.lastLT[1]); - } - }, - - - hideAction : function(){ - this.visible = false; - if(this.useDisplay === true){ - this.setDisplayed(false); - }else{ - this.setLeftTop(-10000,-10000); - } - }, - - - setVisible : function(v, a, d, c, e){ - if(v){ - this.showAction(); - } - if(a && v){ - var cb = function(){ - this.sync(true); - if(c){ - c(); - } - }.createDelegate(this); - supr.setVisible.call(this, true, true, d, cb, e); - }else{ - if(!v){ - this.hideUnders(true); - } - var cb = c; - if(a){ - cb = function(){ - this.hideAction(); - if(c){ - c(); - } - }.createDelegate(this); - } - supr.setVisible.call(this, v, a, d, cb, e); - if(v){ - this.sync(true); - }else if(!a){ - this.hideAction(); - } - } - return this; - }, - - storeXY : function(xy){ - delete this.lastLT; - this.lastXY = xy; - }, - - storeLeftTop : function(left, top){ - delete this.lastXY; - this.lastLT = [left, top]; - }, - - - beforeFx : function(){ - this.beforeAction(); - return Ext.Layer.superclass.beforeFx.apply(this, arguments); - }, - - - afterFx : function(){ - Ext.Layer.superclass.afterFx.apply(this, arguments); - this.sync(this.isVisible()); - }, - - - beforeAction : function(){ - if(!this.updating && this.shadow){ - this.shadow.hide(); - } - }, - - - setLeft : function(left){ - this.storeLeftTop(left, this.getTop(true)); - supr.setLeft.apply(this, arguments); - this.sync(); - return this; - }, - - setTop : function(top){ - this.storeLeftTop(this.getLeft(true), top); - supr.setTop.apply(this, arguments); - this.sync(); - return this; - }, - - setLeftTop : function(left, top){ - this.storeLeftTop(left, top); - supr.setLeftTop.apply(this, arguments); - this.sync(); - return this; - }, - - setXY : function(xy, a, d, c, e){ - this.fixDisplay(); - this.beforeAction(); - this.storeXY(xy); - var cb = this.createCB(c); - supr.setXY.call(this, xy, a, d, cb, e); - if(!a){ - cb(); - } - return this; - }, - - - createCB : function(c){ - var el = this; - return function(){ - el.constrainXY(); - el.sync(true); - if(c){ - c(); - } - }; - }, - - - setX : function(x, a, d, c, e){ - this.setXY([x, this.getY()], a, d, c, e); - return this; - }, - - - setY : function(y, a, d, c, e){ - this.setXY([this.getX(), y], a, d, c, e); - return this; - }, - - - setSize : function(w, h, a, d, c, e){ - this.beforeAction(); - var cb = this.createCB(c); - supr.setSize.call(this, w, h, a, d, cb, e); - if(!a){ - cb(); - } - return this; - }, - - - setWidth : function(w, a, d, c, e){ - this.beforeAction(); - var cb = this.createCB(c); - supr.setWidth.call(this, w, a, d, cb, e); - if(!a){ - cb(); - } - return this; - }, - - - setHeight : function(h, a, d, c, e){ - this.beforeAction(); - var cb = this.createCB(c); - supr.setHeight.call(this, h, a, d, cb, e); - if(!a){ - cb(); - } - return this; - }, - - - setBounds : function(x, y, w, h, a, d, c, e){ - this.beforeAction(); - var cb = this.createCB(c); - if(!a){ - this.storeXY([x, y]); - supr.setXY.call(this, [x, y]); - supr.setSize.call(this, w, h, a, d, cb, e); - cb(); - }else{ - supr.setBounds.call(this, x, y, w, h, a, d, cb, e); - } - return this; - }, - - - setZIndex : function(zindex){ - this.zindex = zindex; - this.setStyle('z-index', zindex + 2); - if(this.shadow){ - this.shadow.setZIndex(zindex + 1); - } - if(this.shim){ - this.shim.setStyle('z-index', zindex); - } - return this; - } -}); -})(); - -Ext.Shadow = function(config) { - Ext.apply(this, config); - if (typeof this.mode != "string") { - this.mode = this.defaultMode; - } - var o = this.offset, - a = { - h: 0 - }, - rad = Math.floor(this.offset / 2); - switch (this.mode.toLowerCase()) { - - case "drop": - a.w = 0; - a.l = a.t = o; - a.t -= 1; - if (Ext.isIE) { - a.l -= this.offset + rad; - a.t -= this.offset + rad; - a.w -= rad; - a.h -= rad; - a.t += 1; - } - break; - case "sides": - a.w = (o * 2); - a.l = -o; - a.t = o - 1; - if (Ext.isIE) { - a.l -= (this.offset - rad); - a.t -= this.offset + rad; - a.l += 1; - a.w -= (this.offset - rad) * 2; - a.w -= rad + 1; - a.h -= 1; - } - break; - case "frame": - a.w = a.h = (o * 2); - a.l = a.t = -o; - a.t += 1; - a.h -= 2; - if (Ext.isIE) { - a.l -= (this.offset - rad); - a.t -= (this.offset - rad); - a.l += 1; - a.w -= (this.offset + rad + 1); - a.h -= (this.offset + rad); - a.h += 1; - } - break; - }; - - this.adjusts = a; -}; - -Ext.Shadow.prototype = { - - - offset: 4, - - - defaultMode: "drop", - - - show: function(target) { - target = Ext.get(target); - if (!this.el) { - this.el = Ext.Shadow.Pool.pull(); - if (this.el.dom.nextSibling != target.dom) { - this.el.insertBefore(target); - } - } - this.el.setStyle("z-index", this.zIndex || parseInt(target.getStyle("z-index"), 10) - 1); - if (Ext.isIE) { - this.el.dom.style.filter = "progid:DXImageTransform.Microsoft.alpha(opacity=50) progid:DXImageTransform.Microsoft.Blur(pixelradius=" + (this.offset) + ")"; - } - this.realign( - target.getLeft(true), - target.getTop(true), - target.getWidth(), - target.getHeight() - ); - this.el.dom.style.display = "block"; - }, - - - isVisible: function() { - return this.el ? true: false; - }, - - - realign: function(l, t, w, h) { - if (!this.el) { - return; - } - var a = this.adjusts, - d = this.el.dom, - s = d.style, - iea = 0, - sw = (w + a.w), - sh = (h + a.h), - sws = sw + "px", - shs = sh + "px", - cn, - sww; - s.left = (l + a.l) + "px"; - s.top = (t + a.t) + "px"; - if (s.width != sws || s.height != shs) { - s.width = sws; - s.height = shs; - if (!Ext.isIE) { - cn = d.childNodes; - sww = Math.max(0, (sw - 12)) + "px"; - cn[0].childNodes[1].style.width = sww; - cn[1].childNodes[1].style.width = sww; - cn[2].childNodes[1].style.width = sww; - cn[1].style.height = Math.max(0, (sh - 12)) + "px"; - } - } - }, - - - hide: function() { - if (this.el) { - this.el.dom.style.display = "none"; - Ext.Shadow.Pool.push(this.el); - delete this.el; - } - }, - - - setZIndex: function(z) { - this.zIndex = z; - if (this.el) { - this.el.setStyle("z-index", z); - } - } -}; - - -Ext.Shadow.Pool = function() { - var p = [], - markup = Ext.isIE ? - '
        ': - '
        '; - return { - pull: function() { - var sh = p.shift(); - if (!sh) { - sh = Ext.get(Ext.DomHelper.insertHtml("beforeBegin", document.body.firstChild, markup)); - sh.autoBoxAdjust = false; - } - return sh; - }, - - push: function(sh) { - p.push(sh); - } - }; -}(); -Ext.BoxComponent = Ext.extend(Ext.Component, { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - initComponent : function(){ - Ext.BoxComponent.superclass.initComponent.call(this); - this.addEvents( - - 'resize', - - 'move' - ); - }, - - - boxReady : false, - - deferHeight: false, - - - setSize : function(w, h){ - - - if(typeof w == 'object'){ - h = w.height; - w = w.width; - } - if (Ext.isDefined(w) && Ext.isDefined(this.boxMinWidth) && (w < this.boxMinWidth)) { - w = this.boxMinWidth; - } - if (Ext.isDefined(h) && Ext.isDefined(this.boxMinHeight) && (h < this.boxMinHeight)) { - h = this.boxMinHeight; - } - if (Ext.isDefined(w) && Ext.isDefined(this.boxMaxWidth) && (w > this.boxMaxWidth)) { - w = this.boxMaxWidth; - } - if (Ext.isDefined(h) && Ext.isDefined(this.boxMaxHeight) && (h > this.boxMaxHeight)) { - h = this.boxMaxHeight; - } - - if(!this.boxReady){ - this.width = w; - this.height = h; - return this; - } - - - if(this.cacheSizes !== false && this.lastSize && this.lastSize.width == w && this.lastSize.height == h){ - return this; - } - this.lastSize = {width: w, height: h}; - var adj = this.adjustSize(w, h), - aw = adj.width, - ah = adj.height, - rz; - if(aw !== undefined || ah !== undefined){ - rz = this.getResizeEl(); - if(!this.deferHeight && aw !== undefined && ah !== undefined){ - rz.setSize(aw, ah); - }else if(!this.deferHeight && ah !== undefined){ - rz.setHeight(ah); - }else if(aw !== undefined){ - rz.setWidth(aw); - } - this.onResize(aw, ah, w, h); - this.fireEvent('resize', this, aw, ah, w, h); - } - return this; - }, - - - setWidth : function(width){ - return this.setSize(width); - }, - - - setHeight : function(height){ - return this.setSize(undefined, height); - }, - - - getSize : function(){ - return this.getResizeEl().getSize(); - }, - - - getWidth : function(){ - return this.getResizeEl().getWidth(); - }, - - - getHeight : function(){ - return this.getResizeEl().getHeight(); - }, - - - getOuterSize : function(){ - var el = this.getResizeEl(); - return {width: el.getWidth() + el.getMargins('lr'), - height: el.getHeight() + el.getMargins('tb')}; - }, - - - getPosition : function(local){ - var el = this.getPositionEl(); - if(local === true){ - return [el.getLeft(true), el.getTop(true)]; - } - return this.xy || el.getXY(); - }, - - - getBox : function(local){ - var pos = this.getPosition(local); - var s = this.getSize(); - s.x = pos[0]; - s.y = pos[1]; - return s; - }, - - - updateBox : function(box){ - this.setSize(box.width, box.height); - this.setPagePosition(box.x, box.y); - return this; - }, - - - getResizeEl : function(){ - return this.resizeEl || this.el; - }, - - - setAutoScroll : function(scroll){ - if(this.rendered){ - this.getContentTarget().setOverflow(scroll ? 'auto' : ''); - } - this.autoScroll = scroll; - return this; - }, - - - setPosition : function(x, y){ - if(x && typeof x[1] == 'number'){ - y = x[1]; - x = x[0]; - } - this.x = x; - this.y = y; - if(!this.boxReady){ - return this; - } - var adj = this.adjustPosition(x, y); - var ax = adj.x, ay = adj.y; - - var el = this.getPositionEl(); - if(ax !== undefined || ay !== undefined){ - if(ax !== undefined && ay !== undefined){ - el.setLeftTop(ax, ay); - }else if(ax !== undefined){ - el.setLeft(ax); - }else if(ay !== undefined){ - el.setTop(ay); - } - this.onPosition(ax, ay); - this.fireEvent('move', this, ax, ay); - } - return this; - }, - - - setPagePosition : function(x, y){ - if(x && typeof x[1] == 'number'){ - y = x[1]; - x = x[0]; - } - this.pageX = x; - this.pageY = y; - if(!this.boxReady){ - return; - } - if(x === undefined || y === undefined){ - return; - } - var p = this.getPositionEl().translatePoints(x, y); - this.setPosition(p.left, p.top); - return this; - }, - - - afterRender : function(){ - Ext.BoxComponent.superclass.afterRender.call(this); - if(this.resizeEl){ - this.resizeEl = Ext.get(this.resizeEl); - } - if(this.positionEl){ - this.positionEl = Ext.get(this.positionEl); - } - this.boxReady = true; - Ext.isDefined(this.autoScroll) && this.setAutoScroll(this.autoScroll); - this.setSize(this.width, this.height); - if(this.x || this.y){ - this.setPosition(this.x, this.y); - }else if(this.pageX || this.pageY){ - this.setPagePosition(this.pageX, this.pageY); - } - }, - - - syncSize : function(){ - delete this.lastSize; - this.setSize(this.autoWidth ? undefined : this.getResizeEl().getWidth(), this.autoHeight ? undefined : this.getResizeEl().getHeight()); - return this; - }, - - - onResize : function(adjWidth, adjHeight, rawWidth, rawHeight){ - }, - - - onPosition : function(x, y){ - - }, - - - adjustSize : function(w, h){ - if(this.autoWidth){ - w = 'auto'; - } - if(this.autoHeight){ - h = 'auto'; - } - return {width : w, height: h}; - }, - - - adjustPosition : function(x, y){ - return {x : x, y: y}; - } -}); -Ext.reg('box', Ext.BoxComponent); - - - -Ext.Spacer = Ext.extend(Ext.BoxComponent, { - autoEl:'div' -}); -Ext.reg('spacer', Ext.Spacer); -Ext.SplitBar = function(dragElement, resizingElement, orientation, placement, existingProxy){ - - - this.el = Ext.get(dragElement, true); - this.el.dom.unselectable = "on"; - - this.resizingEl = Ext.get(resizingElement, true); - - - this.orientation = orientation || Ext.SplitBar.HORIZONTAL; - - - - this.minSize = 0; - - - this.maxSize = 2000; - - - this.animate = false; - - - this.useShim = false; - - - this.shim = null; - - if(!existingProxy){ - - this.proxy = Ext.SplitBar.createProxy(this.orientation); - }else{ - this.proxy = Ext.get(existingProxy).dom; - } - - this.dd = new Ext.dd.DDProxy(this.el.dom.id, "XSplitBars", {dragElId : this.proxy.id}); - - - this.dd.b4StartDrag = this.onStartProxyDrag.createDelegate(this); - - - this.dd.endDrag = this.onEndProxyDrag.createDelegate(this); - - - this.dragSpecs = {}; - - - this.adapter = new Ext.SplitBar.BasicLayoutAdapter(); - this.adapter.init(this); - - if(this.orientation == Ext.SplitBar.HORIZONTAL){ - - this.placement = placement || (this.el.getX() > this.resizingEl.getX() ? Ext.SplitBar.LEFT : Ext.SplitBar.RIGHT); - this.el.addClass("x-splitbar-h"); - }else{ - - this.placement = placement || (this.el.getY() > this.resizingEl.getY() ? Ext.SplitBar.TOP : Ext.SplitBar.BOTTOM); - this.el.addClass("x-splitbar-v"); - } - - this.addEvents( - - "resize", - - "moved", - - "beforeresize", - - "beforeapply" - ); - - Ext.SplitBar.superclass.constructor.call(this); -}; - -Ext.extend(Ext.SplitBar, Ext.util.Observable, { - onStartProxyDrag : function(x, y){ - this.fireEvent("beforeresize", this); - this.overlay = Ext.DomHelper.append(document.body, {cls: "x-drag-overlay", html: " "}, true); - this.overlay.unselectable(); - this.overlay.setSize(Ext.lib.Dom.getViewWidth(true), Ext.lib.Dom.getViewHeight(true)); - this.overlay.show(); - Ext.get(this.proxy).setDisplayed("block"); - var size = this.adapter.getElementSize(this); - this.activeMinSize = this.getMinimumSize(); - this.activeMaxSize = this.getMaximumSize(); - var c1 = size - this.activeMinSize; - var c2 = Math.max(this.activeMaxSize - size, 0); - if(this.orientation == Ext.SplitBar.HORIZONTAL){ - this.dd.resetConstraints(); - this.dd.setXConstraint( - this.placement == Ext.SplitBar.LEFT ? c1 : c2, - this.placement == Ext.SplitBar.LEFT ? c2 : c1, - this.tickSize - ); - this.dd.setYConstraint(0, 0); - }else{ - this.dd.resetConstraints(); - this.dd.setXConstraint(0, 0); - this.dd.setYConstraint( - this.placement == Ext.SplitBar.TOP ? c1 : c2, - this.placement == Ext.SplitBar.TOP ? c2 : c1, - this.tickSize - ); - } - this.dragSpecs.startSize = size; - this.dragSpecs.startPoint = [x, y]; - Ext.dd.DDProxy.prototype.b4StartDrag.call(this.dd, x, y); - }, - - - onEndProxyDrag : function(e){ - Ext.get(this.proxy).setDisplayed(false); - var endPoint = Ext.lib.Event.getXY(e); - if(this.overlay){ - Ext.destroy(this.overlay); - delete this.overlay; - } - var newSize; - if(this.orientation == Ext.SplitBar.HORIZONTAL){ - newSize = this.dragSpecs.startSize + - (this.placement == Ext.SplitBar.LEFT ? - endPoint[0] - this.dragSpecs.startPoint[0] : - this.dragSpecs.startPoint[0] - endPoint[0] - ); - }else{ - newSize = this.dragSpecs.startSize + - (this.placement == Ext.SplitBar.TOP ? - endPoint[1] - this.dragSpecs.startPoint[1] : - this.dragSpecs.startPoint[1] - endPoint[1] - ); - } - newSize = Math.min(Math.max(newSize, this.activeMinSize), this.activeMaxSize); - if(newSize != this.dragSpecs.startSize){ - if(this.fireEvent('beforeapply', this, newSize) !== false){ - this.adapter.setElementSize(this, newSize); - this.fireEvent("moved", this, newSize); - this.fireEvent("resize", this, newSize); - } - } - }, - - - getAdapter : function(){ - return this.adapter; - }, - - - setAdapter : function(adapter){ - this.adapter = adapter; - this.adapter.init(this); - }, - - - getMinimumSize : function(){ - return this.minSize; - }, - - - setMinimumSize : function(minSize){ - this.minSize = minSize; - }, - - - getMaximumSize : function(){ - return this.maxSize; - }, - - - setMaximumSize : function(maxSize){ - this.maxSize = maxSize; - }, - - - setCurrentSize : function(size){ - var oldAnimate = this.animate; - this.animate = false; - this.adapter.setElementSize(this, size); - this.animate = oldAnimate; - }, - - - destroy : function(removeEl){ - Ext.destroy(this.shim, Ext.get(this.proxy)); - this.dd.unreg(); - if(removeEl){ - this.el.remove(); - } - this.purgeListeners(); - } -}); - - -Ext.SplitBar.createProxy = function(dir){ - var proxy = new Ext.Element(document.createElement("div")); - document.body.appendChild(proxy.dom); - proxy.unselectable(); - var cls = 'x-splitbar-proxy'; - proxy.addClass(cls + ' ' + (dir == Ext.SplitBar.HORIZONTAL ? cls +'-h' : cls + '-v')); - return proxy.dom; -}; - - -Ext.SplitBar.BasicLayoutAdapter = function(){ -}; - -Ext.SplitBar.BasicLayoutAdapter.prototype = { - - init : function(s){ - - }, - - getElementSize : function(s){ - if(s.orientation == Ext.SplitBar.HORIZONTAL){ - return s.resizingEl.getWidth(); - }else{ - return s.resizingEl.getHeight(); - } - }, - - - setElementSize : function(s, newSize, onComplete){ - if(s.orientation == Ext.SplitBar.HORIZONTAL){ - if(!s.animate){ - s.resizingEl.setWidth(newSize); - if(onComplete){ - onComplete(s, newSize); - } - }else{ - s.resizingEl.setWidth(newSize, true, .1, onComplete, 'easeOut'); - } - }else{ - - if(!s.animate){ - s.resizingEl.setHeight(newSize); - if(onComplete){ - onComplete(s, newSize); - } - }else{ - s.resizingEl.setHeight(newSize, true, .1, onComplete, 'easeOut'); - } - } - } -}; - - -Ext.SplitBar.AbsoluteLayoutAdapter = function(container){ - this.basic = new Ext.SplitBar.BasicLayoutAdapter(); - this.container = Ext.get(container); -}; - -Ext.SplitBar.AbsoluteLayoutAdapter.prototype = { - init : function(s){ - this.basic.init(s); - }, - - getElementSize : function(s){ - return this.basic.getElementSize(s); - }, - - setElementSize : function(s, newSize, onComplete){ - this.basic.setElementSize(s, newSize, this.moveSplitter.createDelegate(this, [s])); - }, - - moveSplitter : function(s){ - var yes = Ext.SplitBar; - switch(s.placement){ - case yes.LEFT: - s.el.setX(s.resizingEl.getRight()); - break; - case yes.RIGHT: - s.el.setStyle("right", (this.container.getWidth() - s.resizingEl.getLeft()) + "px"); - break; - case yes.TOP: - s.el.setY(s.resizingEl.getBottom()); - break; - case yes.BOTTOM: - s.el.setY(s.resizingEl.getTop() - s.el.getHeight()); - break; - } - } -}; - - -Ext.SplitBar.VERTICAL = 1; - - -Ext.SplitBar.HORIZONTAL = 2; - - -Ext.SplitBar.LEFT = 1; - - -Ext.SplitBar.RIGHT = 2; - - -Ext.SplitBar.TOP = 3; - - -Ext.SplitBar.BOTTOM = 4; - -Ext.Container = Ext.extend(Ext.BoxComponent, { - - - - - bufferResize: 50, - - - - - - - - autoDestroy : true, - - - forceLayout: false, - - - - defaultType : 'panel', - - - resizeEvent: 'resize', - - - bubbleEvents: ['add', 'remove'], - - - initComponent : function(){ - Ext.Container.superclass.initComponent.call(this); - - this.addEvents( - - 'afterlayout', - - 'beforeadd', - - 'beforeremove', - - 'add', - - 'remove' - ); - - - var items = this.items; - if(items){ - delete this.items; - this.add(items); - } - }, - - - initItems : function(){ - if(!this.items){ - this.items = new Ext.util.MixedCollection(false, this.getComponentId); - this.getLayout(); - } - }, - - - setLayout : function(layout){ - if(this.layout && this.layout != layout){ - this.layout.setContainer(null); - } - this.layout = layout; - this.initItems(); - layout.setContainer(this); - }, - - afterRender: function(){ - - - Ext.Container.superclass.afterRender.call(this); - if(!this.layout){ - this.layout = 'auto'; - } - if(Ext.isObject(this.layout) && !this.layout.layout){ - this.layoutConfig = this.layout; - this.layout = this.layoutConfig.type; - } - if(Ext.isString(this.layout)){ - this.layout = new Ext.Container.LAYOUTS[this.layout.toLowerCase()](this.layoutConfig); - } - this.setLayout(this.layout); - - - if(this.activeItem !== undefined && this.layout.setActiveItem){ - var item = this.activeItem; - delete this.activeItem; - this.layout.setActiveItem(item); - } - - - if(!this.ownerCt){ - this.doLayout(false, true); - } - - - - if(this.monitorResize === true){ - Ext.EventManager.onWindowResize(this.doLayout, this, [false]); - } - }, - - - getLayoutTarget : function(){ - return this.el; - }, - - - getComponentId : function(comp){ - return comp.getItemId(); - }, - - - add : function(comp){ - this.initItems(); - var args = arguments.length > 1; - if(args || Ext.isArray(comp)){ - var result = []; - Ext.each(args ? arguments : comp, function(c){ - result.push(this.add(c)); - }, this); - return result; - } - var c = this.lookupComponent(this.applyDefaults(comp)); - var index = this.items.length; - if(this.fireEvent('beforeadd', this, c, index) !== false && this.onBeforeAdd(c) !== false){ - this.items.add(c); - - c.onAdded(this, index); - this.onAdd(c); - this.fireEvent('add', this, c, index); - } - return c; - }, - - onAdd : function(c){ - - }, - - - onAdded : function(container, pos) { - - this.ownerCt = container; - this.initRef(); - - this.cascade(function(c){ - c.initRef(); - }); - this.fireEvent('added', this, container, pos); - }, - - - insert : function(index, comp) { - var args = arguments, - length = args.length, - result = [], - i, c; - - this.initItems(); - - if (length > 2) { - for (i = length - 1; i >= 1; --i) { - result.push(this.insert(index, args[i])); - } - return result; - } - - c = this.lookupComponent(this.applyDefaults(comp)); - index = Math.min(index, this.items.length); - - if (this.fireEvent('beforeadd', this, c, index) !== false && this.onBeforeAdd(c) !== false) { - if (c.ownerCt == this) { - this.items.remove(c); - } - this.items.insert(index, c); - c.onAdded(this, index); - this.onAdd(c); - this.fireEvent('add', this, c, index); - } - - return c; - }, - - - applyDefaults : function(c){ - var d = this.defaults; - if(d){ - if(Ext.isFunction(d)){ - d = d.call(this, c); - } - if(Ext.isString(c)){ - c = Ext.ComponentMgr.get(c); - Ext.apply(c, d); - }else if(!c.events){ - Ext.applyIf(c.isAction ? c.initialConfig : c, d); - }else{ - Ext.apply(c, d); - } - } - return c; - }, - - - onBeforeAdd : function(item){ - if(item.ownerCt){ - item.ownerCt.remove(item, false); - } - if(this.hideBorders === true){ - item.border = (item.border === true); - } - }, - - - remove : function(comp, autoDestroy){ - this.initItems(); - var c = this.getComponent(comp); - if(c && this.fireEvent('beforeremove', this, c) !== false){ - this.doRemove(c, autoDestroy); - this.fireEvent('remove', this, c); - } - return c; - }, - - onRemove: function(c){ - - }, - - - doRemove: function(c, autoDestroy){ - var l = this.layout, - hasLayout = l && this.rendered; - - if(hasLayout){ - l.onRemove(c); - } - this.items.remove(c); - c.onRemoved(); - this.onRemove(c); - if(autoDestroy === true || (autoDestroy !== false && this.autoDestroy)){ - c.destroy(); - } - if(hasLayout){ - l.afterRemove(c); - } - }, - - - removeAll: function(autoDestroy){ - this.initItems(); - var item, rem = [], items = []; - this.items.each(function(i){ - rem.push(i); - }); - for (var i = 0, len = rem.length; i < len; ++i){ - item = rem[i]; - this.remove(item, autoDestroy); - if(item.ownerCt !== this){ - items.push(item); - } - } - return items; - }, - - - getComponent : function(comp){ - if(Ext.isObject(comp)){ - comp = comp.getItemId(); - } - return this.items.get(comp); - }, - - - lookupComponent : function(comp){ - if(Ext.isString(comp)){ - return Ext.ComponentMgr.get(comp); - }else if(!comp.events){ - return this.createComponent(comp); - } - return comp; - }, - - - createComponent : function(config, defaultType){ - if (config.render) { - return config; - } - - - var c = Ext.create(Ext.apply({ - ownerCt: this - }, config), defaultType || this.defaultType); - delete c.initialConfig.ownerCt; - delete c.ownerCt; - return c; - }, - - - canLayout : function() { - var el = this.getVisibilityEl(); - return el && el.dom && !el.isStyle("display", "none"); - }, - - - - doLayout : function(shallow, force){ - var rendered = this.rendered, - forceLayout = force || this.forceLayout; - - if(this.collapsed || !this.canLayout()){ - this.deferLayout = this.deferLayout || !shallow; - if(!forceLayout){ - return; - } - shallow = shallow && !this.deferLayout; - } else { - delete this.deferLayout; - } - if(rendered && this.layout){ - this.layout.layout(); - } - if(shallow !== true && this.items){ - var cs = this.items.items; - for(var i = 0, len = cs.length; i < len; i++){ - var c = cs[i]; - if(c.doLayout){ - c.doLayout(false, forceLayout); - } - } - } - if(rendered){ - this.onLayout(shallow, forceLayout); - } - - this.hasLayout = true; - delete this.forceLayout; - }, - - onLayout : Ext.emptyFn, - - - shouldBufferLayout: function(){ - - var hl = this.hasLayout; - if(this.ownerCt){ - - return hl ? !this.hasLayoutPending() : false; - } - - return hl; - }, - - - hasLayoutPending: function(){ - - var pending = false; - this.ownerCt.bubble(function(c){ - if(c.layoutPending){ - pending = true; - return false; - } - }); - return pending; - }, - - onShow : function(){ - - Ext.Container.superclass.onShow.call(this); - - if(Ext.isDefined(this.deferLayout)){ - delete this.deferLayout; - this.doLayout(true); - } - }, - - - getLayout : function(){ - if(!this.layout){ - var layout = new Ext.layout.AutoLayout(this.layoutConfig); - this.setLayout(layout); - } - return this.layout; - }, - - - beforeDestroy : function(){ - var c; - if(this.items){ - while(c = this.items.first()){ - this.doRemove(c, true); - } - } - if(this.monitorResize){ - Ext.EventManager.removeResizeListener(this.doLayout, this); - } - Ext.destroy(this.layout); - Ext.Container.superclass.beforeDestroy.call(this); - }, - - - cascade : function(fn, scope, args){ - if(fn.apply(scope || this, args || [this]) !== false){ - if(this.items){ - var cs = this.items.items; - for(var i = 0, len = cs.length; i < len; i++){ - if(cs[i].cascade){ - cs[i].cascade(fn, scope, args); - }else{ - fn.apply(scope || cs[i], args || [cs[i]]); - } - } - } - } - return this; - }, - - - findById : function(id){ - var m = null, - ct = this; - this.cascade(function(c){ - if(ct != c && c.id === id){ - m = c; - return false; - } - }); - return m; - }, - - - findByType : function(xtype, shallow){ - return this.findBy(function(c){ - return c.isXType(xtype, shallow); - }); - }, - - - find : function(prop, value){ - return this.findBy(function(c){ - return c[prop] === value; - }); - }, - - - findBy : function(fn, scope){ - var m = [], ct = this; - this.cascade(function(c){ - if(ct != c && fn.call(scope || c, c, ct) === true){ - m.push(c); - } - }); - return m; - }, - - - get : function(key){ - return this.getComponent(key); - } -}); - -Ext.Container.LAYOUTS = {}; -Ext.reg('container', Ext.Container); - -Ext.layout.ContainerLayout = Ext.extend(Object, { - - - - - - - monitorResize:false, - - activeItem : null, - - constructor : function(config){ - this.id = Ext.id(null, 'ext-layout-'); - Ext.apply(this, config); - }, - - type: 'container', - - - IEMeasureHack : function(target, viewFlag) { - var tChildren = target.dom.childNodes, tLen = tChildren.length, c, d = [], e, i, ret; - for (i = 0 ; i < tLen ; i++) { - c = tChildren[i]; - e = Ext.get(c); - if (e) { - d[i] = e.getStyle('display'); - e.setStyle({display: 'none'}); - } - } - ret = target ? target.getViewSize(viewFlag) : {}; - for (i = 0 ; i < tLen ; i++) { - c = tChildren[i]; - e = Ext.get(c); - if (e) { - e.setStyle({display: d[i]}); - } - } - return ret; - }, - - - getLayoutTargetSize : Ext.EmptyFn, - - - layout : function(){ - var ct = this.container, target = ct.getLayoutTarget(); - if(!(this.hasLayout || Ext.isEmpty(this.targetCls))){ - target.addClass(this.targetCls); - } - this.onLayout(ct, target); - ct.fireEvent('afterlayout', ct, this); - }, - - - onLayout : function(ct, target){ - this.renderAll(ct, target); - }, - - - isValidParent : function(c, target){ - return target && c.getPositionEl().dom.parentNode == (target.dom || target); - }, - - - renderAll : function(ct, target){ - var items = ct.items.items, i, c, len = items.length; - for(i = 0; i < len; i++) { - c = items[i]; - if(c && (!c.rendered || !this.isValidParent(c, target))){ - this.renderItem(c, i, target); - } - } - }, - - - renderItem : function(c, position, target){ - if (c) { - if (!c.rendered) { - c.render(target, position); - this.configureItem(c); - } else if (!this.isValidParent(c, target)) { - if (Ext.isNumber(position)) { - position = target.dom.childNodes[position]; - } - - target.dom.insertBefore(c.getPositionEl().dom, position || null); - c.container = target; - this.configureItem(c); - } - } - }, - - - - getRenderedItems: function(ct){ - var t = ct.getLayoutTarget(), cti = ct.items.items, len = cti.length, i, c, items = []; - for (i = 0; i < len; i++) { - if((c = cti[i]).rendered && this.isValidParent(c, t) && c.shouldLayout !== false){ - items.push(c); - } - }; - return items; - }, - - - configureItem: function(c){ - if (this.extraCls) { - var t = c.getPositionEl ? c.getPositionEl() : c; - t.addClass(this.extraCls); - } - - - if (c.doLayout && this.forceLayout) { - c.doLayout(); - } - if (this.renderHidden && c != this.activeItem) { - c.hide(); - } - }, - - onRemove: function(c){ - if(this.activeItem == c){ - delete this.activeItem; - } - if(c.rendered && this.extraCls){ - var t = c.getPositionEl ? c.getPositionEl() : c; - t.removeClass(this.extraCls); - } - }, - - afterRemove: function(c){ - if(c.removeRestore){ - c.removeMode = 'container'; - delete c.removeRestore; - } - }, - - - onResize: function(){ - var ct = this.container, - b; - if(ct.collapsed){ - return; - } - if(b = ct.bufferResize && ct.shouldBufferLayout()){ - if(!this.resizeTask){ - this.resizeTask = new Ext.util.DelayedTask(this.runLayout, this); - this.resizeBuffer = Ext.isNumber(b) ? b : 50; - } - ct.layoutPending = true; - this.resizeTask.delay(this.resizeBuffer); - }else{ - this.runLayout(); - } - }, - - runLayout: function(){ - var ct = this.container; - this.layout(); - ct.onLayout(); - delete ct.layoutPending; - }, - - - setContainer : function(ct){ - - if(this.monitorResize && ct != this.container){ - var old = this.container; - if(old){ - old.un(old.resizeEvent, this.onResize, this); - } - if(ct){ - ct.on(ct.resizeEvent, this.onResize, this); - } - } - this.container = ct; - }, - - - parseMargins : function(v){ - if (Ext.isNumber(v)) { - v = v.toString(); - } - var ms = v.split(' '), - len = ms.length; - - if (len == 1) { - ms[1] = ms[2] = ms[3] = ms[0]; - } else if(len == 2) { - ms[2] = ms[0]; - ms[3] = ms[1]; - } else if(len == 3) { - ms[3] = ms[1]; - } - - return { - top :parseInt(ms[0], 10) || 0, - right :parseInt(ms[1], 10) || 0, - bottom:parseInt(ms[2], 10) || 0, - left :parseInt(ms[3], 10) || 0 - }; - }, - - - fieldTpl: (function() { - var t = new Ext.Template( - '
        ', - '', - '
        ', - '
        ', - '
        ' - ); - t.disableFormats = true; - return t.compile(); - })(), - - - destroy : function(){ - - if(this.resizeTask && this.resizeTask.cancel){ - this.resizeTask.cancel(); - } - if(this.container) { - this.container.un(this.container.resizeEvent, this.onResize, this); - } - if(!Ext.isEmpty(this.targetCls)){ - var target = this.container.getLayoutTarget(); - if(target){ - target.removeClass(this.targetCls); - } - } - } -}); -Ext.layout.AutoLayout = Ext.extend(Ext.layout.ContainerLayout, { - type: 'auto', - - monitorResize: true, - - onLayout : function(ct, target){ - Ext.layout.AutoLayout.superclass.onLayout.call(this, ct, target); - var cs = this.getRenderedItems(ct), len = cs.length, i, c; - for(i = 0; i < len; i++){ - c = cs[i]; - if (c.doLayout){ - - c.doLayout(true); - } - } - } -}); - -Ext.Container.LAYOUTS['auto'] = Ext.layout.AutoLayout; - -Ext.layout.FitLayout = Ext.extend(Ext.layout.ContainerLayout, { - - monitorResize:true, - - type: 'fit', - - getLayoutTargetSize : function() { - var target = this.container.getLayoutTarget(); - if (!target) { - return {}; - } - - return target.getStyleSize(); - }, - - - onLayout : function(ct, target){ - Ext.layout.FitLayout.superclass.onLayout.call(this, ct, target); - if(!ct.collapsed){ - this.setItemSize(this.activeItem || ct.items.itemAt(0), this.getLayoutTargetSize()); - } - }, - - - setItemSize : function(item, size){ - if(item && size.height > 0){ - item.setSize(size); - } - } -}); -Ext.Container.LAYOUTS['fit'] = Ext.layout.FitLayout; -Ext.layout.CardLayout = Ext.extend(Ext.layout.FitLayout, { - - deferredRender : false, - - - layoutOnCardChange : false, - - - - renderHidden : true, - - type: 'card', - - - setActiveItem : function(item){ - var ai = this.activeItem, - ct = this.container; - item = ct.getComponent(item); - - - if(item && ai != item){ - - - if(ai){ - ai.hide(); - if (ai.hidden !== true) { - return false; - } - ai.fireEvent('deactivate', ai); - } - - var layout = item.doLayout && (this.layoutOnCardChange || !item.rendered); - - - this.activeItem = item; - - - - delete item.deferLayout; - - - item.show(); - - this.layout(); - - if(layout){ - item.doLayout(); - } - item.fireEvent('activate', item); - } - }, - - - renderAll : function(ct, target){ - if(this.deferredRender){ - this.renderItem(this.activeItem, undefined, target); - }else{ - Ext.layout.CardLayout.superclass.renderAll.call(this, ct, target); - } - } -}); -Ext.Container.LAYOUTS['card'] = Ext.layout.CardLayout; - -Ext.layout.AnchorLayout = Ext.extend(Ext.layout.ContainerLayout, { - - - - monitorResize : true, - - type : 'anchor', - - - defaultAnchor : '100%', - - parseAnchorRE : /^(r|right|b|bottom)$/i, - - - getLayoutTargetSize : function() { - var target = this.container.getLayoutTarget(), ret = {}; - if (target) { - ret = target.getViewSize(); - - - - - if (Ext.isIE && Ext.isStrict && ret.width == 0){ - ret = target.getStyleSize(); - } - ret.width -= target.getPadding('lr'); - ret.height -= target.getPadding('tb'); - } - return ret; - }, - - - onLayout : function(container, target) { - Ext.layout.AnchorLayout.superclass.onLayout.call(this, container, target); - - var size = this.getLayoutTargetSize(), - containerWidth = size.width, - containerHeight = size.height, - overflow = target.getStyle('overflow'), - components = this.getRenderedItems(container), - len = components.length, - boxes = [], - box, - anchorWidth, - anchorHeight, - component, - anchorSpec, - calcWidth, - calcHeight, - anchorsArray, - totalHeight = 0, - i, - el; - - if(containerWidth < 20 && containerHeight < 20){ - return; - } - - - if(container.anchorSize) { - if(typeof container.anchorSize == 'number') { - anchorWidth = container.anchorSize; - } else { - anchorWidth = container.anchorSize.width; - anchorHeight = container.anchorSize.height; - } - } else { - anchorWidth = container.initialConfig.width; - anchorHeight = container.initialConfig.height; - } - - for(i = 0; i < len; i++) { - component = components[i]; - el = component.getPositionEl(); - - - if (!component.anchor && component.items && !Ext.isNumber(component.width) && !(Ext.isIE6 && Ext.isStrict)){ - component.anchor = this.defaultAnchor; - } - - if(component.anchor) { - anchorSpec = component.anchorSpec; - - if(!anchorSpec){ - anchorsArray = component.anchor.split(' '); - component.anchorSpec = anchorSpec = { - right: this.parseAnchor(anchorsArray[0], component.initialConfig.width, anchorWidth), - bottom: this.parseAnchor(anchorsArray[1], component.initialConfig.height, anchorHeight) - }; - } - calcWidth = anchorSpec.right ? this.adjustWidthAnchor(anchorSpec.right(containerWidth) - el.getMargins('lr'), component) : undefined; - calcHeight = anchorSpec.bottom ? this.adjustHeightAnchor(anchorSpec.bottom(containerHeight) - el.getMargins('tb'), component) : undefined; - - if(calcWidth || calcHeight) { - boxes.push({ - component: component, - width: calcWidth || undefined, - height: calcHeight || undefined - }); - } - } - } - for (i = 0, len = boxes.length; i < len; i++) { - box = boxes[i]; - box.component.setSize(box.width, box.height); - } - - if (overflow && overflow != 'hidden' && !this.adjustmentPass) { - var newTargetSize = this.getLayoutTargetSize(); - if (newTargetSize.width != size.width || newTargetSize.height != size.height){ - this.adjustmentPass = true; - this.onLayout(container, target); - } - } - - delete this.adjustmentPass; - }, - - - parseAnchor : function(a, start, cstart) { - if (a && a != 'none') { - var last; - - if (this.parseAnchorRE.test(a)) { - var diff = cstart - start; - return function(v){ - if(v !== last){ - last = v; - return v - diff; - } - }; - - } else if(a.indexOf('%') != -1) { - var ratio = parseFloat(a.replace('%', ''))*.01; - return function(v){ - if(v !== last){ - last = v; - return Math.floor(v*ratio); - } - }; - - } else { - a = parseInt(a, 10); - if (!isNaN(a)) { - return function(v) { - if (v !== last) { - last = v; - return v + a; - } - }; - } - } - } - return false; - }, - - - adjustWidthAnchor : function(value, comp){ - return value; - }, - - - adjustHeightAnchor : function(value, comp){ - return value; - } - - -}); -Ext.Container.LAYOUTS['anchor'] = Ext.layout.AnchorLayout; - -Ext.layout.ColumnLayout = Ext.extend(Ext.layout.ContainerLayout, { - - monitorResize:true, - - type: 'column', - - extraCls: 'x-column', - - scrollOffset : 0, - - - - targetCls: 'x-column-layout-ct', - - isValidParent : function(c, target){ - return this.innerCt && c.getPositionEl().dom.parentNode == this.innerCt.dom; - }, - - getLayoutTargetSize : function() { - var target = this.container.getLayoutTarget(), ret; - if (target) { - ret = target.getViewSize(); - - - - - if (Ext.isIE && Ext.isStrict && ret.width == 0){ - ret = target.getStyleSize(); - } - - ret.width -= target.getPadding('lr'); - ret.height -= target.getPadding('tb'); - } - return ret; - }, - - renderAll : function(ct, target) { - if(!this.innerCt){ - - - this.innerCt = target.createChild({cls:'x-column-inner'}); - this.innerCt.createChild({cls:'x-clear'}); - } - Ext.layout.ColumnLayout.superclass.renderAll.call(this, ct, this.innerCt); - }, - - - onLayout : function(ct, target){ - var cs = ct.items.items, - len = cs.length, - c, - i, - m, - margins = []; - - this.renderAll(ct, target); - - var size = this.getLayoutTargetSize(); - - if(size.width < 1 && size.height < 1){ - return; - } - - var w = size.width - this.scrollOffset, - h = size.height, - pw = w; - - this.innerCt.setWidth(w); - - - - - for(i = 0; i < len; i++){ - c = cs[i]; - m = c.getPositionEl().getMargins('lr'); - margins[i] = m; - if(!c.columnWidth){ - pw -= (c.getWidth() + m); - } - } - - pw = pw < 0 ? 0 : pw; - - for(i = 0; i < len; i++){ - c = cs[i]; - m = margins[i]; - if(c.columnWidth){ - c.setSize(Math.floor(c.columnWidth * pw) - m); - } - } - - - - if (Ext.isIE) { - if (i = target.getStyle('overflow') && i != 'hidden' && !this.adjustmentPass) { - var ts = this.getLayoutTargetSize(); - if (ts.width != size.width){ - this.adjustmentPass = true; - this.onLayout(ct, target); - } - } - } - delete this.adjustmentPass; - } - - -}); - -Ext.Container.LAYOUTS['column'] = Ext.layout.ColumnLayout; - -Ext.layout.BorderLayout = Ext.extend(Ext.layout.ContainerLayout, { - - monitorResize:true, - - rendered : false, - - type: 'border', - - targetCls: 'x-border-layout-ct', - - getLayoutTargetSize : function() { - var target = this.container.getLayoutTarget(); - return target ? target.getViewSize() : {}; - }, - - - onLayout : function(ct, target){ - var collapsed, i, c, pos, items = ct.items.items, len = items.length; - if(!this.rendered){ - collapsed = []; - for(i = 0; i < len; i++) { - c = items[i]; - pos = c.region; - if(c.collapsed){ - collapsed.push(c); - } - c.collapsed = false; - if(!c.rendered){ - c.render(target, i); - c.getPositionEl().addClass('x-border-panel'); - } - this[pos] = pos != 'center' && c.split ? - new Ext.layout.BorderLayout.SplitRegion(this, c.initialConfig, pos) : - new Ext.layout.BorderLayout.Region(this, c.initialConfig, pos); - this[pos].render(target, c); - } - this.rendered = true; - } - - var size = this.getLayoutTargetSize(); - if(size.width < 20 || size.height < 20){ - if(collapsed){ - this.restoreCollapsed = collapsed; - } - return; - }else if(this.restoreCollapsed){ - collapsed = this.restoreCollapsed; - delete this.restoreCollapsed; - } - - var w = size.width, h = size.height, - centerW = w, centerH = h, centerY = 0, centerX = 0, - n = this.north, s = this.south, west = this.west, e = this.east, c = this.center, - b, m, totalWidth, totalHeight; - if(!c && Ext.layout.BorderLayout.WARN !== false){ - throw 'No center region defined in BorderLayout ' + ct.id; - } - - if(n && n.isVisible()){ - b = n.getSize(); - m = n.getMargins(); - b.width = w - (m.left+m.right); - b.x = m.left; - b.y = m.top; - centerY = b.height + b.y + m.bottom; - centerH -= centerY; - n.applyLayout(b); - } - if(s && s.isVisible()){ - b = s.getSize(); - m = s.getMargins(); - b.width = w - (m.left+m.right); - b.x = m.left; - totalHeight = (b.height + m.top + m.bottom); - b.y = h - totalHeight + m.top; - centerH -= totalHeight; - s.applyLayout(b); - } - if(west && west.isVisible()){ - b = west.getSize(); - m = west.getMargins(); - b.height = centerH - (m.top+m.bottom); - b.x = m.left; - b.y = centerY + m.top; - totalWidth = (b.width + m.left + m.right); - centerX += totalWidth; - centerW -= totalWidth; - west.applyLayout(b); - } - if(e && e.isVisible()){ - b = e.getSize(); - m = e.getMargins(); - b.height = centerH - (m.top+m.bottom); - totalWidth = (b.width + m.left + m.right); - b.x = w - totalWidth + m.left; - b.y = centerY + m.top; - centerW -= totalWidth; - e.applyLayout(b); - } - if(c){ - m = c.getMargins(); - var centerBox = { - x: centerX + m.left, - y: centerY + m.top, - width: centerW - (m.left+m.right), - height: centerH - (m.top+m.bottom) - }; - c.applyLayout(centerBox); - } - if(collapsed){ - for(i = 0, len = collapsed.length; i < len; i++){ - collapsed[i].collapse(false); - } - } - if(Ext.isIE && Ext.isStrict){ - target.repaint(); - } - - if (i = target.getStyle('overflow') && i != 'hidden' && !this.adjustmentPass) { - var ts = this.getLayoutTargetSize(); - if (ts.width != size.width || ts.height != size.height){ - this.adjustmentPass = true; - this.onLayout(ct, target); - } - } - delete this.adjustmentPass; - }, - - destroy: function() { - var r = ['north', 'south', 'east', 'west'], i, region; - for (i = 0; i < r.length; i++) { - region = this[r[i]]; - if(region){ - if(region.destroy){ - region.destroy(); - }else if (region.split){ - region.split.destroy(true); - } - } - } - Ext.layout.BorderLayout.superclass.destroy.call(this); - } - - -}); - - -Ext.layout.BorderLayout.Region = function(layout, config, pos){ - Ext.apply(this, config); - this.layout = layout; - this.position = pos; - this.state = {}; - if(typeof this.margins == 'string'){ - this.margins = this.layout.parseMargins(this.margins); - } - this.margins = Ext.applyIf(this.margins || {}, this.defaultMargins); - if(this.collapsible){ - if(typeof this.cmargins == 'string'){ - this.cmargins = this.layout.parseMargins(this.cmargins); - } - if(this.collapseMode == 'mini' && !this.cmargins){ - this.cmargins = {left:0,top:0,right:0,bottom:0}; - }else{ - this.cmargins = Ext.applyIf(this.cmargins || {}, - pos == 'north' || pos == 'south' ? this.defaultNSCMargins : this.defaultEWCMargins); - } - } -}; - -Ext.layout.BorderLayout.Region.prototype = { - - - - - - - collapsible : false, - - split:false, - - floatable: true, - - minWidth:50, - - minHeight:50, - - - defaultMargins : {left:0,top:0,right:0,bottom:0}, - - defaultNSCMargins : {left:5,top:5,right:5,bottom:5}, - - defaultEWCMargins : {left:5,top:0,right:5,bottom:0}, - floatingZIndex: 100, - - - isCollapsed : false, - - - - - - - render : function(ct, p){ - this.panel = p; - p.el.enableDisplayMode(); - this.targetEl = ct; - this.el = p.el; - - var gs = p.getState, ps = this.position; - p.getState = function(){ - return Ext.apply(gs.call(p) || {}, this.state); - }.createDelegate(this); - - if(ps != 'center'){ - p.allowQueuedExpand = false; - p.on({ - beforecollapse: this.beforeCollapse, - collapse: this.onCollapse, - beforeexpand: this.beforeExpand, - expand: this.onExpand, - hide: this.onHide, - show: this.onShow, - scope: this - }); - if(this.collapsible || this.floatable){ - p.collapseEl = 'el'; - p.slideAnchor = this.getSlideAnchor(); - } - if(p.tools && p.tools.toggle){ - p.tools.toggle.addClass('x-tool-collapse-'+ps); - p.tools.toggle.addClassOnOver('x-tool-collapse-'+ps+'-over'); - } - } - }, - - - getCollapsedEl : function(){ - if(!this.collapsedEl){ - if(!this.toolTemplate){ - var tt = new Ext.Template( - '
         
        ' - ); - tt.disableFormats = true; - tt.compile(); - Ext.layout.BorderLayout.Region.prototype.toolTemplate = tt; - } - this.collapsedEl = this.targetEl.createChild({ - cls: "x-layout-collapsed x-layout-collapsed-"+this.position, - id: this.panel.id + '-xcollapsed' - }); - this.collapsedEl.enableDisplayMode('block'); - - if(this.collapseMode == 'mini'){ - this.collapsedEl.addClass('x-layout-cmini-'+this.position); - this.miniCollapsedEl = this.collapsedEl.createChild({ - cls: "x-layout-mini x-layout-mini-"+this.position, html: " " - }); - this.miniCollapsedEl.addClassOnOver('x-layout-mini-over'); - this.collapsedEl.addClassOnOver("x-layout-collapsed-over"); - this.collapsedEl.on('click', this.onExpandClick, this, {stopEvent:true}); - }else { - if(this.collapsible !== false && !this.hideCollapseTool) { - var t = this.expandToolEl = this.toolTemplate.append( - this.collapsedEl.dom, - {id:'expand-'+this.position}, true); - t.addClassOnOver('x-tool-expand-'+this.position+'-over'); - t.on('click', this.onExpandClick, this, {stopEvent:true}); - } - if(this.floatable !== false || this.titleCollapse){ - this.collapsedEl.addClassOnOver("x-layout-collapsed-over"); - this.collapsedEl.on("click", this[this.floatable ? 'collapseClick' : 'onExpandClick'], this); - } - } - } - return this.collapsedEl; - }, - - - onExpandClick : function(e){ - if(this.isSlid){ - this.panel.expand(false); - }else{ - this.panel.expand(); - } - }, - - - onCollapseClick : function(e){ - this.panel.collapse(); - }, - - - beforeCollapse : function(p, animate){ - this.lastAnim = animate; - if(this.splitEl){ - this.splitEl.hide(); - } - this.getCollapsedEl().show(); - var el = this.panel.getEl(); - this.originalZIndex = el.getStyle('z-index'); - el.setStyle('z-index', 100); - this.isCollapsed = true; - this.layout.layout(); - }, - - - onCollapse : function(animate){ - this.panel.el.setStyle('z-index', 1); - if(this.lastAnim === false || this.panel.animCollapse === false){ - this.getCollapsedEl().dom.style.visibility = 'visible'; - }else{ - this.getCollapsedEl().slideIn(this.panel.slideAnchor, {duration:.2}); - } - this.state.collapsed = true; - this.panel.saveState(); - }, - - - beforeExpand : function(animate){ - if(this.isSlid){ - this.afterSlideIn(); - } - var c = this.getCollapsedEl(); - this.el.show(); - if(this.position == 'east' || this.position == 'west'){ - this.panel.setSize(undefined, c.getHeight()); - }else{ - this.panel.setSize(c.getWidth(), undefined); - } - c.hide(); - c.dom.style.visibility = 'hidden'; - this.panel.el.setStyle('z-index', this.floatingZIndex); - }, - - - onExpand : function(){ - this.isCollapsed = false; - if(this.splitEl){ - this.splitEl.show(); - } - this.layout.layout(); - this.panel.el.setStyle('z-index', this.originalZIndex); - this.state.collapsed = false; - this.panel.saveState(); - }, - - - collapseClick : function(e){ - if(this.isSlid){ - e.stopPropagation(); - this.slideIn(); - }else{ - e.stopPropagation(); - this.slideOut(); - } - }, - - - onHide : function(){ - if(this.isCollapsed){ - this.getCollapsedEl().hide(); - }else if(this.splitEl){ - this.splitEl.hide(); - } - }, - - - onShow : function(){ - if(this.isCollapsed){ - this.getCollapsedEl().show(); - }else if(this.splitEl){ - this.splitEl.show(); - } - }, - - - isVisible : function(){ - return !this.panel.hidden; - }, - - - getMargins : function(){ - return this.isCollapsed && this.cmargins ? this.cmargins : this.margins; - }, - - - getSize : function(){ - return this.isCollapsed ? this.getCollapsedEl().getSize() : this.panel.getSize(); - }, - - - setPanel : function(panel){ - this.panel = panel; - }, - - - getMinWidth: function(){ - return this.minWidth; - }, - - - getMinHeight: function(){ - return this.minHeight; - }, - - - applyLayoutCollapsed : function(box){ - var ce = this.getCollapsedEl(); - ce.setLeftTop(box.x, box.y); - ce.setSize(box.width, box.height); - }, - - - applyLayout : function(box){ - if(this.isCollapsed){ - this.applyLayoutCollapsed(box); - }else{ - this.panel.setPosition(box.x, box.y); - this.panel.setSize(box.width, box.height); - } - }, - - - beforeSlide: function(){ - this.panel.beforeEffect(); - }, - - - afterSlide : function(){ - this.panel.afterEffect(); - }, - - - initAutoHide : function(){ - if(this.autoHide !== false){ - if(!this.autoHideHd){ - this.autoHideSlideTask = new Ext.util.DelayedTask(this.slideIn, this); - this.autoHideHd = { - "mouseout": function(e){ - if(!e.within(this.el, true)){ - this.autoHideSlideTask.delay(500); - } - }, - "mouseover" : function(e){ - this.autoHideSlideTask.cancel(); - }, - scope : this - }; - } - this.el.on(this.autoHideHd); - this.collapsedEl.on(this.autoHideHd); - } - }, - - - clearAutoHide : function(){ - if(this.autoHide !== false){ - this.el.un("mouseout", this.autoHideHd.mouseout); - this.el.un("mouseover", this.autoHideHd.mouseover); - this.collapsedEl.un("mouseout", this.autoHideHd.mouseout); - this.collapsedEl.un("mouseover", this.autoHideHd.mouseover); - } - }, - - - clearMonitor : function(){ - Ext.getDoc().un("click", this.slideInIf, this); - }, - - - slideOut : function(){ - if(this.isSlid || this.el.hasActiveFx()){ - return; - } - this.isSlid = true; - var ts = this.panel.tools, dh, pc; - if(ts && ts.toggle){ - ts.toggle.hide(); - } - this.el.show(); - - - pc = this.panel.collapsed; - this.panel.collapsed = false; - - if(this.position == 'east' || this.position == 'west'){ - - dh = this.panel.deferHeight; - this.panel.deferHeight = false; - - this.panel.setSize(undefined, this.collapsedEl.getHeight()); - - - this.panel.deferHeight = dh; - }else{ - this.panel.setSize(this.collapsedEl.getWidth(), undefined); - } - - - this.panel.collapsed = pc; - - this.restoreLT = [this.el.dom.style.left, this.el.dom.style.top]; - this.el.alignTo(this.collapsedEl, this.getCollapseAnchor()); - this.el.setStyle("z-index", this.floatingZIndex+2); - this.panel.el.replaceClass('x-panel-collapsed', 'x-panel-floating'); - if(this.animFloat !== false){ - this.beforeSlide(); - this.el.slideIn(this.getSlideAnchor(), { - callback: function(){ - this.afterSlide(); - this.initAutoHide(); - Ext.getDoc().on("click", this.slideInIf, this); - }, - scope: this, - block: true - }); - }else{ - this.initAutoHide(); - Ext.getDoc().on("click", this.slideInIf, this); - } - }, - - - afterSlideIn : function(){ - this.clearAutoHide(); - this.isSlid = false; - this.clearMonitor(); - this.el.setStyle("z-index", ""); - this.panel.el.replaceClass('x-panel-floating', 'x-panel-collapsed'); - this.el.dom.style.left = this.restoreLT[0]; - this.el.dom.style.top = this.restoreLT[1]; - - var ts = this.panel.tools; - if(ts && ts.toggle){ - ts.toggle.show(); - } - }, - - - slideIn : function(cb){ - if(!this.isSlid || this.el.hasActiveFx()){ - Ext.callback(cb); - return; - } - this.isSlid = false; - if(this.animFloat !== false){ - this.beforeSlide(); - this.el.slideOut(this.getSlideAnchor(), { - callback: function(){ - this.el.hide(); - this.afterSlide(); - this.afterSlideIn(); - Ext.callback(cb); - }, - scope: this, - block: true - }); - }else{ - this.el.hide(); - this.afterSlideIn(); - } - }, - - - slideInIf : function(e){ - if(!e.within(this.el)){ - this.slideIn(); - } - }, - - - anchors : { - "west" : "left", - "east" : "right", - "north" : "top", - "south" : "bottom" - }, - - - sanchors : { - "west" : "l", - "east" : "r", - "north" : "t", - "south" : "b" - }, - - - canchors : { - "west" : "tl-tr", - "east" : "tr-tl", - "north" : "tl-bl", - "south" : "bl-tl" - }, - - - getAnchor : function(){ - return this.anchors[this.position]; - }, - - - getCollapseAnchor : function(){ - return this.canchors[this.position]; - }, - - - getSlideAnchor : function(){ - return this.sanchors[this.position]; - }, - - - getAlignAdj : function(){ - var cm = this.cmargins; - switch(this.position){ - case "west": - return [0, 0]; - break; - case "east": - return [0, 0]; - break; - case "north": - return [0, 0]; - break; - case "south": - return [0, 0]; - break; - } - }, - - - getExpandAdj : function(){ - var c = this.collapsedEl, cm = this.cmargins; - switch(this.position){ - case "west": - return [-(cm.right+c.getWidth()+cm.left), 0]; - break; - case "east": - return [cm.right+c.getWidth()+cm.left, 0]; - break; - case "north": - return [0, -(cm.top+cm.bottom+c.getHeight())]; - break; - case "south": - return [0, cm.top+cm.bottom+c.getHeight()]; - break; - } - }, - - destroy : function(){ - if (this.autoHideSlideTask && this.autoHideSlideTask.cancel){ - this.autoHideSlideTask.cancel(); - } - Ext.destroyMembers(this, 'miniCollapsedEl', 'collapsedEl', 'expandToolEl'); - } -}; - - -Ext.layout.BorderLayout.SplitRegion = function(layout, config, pos){ - Ext.layout.BorderLayout.SplitRegion.superclass.constructor.call(this, layout, config, pos); - - this.applyLayout = this.applyFns[pos]; -}; - -Ext.extend(Ext.layout.BorderLayout.SplitRegion, Ext.layout.BorderLayout.Region, { - - - splitTip : "Drag to resize.", - - collapsibleSplitTip : "Drag to resize. Double click to hide.", - - useSplitTips : false, - - - splitSettings : { - north : { - orientation: Ext.SplitBar.VERTICAL, - placement: Ext.SplitBar.TOP, - maxFn : 'getVMaxSize', - minProp: 'minHeight', - maxProp: 'maxHeight' - }, - south : { - orientation: Ext.SplitBar.VERTICAL, - placement: Ext.SplitBar.BOTTOM, - maxFn : 'getVMaxSize', - minProp: 'minHeight', - maxProp: 'maxHeight' - }, - east : { - orientation: Ext.SplitBar.HORIZONTAL, - placement: Ext.SplitBar.RIGHT, - maxFn : 'getHMaxSize', - minProp: 'minWidth', - maxProp: 'maxWidth' - }, - west : { - orientation: Ext.SplitBar.HORIZONTAL, - placement: Ext.SplitBar.LEFT, - maxFn : 'getHMaxSize', - minProp: 'minWidth', - maxProp: 'maxWidth' - } - }, - - - applyFns : { - west : function(box){ - if(this.isCollapsed){ - return this.applyLayoutCollapsed(box); - } - var sd = this.splitEl.dom, s = sd.style; - this.panel.setPosition(box.x, box.y); - var sw = sd.offsetWidth; - s.left = (box.x+box.width-sw)+'px'; - s.top = (box.y)+'px'; - s.height = Math.max(0, box.height)+'px'; - this.panel.setSize(box.width-sw, box.height); - }, - east : function(box){ - if(this.isCollapsed){ - return this.applyLayoutCollapsed(box); - } - var sd = this.splitEl.dom, s = sd.style; - var sw = sd.offsetWidth; - this.panel.setPosition(box.x+sw, box.y); - s.left = (box.x)+'px'; - s.top = (box.y)+'px'; - s.height = Math.max(0, box.height)+'px'; - this.panel.setSize(box.width-sw, box.height); - }, - north : function(box){ - if(this.isCollapsed){ - return this.applyLayoutCollapsed(box); - } - var sd = this.splitEl.dom, s = sd.style; - var sh = sd.offsetHeight; - this.panel.setPosition(box.x, box.y); - s.left = (box.x)+'px'; - s.top = (box.y+box.height-sh)+'px'; - s.width = Math.max(0, box.width)+'px'; - this.panel.setSize(box.width, box.height-sh); - }, - south : function(box){ - if(this.isCollapsed){ - return this.applyLayoutCollapsed(box); - } - var sd = this.splitEl.dom, s = sd.style; - var sh = sd.offsetHeight; - this.panel.setPosition(box.x, box.y+sh); - s.left = (box.x)+'px'; - s.top = (box.y)+'px'; - s.width = Math.max(0, box.width)+'px'; - this.panel.setSize(box.width, box.height-sh); - } - }, - - - render : function(ct, p){ - Ext.layout.BorderLayout.SplitRegion.superclass.render.call(this, ct, p); - - var ps = this.position; - - this.splitEl = ct.createChild({ - cls: "x-layout-split x-layout-split-"+ps, html: " ", - id: this.panel.id + '-xsplit' - }); - - if(this.collapseMode == 'mini'){ - this.miniSplitEl = this.splitEl.createChild({ - cls: "x-layout-mini x-layout-mini-"+ps, html: " " - }); - this.miniSplitEl.addClassOnOver('x-layout-mini-over'); - this.miniSplitEl.on('click', this.onCollapseClick, this, {stopEvent:true}); - } - - var s = this.splitSettings[ps]; - - this.split = new Ext.SplitBar(this.splitEl.dom, p.el, s.orientation); - this.split.tickSize = this.tickSize; - this.split.placement = s.placement; - this.split.getMaximumSize = this[s.maxFn].createDelegate(this); - this.split.minSize = this.minSize || this[s.minProp]; - this.split.on("beforeapply", this.onSplitMove, this); - this.split.useShim = this.useShim === true; - this.maxSize = this.maxSize || this[s.maxProp]; - - if(p.hidden){ - this.splitEl.hide(); - } - - if(this.useSplitTips){ - this.splitEl.dom.title = this.collapsible ? this.collapsibleSplitTip : this.splitTip; - } - if(this.collapsible){ - this.splitEl.on("dblclick", this.onCollapseClick, this); - } - }, - - - getSize : function(){ - if(this.isCollapsed){ - return this.collapsedEl.getSize(); - } - var s = this.panel.getSize(); - if(this.position == 'north' || this.position == 'south'){ - s.height += this.splitEl.dom.offsetHeight; - }else{ - s.width += this.splitEl.dom.offsetWidth; - } - return s; - }, - - - getHMaxSize : function(){ - var cmax = this.maxSize || 10000; - var center = this.layout.center; - return Math.min(cmax, (this.el.getWidth()+center.el.getWidth())-center.getMinWidth()); - }, - - - getVMaxSize : function(){ - var cmax = this.maxSize || 10000; - var center = this.layout.center; - return Math.min(cmax, (this.el.getHeight()+center.el.getHeight())-center.getMinHeight()); - }, - - - onSplitMove : function(split, newSize){ - var s = this.panel.getSize(); - this.lastSplitSize = newSize; - if(this.position == 'north' || this.position == 'south'){ - this.panel.setSize(s.width, newSize); - this.state.height = newSize; - }else{ - this.panel.setSize(newSize, s.height); - this.state.width = newSize; - } - this.layout.layout(); - this.panel.saveState(); - return false; - }, - - - getSplitBar : function(){ - return this.split; - }, - - - destroy : function() { - Ext.destroy(this.miniSplitEl, this.split, this.splitEl); - Ext.layout.BorderLayout.SplitRegion.superclass.destroy.call(this); - } -}); - -Ext.Container.LAYOUTS['border'] = Ext.layout.BorderLayout; - -Ext.layout.FormLayout = Ext.extend(Ext.layout.AnchorLayout, { - - - labelSeparator : ':', - - - - - trackLabels: true, - - type: 'form', - - onRemove: function(c){ - Ext.layout.FormLayout.superclass.onRemove.call(this, c); - if(this.trackLabels){ - c.un('show', this.onFieldShow, this); - c.un('hide', this.onFieldHide, this); - } - - var el = c.getPositionEl(), - ct = c.getItemCt && c.getItemCt(); - if (c.rendered && ct) { - if (el && el.dom) { - el.insertAfter(ct); - } - Ext.destroy(ct); - Ext.destroyMembers(c, 'label', 'itemCt'); - if (c.customItemCt) { - Ext.destroyMembers(c, 'getItemCt', 'customItemCt'); - } - } - }, - - - setContainer : function(ct){ - Ext.layout.FormLayout.superclass.setContainer.call(this, ct); - if(ct.labelAlign){ - ct.addClass('x-form-label-'+ct.labelAlign); - } - - if(ct.hideLabels){ - Ext.apply(this, { - labelStyle: 'display:none', - elementStyle: 'padding-left:0;', - labelAdjust: 0 - }); - }else{ - this.labelSeparator = Ext.isDefined(ct.labelSeparator) ? ct.labelSeparator : this.labelSeparator; - ct.labelWidth = ct.labelWidth || 100; - if(Ext.isNumber(ct.labelWidth)){ - var pad = Ext.isNumber(ct.labelPad) ? ct.labelPad : 5; - Ext.apply(this, { - labelAdjust: ct.labelWidth + pad, - labelStyle: 'width:' + ct.labelWidth + 'px;', - elementStyle: 'padding-left:' + (ct.labelWidth + pad) + 'px' - }); - } - if(ct.labelAlign == 'top'){ - Ext.apply(this, { - labelStyle: 'width:auto;', - labelAdjust: 0, - elementStyle: 'padding-left:0;' - }); - } - } - }, - - - isHide: function(c){ - return c.hideLabel || this.container.hideLabels; - }, - - onFieldShow: function(c){ - c.getItemCt().removeClass('x-hide-' + c.hideMode); - - - if (c.isComposite) { - c.doLayout(); - } - }, - - onFieldHide: function(c){ - c.getItemCt().addClass('x-hide-' + c.hideMode); - }, - - - getLabelStyle: function(s){ - var ls = '', items = [this.labelStyle, s]; - for (var i = 0, len = items.length; i < len; ++i){ - if (items[i]){ - ls += items[i]; - if (ls.substr(-1, 1) != ';'){ - ls += ';'; - } - } - } - return ls; - }, - - - - - renderItem : function(c, position, target){ - if(c && (c.isFormField || c.fieldLabel) && c.inputType != 'hidden'){ - var args = this.getTemplateArgs(c); - if(Ext.isNumber(position)){ - position = target.dom.childNodes[position] || null; - } - if(position){ - c.itemCt = this.fieldTpl.insertBefore(position, args, true); - }else{ - c.itemCt = this.fieldTpl.append(target, args, true); - } - if(!c.getItemCt){ - - - Ext.apply(c, { - getItemCt: function(){ - return c.itemCt; - }, - customItemCt: true - }); - } - c.label = c.getItemCt().child('label.x-form-item-label'); - if(!c.rendered){ - c.render('x-form-el-' + c.id); - }else if(!this.isValidParent(c, target)){ - Ext.fly('x-form-el-' + c.id).appendChild(c.getPositionEl()); - } - if(this.trackLabels){ - if(c.hidden){ - this.onFieldHide(c); - } - c.on({ - scope: this, - show: this.onFieldShow, - hide: this.onFieldHide - }); - } - this.configureItem(c); - }else { - Ext.layout.FormLayout.superclass.renderItem.apply(this, arguments); - } - }, - - - getTemplateArgs: function(field) { - var noLabelSep = !field.fieldLabel || field.hideLabel, - itemCls = (field.itemCls || this.container.itemCls || '') + (field.hideLabel ? ' x-hide-label' : ''); - - - if (Ext.isIE9 && Ext.isIEQuirks && field instanceof Ext.form.TextField) { - itemCls += ' x-input-wrapper'; - } - - return { - id : field.id, - label : field.fieldLabel, - itemCls : itemCls, - clearCls : field.clearCls || 'x-form-clear-left', - labelStyle : this.getLabelStyle(field.labelStyle), - elementStyle : this.elementStyle || '', - labelSeparator: noLabelSep ? '' : (Ext.isDefined(field.labelSeparator) ? field.labelSeparator : this.labelSeparator) - }; - }, - - - adjustWidthAnchor: function(value, c){ - if(c.label && !this.isHide(c) && (this.container.labelAlign != 'top')){ - var adjust = Ext.isIE6 || (Ext.isIE && !Ext.isStrict); - return value - this.labelAdjust + (adjust ? -3 : 0); - } - return value; - }, - - adjustHeightAnchor : function(value, c){ - if(c.label && !this.isHide(c) && (this.container.labelAlign == 'top')){ - return value - c.label.getHeight(); - } - return value; - }, - - - isValidParent : function(c, target){ - return target && this.container.getEl().contains(c.getPositionEl()); - } - - -}); - -Ext.Container.LAYOUTS['form'] = Ext.layout.FormLayout; - -Ext.layout.AccordionLayout = Ext.extend(Ext.layout.FitLayout, { - - fill : true, - - autoWidth : true, - - titleCollapse : true, - - hideCollapseTool : false, - - collapseFirst : false, - - animate : false, - - sequence : false, - - activeOnTop : false, - - type: 'accordion', - - renderItem : function(c){ - if(this.animate === false){ - c.animCollapse = false; - } - c.collapsible = true; - if(this.autoWidth){ - c.autoWidth = true; - } - if(this.titleCollapse){ - c.titleCollapse = true; - } - if(this.hideCollapseTool){ - c.hideCollapseTool = true; - } - if(this.collapseFirst !== undefined){ - c.collapseFirst = this.collapseFirst; - } - if(!this.activeItem && !c.collapsed){ - this.setActiveItem(c, true); - }else if(this.activeItem && this.activeItem != c){ - c.collapsed = true; - } - Ext.layout.AccordionLayout.superclass.renderItem.apply(this, arguments); - c.header.addClass('x-accordion-hd'); - c.on('beforeexpand', this.beforeExpand, this); - }, - - onRemove: function(c){ - Ext.layout.AccordionLayout.superclass.onRemove.call(this, c); - if(c.rendered){ - c.header.removeClass('x-accordion-hd'); - } - c.un('beforeexpand', this.beforeExpand, this); - }, - - - beforeExpand : function(p, anim){ - var ai = this.activeItem; - if(ai){ - if(this.sequence){ - delete this.activeItem; - if (!ai.collapsed){ - ai.collapse({callback:function(){ - p.expand(anim || true); - }, scope: this}); - return false; - } - }else{ - ai.collapse(this.animate); - } - } - this.setActive(p); - if(this.activeOnTop){ - p.el.dom.parentNode.insertBefore(p.el.dom, p.el.dom.parentNode.firstChild); - } - - this.layout(); - }, - - - setItemSize : function(item, size){ - if(this.fill && item){ - var hh = 0, i, ct = this.getRenderedItems(this.container), len = ct.length, p; - - for (i = 0; i < len; i++) { - if((p = ct[i]) != item && !p.hidden){ - hh += p.header.getHeight(); - } - }; - - size.height -= hh; - - - item.setSize(size); - } - }, - - - setActiveItem : function(item){ - this.setActive(item, true); - }, - - - setActive : function(item, expand){ - var ai = this.activeItem; - item = this.container.getComponent(item); - if(ai != item){ - if(item.rendered && item.collapsed && expand){ - item.expand(); - }else{ - if(ai){ - ai.fireEvent('deactivate', ai); - } - this.activeItem = item; - item.fireEvent('activate', item); - } - } - } -}); -Ext.Container.LAYOUTS.accordion = Ext.layout.AccordionLayout; - - -Ext.layout.Accordion = Ext.layout.AccordionLayout; -Ext.layout.TableLayout = Ext.extend(Ext.layout.ContainerLayout, { - - - - monitorResize:false, - - type: 'table', - - targetCls: 'x-table-layout-ct', - - - tableAttrs:null, - - - setContainer : function(ct){ - Ext.layout.TableLayout.superclass.setContainer.call(this, ct); - - this.currentRow = 0; - this.currentColumn = 0; - this.cells = []; - }, - - - onLayout : function(ct, target){ - var cs = ct.items.items, len = cs.length, c, i; - - if(!this.table){ - target.addClass('x-table-layout-ct'); - - this.table = target.createChild( - Ext.apply({tag:'table', cls:'x-table-layout', cellspacing: 0, cn: {tag: 'tbody'}}, this.tableAttrs), null, true); - } - this.renderAll(ct, target); - }, - - - getRow : function(index){ - var row = this.table.tBodies[0].childNodes[index]; - if(!row){ - row = document.createElement('tr'); - this.table.tBodies[0].appendChild(row); - } - return row; - }, - - - getNextCell : function(c){ - var cell = this.getNextNonSpan(this.currentColumn, this.currentRow); - var curCol = this.currentColumn = cell[0], curRow = this.currentRow = cell[1]; - for(var rowIndex = curRow; rowIndex < curRow + (c.rowspan || 1); rowIndex++){ - if(!this.cells[rowIndex]){ - this.cells[rowIndex] = []; - } - for(var colIndex = curCol; colIndex < curCol + (c.colspan || 1); colIndex++){ - this.cells[rowIndex][colIndex] = true; - } - } - var td = document.createElement('td'); - if(c.cellId){ - td.id = c.cellId; - } - var cls = 'x-table-layout-cell'; - if(c.cellCls){ - cls += ' ' + c.cellCls; - } - td.className = cls; - if(c.colspan){ - td.colSpan = c.colspan; - } - if(c.rowspan){ - td.rowSpan = c.rowspan; - } - this.getRow(curRow).appendChild(td); - return td; - }, - - - getNextNonSpan: function(colIndex, rowIndex){ - var cols = this.columns; - while((cols && colIndex >= cols) || (this.cells[rowIndex] && this.cells[rowIndex][colIndex])) { - if(cols && colIndex >= cols){ - rowIndex++; - colIndex = 0; - }else{ - colIndex++; - } - } - return [colIndex, rowIndex]; - }, - - - renderItem : function(c, position, target){ - - if(!this.table){ - this.table = target.createChild( - Ext.apply({tag:'table', cls:'x-table-layout', cellspacing: 0, cn: {tag: 'tbody'}}, this.tableAttrs), null, true); - } - if(c && !c.rendered){ - c.render(this.getNextCell(c)); - this.configureItem(c); - }else if(c && !this.isValidParent(c, target)){ - var container = this.getNextCell(c); - container.insertBefore(c.getPositionEl().dom, null); - c.container = Ext.get(container); - this.configureItem(c); - } - }, - - - isValidParent : function(c, target){ - return c.getPositionEl().up('table', 5).dom.parentNode === (target.dom || target); - }, - - destroy: function(){ - delete this.table; - Ext.layout.TableLayout.superclass.destroy.call(this); - } - - -}); - -Ext.Container.LAYOUTS['table'] = Ext.layout.TableLayout; -Ext.layout.AbsoluteLayout = Ext.extend(Ext.layout.AnchorLayout, { - - extraCls: 'x-abs-layout-item', - - type: 'absolute', - - onLayout : function(ct, target){ - target.position(); - this.paddingLeft = target.getPadding('l'); - this.paddingTop = target.getPadding('t'); - Ext.layout.AbsoluteLayout.superclass.onLayout.call(this, ct, target); - }, - - - adjustWidthAnchor : function(value, comp){ - return value ? value - comp.getPosition(true)[0] + this.paddingLeft : value; - }, - - - adjustHeightAnchor : function(value, comp){ - return value ? value - comp.getPosition(true)[1] + this.paddingTop : value; - } - -}); -Ext.Container.LAYOUTS['absolute'] = Ext.layout.AbsoluteLayout; - -Ext.layout.BoxLayout = Ext.extend(Ext.layout.ContainerLayout, { - - defaultMargins : {left:0,top:0,right:0,bottom:0}, - - padding : '0', - - pack : 'start', - - - monitorResize : true, - type: 'box', - scrollOffset : 0, - extraCls : 'x-box-item', - targetCls : 'x-box-layout-ct', - innerCls : 'x-box-inner', - - constructor : function(config){ - Ext.layout.BoxLayout.superclass.constructor.call(this, config); - - if (Ext.isString(this.defaultMargins)) { - this.defaultMargins = this.parseMargins(this.defaultMargins); - } - - var handler = this.overflowHandler; - - if (typeof handler == 'string') { - handler = { - type: handler - }; - } - - var handlerType = 'none'; - if (handler && handler.type != undefined) { - handlerType = handler.type; - } - - var constructor = Ext.layout.boxOverflow[handlerType]; - if (constructor[this.type]) { - constructor = constructor[this.type]; - } - - this.overflowHandler = new constructor(this, handler); - }, - - - onLayout: function(container, target) { - Ext.layout.BoxLayout.superclass.onLayout.call(this, container, target); - - var tSize = this.getLayoutTargetSize(), - items = this.getVisibleItems(container), - calcs = this.calculateChildBoxes(items, tSize), - boxes = calcs.boxes, - meta = calcs.meta; - - - if (tSize.width > 0) { - var handler = this.overflowHandler, - method = meta.tooNarrow ? 'handleOverflow' : 'clearOverflow'; - - var results = handler[method](calcs, tSize); - - if (results) { - if (results.targetSize) { - tSize = results.targetSize; - } - - if (results.recalculate) { - items = this.getVisibleItems(container); - calcs = this.calculateChildBoxes(items, tSize); - boxes = calcs.boxes; - } - } - } - - - this.layoutTargetLastSize = tSize; - - - this.childBoxCache = calcs; - - this.updateInnerCtSize(tSize, calcs); - this.updateChildBoxes(boxes); - - - this.handleTargetOverflow(tSize, container, target); - }, - - - updateChildBoxes: function(boxes) { - for (var i = 0, length = boxes.length; i < length; i++) { - var box = boxes[i], - comp = box.component; - - if (box.dirtySize) { - comp.setSize(box.width, box.height); - } - - if (isNaN(box.left) || isNaN(box.top)) { - continue; - } - - comp.setPosition(box.left, box.top); - } - }, - - - updateInnerCtSize: function(tSize, calcs) { - var align = this.align, - padding = this.padding, - width = tSize.width, - height = tSize.height; - - if (this.type == 'hbox') { - var innerCtWidth = width, - innerCtHeight = calcs.meta.maxHeight + padding.top + padding.bottom; - - if (align == 'stretch') { - innerCtHeight = height; - } else if (align == 'middle') { - innerCtHeight = Math.max(height, innerCtHeight); - } - } else { - var innerCtHeight = height, - innerCtWidth = calcs.meta.maxWidth + padding.left + padding.right; - - if (align == 'stretch') { - innerCtWidth = width; - } else if (align == 'center') { - innerCtWidth = Math.max(width, innerCtWidth); - } - } - - this.innerCt.setSize(innerCtWidth || undefined, innerCtHeight || undefined); - }, - - - handleTargetOverflow: function(previousTargetSize, container, target) { - var overflow = target.getStyle('overflow'); - - if (overflow && overflow != 'hidden' &&!this.adjustmentPass) { - var newTargetSize = this.getLayoutTargetSize(); - if (newTargetSize.width != previousTargetSize.width || newTargetSize.height != previousTargetSize.height){ - this.adjustmentPass = true; - this.onLayout(container, target); - } - } - - delete this.adjustmentPass; - }, - - - isValidParent : function(c, target) { - return this.innerCt && c.getPositionEl().dom.parentNode == this.innerCt.dom; - }, - - - getVisibleItems: function(ct) { - var ct = ct || this.container, - t = ct.getLayoutTarget(), - cti = ct.items.items, - len = cti.length, - - i, c, items = []; - - for (i = 0; i < len; i++) { - if((c = cti[i]).rendered && this.isValidParent(c, t) && c.hidden !== true && c.collapsed !== true && c.shouldLayout !== false){ - items.push(c); - } - } - - return items; - }, - - - renderAll : function(ct, target) { - if (!this.innerCt) { - - this.innerCt = target.createChild({cls:this.innerCls}); - this.padding = this.parseMargins(this.padding); - } - Ext.layout.BoxLayout.superclass.renderAll.call(this, ct, this.innerCt); - }, - - getLayoutTargetSize : function() { - var target = this.container.getLayoutTarget(), ret; - - if (target) { - ret = target.getViewSize(); - - - - - if (Ext.isIE && Ext.isStrict && ret.width == 0){ - ret = target.getStyleSize(); - } - - ret.width -= target.getPadding('lr'); - ret.height -= target.getPadding('tb'); - } - - return ret; - }, - - - renderItem : function(c) { - if(Ext.isString(c.margins)){ - c.margins = this.parseMargins(c.margins); - }else if(!c.margins){ - c.margins = this.defaultMargins; - } - Ext.layout.BoxLayout.superclass.renderItem.apply(this, arguments); - }, - - - destroy: function() { - Ext.destroy(this.overflowHandler); - - Ext.layout.BoxLayout.superclass.destroy.apply(this, arguments); - } -}); - - - -Ext.layout.boxOverflow.None = Ext.extend(Object, { - constructor: function(layout, config) { - this.layout = layout; - - Ext.apply(this, config || {}); - }, - - handleOverflow: Ext.emptyFn, - - clearOverflow: Ext.emptyFn -}); - - -Ext.layout.boxOverflow.none = Ext.layout.boxOverflow.None; - -Ext.layout.boxOverflow.Menu = Ext.extend(Ext.layout.boxOverflow.None, { - - afterCls: 'x-strip-right', - - - noItemsMenuText : '
        (None)
        ', - - constructor: function(layout) { - Ext.layout.boxOverflow.Menu.superclass.constructor.apply(this, arguments); - - - this.menuItems = []; - }, - - - createInnerElements: function() { - if (!this.afterCt) { - this.afterCt = this.layout.innerCt.insertSibling({cls: this.afterCls}, 'before'); - } - }, - - - clearOverflow: function(calculations, targetSize) { - var newWidth = targetSize.width + (this.afterCt ? this.afterCt.getWidth() : 0), - items = this.menuItems; - - this.hideTrigger(); - - for (var index = 0, length = items.length; index < length; index++) { - items.pop().component.show(); - } - - return { - targetSize: { - height: targetSize.height, - width : newWidth - } - }; - }, - - - showTrigger: function() { - this.createMenu(); - this.menuTrigger.show(); - }, - - - hideTrigger: function() { - if (this.menuTrigger != undefined) { - this.menuTrigger.hide(); - } - }, - - - beforeMenuShow: function(menu) { - var items = this.menuItems, - len = items.length, - item, - prev; - - var needsSep = function(group, item){ - return group.isXType('buttongroup') && !(item instanceof Ext.Toolbar.Separator); - }; - - this.clearMenu(); - menu.removeAll(); - - for (var i = 0; i < len; i++) { - item = items[i].component; - - if (prev && (needsSep(item, prev) || needsSep(prev, item))) { - menu.add('-'); - } - - this.addComponentToMenu(menu, item); - prev = item; - } - - - if (menu.items.length < 1) { - menu.add(this.noItemsMenuText); - } - }, - - - createMenuConfig : function(component, hideOnClick){ - var config = Ext.apply({}, component.initialConfig), - group = component.toggleGroup; - - Ext.copyTo(config, component, [ - 'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu' - ]); - - Ext.apply(config, { - text : component.overflowText || component.text, - hideOnClick: hideOnClick - }); - - if (group || component.enableToggle) { - Ext.apply(config, { - group : group, - checked: component.pressed, - listeners: { - checkchange: function(item, checked){ - component.toggle(checked); - } - } - }); - } - - delete config.ownerCt; - delete config.xtype; - delete config.id; - - return config; - }, - - - addComponentToMenu : function(menu, component) { - if (component instanceof Ext.Toolbar.Separator) { - menu.add('-'); - - } else if (Ext.isFunction(component.isXType)) { - if (component.isXType('splitbutton')) { - menu.add(this.createMenuConfig(component, true)); - - } else if (component.isXType('button')) { - menu.add(this.createMenuConfig(component, !component.menu)); - - } else if (component.isXType('buttongroup')) { - component.items.each(function(item){ - this.addComponentToMenu(menu, item); - }, this); - } - } - }, - - - clearMenu : function(){ - var menu = this.moreMenu; - if (menu && menu.items) { - menu.items.each(function(item){ - delete item.menu; - }); - } - }, - - - createMenu: function() { - if (!this.menuTrigger) { - this.createInnerElements(); - - - this.menu = new Ext.menu.Menu({ - ownerCt : this.layout.container, - listeners: { - scope: this, - beforeshow: this.beforeMenuShow - } - }); - - - this.menuTrigger = new Ext.Button({ - iconCls : 'x-toolbar-more-icon', - cls : 'x-toolbar-more', - menu : this.menu, - renderTo: this.afterCt - }); - } - }, - - - destroy: function() { - Ext.destroy(this.menu, this.menuTrigger); - } -}); - -Ext.layout.boxOverflow.menu = Ext.layout.boxOverflow.Menu; - - - -Ext.layout.boxOverflow.HorizontalMenu = Ext.extend(Ext.layout.boxOverflow.Menu, { - - constructor: function() { - Ext.layout.boxOverflow.HorizontalMenu.superclass.constructor.apply(this, arguments); - - var me = this, - layout = me.layout, - origFunction = layout.calculateChildBoxes; - - layout.calculateChildBoxes = function(visibleItems, targetSize) { - var calcs = origFunction.apply(layout, arguments), - meta = calcs.meta, - items = me.menuItems; - - - - var hiddenWidth = 0; - for (var index = 0, length = items.length; index < length; index++) { - hiddenWidth += items[index].width; - } - - meta.minimumWidth += hiddenWidth; - meta.tooNarrow = meta.minimumWidth > targetSize.width; - - return calcs; - }; - }, - - handleOverflow: function(calculations, targetSize) { - this.showTrigger(); - - var newWidth = targetSize.width - this.afterCt.getWidth(), - boxes = calculations.boxes, - usedWidth = 0, - recalculate = false; - - - for (var index = 0, length = boxes.length; index < length; index++) { - usedWidth += boxes[index].width; - } - - var spareWidth = newWidth - usedWidth, - showCount = 0; - - - for (var index = 0, length = this.menuItems.length; index < length; index++) { - var hidden = this.menuItems[index], - comp = hidden.component, - width = hidden.width; - - if (width < spareWidth) { - comp.show(); - - spareWidth -= width; - showCount ++; - recalculate = true; - } else { - break; - } - } - - if (recalculate) { - this.menuItems = this.menuItems.slice(showCount); - } else { - for (var i = boxes.length - 1; i >= 0; i--) { - var item = boxes[i].component, - right = boxes[i].left + boxes[i].width; - - if (right >= newWidth) { - this.menuItems.unshift({ - component: item, - width : boxes[i].width - }); - - item.hide(); - } else { - break; - } - } - } - - if (this.menuItems.length == 0) { - this.hideTrigger(); - } - - return { - targetSize: { - height: targetSize.height, - width : newWidth - }, - recalculate: recalculate - }; - } -}); - -Ext.layout.boxOverflow.menu.hbox = Ext.layout.boxOverflow.HorizontalMenu; -Ext.layout.boxOverflow.Scroller = Ext.extend(Ext.layout.boxOverflow.None, { - - animateScroll: true, - - - scrollIncrement: 100, - - - wheelIncrement: 3, - - - scrollRepeatInterval: 400, - - - scrollDuration: 0.4, - - - beforeCls: 'x-strip-left', - - - afterCls: 'x-strip-right', - - - scrollerCls: 'x-strip-scroller', - - - beforeScrollerCls: 'x-strip-scroller-left', - - - afterScrollerCls: 'x-strip-scroller-right', - - - createWheelListener: function() { - this.layout.innerCt.on({ - scope : this, - mousewheel: function(e) { - e.stopEvent(); - - this.scrollBy(e.getWheelDelta() * this.wheelIncrement * -1, false); - } - }); - }, - - - handleOverflow: function(calculations, targetSize) { - this.createInnerElements(); - this.showScrollers(); - }, - - - clearOverflow: function() { - this.hideScrollers(); - }, - - - showScrollers: function() { - this.createScrollers(); - - this.beforeScroller.show(); - this.afterScroller.show(); - - this.updateScrollButtons(); - }, - - - hideScrollers: function() { - if (this.beforeScroller != undefined) { - this.beforeScroller.hide(); - this.afterScroller.hide(); - } - }, - - - createScrollers: function() { - if (!this.beforeScroller && !this.afterScroller) { - var before = this.beforeCt.createChild({ - cls: String.format("{0} {1} ", this.scrollerCls, this.beforeScrollerCls) - }); - - var after = this.afterCt.createChild({ - cls: String.format("{0} {1}", this.scrollerCls, this.afterScrollerCls) - }); - - before.addClassOnOver(this.beforeScrollerCls + '-hover'); - after.addClassOnOver(this.afterScrollerCls + '-hover'); - - before.setVisibilityMode(Ext.Element.DISPLAY); - after.setVisibilityMode(Ext.Element.DISPLAY); - - this.beforeRepeater = new Ext.util.ClickRepeater(before, { - interval: this.scrollRepeatInterval, - handler : this.scrollLeft, - scope : this - }); - - this.afterRepeater = new Ext.util.ClickRepeater(after, { - interval: this.scrollRepeatInterval, - handler : this.scrollRight, - scope : this - }); - - - this.beforeScroller = before; - - - this.afterScroller = after; - } - }, - - - destroy: function() { - Ext.destroy(this.beforeScroller, this.afterScroller, this.beforeRepeater, this.afterRepeater, this.beforeCt, this.afterCt); - }, - - - scrollBy: function(delta, animate) { - this.scrollTo(this.getScrollPosition() + delta, animate); - }, - - - getItem: function(item) { - if (Ext.isString(item)) { - item = Ext.getCmp(item); - } else if (Ext.isNumber(item)) { - item = this.items[item]; - } - - return item; - }, - - - getScrollAnim: function() { - return { - duration: this.scrollDuration, - callback: this.updateScrollButtons, - scope : this - }; - }, - - - updateScrollButtons: function() { - if (this.beforeScroller == undefined || this.afterScroller == undefined) { - return; - } - - var beforeMeth = this.atExtremeBefore() ? 'addClass' : 'removeClass', - afterMeth = this.atExtremeAfter() ? 'addClass' : 'removeClass', - beforeCls = this.beforeScrollerCls + '-disabled', - afterCls = this.afterScrollerCls + '-disabled'; - - this.beforeScroller[beforeMeth](beforeCls); - this.afterScroller[afterMeth](afterCls); - this.scrolling = false; - }, - - - atExtremeBefore: function() { - return this.getScrollPosition() === 0; - }, - - - scrollLeft: function(animate) { - this.scrollBy(-this.scrollIncrement, animate); - }, - - - scrollRight: function(animate) { - this.scrollBy(this.scrollIncrement, animate); - }, - - - scrollToItem: function(item, animate) { - item = this.getItem(item); - - if (item != undefined) { - var visibility = this.getItemVisibility(item); - - if (!visibility.fullyVisible) { - var box = item.getBox(true, true), - newX = box.x; - - if (visibility.hiddenRight) { - newX -= (this.layout.innerCt.getWidth() - box.width); - } - - this.scrollTo(newX, animate); - } - } - }, - - - getItemVisibility: function(item) { - var box = this.getItem(item).getBox(true, true), - itemLeft = box.x, - itemRight = box.x + box.width, - scrollLeft = this.getScrollPosition(), - scrollRight = this.layout.innerCt.getWidth() + scrollLeft; - - return { - hiddenLeft : itemLeft < scrollLeft, - hiddenRight : itemRight > scrollRight, - fullyVisible: itemLeft > scrollLeft && itemRight < scrollRight - }; - } -}); - -Ext.layout.boxOverflow.scroller = Ext.layout.boxOverflow.Scroller; - - - -Ext.layout.boxOverflow.VerticalScroller = Ext.extend(Ext.layout.boxOverflow.Scroller, { - scrollIncrement: 75, - wheelIncrement : 2, - - handleOverflow: function(calculations, targetSize) { - Ext.layout.boxOverflow.VerticalScroller.superclass.handleOverflow.apply(this, arguments); - - return { - targetSize: { - height: targetSize.height - (this.beforeCt.getHeight() + this.afterCt.getHeight()), - width : targetSize.width - } - }; - }, - - - createInnerElements: function() { - var target = this.layout.innerCt; - - - - if (!this.beforeCt) { - this.beforeCt = target.insertSibling({cls: this.beforeCls}, 'before'); - this.afterCt = target.insertSibling({cls: this.afterCls}, 'after'); - - this.createWheelListener(); - } - }, - - - scrollTo: function(position, animate) { - var oldPosition = this.getScrollPosition(), - newPosition = position.constrain(0, this.getMaxScrollBottom()); - - if (newPosition != oldPosition && !this.scrolling) { - if (animate == undefined) { - animate = this.animateScroll; - } - - this.layout.innerCt.scrollTo('top', newPosition, animate ? this.getScrollAnim() : false); - - if (animate) { - this.scrolling = true; - } else { - this.scrolling = false; - this.updateScrollButtons(); - } - } - }, - - - getScrollPosition: function(){ - return parseInt(this.layout.innerCt.dom.scrollTop, 10) || 0; - }, - - - getMaxScrollBottom: function() { - return this.layout.innerCt.dom.scrollHeight - this.layout.innerCt.getHeight(); - }, - - - atExtremeAfter: function() { - return this.getScrollPosition() >= this.getMaxScrollBottom(); - } -}); - -Ext.layout.boxOverflow.scroller.vbox = Ext.layout.boxOverflow.VerticalScroller; - - - -Ext.layout.boxOverflow.HorizontalScroller = Ext.extend(Ext.layout.boxOverflow.Scroller, { - handleOverflow: function(calculations, targetSize) { - Ext.layout.boxOverflow.HorizontalScroller.superclass.handleOverflow.apply(this, arguments); - - return { - targetSize: { - height: targetSize.height, - width : targetSize.width - (this.beforeCt.getWidth() + this.afterCt.getWidth()) - } - }; - }, - - - createInnerElements: function() { - var target = this.layout.innerCt; - - - - if (!this.beforeCt) { - this.afterCt = target.insertSibling({cls: this.afterCls}, 'before'); - this.beforeCt = target.insertSibling({cls: this.beforeCls}, 'before'); - - this.createWheelListener(); - } - }, - - - scrollTo: function(position, animate) { - var oldPosition = this.getScrollPosition(), - newPosition = position.constrain(0, this.getMaxScrollRight()); - - if (newPosition != oldPosition && !this.scrolling) { - if (animate == undefined) { - animate = this.animateScroll; - } - - this.layout.innerCt.scrollTo('left', newPosition, animate ? this.getScrollAnim() : false); - - if (animate) { - this.scrolling = true; - } else { - this.scrolling = false; - this.updateScrollButtons(); - } - } - }, - - - getScrollPosition: function(){ - return parseInt(this.layout.innerCt.dom.scrollLeft, 10) || 0; - }, - - - getMaxScrollRight: function() { - return this.layout.innerCt.dom.scrollWidth - this.layout.innerCt.getWidth(); - }, - - - atExtremeAfter: function() { - return this.getScrollPosition() >= this.getMaxScrollRight(); - } -}); - -Ext.layout.boxOverflow.scroller.hbox = Ext.layout.boxOverflow.HorizontalScroller; -Ext.layout.HBoxLayout = Ext.extend(Ext.layout.BoxLayout, { - - align: 'top', - - type : 'hbox', - - - - - - calculateChildBoxes: function(visibleItems, targetSize) { - var visibleCount = visibleItems.length, - - padding = this.padding, - topOffset = padding.top, - leftOffset = padding.left, - paddingVert = topOffset + padding.bottom, - paddingHoriz = leftOffset + padding.right, - - width = targetSize.width - this.scrollOffset, - height = targetSize.height, - availHeight = Math.max(0, height - paddingVert), - - isStart = this.pack == 'start', - isCenter = this.pack == 'center', - isEnd = this.pack == 'end', - - nonFlexWidth = 0, - maxHeight = 0, - totalFlex = 0, - desiredWidth = 0, - minimumWidth = 0, - - - boxes = [], - - - child, childWidth, childHeight, childSize, childMargins, canLayout, i, calcs, flexedWidth, - horizMargins, vertMargins, stretchHeight; - - - for (i = 0; i < visibleCount; i++) { - child = visibleItems[i]; - childHeight = child.height; - childWidth = child.width; - canLayout = !child.hasLayout && typeof child.doLayout == 'function'; - - - if (typeof childWidth != 'number') { - - - if (child.flex && !childWidth) { - totalFlex += child.flex; - - - } else { - - - if (!childWidth && canLayout) { - child.doLayout(); - } - - childSize = child.getSize(); - childWidth = childSize.width; - childHeight = childSize.height; - } - } - - childMargins = child.margins; - horizMargins = childMargins.left + childMargins.right; - - nonFlexWidth += horizMargins + (childWidth || 0); - desiredWidth += horizMargins + (child.flex ? child.minWidth || 0 : childWidth); - minimumWidth += horizMargins + (child.minWidth || childWidth || 0); - - - if (typeof childHeight != 'number') { - if (canLayout) { - child.doLayout(); - } - childHeight = child.getHeight(); - } - - maxHeight = Math.max(maxHeight, childHeight + childMargins.top + childMargins.bottom); - - - boxes.push({ - component: child, - height : childHeight || undefined, - width : childWidth || undefined - }); - } - - var shortfall = desiredWidth - width, - tooNarrow = minimumWidth > width; - - - var availableWidth = Math.max(0, width - nonFlexWidth - paddingHoriz); - - if (tooNarrow) { - for (i = 0; i < visibleCount; i++) { - boxes[i].width = visibleItems[i].minWidth || visibleItems[i].width || boxes[i].width; - } - } else { - - - if (shortfall > 0) { - var minWidths = []; - - - for (var index = 0, length = visibleCount; index < length; index++) { - var item = visibleItems[index], - minWidth = item.minWidth || 0; - - - - if (item.flex) { - boxes[index].width = minWidth; - } else { - minWidths.push({ - minWidth : minWidth, - available: boxes[index].width - minWidth, - index : index - }); - } - } - - - minWidths.sort(function(a, b) { - return a.available > b.available ? 1 : -1; - }); - - - for (var i = 0, length = minWidths.length; i < length; i++) { - var itemIndex = minWidths[i].index; - - if (itemIndex == undefined) { - continue; - } - - var item = visibleItems[itemIndex], - box = boxes[itemIndex], - oldWidth = box.width, - minWidth = item.minWidth, - newWidth = Math.max(minWidth, oldWidth - Math.ceil(shortfall / (length - i))), - reduction = oldWidth - newWidth; - - boxes[itemIndex].width = newWidth; - shortfall -= reduction; - } - } else { - - var remainingWidth = availableWidth, - remainingFlex = totalFlex; - - - for (i = 0; i < visibleCount; i++) { - child = visibleItems[i]; - calcs = boxes[i]; - - childMargins = child.margins; - vertMargins = childMargins.top + childMargins.bottom; - - if (isStart && child.flex && !child.width) { - flexedWidth = Math.ceil((child.flex / remainingFlex) * remainingWidth); - remainingWidth -= flexedWidth; - remainingFlex -= child.flex; - - calcs.width = flexedWidth; - calcs.dirtySize = true; - } - } - } - } - - if (isCenter) { - leftOffset += availableWidth / 2; - } else if (isEnd) { - leftOffset += availableWidth; - } - - - for (i = 0; i < visibleCount; i++) { - child = visibleItems[i]; - calcs = boxes[i]; - - childMargins = child.margins; - leftOffset += childMargins.left; - vertMargins = childMargins.top + childMargins.bottom; - - calcs.left = leftOffset; - calcs.top = topOffset + childMargins.top; - - switch (this.align) { - case 'stretch': - stretchHeight = availHeight - vertMargins; - calcs.height = stretchHeight.constrain(child.minHeight || 0, child.maxHeight || 1000000); - calcs.dirtySize = true; - break; - case 'stretchmax': - stretchHeight = maxHeight - vertMargins; - calcs.height = stretchHeight.constrain(child.minHeight || 0, child.maxHeight || 1000000); - calcs.dirtySize = true; - break; - case 'middle': - var diff = availHeight - calcs.height - vertMargins; - if (diff > 0) { - calcs.top = topOffset + vertMargins + (diff / 2); - } - } - - leftOffset += calcs.width + childMargins.right; - } - - return { - boxes: boxes, - meta : { - maxHeight : maxHeight, - nonFlexWidth: nonFlexWidth, - desiredWidth: desiredWidth, - minimumWidth: minimumWidth, - shortfall : desiredWidth - width, - tooNarrow : tooNarrow - } - }; - } -}); - -Ext.Container.LAYOUTS.hbox = Ext.layout.HBoxLayout; -Ext.layout.VBoxLayout = Ext.extend(Ext.layout.BoxLayout, { - - align : 'left', - type: 'vbox', - - - - - - - calculateChildBoxes: function(visibleItems, targetSize) { - var visibleCount = visibleItems.length, - - padding = this.padding, - topOffset = padding.top, - leftOffset = padding.left, - paddingVert = topOffset + padding.bottom, - paddingHoriz = leftOffset + padding.right, - - width = targetSize.width - this.scrollOffset, - height = targetSize.height, - availWidth = Math.max(0, width - paddingHoriz), - - isStart = this.pack == 'start', - isCenter = this.pack == 'center', - isEnd = this.pack == 'end', - - nonFlexHeight= 0, - maxWidth = 0, - totalFlex = 0, - desiredHeight= 0, - minimumHeight= 0, - - - boxes = [], - - - child, childWidth, childHeight, childSize, childMargins, canLayout, i, calcs, flexedHeight, - horizMargins, vertMargins, stretchWidth, length; - - - for (i = 0; i < visibleCount; i++) { - child = visibleItems[i]; - childHeight = child.height; - childWidth = child.width; - canLayout = !child.hasLayout && typeof child.doLayout == 'function'; - - - if (typeof childHeight != 'number') { - - - if (child.flex && !childHeight) { - totalFlex += child.flex; - - - } else { - - - if (!childHeight && canLayout) { - child.doLayout(); - } - - childSize = child.getSize(); - childWidth = childSize.width; - childHeight = childSize.height; - } - } - - childMargins = child.margins; - vertMargins = childMargins.top + childMargins.bottom; - - nonFlexHeight += vertMargins + (childHeight || 0); - desiredHeight += vertMargins + (child.flex ? child.minHeight || 0 : childHeight); - minimumHeight += vertMargins + (child.minHeight || childHeight || 0); - - - if (typeof childWidth != 'number') { - if (canLayout) { - child.doLayout(); - } - childWidth = child.getWidth(); - } - - maxWidth = Math.max(maxWidth, childWidth + childMargins.left + childMargins.right); - - - boxes.push({ - component: child, - height : childHeight || undefined, - width : childWidth || undefined - }); - } - - var shortfall = desiredHeight - height, - tooNarrow = minimumHeight > height; - - - var availableHeight = Math.max(0, (height - nonFlexHeight - paddingVert)); - - if (tooNarrow) { - for (i = 0, length = visibleCount; i < length; i++) { - boxes[i].height = visibleItems[i].minHeight || visibleItems[i].height || boxes[i].height; - } - } else { - - - if (shortfall > 0) { - var minHeights = []; - - - for (var index = 0, length = visibleCount; index < length; index++) { - var item = visibleItems[index], - minHeight = item.minHeight || 0; - - - - if (item.flex) { - boxes[index].height = minHeight; - } else { - minHeights.push({ - minHeight: minHeight, - available: boxes[index].height - minHeight, - index : index - }); - } - } - - - minHeights.sort(function(a, b) { - return a.available > b.available ? 1 : -1; - }); - - - for (var i = 0, length = minHeights.length; i < length; i++) { - var itemIndex = minHeights[i].index; - - if (itemIndex == undefined) { - continue; - } - - var item = visibleItems[itemIndex], - box = boxes[itemIndex], - oldHeight = box.height, - minHeight = item.minHeight, - newHeight = Math.max(minHeight, oldHeight - Math.ceil(shortfall / (length - i))), - reduction = oldHeight - newHeight; - - boxes[itemIndex].height = newHeight; - shortfall -= reduction; - } - } else { - - var remainingHeight = availableHeight, - remainingFlex = totalFlex; - - - for (i = 0; i < visibleCount; i++) { - child = visibleItems[i]; - calcs = boxes[i]; - - childMargins = child.margins; - horizMargins = childMargins.left + childMargins.right; - - if (isStart && child.flex && !child.height) { - flexedHeight = Math.ceil((child.flex / remainingFlex) * remainingHeight); - remainingHeight -= flexedHeight; - remainingFlex -= child.flex; - - calcs.height = flexedHeight; - calcs.dirtySize = true; - } - } - } - } - - if (isCenter) { - topOffset += availableHeight / 2; - } else if (isEnd) { - topOffset += availableHeight; - } - - - for (i = 0; i < visibleCount; i++) { - child = visibleItems[i]; - calcs = boxes[i]; - - childMargins = child.margins; - topOffset += childMargins.top; - horizMargins = childMargins.left + childMargins.right; - - - calcs.left = leftOffset + childMargins.left; - calcs.top = topOffset; - - switch (this.align) { - case 'stretch': - stretchWidth = availWidth - horizMargins; - calcs.width = stretchWidth.constrain(child.minWidth || 0, child.maxWidth || 1000000); - calcs.dirtySize = true; - break; - case 'stretchmax': - stretchWidth = maxWidth - horizMargins; - calcs.width = stretchWidth.constrain(child.minWidth || 0, child.maxWidth || 1000000); - calcs.dirtySize = true; - break; - case 'center': - var diff = availWidth - calcs.width - horizMargins; - if (diff > 0) { - calcs.left = leftOffset + horizMargins + (diff / 2); - } - } - - topOffset += calcs.height + childMargins.bottom; - } - - return { - boxes: boxes, - meta : { - maxWidth : maxWidth, - nonFlexHeight: nonFlexHeight, - desiredHeight: desiredHeight, - minimumHeight: minimumHeight, - shortfall : desiredHeight - height, - tooNarrow : tooNarrow - } - }; - } -}); - -Ext.Container.LAYOUTS.vbox = Ext.layout.VBoxLayout; - -Ext.layout.ToolbarLayout = Ext.extend(Ext.layout.ContainerLayout, { - monitorResize : true, - - type: 'toolbar', - - - triggerWidth: 18, - - - noItemsMenuText : '
        (None)
        ', - - - lastOverflow: false, - - - tableHTML: [ - '', - '', - '', - '', - '', - '', - '', - '
        ', - '', - '', - '', - '', - '
        ', - '
        ', - '', - '', - '', - '', - '', - '', - '', - '
        ', - '', - '', - '', - '', - '
        ', - '
        ', - '', - '', - '', - '', - '
        ', - '
        ', - '
        ' - ].join(""), - - - onLayout : function(ct, target) { - - if (!this.leftTr) { - var align = ct.buttonAlign == 'center' ? 'center' : 'left'; - - target.addClass('x-toolbar-layout-ct'); - target.insertHtml('beforeEnd', String.format(this.tableHTML, align)); - - this.leftTr = target.child('tr.x-toolbar-left-row', true); - this.rightTr = target.child('tr.x-toolbar-right-row', true); - this.extrasTr = target.child('tr.x-toolbar-extras-row', true); - - if (this.hiddenItem == undefined) { - - this.hiddenItems = []; - } - } - - var side = ct.buttonAlign == 'right' ? this.rightTr : this.leftTr, - items = ct.items.items, - position = 0; - - - for (var i = 0, len = items.length, c; i < len; i++, position++) { - c = items[i]; - - if (c.isFill) { - side = this.rightTr; - position = -1; - } else if (!c.rendered) { - c.render(this.insertCell(c, side, position)); - this.configureItem(c); - } else { - if (!c.xtbHidden && !this.isValidParent(c, side.childNodes[position])) { - var td = this.insertCell(c, side, position); - td.appendChild(c.getPositionEl().dom); - c.container = Ext.get(td); - } - } - } - - - this.cleanup(this.leftTr); - this.cleanup(this.rightTr); - this.cleanup(this.extrasTr); - this.fitToSize(target); - }, - - - cleanup : function(el) { - var cn = el.childNodes, i, c; - - for (i = cn.length-1; i >= 0 && (c = cn[i]); i--) { - if (!c.firstChild) { - el.removeChild(c); - } - } - }, - - - insertCell : function(c, target, position) { - var td = document.createElement('td'); - td.className = 'x-toolbar-cell'; - - target.insertBefore(td, target.childNodes[position] || null); - - return td; - }, - - - hideItem : function(item) { - this.hiddenItems.push(item); - - item.xtbHidden = true; - item.xtbWidth = item.getPositionEl().dom.parentNode.offsetWidth; - item.hide(); - }, - - - unhideItem : function(item) { - item.show(); - item.xtbHidden = false; - this.hiddenItems.remove(item); - }, - - - getItemWidth : function(c) { - return c.hidden ? (c.xtbWidth || 0) : c.getPositionEl().dom.parentNode.offsetWidth; - }, - - - fitToSize : function(target) { - if (this.container.enableOverflow === false) { - return; - } - - var width = target.dom.clientWidth, - tableWidth = target.dom.firstChild.offsetWidth, - clipWidth = width - this.triggerWidth, - lastWidth = this.lastWidth || 0, - - hiddenItems = this.hiddenItems, - hasHiddens = hiddenItems.length != 0, - isLarger = width >= lastWidth; - - this.lastWidth = width; - - if (tableWidth > width || (hasHiddens && isLarger)) { - var items = this.container.items.items, - len = items.length, - loopWidth = 0, - item; - - for (var i = 0; i < len; i++) { - item = items[i]; - - if (!item.isFill) { - loopWidth += this.getItemWidth(item); - if (loopWidth > clipWidth) { - if (!(item.hidden || item.xtbHidden)) { - this.hideItem(item); - } - } else if (item.xtbHidden) { - this.unhideItem(item); - } - } - } - } - - - hasHiddens = hiddenItems.length != 0; - - if (hasHiddens) { - this.initMore(); - - if (!this.lastOverflow) { - this.container.fireEvent('overflowchange', this.container, true); - this.lastOverflow = true; - } - } else if (this.more) { - this.clearMenu(); - this.more.destroy(); - delete this.more; - - if (this.lastOverflow) { - this.container.fireEvent('overflowchange', this.container, false); - this.lastOverflow = false; - } - } - }, - - - createMenuConfig : function(component, hideOnClick){ - var config = Ext.apply({}, component.initialConfig), - group = component.toggleGroup; - - Ext.copyTo(config, component, [ - 'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu' - ]); - - Ext.apply(config, { - text : component.overflowText || component.text, - hideOnClick: hideOnClick - }); - - if (group || component.enableToggle) { - Ext.apply(config, { - group : group, - checked: component.pressed, - listeners: { - checkchange: function(item, checked){ - component.toggle(checked); - } - } - }); - } - - delete config.ownerCt; - delete config.xtype; - delete config.id; - - return config; - }, - - - addComponentToMenu : function(menu, component) { - if (component instanceof Ext.Toolbar.Separator) { - menu.add('-'); - - } else if (Ext.isFunction(component.isXType)) { - if (component.isXType('splitbutton')) { - menu.add(this.createMenuConfig(component, true)); - - } else if (component.isXType('button')) { - menu.add(this.createMenuConfig(component, !component.menu)); - - } else if (component.isXType('buttongroup')) { - component.items.each(function(item){ - this.addComponentToMenu(menu, item); - }, this); - } - } - }, - - - clearMenu : function(){ - var menu = this.moreMenu; - if (menu && menu.items) { - menu.items.each(function(item){ - delete item.menu; - }); - } - }, - - - beforeMoreShow : function(menu) { - var items = this.container.items.items, - len = items.length, - item, - prev; - - var needsSep = function(group, item){ - return group.isXType('buttongroup') && !(item instanceof Ext.Toolbar.Separator); - }; - - this.clearMenu(); - menu.removeAll(); - for (var i = 0; i < len; i++) { - item = items[i]; - if (item.xtbHidden) { - if (prev && (needsSep(item, prev) || needsSep(prev, item))) { - menu.add('-'); - } - this.addComponentToMenu(menu, item); - prev = item; - } - } - - - if (menu.items.length < 1) { - menu.add(this.noItemsMenuText); - } - }, - - - initMore : function(){ - if (!this.more) { - - this.moreMenu = new Ext.menu.Menu({ - ownerCt : this.container, - listeners: { - beforeshow: this.beforeMoreShow, - scope: this - } - }); - - - this.more = new Ext.Button({ - iconCls: 'x-toolbar-more-icon', - cls : 'x-toolbar-more', - menu : this.moreMenu, - ownerCt: this.container - }); - - var td = this.insertCell(this.more, this.extrasTr, 100); - this.more.render(td); - } - }, - - destroy : function(){ - Ext.destroy(this.more, this.moreMenu); - delete this.leftTr; - delete this.rightTr; - delete this.extrasTr; - Ext.layout.ToolbarLayout.superclass.destroy.call(this); - } -}); - -Ext.Container.LAYOUTS.toolbar = Ext.layout.ToolbarLayout; - - Ext.layout.MenuLayout = Ext.extend(Ext.layout.ContainerLayout, { - monitorResize : true, - - type: 'menu', - - setContainer : function(ct){ - this.monitorResize = !ct.floating; - - - ct.on('autosize', this.doAutoSize, this); - Ext.layout.MenuLayout.superclass.setContainer.call(this, ct); - }, - - renderItem : function(c, position, target){ - if (!this.itemTpl) { - this.itemTpl = Ext.layout.MenuLayout.prototype.itemTpl = new Ext.XTemplate( - '
      • ', - '', - '{altText}', - '', - '
      • ' - ); - } - - if(c && !c.rendered){ - if(Ext.isNumber(position)){ - position = target.dom.childNodes[position]; - } - var a = this.getItemArgs(c); - - - c.render(c.positionEl = position ? - this.itemTpl.insertBefore(position, a, true) : - this.itemTpl.append(target, a, true)); - - - c.positionEl.menuItemId = c.getItemId(); - - - - if (!a.isMenuItem && a.needsIcon) { - c.positionEl.addClass('x-menu-list-item-indent'); - } - this.configureItem(c); - }else if(c && !this.isValidParent(c, target)){ - if(Ext.isNumber(position)){ - position = target.dom.childNodes[position]; - } - target.dom.insertBefore(c.getActionEl().dom, position || null); - } - }, - - getItemArgs : function(c) { - var isMenuItem = c instanceof Ext.menu.Item, - canHaveIcon = !(isMenuItem || c instanceof Ext.menu.Separator); - - return { - isMenuItem: isMenuItem, - needsIcon: canHaveIcon && (c.icon || c.iconCls), - icon: c.icon || Ext.BLANK_IMAGE_URL, - iconCls: 'x-menu-item-icon ' + (c.iconCls || ''), - itemId: 'x-menu-el-' + c.id, - itemCls: 'x-menu-list-item ', - altText: c.altText || '' - }; - }, - - - isValidParent : function(c, target) { - return c.el.up('li.x-menu-list-item', 5).dom.parentNode === (target.dom || target); - }, - - onLayout : function(ct, target){ - Ext.layout.MenuLayout.superclass.onLayout.call(this, ct, target); - this.doAutoSize(); - }, - - doAutoSize : function(){ - var ct = this.container, w = ct.width; - if(ct.floating){ - if(w){ - ct.setWidth(w); - }else if(Ext.isIE){ - ct.setWidth(Ext.isStrict && (Ext.isIE7 || Ext.isIE8 || Ext.isIE9) ? 'auto' : ct.minWidth); - var el = ct.getEl(), t = el.dom.offsetWidth; - ct.setWidth(ct.getLayoutTarget().getWidth() + el.getFrameWidth('lr')); - } - } - } -}); -Ext.Container.LAYOUTS['menu'] = Ext.layout.MenuLayout; - -Ext.Viewport = Ext.extend(Ext.Container, { - - - - - - - - - - - - - - initComponent : function() { - Ext.Viewport.superclass.initComponent.call(this); - document.getElementsByTagName('html')[0].className += ' x-viewport'; - this.el = Ext.getBody(); - this.el.setHeight = Ext.emptyFn; - this.el.setWidth = Ext.emptyFn; - this.el.setSize = Ext.emptyFn; - this.el.dom.scroll = 'no'; - this.allowDomMove = false; - this.autoWidth = true; - this.autoHeight = true; - Ext.EventManager.onWindowResize(this.fireResize, this); - this.renderTo = this.el; - }, - - fireResize : function(w, h){ - this.fireEvent('resize', this, w, h, w, h); - } -}); -Ext.reg('viewport', Ext.Viewport); - -Ext.Panel = Ext.extend(Ext.Container, { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - baseCls : 'x-panel', - - collapsedCls : 'x-panel-collapsed', - - maskDisabled : true, - - animCollapse : Ext.enableFx, - - headerAsText : true, - - buttonAlign : 'right', - - collapsed : false, - - collapseFirst : true, - - minButtonWidth : 75, - - - elements : 'body', - - preventBodyReset : false, - - - padding: undefined, - - - resizeEvent: 'bodyresize', - - - - - toolTarget : 'header', - collapseEl : 'bwrap', - slideAnchor : 't', - disabledClass : '', - - - deferHeight : true, - - expandDefaults: { - duration : 0.25 - }, - - collapseDefaults : { - duration : 0.25 - }, - - - initComponent : function(){ - Ext.Panel.superclass.initComponent.call(this); - - this.addEvents( - - 'bodyresize', - - 'titlechange', - - 'iconchange', - - 'collapse', - - 'expand', - - 'beforecollapse', - - 'beforeexpand', - - 'beforeclose', - - 'close', - - 'activate', - - 'deactivate' - ); - - if(this.unstyled){ - this.baseCls = 'x-plain'; - } - - - this.toolbars = []; - - if(this.tbar){ - this.elements += ',tbar'; - this.topToolbar = this.createToolbar(this.tbar); - this.tbar = null; - - } - if(this.bbar){ - this.elements += ',bbar'; - this.bottomToolbar = this.createToolbar(this.bbar); - this.bbar = null; - } - - if(this.header === true){ - this.elements += ',header'; - this.header = null; - }else if(this.headerCfg || (this.title && this.header !== false)){ - this.elements += ',header'; - } - - if(this.footerCfg || this.footer === true){ - this.elements += ',footer'; - this.footer = null; - } - - if(this.buttons){ - this.fbar = this.buttons; - this.buttons = null; - } - if(this.fbar){ - this.createFbar(this.fbar); - } - if(this.autoLoad){ - this.on('render', this.doAutoLoad, this, {delay:10}); - } - }, - - - createFbar : function(fbar){ - var min = this.minButtonWidth; - this.elements += ',footer'; - this.fbar = this.createToolbar(fbar, { - buttonAlign: this.buttonAlign, - toolbarCls: 'x-panel-fbar', - enableOverflow: false, - defaults: function(c){ - return { - minWidth: c.minWidth || min - }; - } - }); - - - - this.fbar.items.each(function(c){ - c.minWidth = c.minWidth || this.minButtonWidth; - }, this); - this.buttons = this.fbar.items.items; - }, - - - createToolbar: function(tb, options){ - var result; - - if(Ext.isArray(tb)){ - tb = { - items: tb - }; - } - result = tb.events ? Ext.apply(tb, options) : this.createComponent(Ext.apply({}, tb, options), 'toolbar'); - this.toolbars.push(result); - return result; - }, - - - createElement : function(name, pnode){ - if(this[name]){ - pnode.appendChild(this[name].dom); - return; - } - - if(name === 'bwrap' || this.elements.indexOf(name) != -1){ - if(this[name+'Cfg']){ - this[name] = Ext.fly(pnode).createChild(this[name+'Cfg']); - }else{ - var el = document.createElement('div'); - el.className = this[name+'Cls']; - this[name] = Ext.get(pnode.appendChild(el)); - } - if(this[name+'CssClass']){ - this[name].addClass(this[name+'CssClass']); - } - if(this[name+'Style']){ - this[name].applyStyles(this[name+'Style']); - } - } - }, - - - onRender : function(ct, position){ - Ext.Panel.superclass.onRender.call(this, ct, position); - this.createClasses(); - - var el = this.el, - d = el.dom, - bw, - ts; - - - if(this.collapsible && !this.hideCollapseTool){ - this.tools = this.tools ? this.tools.slice(0) : []; - this.tools[this.collapseFirst?'unshift':'push']({ - id: 'toggle', - handler : this.toggleCollapse, - scope: this - }); - } - - if(this.tools){ - ts = this.tools; - this.elements += (this.header !== false) ? ',header' : ''; - } - this.tools = {}; - - el.addClass(this.baseCls); - if(d.firstChild){ - this.header = el.down('.'+this.headerCls); - this.bwrap = el.down('.'+this.bwrapCls); - var cp = this.bwrap ? this.bwrap : el; - this.tbar = cp.down('.'+this.tbarCls); - this.body = cp.down('.'+this.bodyCls); - this.bbar = cp.down('.'+this.bbarCls); - this.footer = cp.down('.'+this.footerCls); - this.fromMarkup = true; - } - if (this.preventBodyReset === true) { - el.addClass('x-panel-reset'); - } - if(this.cls){ - el.addClass(this.cls); - } - - if(this.buttons){ - this.elements += ',footer'; - } - - - - - if(this.frame){ - el.insertHtml('afterBegin', String.format(Ext.Element.boxMarkup, this.baseCls)); - - this.createElement('header', d.firstChild.firstChild.firstChild); - this.createElement('bwrap', d); - - - bw = this.bwrap.dom; - var ml = d.childNodes[1], bl = d.childNodes[2]; - bw.appendChild(ml); - bw.appendChild(bl); - - var mc = bw.firstChild.firstChild.firstChild; - this.createElement('tbar', mc); - this.createElement('body', mc); - this.createElement('bbar', mc); - this.createElement('footer', bw.lastChild.firstChild.firstChild); - - if(!this.footer){ - this.bwrap.dom.lastChild.className += ' x-panel-nofooter'; - } - - this.ft = Ext.get(this.bwrap.dom.lastChild); - this.mc = Ext.get(mc); - }else{ - this.createElement('header', d); - this.createElement('bwrap', d); - - - bw = this.bwrap.dom; - this.createElement('tbar', bw); - this.createElement('body', bw); - this.createElement('bbar', bw); - this.createElement('footer', bw); - - if(!this.header){ - this.body.addClass(this.bodyCls + '-noheader'); - if(this.tbar){ - this.tbar.addClass(this.tbarCls + '-noheader'); - } - } - } - - if(Ext.isDefined(this.padding)){ - this.body.setStyle('padding', this.body.addUnits(this.padding)); - } - - if(this.border === false){ - this.el.addClass(this.baseCls + '-noborder'); - this.body.addClass(this.bodyCls + '-noborder'); - if(this.header){ - this.header.addClass(this.headerCls + '-noborder'); - } - if(this.footer){ - this.footer.addClass(this.footerCls + '-noborder'); - } - if(this.tbar){ - this.tbar.addClass(this.tbarCls + '-noborder'); - } - if(this.bbar){ - this.bbar.addClass(this.bbarCls + '-noborder'); - } - } - - if(this.bodyBorder === false){ - this.body.addClass(this.bodyCls + '-noborder'); - } - - this.bwrap.enableDisplayMode('block'); - - if(this.header){ - this.header.unselectable(); - - - if(this.headerAsText){ - this.header.dom.innerHTML = - ''+this.header.dom.innerHTML+''; - - if(this.iconCls){ - this.setIconClass(this.iconCls); - } - } - } - - if(this.floating){ - this.makeFloating(this.floating); - } - - if(this.collapsible && this.titleCollapse && this.header){ - this.mon(this.header, 'click', this.toggleCollapse, this); - this.header.setStyle('cursor', 'pointer'); - } - if(ts){ - this.addTool.apply(this, ts); - } - - - if(this.fbar){ - this.footer.addClass('x-panel-btns'); - this.fbar.ownerCt = this; - this.fbar.render(this.footer); - this.footer.createChild({cls:'x-clear'}); - } - if(this.tbar && this.topToolbar){ - this.topToolbar.ownerCt = this; - this.topToolbar.render(this.tbar); - } - if(this.bbar && this.bottomToolbar){ - this.bottomToolbar.ownerCt = this; - this.bottomToolbar.render(this.bbar); - } - }, - - - setIconClass : function(cls){ - var old = this.iconCls; - this.iconCls = cls; - if(this.rendered && this.header){ - if(this.frame){ - this.header.addClass('x-panel-icon'); - this.header.replaceClass(old, this.iconCls); - }else{ - var hd = this.header, - img = hd.child('img.x-panel-inline-icon'); - if(img){ - Ext.fly(img).replaceClass(old, this.iconCls); - }else{ - var hdspan = hd.child('span.' + this.headerTextCls); - if (hdspan) { - Ext.DomHelper.insertBefore(hdspan.dom, { - tag:'img', alt: '', src: Ext.BLANK_IMAGE_URL, cls:'x-panel-inline-icon '+this.iconCls - }); - } - } - } - } - this.fireEvent('iconchange', this, cls, old); - }, - - - makeFloating : function(cfg){ - this.floating = true; - this.el = new Ext.Layer(Ext.apply({}, cfg, { - shadow: Ext.isDefined(this.shadow) ? this.shadow : 'sides', - shadowOffset: this.shadowOffset, - constrain:false, - shim: this.shim === false ? false : undefined - }), this.el); - }, - - - getTopToolbar : function(){ - return this.topToolbar; - }, - - - getBottomToolbar : function(){ - return this.bottomToolbar; - }, - - - getFooterToolbar : function() { - return this.fbar; - }, - - - addButton : function(config, handler, scope){ - if(!this.fbar){ - this.createFbar([]); - } - if(handler){ - if(Ext.isString(config)){ - config = {text: config}; - } - config = Ext.apply({ - handler: handler, - scope: scope - }, config); - } - return this.fbar.add(config); - }, - - - addTool : function(){ - if(!this.rendered){ - if(!this.tools){ - this.tools = []; - } - Ext.each(arguments, function(arg){ - this.tools.push(arg); - }, this); - return; - } - - if(!this[this.toolTarget]){ - return; - } - if(!this.toolTemplate){ - - var tt = new Ext.Template( - '
         
        ' - ); - tt.disableFormats = true; - tt.compile(); - Ext.Panel.prototype.toolTemplate = tt; - } - for(var i = 0, a = arguments, len = a.length; i < len; i++) { - var tc = a[i]; - if(!this.tools[tc.id]){ - var overCls = 'x-tool-'+tc.id+'-over'; - var t = this.toolTemplate.insertFirst(this[this.toolTarget], tc, true); - this.tools[tc.id] = t; - t.enableDisplayMode('block'); - this.mon(t, 'click', this.createToolHandler(t, tc, overCls, this)); - if(tc.on){ - this.mon(t, tc.on); - } - if(tc.hidden){ - t.hide(); - } - if(tc.qtip){ - if(Ext.isObject(tc.qtip)){ - Ext.QuickTips.register(Ext.apply({ - target: t.id - }, tc.qtip)); - } else { - t.dom.qtip = tc.qtip; - } - } - t.addClassOnOver(overCls); - } - } - }, - - onLayout : function(shallow, force){ - Ext.Panel.superclass.onLayout.apply(this, arguments); - if(this.hasLayout && this.toolbars.length > 0){ - Ext.each(this.toolbars, function(tb){ - tb.doLayout(undefined, force); - }); - this.syncHeight(); - } - }, - - syncHeight : function(){ - var h = this.toolbarHeight, - bd = this.body, - lsh = this.lastSize.height, - sz; - - if(this.autoHeight || !Ext.isDefined(lsh) || lsh == 'auto'){ - return; - } - - - if(h != this.getToolbarHeight()){ - h = Math.max(0, lsh - this.getFrameHeight()); - bd.setHeight(h); - sz = bd.getSize(); - this.toolbarHeight = this.getToolbarHeight(); - this.onBodyResize(sz.width, sz.height); - } - }, - - - onShow : function(){ - if(this.floating){ - return this.el.show(); - } - Ext.Panel.superclass.onShow.call(this); - }, - - - onHide : function(){ - if(this.floating){ - return this.el.hide(); - } - Ext.Panel.superclass.onHide.call(this); - }, - - - createToolHandler : function(t, tc, overCls, panel){ - return function(e){ - t.removeClass(overCls); - if(tc.stopEvent !== false){ - e.stopEvent(); - } - if(tc.handler){ - tc.handler.call(tc.scope || t, e, t, panel, tc); - } - }; - }, - - - afterRender : function(){ - if(this.floating && !this.hidden){ - this.el.show(); - } - if(this.title){ - this.setTitle(this.title); - } - Ext.Panel.superclass.afterRender.call(this); - if (this.collapsed) { - this.collapsed = false; - this.collapse(false); - } - this.initEvents(); - }, - - - getKeyMap : function(){ - if(!this.keyMap){ - this.keyMap = new Ext.KeyMap(this.el, this.keys); - } - return this.keyMap; - }, - - - initEvents : function(){ - if(this.keys){ - this.getKeyMap(); - } - if(this.draggable){ - this.initDraggable(); - } - if(this.toolbars.length > 0){ - Ext.each(this.toolbars, function(tb){ - tb.doLayout(); - tb.on({ - scope: this, - afterlayout: this.syncHeight, - remove: this.syncHeight - }); - }, this); - this.syncHeight(); - } - - }, - - - initDraggable : function(){ - - this.dd = new Ext.Panel.DD(this, Ext.isBoolean(this.draggable) ? null : this.draggable); - }, - - - beforeEffect : function(anim){ - if(this.floating){ - this.el.beforeAction(); - } - if(anim !== false){ - this.el.addClass('x-panel-animated'); - } - }, - - - afterEffect : function(anim){ - this.syncShadow(); - this.el.removeClass('x-panel-animated'); - }, - - - createEffect : function(a, cb, scope){ - var o = { - scope:scope, - block:true - }; - if(a === true){ - o.callback = cb; - return o; - }else if(!a.callback){ - o.callback = cb; - }else { - o.callback = function(){ - cb.call(scope); - Ext.callback(a.callback, a.scope); - }; - } - return Ext.applyIf(o, a); - }, - - - collapse : function(animate){ - if(this.collapsed || this.el.hasFxBlock() || this.fireEvent('beforecollapse', this, animate) === false){ - return; - } - var doAnim = animate === true || (animate !== false && this.animCollapse); - this.beforeEffect(doAnim); - this.onCollapse(doAnim, animate); - return this; - }, - - - onCollapse : function(doAnim, animArg){ - if(doAnim){ - this[this.collapseEl].slideOut(this.slideAnchor, - Ext.apply(this.createEffect(animArg||true, this.afterCollapse, this), - this.collapseDefaults)); - }else{ - this[this.collapseEl].hide(this.hideMode); - this.afterCollapse(false); - } - }, - - - afterCollapse : function(anim){ - this.collapsed = true; - this.el.addClass(this.collapsedCls); - if(anim !== false){ - this[this.collapseEl].hide(this.hideMode); - } - this.afterEffect(anim); - - - this.cascade(function(c) { - if (c.lastSize) { - c.lastSize = { width: undefined, height: undefined }; - } - }); - this.fireEvent('collapse', this); - }, - - - expand : function(animate){ - if(!this.collapsed || this.el.hasFxBlock() || this.fireEvent('beforeexpand', this, animate) === false){ - return; - } - var doAnim = animate === true || (animate !== false && this.animCollapse); - this.el.removeClass(this.collapsedCls); - this.beforeEffect(doAnim); - this.onExpand(doAnim, animate); - return this; - }, - - - onExpand : function(doAnim, animArg){ - if(doAnim){ - this[this.collapseEl].slideIn(this.slideAnchor, - Ext.apply(this.createEffect(animArg||true, this.afterExpand, this), - this.expandDefaults)); - }else{ - this[this.collapseEl].show(this.hideMode); - this.afterExpand(false); - } - }, - - - afterExpand : function(anim){ - this.collapsed = false; - if(anim !== false){ - this[this.collapseEl].show(this.hideMode); - } - this.afterEffect(anim); - if (this.deferLayout) { - delete this.deferLayout; - this.doLayout(true); - } - this.fireEvent('expand', this); - }, - - - toggleCollapse : function(animate){ - this[this.collapsed ? 'expand' : 'collapse'](animate); - return this; - }, - - - onDisable : function(){ - if(this.rendered && this.maskDisabled){ - this.el.mask(); - } - Ext.Panel.superclass.onDisable.call(this); - }, - - - onEnable : function(){ - if(this.rendered && this.maskDisabled){ - this.el.unmask(); - } - Ext.Panel.superclass.onEnable.call(this); - }, - - - onResize : function(adjWidth, adjHeight, rawWidth, rawHeight){ - var w = adjWidth, - h = adjHeight; - - if(Ext.isDefined(w) || Ext.isDefined(h)){ - if(!this.collapsed){ - - - - - if(Ext.isNumber(w)){ - this.body.setWidth(w = this.adjustBodyWidth(w - this.getFrameWidth())); - } else if (w == 'auto') { - w = this.body.setWidth('auto').dom.offsetWidth; - } else { - w = this.body.dom.offsetWidth; - } - - if(this.tbar){ - this.tbar.setWidth(w); - if(this.topToolbar){ - this.topToolbar.setSize(w); - } - } - if(this.bbar){ - this.bbar.setWidth(w); - if(this.bottomToolbar){ - this.bottomToolbar.setSize(w); - - if (Ext.isIE) { - this.bbar.setStyle('position', 'static'); - this.bbar.setStyle('position', ''); - } - } - } - if(this.footer){ - this.footer.setWidth(w); - if(this.fbar){ - this.fbar.setSize(Ext.isIE ? (w - this.footer.getFrameWidth('lr')) : 'auto'); - } - } - - - if(Ext.isNumber(h)){ - h = Math.max(0, h - this.getFrameHeight()); - - this.body.setHeight(h); - }else if(h == 'auto'){ - this.body.setHeight(h); - } - - if(this.disabled && this.el._mask){ - this.el._mask.setSize(this.el.dom.clientWidth, this.el.getHeight()); - } - }else{ - - this.queuedBodySize = {width: w, height: h}; - if(!this.queuedExpand && this.allowQueuedExpand !== false){ - this.queuedExpand = true; - this.on('expand', function(){ - delete this.queuedExpand; - this.onResize(this.queuedBodySize.width, this.queuedBodySize.height); - }, this, {single:true}); - } - } - this.onBodyResize(w, h); - } - this.syncShadow(); - Ext.Panel.superclass.onResize.call(this, adjWidth, adjHeight, rawWidth, rawHeight); - - }, - - - onBodyResize: function(w, h){ - this.fireEvent('bodyresize', this, w, h); - }, - - - getToolbarHeight: function(){ - var h = 0; - if(this.rendered){ - Ext.each(this.toolbars, function(tb){ - h += tb.getHeight(); - }, this); - } - return h; - }, - - - adjustBodyHeight : function(h){ - return h; - }, - - - adjustBodyWidth : function(w){ - return w; - }, - - - onPosition : function(){ - this.syncShadow(); - }, - - - getFrameWidth : function(){ - var w = this.el.getFrameWidth('lr') + this.bwrap.getFrameWidth('lr'); - - if(this.frame){ - var l = this.bwrap.dom.firstChild; - w += (Ext.fly(l).getFrameWidth('l') + Ext.fly(l.firstChild).getFrameWidth('r')); - w += this.mc.getFrameWidth('lr'); - } - return w; - }, - - - getFrameHeight : function() { - var h = this.el.getFrameWidth('tb') + this.bwrap.getFrameWidth('tb'); - h += (this.tbar ? this.tbar.getHeight() : 0) + - (this.bbar ? this.bbar.getHeight() : 0); - - if(this.frame){ - h += this.el.dom.firstChild.offsetHeight + this.ft.dom.offsetHeight + this.mc.getFrameWidth('tb'); - }else{ - h += (this.header ? this.header.getHeight() : 0) + - (this.footer ? this.footer.getHeight() : 0); - } - return h; - }, - - - getInnerWidth : function(){ - return this.getSize().width - this.getFrameWidth(); - }, - - - getInnerHeight : function(){ - return this.body.getHeight(); - - }, - - - syncShadow : function(){ - if(this.floating){ - this.el.sync(true); - } - }, - - - getLayoutTarget : function(){ - return this.body; - }, - - - getContentTarget : function(){ - return this.body; - }, - - - setTitle : function(title, iconCls){ - this.title = title; - if(this.header && this.headerAsText){ - this.header.child('span').update(title); - } - if(iconCls){ - this.setIconClass(iconCls); - } - this.fireEvent('titlechange', this, title); - return this; - }, - - - getUpdater : function(){ - return this.body.getUpdater(); - }, - - - load : function(){ - var um = this.body.getUpdater(); - um.update.apply(um, arguments); - return this; - }, - - - beforeDestroy : function(){ - Ext.Panel.superclass.beforeDestroy.call(this); - if(this.header){ - this.header.removeAllListeners(); - } - if(this.tools){ - for(var k in this.tools){ - Ext.destroy(this.tools[k]); - } - } - if(this.toolbars.length > 0){ - Ext.each(this.toolbars, function(tb){ - tb.un('afterlayout', this.syncHeight, this); - tb.un('remove', this.syncHeight, this); - }, this); - } - if(Ext.isArray(this.buttons)){ - while(this.buttons.length) { - Ext.destroy(this.buttons[0]); - } - } - if(this.rendered){ - Ext.destroy( - this.ft, - this.header, - this.footer, - this.tbar, - this.bbar, - this.body, - this.mc, - this.bwrap, - this.dd - ); - if (this.fbar) { - Ext.destroy( - this.fbar, - this.fbar.el - ); - } - } - Ext.destroy(this.toolbars); - }, - - - createClasses : function(){ - this.headerCls = this.baseCls + '-header'; - this.headerTextCls = this.baseCls + '-header-text'; - this.bwrapCls = this.baseCls + '-bwrap'; - this.tbarCls = this.baseCls + '-tbar'; - this.bodyCls = this.baseCls + '-body'; - this.bbarCls = this.baseCls + '-bbar'; - this.footerCls = this.baseCls + '-footer'; - }, - - - createGhost : function(cls, useShim, appendTo){ - var el = document.createElement('div'); - el.className = 'x-panel-ghost ' + (cls ? cls : ''); - if(this.header){ - el.appendChild(this.el.dom.firstChild.cloneNode(true)); - } - Ext.fly(el.appendChild(document.createElement('ul'))).setHeight(this.bwrap.getHeight()); - el.style.width = this.el.dom.offsetWidth + 'px';; - if(!appendTo){ - this.container.dom.appendChild(el); - }else{ - Ext.getDom(appendTo).appendChild(el); - } - if(useShim !== false && this.el.useShim !== false){ - var layer = new Ext.Layer({shadow:false, useDisplay:true, constrain:false}, el); - layer.show(); - return layer; - }else{ - return new Ext.Element(el); - } - }, - - - doAutoLoad : function(){ - var u = this.body.getUpdater(); - if(this.renderer){ - u.setRenderer(this.renderer); - } - u.update(Ext.isObject(this.autoLoad) ? this.autoLoad : {url: this.autoLoad}); - }, - - - getTool : function(id) { - return this.tools[id]; - } - - -}); -Ext.reg('panel', Ext.Panel); - -Ext.Editor = function(field, config){ - if(field.field){ - this.field = Ext.create(field.field, 'textfield'); - config = Ext.apply({}, field); - delete config.field; - }else{ - this.field = field; - } - Ext.Editor.superclass.constructor.call(this, config); -}; - -Ext.extend(Ext.Editor, Ext.Component, { - - - allowBlur: true, - - - - - - value : "", - - alignment: "c-c?", - - offsets: [0, 0], - - shadow : "frame", - - constrain : false, - - swallowKeys : true, - - completeOnEnter : true, - - cancelOnEsc : true, - - updateEl : false, - - initComponent : function(){ - Ext.Editor.superclass.initComponent.call(this); - this.addEvents( - - "beforestartedit", - - "startedit", - - "beforecomplete", - - "complete", - - "canceledit", - - "specialkey" - ); - }, - - - onRender : function(ct, position){ - this.el = new Ext.Layer({ - shadow: this.shadow, - cls: "x-editor", - parentEl : ct, - shim : this.shim, - shadowOffset: this.shadowOffset || 4, - id: this.id, - constrain: this.constrain - }); - if(this.zIndex){ - this.el.setZIndex(this.zIndex); - } - this.el.setStyle("overflow", Ext.isGecko ? "auto" : "hidden"); - if(this.field.msgTarget != 'title'){ - this.field.msgTarget = 'qtip'; - } - this.field.inEditor = true; - this.mon(this.field, { - scope: this, - blur: this.onBlur, - specialkey: this.onSpecialKey - }); - if(this.field.grow){ - this.mon(this.field, "autosize", this.el.sync, this.el, {delay:1}); - } - this.field.render(this.el).show(); - this.field.getEl().dom.name = ''; - if(this.swallowKeys){ - this.field.el.swallowEvent([ - 'keypress', - 'keydown' - ]); - } - }, - - - onSpecialKey : function(field, e){ - var key = e.getKey(), - complete = this.completeOnEnter && key == e.ENTER, - cancel = this.cancelOnEsc && key == e.ESC; - if(complete || cancel){ - e.stopEvent(); - if(complete){ - this.completeEdit(); - }else{ - this.cancelEdit(); - } - if(field.triggerBlur){ - field.triggerBlur(); - } - } - this.fireEvent('specialkey', field, e); - }, - - - startEdit : function(el, value){ - if(this.editing){ - this.completeEdit(); - } - this.boundEl = Ext.get(el); - var v = value !== undefined ? value : this.boundEl.dom.innerHTML; - if(!this.rendered){ - this.render(this.parentEl || document.body); - } - if(this.fireEvent("beforestartedit", this, this.boundEl, v) !== false){ - this.startValue = v; - this.field.reset(); - this.field.setValue(v); - this.realign(true); - this.editing = true; - this.show(); - } - }, - - - doAutoSize : function(){ - if(this.autoSize){ - var sz = this.boundEl.getSize(), - fs = this.field.getSize(); - - switch(this.autoSize){ - case "width": - this.setSize(sz.width, fs.height); - break; - case "height": - this.setSize(fs.width, sz.height); - break; - case "none": - this.setSize(fs.width, fs.height); - break; - default: - this.setSize(sz.width, sz.height); - } - } - }, - - - setSize : function(w, h){ - delete this.field.lastSize; - this.field.setSize(w, h); - if(this.el){ - - if(Ext.isGecko2 || Ext.isOpera || (Ext.isIE7 && Ext.isStrict)){ - - this.el.setSize(w, h); - } - this.el.sync(); - } - }, - - - realign : function(autoSize){ - if(autoSize === true){ - this.doAutoSize(); - } - this.el.alignTo(this.boundEl, this.alignment, this.offsets); - }, - - - completeEdit : function(remainVisible){ - if(!this.editing){ - return; - } - - if (this.field.assertValue) { - this.field.assertValue(); - } - var v = this.getValue(); - if(!this.field.isValid()){ - if(this.revertInvalid !== false){ - this.cancelEdit(remainVisible); - } - return; - } - if(String(v) === String(this.startValue) && this.ignoreNoChange){ - this.hideEdit(remainVisible); - return; - } - if(this.fireEvent("beforecomplete", this, v, this.startValue) !== false){ - v = this.getValue(); - if(this.updateEl && this.boundEl){ - this.boundEl.update(v); - } - this.hideEdit(remainVisible); - this.fireEvent("complete", this, v, this.startValue); - } - }, - - - onShow : function(){ - this.el.show(); - if(this.hideEl !== false){ - this.boundEl.hide(); - } - this.field.show().focus(false, true); - this.fireEvent("startedit", this.boundEl, this.startValue); - }, - - - cancelEdit : function(remainVisible){ - if(this.editing){ - var v = this.getValue(); - this.setValue(this.startValue); - this.hideEdit(remainVisible); - this.fireEvent("canceledit", this, v, this.startValue); - } - }, - - - hideEdit: function(remainVisible){ - if(remainVisible !== true){ - this.editing = false; - this.hide(); - } - }, - - - onBlur : function(){ - - if(this.allowBlur === true && this.editing && this.selectSameEditor !== true){ - this.completeEdit(); - } - }, - - - onHide : function(){ - if(this.editing){ - this.completeEdit(); - return; - } - this.field.blur(); - if(this.field.collapse){ - this.field.collapse(); - } - this.el.hide(); - if(this.hideEl !== false){ - this.boundEl.show(); - } - }, - - - setValue : function(v){ - this.field.setValue(v); - }, - - - getValue : function(){ - return this.field.getValue(); - }, - - beforeDestroy : function(){ - Ext.destroyMembers(this, 'field'); - - delete this.parentEl; - delete this.boundEl; - } -}); -Ext.reg('editor', Ext.Editor); - -Ext.ColorPalette = Ext.extend(Ext.Component, { - - - itemCls : 'x-color-palette', - - value : null, - - clickEvent :'click', - - ctype : 'Ext.ColorPalette', - - - allowReselect : false, - - - colors : [ - '000000', '993300', '333300', '003300', '003366', '000080', '333399', '333333', - '800000', 'FF6600', '808000', '008000', '008080', '0000FF', '666699', '808080', - 'FF0000', 'FF9900', '99CC00', '339966', '33CCCC', '3366FF', '800080', '969696', - 'FF00FF', 'FFCC00', 'FFFF00', '00FF00', '00FFFF', '00CCFF', '993366', 'C0C0C0', - 'FF99CC', 'FFCC99', 'FFFF99', 'CCFFCC', 'CCFFFF', '99CCFF', 'CC99FF', 'FFFFFF' - ], - - - - - - initComponent : function(){ - Ext.ColorPalette.superclass.initComponent.call(this); - this.addEvents( - - 'select' - ); - - if(this.handler){ - this.on('select', this.handler, this.scope, true); - } - }, - - - onRender : function(container, position){ - this.autoEl = { - tag: 'div', - cls: this.itemCls - }; - Ext.ColorPalette.superclass.onRender.call(this, container, position); - var t = this.tpl || new Ext.XTemplate( - ' ' - ); - t.overwrite(this.el, this.colors); - this.mon(this.el, this.clickEvent, this.handleClick, this, {delegate: 'a'}); - if(this.clickEvent != 'click'){ - this.mon(this.el, 'click', Ext.emptyFn, this, {delegate: 'a', preventDefault: true}); - } - }, - - - afterRender : function(){ - Ext.ColorPalette.superclass.afterRender.call(this); - if(this.value){ - var s = this.value; - this.value = null; - this.select(s, true); - } - }, - - - handleClick : function(e, t){ - e.preventDefault(); - if(!this.disabled){ - var c = t.className.match(/(?:^|\s)color-(.{6})(?:\s|$)/)[1]; - this.select(c.toUpperCase()); - } - }, - - - select : function(color, suppressEvent){ - color = color.replace('#', ''); - if(color != this.value || this.allowReselect){ - var el = this.el; - if(this.value){ - el.child('a.color-'+this.value).removeClass('x-color-palette-sel'); - } - el.child('a.color-'+color).addClass('x-color-palette-sel'); - this.value = color; - if(suppressEvent !== true){ - this.fireEvent('select', this, color); - } - } - } - - -}); -Ext.reg('colorpalette', Ext.ColorPalette); -Ext.DatePicker = Ext.extend(Ext.BoxComponent, { - - todayText : 'Today', - - okText : ' OK ', - - cancelText : 'Cancel', - - - - todayTip : '{0} (Spacebar)', - - minText : 'This date is before the minimum date', - - maxText : 'This date is after the maximum date', - - format : 'm/d/y', - - disabledDaysText : 'Disabled', - - disabledDatesText : 'Disabled', - - monthNames : Date.monthNames, - - dayNames : Date.dayNames, - - nextText : 'Next Month (Control+Right)', - - prevText : 'Previous Month (Control+Left)', - - monthYearText : 'Choose a month (Control+Up/Down to move years)', - - startDay : 0, - - showToday : true, - - - - - - - - - focusOnSelect: true, - - - - initHour: 12, - - - initComponent : function(){ - Ext.DatePicker.superclass.initComponent.call(this); - - this.value = this.value ? - this.value.clearTime(true) : new Date().clearTime(); - - this.addEvents( - - 'select' - ); - - if(this.handler){ - this.on('select', this.handler, this.scope || this); - } - - this.initDisabledDays(); - }, - - - initDisabledDays : function(){ - if(!this.disabledDatesRE && this.disabledDates){ - var dd = this.disabledDates, - len = dd.length - 1, - re = '(?:'; - - Ext.each(dd, function(d, i){ - re += Ext.isDate(d) ? '^' + Ext.escapeRe(d.dateFormat(this.format)) + '$' : dd[i]; - if(i != len){ - re += '|'; - } - }, this); - this.disabledDatesRE = new RegExp(re + ')'); - } - }, - - - setDisabledDates : function(dd){ - if(Ext.isArray(dd)){ - this.disabledDates = dd; - this.disabledDatesRE = null; - }else{ - this.disabledDatesRE = dd; - } - this.initDisabledDays(); - this.update(this.value, true); - }, - - - setDisabledDays : function(dd){ - this.disabledDays = dd; - this.update(this.value, true); - }, - - - setMinDate : function(dt){ - this.minDate = dt; - this.update(this.value, true); - }, - - - setMaxDate : function(dt){ - this.maxDate = dt; - this.update(this.value, true); - }, - - - setValue : function(value){ - this.value = value.clearTime(true); - this.update(this.value); - }, - - - getValue : function(){ - return this.value; - }, - - - focus : function(){ - this.update(this.activeDate); - }, - - - onEnable: function(initial){ - Ext.DatePicker.superclass.onEnable.call(this); - this.doDisabled(false); - this.update(initial ? this.value : this.activeDate); - if(Ext.isIE){ - this.el.repaint(); - } - - }, - - - onDisable : function(){ - Ext.DatePicker.superclass.onDisable.call(this); - this.doDisabled(true); - if(Ext.isIE && !Ext.isIE8){ - - Ext.each([].concat(this.textNodes, this.el.query('th span')), function(el){ - Ext.fly(el).repaint(); - }); - } - }, - - - doDisabled : function(disabled){ - this.keyNav.setDisabled(disabled); - this.prevRepeater.setDisabled(disabled); - this.nextRepeater.setDisabled(disabled); - if(this.showToday){ - this.todayKeyListener.setDisabled(disabled); - this.todayBtn.setDisabled(disabled); - } - }, - - - onRender : function(container, position){ - var m = [ - '', - '', - '', - this.showToday ? '' : '', - '
          
        '], - dn = this.dayNames, - i; - for(i = 0; i < 7; i++){ - var d = this.startDay+i; - if(d > 6){ - d = d-7; - } - m.push(''); - } - m[m.length] = ''; - for(i = 0; i < 42; i++) { - if(i % 7 === 0 && i !== 0){ - m[m.length] = ''; - } - m[m.length] = ''; - } - m.push('
        ', dn[d].substr(0,1), '
        '); - - var el = document.createElement('div'); - el.className = 'x-date-picker'; - el.innerHTML = m.join(''); - - container.dom.insertBefore(el, position); - - this.el = Ext.get(el); - this.eventEl = Ext.get(el.firstChild); - - this.prevRepeater = new Ext.util.ClickRepeater(this.el.child('td.x-date-left a'), { - handler: this.showPrevMonth, - scope: this, - preventDefault:true, - stopDefault:true - }); - - this.nextRepeater = new Ext.util.ClickRepeater(this.el.child('td.x-date-right a'), { - handler: this.showNextMonth, - scope: this, - preventDefault:true, - stopDefault:true - }); - - this.monthPicker = this.el.down('div.x-date-mp'); - this.monthPicker.enableDisplayMode('block'); - - this.keyNav = new Ext.KeyNav(this.eventEl, { - 'left' : function(e){ - if(e.ctrlKey){ - this.showPrevMonth(); - }else{ - this.update(this.activeDate.add('d', -1)); - } - }, - - 'right' : function(e){ - if(e.ctrlKey){ - this.showNextMonth(); - }else{ - this.update(this.activeDate.add('d', 1)); - } - }, - - 'up' : function(e){ - if(e.ctrlKey){ - this.showNextYear(); - }else{ - this.update(this.activeDate.add('d', -7)); - } - }, - - 'down' : function(e){ - if(e.ctrlKey){ - this.showPrevYear(); - }else{ - this.update(this.activeDate.add('d', 7)); - } - }, - - 'pageUp' : function(e){ - this.showNextMonth(); - }, - - 'pageDown' : function(e){ - this.showPrevMonth(); - }, - - 'enter' : function(e){ - e.stopPropagation(); - return true; - }, - - scope : this - }); - - this.el.unselectable(); - - this.cells = this.el.select('table.x-date-inner tbody td'); - this.textNodes = this.el.query('table.x-date-inner tbody span'); - - this.mbtn = new Ext.Button({ - text: ' ', - tooltip: this.monthYearText, - renderTo: this.el.child('td.x-date-middle', true) - }); - this.mbtn.el.child('em').addClass('x-btn-arrow'); - - if(this.showToday){ - this.todayKeyListener = this.eventEl.addKeyListener(Ext.EventObject.SPACE, this.selectToday, this); - var today = (new Date()).dateFormat(this.format); - this.todayBtn = new Ext.Button({ - renderTo: this.el.child('td.x-date-bottom', true), - text: String.format(this.todayText, today), - tooltip: String.format(this.todayTip, today), - handler: this.selectToday, - scope: this - }); - } - this.mon(this.eventEl, 'mousewheel', this.handleMouseWheel, this); - this.mon(this.eventEl, 'click', this.handleDateClick, this, {delegate: 'a.x-date-date'}); - this.mon(this.mbtn, 'click', this.showMonthPicker, this); - this.onEnable(true); - }, - - - createMonthPicker : function(){ - if(!this.monthPicker.dom.firstChild){ - var buf = ['']; - for(var i = 0; i < 6; i++){ - buf.push( - '', - '', - i === 0 ? - '' : - '' - ); - } - buf.push( - '', - '
        ', Date.getShortMonthName(i), '', Date.getShortMonthName(i + 6), '
        ' - ); - this.monthPicker.update(buf.join('')); - - this.mon(this.monthPicker, 'click', this.onMonthClick, this); - this.mon(this.monthPicker, 'dblclick', this.onMonthDblClick, this); - - this.mpMonths = this.monthPicker.select('td.x-date-mp-month'); - this.mpYears = this.monthPicker.select('td.x-date-mp-year'); - - this.mpMonths.each(function(m, a, i){ - i += 1; - if((i%2) === 0){ - m.dom.xmonth = 5 + Math.round(i * 0.5); - }else{ - m.dom.xmonth = Math.round((i-1) * 0.5); - } - }); - } - }, - - - showMonthPicker : function(){ - if(!this.disabled){ - this.createMonthPicker(); - var size = this.el.getSize(); - this.monthPicker.setSize(size); - this.monthPicker.child('table').setSize(size); - - this.mpSelMonth = (this.activeDate || this.value).getMonth(); - this.updateMPMonth(this.mpSelMonth); - this.mpSelYear = (this.activeDate || this.value).getFullYear(); - this.updateMPYear(this.mpSelYear); - - this.monthPicker.slideIn('t', {duration:0.2}); - } - }, - - - updateMPYear : function(y){ - this.mpyear = y; - var ys = this.mpYears.elements; - for(var i = 1; i <= 10; i++){ - var td = ys[i-1], y2; - if((i%2) === 0){ - y2 = y + Math.round(i * 0.5); - td.firstChild.innerHTML = y2; - td.xyear = y2; - }else{ - y2 = y - (5-Math.round(i * 0.5)); - td.firstChild.innerHTML = y2; - td.xyear = y2; - } - this.mpYears.item(i-1)[y2 == this.mpSelYear ? 'addClass' : 'removeClass']('x-date-mp-sel'); - } - }, - - - updateMPMonth : function(sm){ - this.mpMonths.each(function(m, a, i){ - m[m.dom.xmonth == sm ? 'addClass' : 'removeClass']('x-date-mp-sel'); - }); - }, - - - selectMPMonth : function(m){ - - }, - - - onMonthClick : function(e, t){ - e.stopEvent(); - var el = new Ext.Element(t), pn; - if(el.is('button.x-date-mp-cancel')){ - this.hideMonthPicker(); - } - else if(el.is('button.x-date-mp-ok')){ - var d = new Date(this.mpSelYear, this.mpSelMonth, (this.activeDate || this.value).getDate()); - if(d.getMonth() != this.mpSelMonth){ - - d = new Date(this.mpSelYear, this.mpSelMonth, 1).getLastDateOfMonth(); - } - this.update(d); - this.hideMonthPicker(); - } - else if((pn = el.up('td.x-date-mp-month', 2))){ - this.mpMonths.removeClass('x-date-mp-sel'); - pn.addClass('x-date-mp-sel'); - this.mpSelMonth = pn.dom.xmonth; - } - else if((pn = el.up('td.x-date-mp-year', 2))){ - this.mpYears.removeClass('x-date-mp-sel'); - pn.addClass('x-date-mp-sel'); - this.mpSelYear = pn.dom.xyear; - } - else if(el.is('a.x-date-mp-prev')){ - this.updateMPYear(this.mpyear-10); - } - else if(el.is('a.x-date-mp-next')){ - this.updateMPYear(this.mpyear+10); - } - }, - - - onMonthDblClick : function(e, t){ - e.stopEvent(); - var el = new Ext.Element(t), pn; - if((pn = el.up('td.x-date-mp-month', 2))){ - this.update(new Date(this.mpSelYear, pn.dom.xmonth, (this.activeDate || this.value).getDate())); - this.hideMonthPicker(); - } - else if((pn = el.up('td.x-date-mp-year', 2))){ - this.update(new Date(pn.dom.xyear, this.mpSelMonth, (this.activeDate || this.value).getDate())); - this.hideMonthPicker(); - } - }, - - - hideMonthPicker : function(disableAnim){ - if(this.monthPicker){ - if(disableAnim === true){ - this.monthPicker.hide(); - }else{ - this.monthPicker.slideOut('t', {duration:0.2}); - } - } - }, - - - showPrevMonth : function(e){ - this.update(this.activeDate.add('mo', -1)); - }, - - - showNextMonth : function(e){ - this.update(this.activeDate.add('mo', 1)); - }, - - - showPrevYear : function(){ - this.update(this.activeDate.add('y', -1)); - }, - - - showNextYear : function(){ - this.update(this.activeDate.add('y', 1)); - }, - - - handleMouseWheel : function(e){ - e.stopEvent(); - if(!this.disabled){ - var delta = e.getWheelDelta(); - if(delta > 0){ - this.showPrevMonth(); - } else if(delta < 0){ - this.showNextMonth(); - } - } - }, - - - handleDateClick : function(e, t){ - e.stopEvent(); - if(!this.disabled && t.dateValue && !Ext.fly(t.parentNode).hasClass('x-date-disabled')){ - this.cancelFocus = this.focusOnSelect === false; - this.setValue(new Date(t.dateValue)); - delete this.cancelFocus; - this.fireEvent('select', this, this.value); - } - }, - - - selectToday : function(){ - if(this.todayBtn && !this.todayBtn.disabled){ - this.setValue(new Date().clearTime()); - this.fireEvent('select', this, this.value); - } - }, - - - update : function(date, forceRefresh){ - if(this.rendered){ - var vd = this.activeDate, vis = this.isVisible(); - this.activeDate = date; - if(!forceRefresh && vd && this.el){ - var t = date.getTime(); - if(vd.getMonth() == date.getMonth() && vd.getFullYear() == date.getFullYear()){ - this.cells.removeClass('x-date-selected'); - this.cells.each(function(c){ - if(c.dom.firstChild.dateValue == t){ - c.addClass('x-date-selected'); - if(vis && !this.cancelFocus){ - Ext.fly(c.dom.firstChild).focus(50); - } - return false; - } - }, this); - return; - } - } - var days = date.getDaysInMonth(), - firstOfMonth = date.getFirstDateOfMonth(), - startingPos = firstOfMonth.getDay()-this.startDay; - - if(startingPos < 0){ - startingPos += 7; - } - days += startingPos; - - var pm = date.add('mo', -1), - prevStart = pm.getDaysInMonth()-startingPos, - cells = this.cells.elements, - textEls = this.textNodes, - - d = (new Date(pm.getFullYear(), pm.getMonth(), prevStart, this.initHour)), - today = new Date().clearTime().getTime(), - sel = date.clearTime(true).getTime(), - min = this.minDate ? this.minDate.clearTime(true) : Number.NEGATIVE_INFINITY, - max = this.maxDate ? this.maxDate.clearTime(true) : Number.POSITIVE_INFINITY, - ddMatch = this.disabledDatesRE, - ddText = this.disabledDatesText, - ddays = this.disabledDays ? this.disabledDays.join('') : false, - ddaysText = this.disabledDaysText, - format = this.format; - - if(this.showToday){ - var td = new Date().clearTime(), - disable = (td < min || td > max || - (ddMatch && format && ddMatch.test(td.dateFormat(format))) || - (ddays && ddays.indexOf(td.getDay()) != -1)); - - if(!this.disabled){ - this.todayBtn.setDisabled(disable); - this.todayKeyListener[disable ? 'disable' : 'enable'](); - } - } - - var setCellClass = function(cal, cell){ - cell.title = ''; - var t = d.clearTime(true).getTime(); - cell.firstChild.dateValue = t; - if(t == today){ - cell.className += ' x-date-today'; - cell.title = cal.todayText; - } - if(t == sel){ - cell.className += ' x-date-selected'; - if(vis){ - Ext.fly(cell.firstChild).focus(50); - } - } - - if(t < min) { - cell.className = ' x-date-disabled'; - cell.title = cal.minText; - return; - } - if(t > max) { - cell.className = ' x-date-disabled'; - cell.title = cal.maxText; - return; - } - if(ddays){ - if(ddays.indexOf(d.getDay()) != -1){ - cell.title = ddaysText; - cell.className = ' x-date-disabled'; - } - } - if(ddMatch && format){ - var fvalue = d.dateFormat(format); - if(ddMatch.test(fvalue)){ - cell.title = ddText.replace('%0', fvalue); - cell.className = ' x-date-disabled'; - } - } - }; - - var i = 0; - for(; i < startingPos; i++) { - textEls[i].innerHTML = (++prevStart); - d.setDate(d.getDate()+1); - cells[i].className = 'x-date-prevday'; - setCellClass(this, cells[i]); - } - for(; i < days; i++){ - var intDay = i - startingPos + 1; - textEls[i].innerHTML = (intDay); - d.setDate(d.getDate()+1); - cells[i].className = 'x-date-active'; - setCellClass(this, cells[i]); - } - var extraDays = 0; - for(; i < 42; i++) { - textEls[i].innerHTML = (++extraDays); - d.setDate(d.getDate()+1); - cells[i].className = 'x-date-nextday'; - setCellClass(this, cells[i]); - } - - this.mbtn.setText(this.monthNames[date.getMonth()] + ' ' + date.getFullYear()); - - if(!this.internalRender){ - var main = this.el.dom.firstChild, - w = main.offsetWidth; - this.el.setWidth(w + this.el.getBorderWidth('lr')); - Ext.fly(main).setWidth(w); - this.internalRender = true; - - - - if(Ext.isOpera && !this.secondPass){ - main.rows[0].cells[1].style.width = (w - (main.rows[0].cells[0].offsetWidth+main.rows[0].cells[2].offsetWidth)) + 'px'; - this.secondPass = true; - this.update.defer(10, this, [date]); - } - } - } - }, - - - beforeDestroy : function() { - if(this.rendered){ - Ext.destroy( - this.keyNav, - this.monthPicker, - this.eventEl, - this.mbtn, - this.nextRepeater, - this.prevRepeater, - this.cells.el, - this.todayBtn - ); - delete this.textNodes; - delete this.cells.elements; - } - } - - -}); - -Ext.reg('datepicker', Ext.DatePicker); - -Ext.LoadMask = function(el, config){ - this.el = Ext.get(el); - Ext.apply(this, config); - if(this.store){ - this.store.on({ - scope: this, - beforeload: this.onBeforeLoad, - load: this.onLoad, - exception: this.onLoad - }); - this.removeMask = Ext.value(this.removeMask, false); - }else{ - var um = this.el.getUpdater(); - um.showLoadIndicator = false; - um.on({ - scope: this, - beforeupdate: this.onBeforeLoad, - update: this.onLoad, - failure: this.onLoad - }); - this.removeMask = Ext.value(this.removeMask, true); - } -}; - -Ext.LoadMask.prototype = { - - - - msg : 'Loading...', - - msgCls : 'x-mask-loading', - - - disabled: false, - - - disable : function(){ - this.disabled = true; - }, - - - enable : function(){ - this.disabled = false; - }, - - - onLoad : function(){ - this.el.unmask(this.removeMask); - }, - - - onBeforeLoad : function(){ - if(!this.disabled){ - this.el.mask(this.msg, this.msgCls); - } - }, - - - show: function(){ - this.onBeforeLoad(); - }, - - - hide: function(){ - this.onLoad(); - }, - - - destroy : function(){ - if(this.store){ - this.store.un('beforeload', this.onBeforeLoad, this); - this.store.un('load', this.onLoad, this); - this.store.un('exception', this.onLoad, this); - }else{ - var um = this.el.getUpdater(); - um.un('beforeupdate', this.onBeforeLoad, this); - um.un('update', this.onLoad, this); - um.un('failure', this.onLoad, this); - } - } -}; -Ext.slider.Thumb = Ext.extend(Object, { - - - dragging: false, - - - constructor: function(config) { - - Ext.apply(this, config || {}, { - cls: 'x-slider-thumb', - - - constrain: false - }); - - Ext.slider.Thumb.superclass.constructor.call(this, config); - - if (this.slider.vertical) { - Ext.apply(this, Ext.slider.Thumb.Vertical); - } - }, - - - render: function() { - this.el = this.slider.innerEl.insertFirst({cls: this.cls}); - - this.initEvents(); - }, - - - enable: function() { - this.disabled = false; - this.el.removeClass(this.slider.disabledClass); - }, - - - disable: function() { - this.disabled = true; - this.el.addClass(this.slider.disabledClass); - }, - - - initEvents: function() { - var el = this.el; - - el.addClassOnOver('x-slider-thumb-over'); - - this.tracker = new Ext.dd.DragTracker({ - onBeforeStart: this.onBeforeDragStart.createDelegate(this), - onStart : this.onDragStart.createDelegate(this), - onDrag : this.onDrag.createDelegate(this), - onEnd : this.onDragEnd.createDelegate(this), - tolerance : 3, - autoStart : 300 - }); - - this.tracker.initEl(el); - }, - - - onBeforeDragStart : function(e) { - if (this.disabled) { - return false; - } else { - this.slider.promoteThumb(this); - return true; - } - }, - - - onDragStart: function(e){ - this.el.addClass('x-slider-thumb-drag'); - this.dragging = true; - this.dragStartValue = this.value; - - this.slider.fireEvent('dragstart', this.slider, e, this); - }, - - - onDrag: function(e) { - var slider = this.slider, - index = this.index, - newValue = this.getNewValue(); - - if (this.constrain) { - var above = slider.thumbs[index + 1], - below = slider.thumbs[index - 1]; - - if (below != undefined && newValue <= below.value) newValue = below.value; - if (above != undefined && newValue >= above.value) newValue = above.value; - } - - slider.setValue(index, newValue, false); - slider.fireEvent('drag', slider, e, this); - }, - - getNewValue: function() { - var slider = this.slider, - pos = slider.innerEl.translatePoints(this.tracker.getXY()); - - return Ext.util.Format.round(slider.reverseValue(pos.left), slider.decimalPrecision); - }, - - - onDragEnd: function(e) { - var slider = this.slider, - value = this.value; - - this.el.removeClass('x-slider-thumb-drag'); - - this.dragging = false; - slider.fireEvent('dragend', slider, e); - - if (this.dragStartValue != value) { - slider.fireEvent('changecomplete', slider, value, this); - } - }, - - - destroy: function(){ - Ext.destroyMembers(this, 'tracker', 'el'); - } -}); - - -Ext.slider.MultiSlider = Ext.extend(Ext.BoxComponent, { - - - vertical: false, - - minValue: 0, - - maxValue: 100, - - decimalPrecision: 0, - - keyIncrement: 1, - - increment: 0, - - - clickRange: [5,15], - - - clickToChange : true, - - animate: true, - - constrainThumbs: true, - - - topThumbZIndex: 10000, - - - initComponent : function(){ - if(!Ext.isDefined(this.value)){ - this.value = this.minValue; - } - - - this.thumbs = []; - - Ext.slider.MultiSlider.superclass.initComponent.call(this); - - this.keyIncrement = Math.max(this.increment, this.keyIncrement); - this.addEvents( - - 'beforechange', - - - 'change', - - - 'changecomplete', - - - 'dragstart', - - - 'drag', - - - 'dragend' - ); - - - if (this.values == undefined || Ext.isEmpty(this.values)) this.values = [0]; - - var values = this.values; - - for (var i=0; i < values.length; i++) { - this.addThumb(values[i]); - } - - if(this.vertical){ - Ext.apply(this, Ext.slider.Vertical); - } - }, - - - addThumb: function(value) { - var thumb = new Ext.slider.Thumb({ - value : value, - slider : this, - index : this.thumbs.length, - constrain: this.constrainThumbs - }); - this.thumbs.push(thumb); - - - if (this.rendered) thumb.render(); - }, - - - promoteThumb: function(topThumb) { - var thumbs = this.thumbs, - zIndex, thumb; - - for (var i = 0, j = thumbs.length; i < j; i++) { - thumb = thumbs[i]; - - if (thumb == topThumb) { - zIndex = this.topThumbZIndex; - } else { - zIndex = ''; - } - - thumb.el.setStyle('zIndex', zIndex); - } - }, - - - onRender : function() { - this.autoEl = { - cls: 'x-slider ' + (this.vertical ? 'x-slider-vert' : 'x-slider-horz'), - cn : { - cls: 'x-slider-end', - cn : { - cls:'x-slider-inner', - cn : [{tag:'a', cls:'x-slider-focus', href:"#", tabIndex: '-1', hidefocus:'on'}] - } - } - }; - - Ext.slider.MultiSlider.superclass.onRender.apply(this, arguments); - - this.endEl = this.el.first(); - this.innerEl = this.endEl.first(); - this.focusEl = this.innerEl.child('.x-slider-focus'); - - - for (var i=0; i < this.thumbs.length; i++) { - this.thumbs[i].render(); - } - - - var thumb = this.innerEl.child('.x-slider-thumb'); - this.halfThumb = (this.vertical ? thumb.getHeight() : thumb.getWidth()) / 2; - - this.initEvents(); - }, - - - initEvents : function(){ - this.mon(this.el, { - scope : this, - mousedown: this.onMouseDown, - keydown : this.onKeyDown - }); - - this.focusEl.swallowEvent("click", true); - }, - - - onMouseDown : function(e){ - if(this.disabled){ - return; - } - - - var thumbClicked = false; - for (var i=0; i < this.thumbs.length; i++) { - thumbClicked = thumbClicked || e.target == this.thumbs[i].el.dom; - } - - if (this.clickToChange && !thumbClicked) { - var local = this.innerEl.translatePoints(e.getXY()); - this.onClickChange(local); - } - this.focus(); - }, - - - onClickChange : function(local) { - if (local.top > this.clickRange[0] && local.top < this.clickRange[1]) { - - var thumb = this.getNearest(local, 'left'), - index = thumb.index; - - this.setValue(index, Ext.util.Format.round(this.reverseValue(local.left), this.decimalPrecision), undefined, true); - } - }, - - - getNearest: function(local, prop) { - var localValue = prop == 'top' ? this.innerEl.getHeight() - local[prop] : local[prop], - clickValue = this.reverseValue(localValue), - nearestDistance = (this.maxValue - this.minValue) + 5, - index = 0, - nearest = null; - - for (var i=0; i < this.thumbs.length; i++) { - var thumb = this.thumbs[i], - value = thumb.value, - dist = Math.abs(value - clickValue); - - if (Math.abs(dist <= nearestDistance)) { - nearest = thumb; - index = i; - nearestDistance = dist; - } - } - return nearest; - }, - - - onKeyDown : function(e){ - - if(this.disabled || this.thumbs.length !== 1){ - e.preventDefault(); - return; - } - var k = e.getKey(), - val; - switch(k){ - case e.UP: - case e.RIGHT: - e.stopEvent(); - val = e.ctrlKey ? this.maxValue : this.getValue(0) + this.keyIncrement; - this.setValue(0, val, undefined, true); - break; - case e.DOWN: - case e.LEFT: - e.stopEvent(); - val = e.ctrlKey ? this.minValue : this.getValue(0) - this.keyIncrement; - this.setValue(0, val, undefined, true); - break; - default: - e.preventDefault(); - } - }, - - - doSnap : function(value){ - if (!(this.increment && value)) { - return value; - } - var newValue = value, - inc = this.increment, - m = value % inc; - if (m != 0) { - newValue -= m; - if (m * 2 >= inc) { - newValue += inc; - } else if (m * 2 < -inc) { - newValue -= inc; - } - } - return newValue.constrain(this.minValue, this.maxValue); - }, - - - afterRender : function(){ - Ext.slider.MultiSlider.superclass.afterRender.apply(this, arguments); - - for (var i=0; i < this.thumbs.length; i++) { - var thumb = this.thumbs[i]; - - if (thumb.value !== undefined) { - var v = this.normalizeValue(thumb.value); - - if (v !== thumb.value) { - - this.setValue(i, v, false); - } else { - this.moveThumb(i, this.translateValue(v), false); - } - } - }; - }, - - - getRatio : function(){ - var w = this.innerEl.getWidth(), - v = this.maxValue - this.minValue; - return v == 0 ? w : (w/v); - }, - - - normalizeValue : function(v){ - v = this.doSnap(v); - v = Ext.util.Format.round(v, this.decimalPrecision); - v = v.constrain(this.minValue, this.maxValue); - return v; - }, - - - setMinValue : function(val){ - this.minValue = val; - var i = 0, - thumbs = this.thumbs, - len = thumbs.length, - t; - - for(; i < len; ++i){ - t = thumbs[i]; - t.value = t.value < val ? val : t.value; - } - this.syncThumb(); - }, - - - setMaxValue : function(val){ - this.maxValue = val; - var i = 0, - thumbs = this.thumbs, - len = thumbs.length, - t; - - for(; i < len; ++i){ - t = thumbs[i]; - t.value = t.value > val ? val : t.value; - } - this.syncThumb(); - }, - - - setValue : function(index, v, animate, changeComplete) { - var thumb = this.thumbs[index], - el = thumb.el; - - v = this.normalizeValue(v); - - if (v !== thumb.value && this.fireEvent('beforechange', this, v, thumb.value, thumb) !== false) { - thumb.value = v; - if(this.rendered){ - this.moveThumb(index, this.translateValue(v), animate !== false); - this.fireEvent('change', this, v, thumb); - if(changeComplete){ - this.fireEvent('changecomplete', this, v, thumb); - } - } - } - }, - - - translateValue : function(v) { - var ratio = this.getRatio(); - return (v * ratio) - (this.minValue * ratio) - this.halfThumb; - }, - - - reverseValue : function(pos){ - var ratio = this.getRatio(); - return (pos + (this.minValue * ratio)) / ratio; - }, - - - moveThumb: function(index, v, animate){ - var thumb = this.thumbs[index].el; - - if(!animate || this.animate === false){ - thumb.setLeft(v); - }else{ - thumb.shift({left: v, stopFx: true, duration:.35}); - } - }, - - - focus : function(){ - this.focusEl.focus(10); - }, - - - onResize : function(w, h){ - var thumbs = this.thumbs, - len = thumbs.length, - i = 0; - - - for(; i < len; ++i){ - thumbs[i].el.stopFx(); - } - - if(Ext.isNumber(w)){ - this.innerEl.setWidth(w - (this.el.getPadding('l') + this.endEl.getPadding('r'))); - } - this.syncThumb(); - Ext.slider.MultiSlider.superclass.onResize.apply(this, arguments); - }, - - - onDisable: function(){ - Ext.slider.MultiSlider.superclass.onDisable.call(this); - - for (var i=0; i < this.thumbs.length; i++) { - var thumb = this.thumbs[i], - el = thumb.el; - - thumb.disable(); - - if(Ext.isIE){ - - - var xy = el.getXY(); - el.hide(); - - this.innerEl.addClass(this.disabledClass).dom.disabled = true; - - if (!this.thumbHolder) { - this.thumbHolder = this.endEl.createChild({cls: 'x-slider-thumb ' + this.disabledClass}); - } - - this.thumbHolder.show().setXY(xy); - } - } - }, - - - onEnable: function(){ - Ext.slider.MultiSlider.superclass.onEnable.call(this); - - for (var i=0; i < this.thumbs.length; i++) { - var thumb = this.thumbs[i], - el = thumb.el; - - thumb.enable(); - - if (Ext.isIE) { - this.innerEl.removeClass(this.disabledClass).dom.disabled = false; - - if (this.thumbHolder) this.thumbHolder.hide(); - - el.show(); - this.syncThumb(); - } - } - }, - - - syncThumb : function() { - if (this.rendered) { - for (var i=0; i < this.thumbs.length; i++) { - this.moveThumb(i, this.translateValue(this.thumbs[i].value)); - } - } - }, - - - getValue : function(index) { - return this.thumbs[index].value; - }, - - - getValues: function() { - var values = []; - - for (var i=0; i < this.thumbs.length; i++) { - values.push(this.thumbs[i].value); - } - - return values; - }, - - - beforeDestroy : function(){ - var thumbs = this.thumbs; - for(var i = 0, len = thumbs.length; i < len; ++i){ - thumbs[i].destroy(); - thumbs[i] = null; - } - Ext.destroyMembers(this, 'endEl', 'innerEl', 'focusEl', 'thumbHolder'); - Ext.slider.MultiSlider.superclass.beforeDestroy.call(this); - } -}); - -Ext.reg('multislider', Ext.slider.MultiSlider); - - -Ext.slider.SingleSlider = Ext.extend(Ext.slider.MultiSlider, { - constructor: function(config) { - config = config || {}; - - Ext.applyIf(config, { - values: [config.value || 0] - }); - - Ext.slider.SingleSlider.superclass.constructor.call(this, config); - }, - - - getValue: function() { - - return Ext.slider.SingleSlider.superclass.getValue.call(this, 0); - }, - - - setValue: function(value, animate) { - var args = Ext.toArray(arguments), - len = args.length; - - - - - if (len == 1 || (len <= 3 && typeof arguments[1] != 'number')) { - args.unshift(0); - } - - return Ext.slider.SingleSlider.superclass.setValue.apply(this, args); - }, - - - syncThumb : function() { - return Ext.slider.SingleSlider.superclass.syncThumb.apply(this, [0].concat(arguments)); - }, - - - getNearest : function(){ - - return this.thumbs[0]; - } -}); - - -Ext.Slider = Ext.slider.SingleSlider; - -Ext.reg('slider', Ext.slider.SingleSlider); - - -Ext.slider.Vertical = { - onResize : function(w, h){ - this.innerEl.setHeight(h - (this.el.getPadding('t') + this.endEl.getPadding('b'))); - this.syncThumb(); - }, - - getRatio : function(){ - var h = this.innerEl.getHeight(), - v = this.maxValue - this.minValue; - return h/v; - }, - - moveThumb: function(index, v, animate) { - var thumb = this.thumbs[index], - el = thumb.el; - - if (!animate || this.animate === false) { - el.setBottom(v); - } else { - el.shift({bottom: v, stopFx: true, duration:.35}); - } - }, - - onClickChange : function(local) { - if (local.left > this.clickRange[0] && local.left < this.clickRange[1]) { - var thumb = this.getNearest(local, 'top'), - index = thumb.index, - value = this.minValue + this.reverseValue(this.innerEl.getHeight() - local.top); - - this.setValue(index, Ext.util.Format.round(value, this.decimalPrecision), undefined, true); - } - } -}; - - -Ext.slider.Thumb.Vertical = { - getNewValue: function() { - var slider = this.slider, - innerEl = slider.innerEl, - pos = innerEl.translatePoints(this.tracker.getXY()), - bottom = innerEl.getHeight() - pos.top; - - return slider.minValue + Ext.util.Format.round(bottom / slider.getRatio(), slider.decimalPrecision); - } -}; - -Ext.ProgressBar = Ext.extend(Ext.BoxComponent, { - - baseCls : 'x-progress', - - - animate : false, - - - waitTimer : null, - - - initComponent : function(){ - Ext.ProgressBar.superclass.initComponent.call(this); - this.addEvents( - - "update" - ); - }, - - - onRender : function(ct, position){ - var tpl = new Ext.Template( - '
        ', - '
        ', - '
        ', - '
        ', - '
         
        ', - '
        ', - '
        ', - '
        ', - '
         
        ', - '
        ', - '
        ', - '
        ' - ); - - this.el = position ? tpl.insertBefore(position, {cls: this.baseCls}, true) - : tpl.append(ct, {cls: this.baseCls}, true); - - if(this.id){ - this.el.dom.id = this.id; - } - var inner = this.el.dom.firstChild; - this.progressBar = Ext.get(inner.firstChild); - - if(this.textEl){ - - this.textEl = Ext.get(this.textEl); - delete this.textTopEl; - }else{ - - this.textTopEl = Ext.get(this.progressBar.dom.firstChild); - var textBackEl = Ext.get(inner.childNodes[1]); - this.textTopEl.setStyle("z-index", 99).addClass('x-hidden'); - this.textEl = new Ext.CompositeElement([this.textTopEl.dom.firstChild, textBackEl.dom.firstChild]); - this.textEl.setWidth(inner.offsetWidth); - } - this.progressBar.setHeight(inner.offsetHeight); - }, - - - afterRender : function(){ - Ext.ProgressBar.superclass.afterRender.call(this); - if(this.value){ - this.updateProgress(this.value, this.text); - }else{ - this.updateText(this.text); - } - }, - - - updateProgress : function(value, text, animate){ - this.value = value || 0; - if(text){ - this.updateText(text); - } - if(this.rendered && !this.isDestroyed){ - var w = Math.floor(value*this.el.dom.firstChild.offsetWidth); - this.progressBar.setWidth(w, animate === true || (animate !== false && this.animate)); - if(this.textTopEl){ - - this.textTopEl.removeClass('x-hidden').setWidth(w); - } - } - this.fireEvent('update', this, value, text); - return this; - }, - - - wait : function(o){ - if(!this.waitTimer){ - var scope = this; - o = o || {}; - this.updateText(o.text); - this.waitTimer = Ext.TaskMgr.start({ - run: function(i){ - var inc = o.increment || 10; - i -= 1; - this.updateProgress(((((i+inc)%inc)+1)*(100/inc))*0.01, null, o.animate); - }, - interval: o.interval || 1000, - duration: o.duration, - onStop: function(){ - if(o.fn){ - o.fn.apply(o.scope || this); - } - this.reset(); - }, - scope: scope - }); - } - return this; - }, - - - isWaiting : function(){ - return this.waitTimer !== null; - }, - - - updateText : function(text){ - this.text = text || ' '; - if(this.rendered){ - this.textEl.update(this.text); - } - return this; - }, - - - syncProgressBar : function(){ - if(this.value){ - this.updateProgress(this.value, this.text); - } - return this; - }, - - - setSize : function(w, h){ - Ext.ProgressBar.superclass.setSize.call(this, w, h); - if(this.textTopEl){ - var inner = this.el.dom.firstChild; - this.textEl.setSize(inner.offsetWidth, inner.offsetHeight); - } - this.syncProgressBar(); - return this; - }, - - - reset : function(hide){ - this.updateProgress(0); - if(this.textTopEl){ - this.textTopEl.addClass('x-hidden'); - } - this.clearTimer(); - if(hide === true){ - this.hide(); - } - return this; - }, - - - clearTimer : function(){ - if(this.waitTimer){ - this.waitTimer.onStop = null; - Ext.TaskMgr.stop(this.waitTimer); - this.waitTimer = null; - } - }, - - onDestroy: function(){ - this.clearTimer(); - if(this.rendered){ - if(this.textEl.isComposite){ - this.textEl.clear(); - } - Ext.destroyMembers(this, 'textEl', 'progressBar', 'textTopEl'); - } - Ext.ProgressBar.superclass.onDestroy.call(this); - } -}); -Ext.reg('progress', Ext.ProgressBar); - -(function() { - -var Event=Ext.EventManager; -var Dom=Ext.lib.Dom; - - -Ext.dd.DragDrop = function(id, sGroup, config) { - if(id) { - this.init(id, sGroup, config); - } -}; - -Ext.dd.DragDrop.prototype = { - - - - - id: null, - - - config: null, - - - dragElId: null, - - - handleElId: null, - - - invalidHandleTypes: null, - - - invalidHandleIds: null, - - - invalidHandleClasses: null, - - - startPageX: 0, - - - startPageY: 0, - - - groups: null, - - - locked: false, - - - lock: function() { - this.locked = true; - }, - - - moveOnly: false, - - - unlock: function() { - this.locked = false; - }, - - - isTarget: true, - - - padding: null, - - - _domRef: null, - - - __ygDragDrop: true, - - - constrainX: false, - - - constrainY: false, - - - minX: 0, - - - maxX: 0, - - - minY: 0, - - - maxY: 0, - - - maintainOffset: false, - - - xTicks: null, - - - yTicks: null, - - - primaryButtonOnly: true, - - - available: false, - - - hasOuterHandles: false, - - - b4StartDrag: function(x, y) { }, - - - startDrag: function(x, y) { }, - - - b4Drag: function(e) { }, - - - onDrag: function(e) { }, - - - onDragEnter: function(e, id) { }, - - - b4DragOver: function(e) { }, - - - onDragOver: function(e, id) { }, - - - b4DragOut: function(e) { }, - - - onDragOut: function(e, id) { }, - - - b4DragDrop: function(e) { }, - - - onDragDrop: function(e, id) { }, - - - onInvalidDrop: function(e) { }, - - - b4EndDrag: function(e) { }, - - - endDrag: function(e) { }, - - - b4MouseDown: function(e) { }, - - - onMouseDown: function(e) { }, - - - onMouseUp: function(e) { }, - - - onAvailable: function () { - }, - - - defaultPadding : {left:0, right:0, top:0, bottom:0}, - - - constrainTo : function(constrainTo, pad, inContent){ - if(Ext.isNumber(pad)){ - pad = {left: pad, right:pad, top:pad, bottom:pad}; - } - pad = pad || this.defaultPadding; - var b = Ext.get(this.getEl()).getBox(), - ce = Ext.get(constrainTo), - s = ce.getScroll(), - c, - cd = ce.dom; - if(cd == document.body){ - c = { x: s.left, y: s.top, width: Ext.lib.Dom.getViewWidth(), height: Ext.lib.Dom.getViewHeight()}; - }else{ - var xy = ce.getXY(); - c = {x : xy[0], y: xy[1], width: cd.clientWidth, height: cd.clientHeight}; - } - - - var topSpace = b.y - c.y, - leftSpace = b.x - c.x; - - this.resetConstraints(); - this.setXConstraint(leftSpace - (pad.left||0), - c.width - leftSpace - b.width - (pad.right||0), - this.xTickSize - ); - this.setYConstraint(topSpace - (pad.top||0), - c.height - topSpace - b.height - (pad.bottom||0), - this.yTickSize - ); - }, - - - getEl: function() { - if (!this._domRef) { - this._domRef = Ext.getDom(this.id); - } - - return this._domRef; - }, - - - getDragEl: function() { - return Ext.getDom(this.dragElId); - }, - - - init: function(id, sGroup, config) { - this.initTarget(id, sGroup, config); - Event.on(this.id, "mousedown", this.handleMouseDown, this); - - }, - - - initTarget: function(id, sGroup, config) { - - - this.config = config || {}; - - - this.DDM = Ext.dd.DDM; - - this.groups = {}; - - - - if (typeof id !== "string") { - id = Ext.id(id); - } - - - this.id = id; - - - this.addToGroup((sGroup) ? sGroup : "default"); - - - - this.handleElId = id; - - - this.setDragElId(id); - - - this.invalidHandleTypes = { A: "A" }; - this.invalidHandleIds = {}; - this.invalidHandleClasses = []; - - this.applyConfig(); - - this.handleOnAvailable(); - }, - - - applyConfig: function() { - - - - this.padding = this.config.padding || [0, 0, 0, 0]; - this.isTarget = (this.config.isTarget !== false); - this.maintainOffset = (this.config.maintainOffset); - this.primaryButtonOnly = (this.config.primaryButtonOnly !== false); - - }, - - - handleOnAvailable: function() { - this.available = true; - this.resetConstraints(); - this.onAvailable(); - }, - - - setPadding: function(iTop, iRight, iBot, iLeft) { - - if (!iRight && 0 !== iRight) { - this.padding = [iTop, iTop, iTop, iTop]; - } else if (!iBot && 0 !== iBot) { - this.padding = [iTop, iRight, iTop, iRight]; - } else { - this.padding = [iTop, iRight, iBot, iLeft]; - } - }, - - - setInitPosition: function(diffX, diffY) { - var el = this.getEl(); - - if (!this.DDM.verifyEl(el)) { - return; - } - - var dx = diffX || 0; - var dy = diffY || 0; - - var p = Dom.getXY( el ); - - this.initPageX = p[0] - dx; - this.initPageY = p[1] - dy; - - this.lastPageX = p[0]; - this.lastPageY = p[1]; - - this.setStartPosition(p); - }, - - - setStartPosition: function(pos) { - var p = pos || Dom.getXY( this.getEl() ); - this.deltaSetXY = null; - - this.startPageX = p[0]; - this.startPageY = p[1]; - }, - - - addToGroup: function(sGroup) { - this.groups[sGroup] = true; - this.DDM.regDragDrop(this, sGroup); - }, - - - removeFromGroup: function(sGroup) { - if (this.groups[sGroup]) { - delete this.groups[sGroup]; - } - - this.DDM.removeDDFromGroup(this, sGroup); - }, - - - setDragElId: function(id) { - this.dragElId = id; - }, - - - setHandleElId: function(id) { - if (typeof id !== "string") { - id = Ext.id(id); - } - this.handleElId = id; - this.DDM.regHandle(this.id, id); - }, - - - setOuterHandleElId: function(id) { - if (typeof id !== "string") { - id = Ext.id(id); - } - Event.on(id, "mousedown", - this.handleMouseDown, this); - this.setHandleElId(id); - - this.hasOuterHandles = true; - }, - - - unreg: function() { - Event.un(this.id, "mousedown", - this.handleMouseDown); - this._domRef = null; - this.DDM._remove(this); - }, - - destroy : function(){ - this.unreg(); - }, - - - isLocked: function() { - return (this.DDM.isLocked() || this.locked); - }, - - - handleMouseDown: function(e, oDD){ - if (this.primaryButtonOnly && e.button != 0) { - return; - } - - if (this.isLocked()) { - return; - } - - this.DDM.refreshCache(this.groups); - - var pt = new Ext.lib.Point(Ext.lib.Event.getPageX(e), Ext.lib.Event.getPageY(e)); - if (!this.hasOuterHandles && !this.DDM.isOverTarget(pt, this) ) { - } else { - if (this.clickValidator(e)) { - - - this.setStartPosition(); - - this.b4MouseDown(e); - this.onMouseDown(e); - - this.DDM.handleMouseDown(e, this); - - this.DDM.stopEvent(e); - } else { - - - } - } - }, - - clickValidator: function(e) { - var target = e.getTarget(); - return ( this.isValidHandleChild(target) && - (this.id == this.handleElId || - this.DDM.handleWasClicked(target, this.id)) ); - }, - - - addInvalidHandleType: function(tagName) { - var type = tagName.toUpperCase(); - this.invalidHandleTypes[type] = type; - }, - - - addInvalidHandleId: function(id) { - if (typeof id !== "string") { - id = Ext.id(id); - } - this.invalidHandleIds[id] = id; - }, - - - addInvalidHandleClass: function(cssClass) { - this.invalidHandleClasses.push(cssClass); - }, - - - removeInvalidHandleType: function(tagName) { - var type = tagName.toUpperCase(); - - delete this.invalidHandleTypes[type]; - }, - - - removeInvalidHandleId: function(id) { - if (typeof id !== "string") { - id = Ext.id(id); - } - delete this.invalidHandleIds[id]; - }, - - - removeInvalidHandleClass: function(cssClass) { - for (var i=0, len=this.invalidHandleClasses.length; i= this.minX; i = i - iTickSize) { - if (!tickMap[i]) { - this.xTicks[this.xTicks.length] = i; - tickMap[i] = true; - } - } - - for (i = this.initPageX; i <= this.maxX; i = i + iTickSize) { - if (!tickMap[i]) { - this.xTicks[this.xTicks.length] = i; - tickMap[i] = true; - } - } - - this.xTicks.sort(this.DDM.numericSort) ; - }, - - - setYTicks: function(iStartY, iTickSize) { - this.yTicks = []; - this.yTickSize = iTickSize; - - var tickMap = {}; - - for (var i = this.initPageY; i >= this.minY; i = i - iTickSize) { - if (!tickMap[i]) { - this.yTicks[this.yTicks.length] = i; - tickMap[i] = true; - } - } - - for (i = this.initPageY; i <= this.maxY; i = i + iTickSize) { - if (!tickMap[i]) { - this.yTicks[this.yTicks.length] = i; - tickMap[i] = true; - } - } - - this.yTicks.sort(this.DDM.numericSort) ; - }, - - - setXConstraint: function(iLeft, iRight, iTickSize) { - this.leftConstraint = iLeft; - this.rightConstraint = iRight; - - this.minX = this.initPageX - iLeft; - this.maxX = this.initPageX + iRight; - if (iTickSize) { this.setXTicks(this.initPageX, iTickSize); } - - this.constrainX = true; - }, - - - clearConstraints: function() { - this.constrainX = false; - this.constrainY = false; - this.clearTicks(); - }, - - - clearTicks: function() { - this.xTicks = null; - this.yTicks = null; - this.xTickSize = 0; - this.yTickSize = 0; - }, - - - setYConstraint: function(iUp, iDown, iTickSize) { - this.topConstraint = iUp; - this.bottomConstraint = iDown; - - this.minY = this.initPageY - iUp; - this.maxY = this.initPageY + iDown; - if (iTickSize) { this.setYTicks(this.initPageY, iTickSize); } - - this.constrainY = true; - - }, - - - resetConstraints: function() { - - if (this.initPageX || this.initPageX === 0) { - - var dx = (this.maintainOffset) ? this.lastPageX - this.initPageX : 0; - var dy = (this.maintainOffset) ? this.lastPageY - this.initPageY : 0; - - this.setInitPosition(dx, dy); - - - } else { - this.setInitPosition(); - } - - if (this.constrainX) { - this.setXConstraint( this.leftConstraint, - this.rightConstraint, - this.xTickSize ); - } - - if (this.constrainY) { - this.setYConstraint( this.topConstraint, - this.bottomConstraint, - this.yTickSize ); - } - }, - - - getTick: function(val, tickArray) { - if (!tickArray) { - - - return val; - } else if (tickArray[0] >= val) { - - - return tickArray[0]; - } else { - for (var i=0, len=tickArray.length; i= val) { - var diff1 = val - tickArray[i]; - var diff2 = tickArray[next] - val; - return (diff2 > diff1) ? tickArray[i] : tickArray[next]; - } - } - - - - return tickArray[tickArray.length - 1]; - } - }, - - - toString: function() { - return ("DragDrop " + this.id); - } - -}; - -})(); - - - - -if (!Ext.dd.DragDropMgr) { - - -Ext.dd.DragDropMgr = function() { - - var Event = Ext.EventManager; - - return { - - - ids: {}, - - - handleIds: {}, - - - dragCurrent: null, - - - dragOvers: {}, - - - deltaX: 0, - - - deltaY: 0, - - - preventDefault: true, - - - stopPropagation: true, - - - initialized: false, - - - locked: false, - - - init: function() { - this.initialized = true; - }, - - - POINT: 0, - - - INTERSECT: 1, - - - mode: 0, - - - _execOnAll: function(sMethod, args) { - for (var i in this.ids) { - for (var j in this.ids[i]) { - var oDD = this.ids[i][j]; - if (! this.isTypeOfDD(oDD)) { - continue; - } - oDD[sMethod].apply(oDD, args); - } - } - }, - - - _onLoad: function() { - - this.init(); - - - Event.on(document, "mouseup", this.handleMouseUp, this, true); - Event.on(document, "mousemove", this.handleMouseMove, this, true); - Event.on(window, "unload", this._onUnload, this, true); - Event.on(window, "resize", this._onResize, this, true); - - - }, - - - _onResize: function(e) { - this._execOnAll("resetConstraints", []); - }, - - - lock: function() { this.locked = true; }, - - - unlock: function() { this.locked = false; }, - - - isLocked: function() { return this.locked; }, - - - locationCache: {}, - - - useCache: true, - - - clickPixelThresh: 3, - - - clickTimeThresh: 350, - - - dragThreshMet: false, - - - clickTimeout: null, - - - startX: 0, - - - startY: 0, - - - regDragDrop: function(oDD, sGroup) { - if (!this.initialized) { this.init(); } - - if (!this.ids[sGroup]) { - this.ids[sGroup] = {}; - } - this.ids[sGroup][oDD.id] = oDD; - }, - - - removeDDFromGroup: function(oDD, sGroup) { - if (!this.ids[sGroup]) { - this.ids[sGroup] = {}; - } - - var obj = this.ids[sGroup]; - if (obj && obj[oDD.id]) { - delete obj[oDD.id]; - } - }, - - - _remove: function(oDD) { - for (var g in oDD.groups) { - if (g && this.ids[g] && this.ids[g][oDD.id]) { - delete this.ids[g][oDD.id]; - } - } - delete this.handleIds[oDD.id]; - }, - - - regHandle: function(sDDId, sHandleId) { - if (!this.handleIds[sDDId]) { - this.handleIds[sDDId] = {}; - } - this.handleIds[sDDId][sHandleId] = sHandleId; - }, - - - isDragDrop: function(id) { - return ( this.getDDById(id) ) ? true : false; - }, - - - getRelated: function(p_oDD, bTargetsOnly) { - var oDDs = []; - for (var i in p_oDD.groups) { - for (var j in this.ids[i]) { - var dd = this.ids[i][j]; - if (! this.isTypeOfDD(dd)) { - continue; - } - if (!bTargetsOnly || dd.isTarget) { - oDDs[oDDs.length] = dd; - } - } - } - - return oDDs; - }, - - - isLegalTarget: function (oDD, oTargetDD) { - var targets = this.getRelated(oDD, true); - for (var i=0, len=targets.length;i this.clickPixelThresh || - diffY > this.clickPixelThresh) { - this.startDrag(this.startX, this.startY); - } - } - - if (this.dragThreshMet) { - this.dragCurrent.b4Drag(e); - this.dragCurrent.onDrag(e); - if(!this.dragCurrent.moveOnly){ - this.fireEvents(e, false); - } - } - - this.stopEvent(e); - - return true; - }, - - - fireEvents: function(e, isDrop) { - var dc = this.dragCurrent; - - - - if (!dc || dc.isLocked()) { - return; - } - - var pt = e.getPoint(); - - - var oldOvers = []; - - var outEvts = []; - var overEvts = []; - var dropEvts = []; - var enterEvts = []; - - - - for (var i in this.dragOvers) { - - var ddo = this.dragOvers[i]; - - if (! this.isTypeOfDD(ddo)) { - continue; - } - - if (! this.isOverTarget(pt, ddo, this.mode)) { - outEvts.push( ddo ); - } - - oldOvers[i] = true; - delete this.dragOvers[i]; - } - - for (var sGroup in dc.groups) { - - if ("string" != typeof sGroup) { - continue; - } - - for (i in this.ids[sGroup]) { - var oDD = this.ids[sGroup][i]; - if (! this.isTypeOfDD(oDD)) { - continue; - } - - if (oDD.isTarget && !oDD.isLocked() && ((oDD != dc) || (dc.ignoreSelf === false))) { - if (this.isOverTarget(pt, oDD, this.mode)) { - - if (isDrop) { - dropEvts.push( oDD ); - - } else { - - - if (!oldOvers[oDD.id]) { - enterEvts.push( oDD ); - - } else { - overEvts.push( oDD ); - } - - this.dragOvers[oDD.id] = oDD; - } - } - } - } - } - - if (this.mode) { - if (outEvts.length) { - dc.b4DragOut(e, outEvts); - dc.onDragOut(e, outEvts); - } - - if (enterEvts.length) { - dc.onDragEnter(e, enterEvts); - } - - if (overEvts.length) { - dc.b4DragOver(e, overEvts); - dc.onDragOver(e, overEvts); - } - - if (dropEvts.length) { - dc.b4DragDrop(e, dropEvts); - dc.onDragDrop(e, dropEvts); - } - - } else { - - var len = 0; - for (i=0, len=outEvts.length; i 2000) { - } else { - setTimeout(DDM._addListeners, 10); - if (document && document.body) { - DDM._timeoutCount += 1; - } - } - } - }, - - - handleWasClicked: function(node, id) { - if (this.isHandle(id, node.id)) { - return true; - } else { - - var p = node.parentNode; - - while (p) { - if (this.isHandle(id, p.id)) { - return true; - } else { - p = p.parentNode; - } - } - } - - return false; - } - - }; - -}(); - - -Ext.dd.DDM = Ext.dd.DragDropMgr; -Ext.dd.DDM._addListeners(); - -} - - -Ext.dd.DD = function(id, sGroup, config) { - if (id) { - this.init(id, sGroup, config); - } -}; - -Ext.extend(Ext.dd.DD, Ext.dd.DragDrop, { - - - scroll: true, - - - autoOffset: function(iPageX, iPageY) { - var x = iPageX - this.startPageX; - var y = iPageY - this.startPageY; - this.setDelta(x, y); - }, - - - setDelta: function(iDeltaX, iDeltaY) { - this.deltaX = iDeltaX; - this.deltaY = iDeltaY; - }, - - - setDragElPos: function(iPageX, iPageY) { - - - - var el = this.getDragEl(); - this.alignElWithMouse(el, iPageX, iPageY); - }, - - - alignElWithMouse: function(el, iPageX, iPageY) { - var oCoord = this.getTargetCoord(iPageX, iPageY); - var fly = el.dom ? el : Ext.fly(el, '_dd'); - if (!this.deltaSetXY) { - var aCoord = [oCoord.x, oCoord.y]; - fly.setXY(aCoord); - var newLeft = fly.getLeft(true); - var newTop = fly.getTop(true); - this.deltaSetXY = [ newLeft - oCoord.x, newTop - oCoord.y ]; - } else { - fly.setLeftTop(oCoord.x + this.deltaSetXY[0], oCoord.y + this.deltaSetXY[1]); - } - - this.cachePosition(oCoord.x, oCoord.y); - this.autoScroll(oCoord.x, oCoord.y, el.offsetHeight, el.offsetWidth); - return oCoord; - }, - - - cachePosition: function(iPageX, iPageY) { - if (iPageX) { - this.lastPageX = iPageX; - this.lastPageY = iPageY; - } else { - var aCoord = Ext.lib.Dom.getXY(this.getEl()); - this.lastPageX = aCoord[0]; - this.lastPageY = aCoord[1]; - } - }, - - - autoScroll: function(x, y, h, w) { - - if (this.scroll) { - - var clientH = Ext.lib.Dom.getViewHeight(); - - - var clientW = Ext.lib.Dom.getViewWidth(); - - - var st = this.DDM.getScrollTop(); - - - var sl = this.DDM.getScrollLeft(); - - - var bot = h + y; - - - var right = w + x; - - - - - var toBot = (clientH + st - y - this.deltaY); - - - var toRight = (clientW + sl - x - this.deltaX); - - - - - var thresh = 40; - - - - - var scrAmt = (document.all) ? 80 : 30; - - - - if ( bot > clientH && toBot < thresh ) { - window.scrollTo(sl, st + scrAmt); - } - - - - if ( y < st && st > 0 && y - st < thresh ) { - window.scrollTo(sl, st - scrAmt); - } - - - - if ( right > clientW && toRight < thresh ) { - window.scrollTo(sl + scrAmt, st); - } - - - - if ( x < sl && sl > 0 && x - sl < thresh ) { - window.scrollTo(sl - scrAmt, st); - } - } - }, - - - getTargetCoord: function(iPageX, iPageY) { - var x = iPageX - this.deltaX; - var y = iPageY - this.deltaY; - - if (this.constrainX) { - if (x < this.minX) { x = this.minX; } - if (x > this.maxX) { x = this.maxX; } - } - - if (this.constrainY) { - if (y < this.minY) { y = this.minY; } - if (y > this.maxY) { y = this.maxY; } - } - - x = this.getTick(x, this.xTicks); - y = this.getTick(y, this.yTicks); - - - return {x:x, y:y}; - }, - - - applyConfig: function() { - Ext.dd.DD.superclass.applyConfig.call(this); - this.scroll = (this.config.scroll !== false); - }, - - - b4MouseDown: function(e) { - - this.autoOffset(e.getPageX(), - e.getPageY()); - }, - - - b4Drag: function(e) { - this.setDragElPos(e.getPageX(), - e.getPageY()); - }, - - toString: function() { - return ("DD " + this.id); - } - - - - - - -}); - -Ext.dd.DDProxy = function(id, sGroup, config) { - if (id) { - this.init(id, sGroup, config); - this.initFrame(); - } -}; - - -Ext.dd.DDProxy.dragElId = "ygddfdiv"; - -Ext.extend(Ext.dd.DDProxy, Ext.dd.DD, { - - - resizeFrame: true, - - - centerFrame: false, - - - createFrame: function() { - var self = this; - var body = document.body; - - if (!body || !body.firstChild) { - setTimeout( function() { self.createFrame(); }, 50 ); - return; - } - - var div = this.getDragEl(); - - if (!div) { - div = document.createElement("div"); - div.id = this.dragElId; - var s = div.style; - - s.position = "absolute"; - s.visibility = "hidden"; - s.cursor = "move"; - s.border = "2px solid #aaa"; - s.zIndex = 999; - - - - - body.insertBefore(div, body.firstChild); - } - }, - - - initFrame: function() { - this.createFrame(); - }, - - applyConfig: function() { - Ext.dd.DDProxy.superclass.applyConfig.call(this); - - this.resizeFrame = (this.config.resizeFrame !== false); - this.centerFrame = (this.config.centerFrame); - this.setDragElId(this.config.dragElId || Ext.dd.DDProxy.dragElId); - }, - - - showFrame: function(iPageX, iPageY) { - var el = this.getEl(); - var dragEl = this.getDragEl(); - var s = dragEl.style; - - this._resizeProxy(); - - if (this.centerFrame) { - this.setDelta( Math.round(parseInt(s.width, 10)/2), - Math.round(parseInt(s.height, 10)/2) ); - } - - this.setDragElPos(iPageX, iPageY); - - Ext.fly(dragEl).show(); - }, - - - _resizeProxy: function() { - if (this.resizeFrame) { - var el = this.getEl(); - Ext.fly(this.getDragEl()).setSize(el.offsetWidth, el.offsetHeight); - } - }, - - - b4MouseDown: function(e) { - var x = e.getPageX(); - var y = e.getPageY(); - this.autoOffset(x, y); - this.setDragElPos(x, y); - }, - - - b4StartDrag: function(x, y) { - - this.showFrame(x, y); - }, - - - b4EndDrag: function(e) { - Ext.fly(this.getDragEl()).hide(); - }, - - - - - endDrag: function(e) { - - var lel = this.getEl(); - var del = this.getDragEl(); - - - del.style.visibility = ""; - - this.beforeMove(); - - - lel.style.visibility = "hidden"; - Ext.dd.DDM.moveToEl(lel, del); - del.style.visibility = "hidden"; - lel.style.visibility = ""; - - this.afterDrag(); - }, - - beforeMove : function(){ - - }, - - afterDrag : function(){ - - }, - - toString: function() { - return ("DDProxy " + this.id); - } - -}); - -Ext.dd.DDTarget = function(id, sGroup, config) { - if (id) { - this.initTarget(id, sGroup, config); - } -}; - - -Ext.extend(Ext.dd.DDTarget, Ext.dd.DragDrop, { - - getDragEl: Ext.emptyFn, - - isValidHandleChild: Ext.emptyFn, - - startDrag: Ext.emptyFn, - - endDrag: Ext.emptyFn, - - onDrag: Ext.emptyFn, - - onDragDrop: Ext.emptyFn, - - onDragEnter: Ext.emptyFn, - - onDragOut: Ext.emptyFn, - - onDragOver: Ext.emptyFn, - - onInvalidDrop: Ext.emptyFn, - - onMouseDown: Ext.emptyFn, - - onMouseUp: Ext.emptyFn, - - setXConstraint: Ext.emptyFn, - - setYConstraint: Ext.emptyFn, - - resetConstraints: Ext.emptyFn, - - clearConstraints: Ext.emptyFn, - - clearTicks: Ext.emptyFn, - - setInitPosition: Ext.emptyFn, - - setDragElId: Ext.emptyFn, - - setHandleElId: Ext.emptyFn, - - setOuterHandleElId: Ext.emptyFn, - - addInvalidHandleClass: Ext.emptyFn, - - addInvalidHandleId: Ext.emptyFn, - - addInvalidHandleType: Ext.emptyFn, - - removeInvalidHandleClass: Ext.emptyFn, - - removeInvalidHandleId: Ext.emptyFn, - - removeInvalidHandleType: Ext.emptyFn, - - toString: function() { - return ("DDTarget " + this.id); - } -}); -Ext.dd.DragTracker = Ext.extend(Ext.util.Observable, { - - active: false, - - tolerance: 5, - - autoStart: false, - - constructor : function(config){ - Ext.apply(this, config); - this.addEvents( - - 'mousedown', - - 'mouseup', - - 'mousemove', - - 'dragstart', - - 'dragend', - - 'drag' - ); - - this.dragRegion = new Ext.lib.Region(0,0,0,0); - - if(this.el){ - this.initEl(this.el); - } - Ext.dd.DragTracker.superclass.constructor.call(this, config); - }, - - initEl: function(el){ - this.el = Ext.get(el); - el.on('mousedown', this.onMouseDown, this, - this.delegate ? {delegate: this.delegate} : undefined); - }, - - destroy : function(){ - this.el.un('mousedown', this.onMouseDown, this); - delete this.el; - }, - - onMouseDown: function(e, target){ - if(this.fireEvent('mousedown', this, e) !== false && this.onBeforeStart(e) !== false){ - this.startXY = this.lastXY = e.getXY(); - this.dragTarget = this.delegate ? target : this.el.dom; - if(this.preventDefault !== false){ - e.preventDefault(); - } - Ext.getDoc().on({ - scope: this, - mouseup: this.onMouseUp, - mousemove: this.onMouseMove, - selectstart: this.stopSelect - }); - if(this.autoStart){ - this.timer = this.triggerStart.defer(this.autoStart === true ? 1000 : this.autoStart, this, [e]); - } - } - }, - - onMouseMove: function(e, target){ - - if(this.active && Ext.isIE && !e.browserEvent.button){ - e.preventDefault(); - this.onMouseUp(e); - return; - } - - e.preventDefault(); - var xy = e.getXY(), s = this.startXY; - this.lastXY = xy; - if(!this.active){ - if(Math.abs(s[0]-xy[0]) > this.tolerance || Math.abs(s[1]-xy[1]) > this.tolerance){ - this.triggerStart(e); - }else{ - return; - } - } - this.fireEvent('mousemove', this, e); - this.onDrag(e); - this.fireEvent('drag', this, e); - }, - - onMouseUp: function(e) { - var doc = Ext.getDoc(), - wasActive = this.active; - - doc.un('mousemove', this.onMouseMove, this); - doc.un('mouseup', this.onMouseUp, this); - doc.un('selectstart', this.stopSelect, this); - e.preventDefault(); - this.clearStart(); - this.active = false; - delete this.elRegion; - this.fireEvent('mouseup', this, e); - if(wasActive){ - this.onEnd(e); - this.fireEvent('dragend', this, e); - } - }, - - triggerStart: function(e) { - this.clearStart(); - this.active = true; - this.onStart(e); - this.fireEvent('dragstart', this, e); - }, - - clearStart : function() { - if(this.timer){ - clearTimeout(this.timer); - delete this.timer; - } - }, - - stopSelect : function(e) { - e.stopEvent(); - return false; - }, - - - onBeforeStart : function(e) { - - }, - - - onStart : function(xy) { - - }, - - - onDrag : function(e) { - - }, - - - onEnd : function(e) { - - }, - - - getDragTarget : function(){ - return this.dragTarget; - }, - - getDragCt : function(){ - return this.el; - }, - - getXY : function(constrain){ - return constrain ? - this.constrainModes[constrain].call(this, this.lastXY) : this.lastXY; - }, - - getOffset : function(constrain){ - var xy = this.getXY(constrain), - s = this.startXY; - return [s[0]-xy[0], s[1]-xy[1]]; - }, - - constrainModes: { - 'point' : function(xy){ - - if(!this.elRegion){ - this.elRegion = this.getDragCt().getRegion(); - } - - var dr = this.dragRegion; - - dr.left = xy[0]; - dr.top = xy[1]; - dr.right = xy[0]; - dr.bottom = xy[1]; - - dr.constrainTo(this.elRegion); - - return [dr.left, dr.top]; - } - } -}); -Ext.dd.ScrollManager = function(){ - var ddm = Ext.dd.DragDropMgr; - var els = {}; - var dragEl = null; - var proc = {}; - - var onStop = function(e){ - dragEl = null; - clearProc(); - }; - - var triggerRefresh = function(){ - if(ddm.dragCurrent){ - ddm.refreshCache(ddm.dragCurrent.groups); - } - }; - - var doScroll = function(){ - if(ddm.dragCurrent){ - var dds = Ext.dd.ScrollManager; - var inc = proc.el.ddScrollConfig ? - proc.el.ddScrollConfig.increment : dds.increment; - if(!dds.animate){ - if(proc.el.scroll(proc.dir, inc)){ - triggerRefresh(); - } - }else{ - proc.el.scroll(proc.dir, inc, true, dds.animDuration, triggerRefresh); - } - } - }; - - var clearProc = function(){ - if(proc.id){ - clearInterval(proc.id); - } - proc.id = 0; - proc.el = null; - proc.dir = ""; - }; - - var startProc = function(el, dir){ - clearProc(); - proc.el = el; - proc.dir = dir; - var group = el.ddScrollConfig ? el.ddScrollConfig.ddGroup : undefined, - freq = (el.ddScrollConfig && el.ddScrollConfig.frequency) - ? el.ddScrollConfig.frequency - : Ext.dd.ScrollManager.frequency; - - if (group === undefined || ddm.dragCurrent.ddGroup == group) { - proc.id = setInterval(doScroll, freq); - } - }; - - var onFire = function(e, isDrop){ - if(isDrop || !ddm.dragCurrent){ return; } - var dds = Ext.dd.ScrollManager; - if(!dragEl || dragEl != ddm.dragCurrent){ - dragEl = ddm.dragCurrent; - - dds.refreshCache(); - } - - var xy = Ext.lib.Event.getXY(e); - var pt = new Ext.lib.Point(xy[0], xy[1]); - for(var id in els){ - var el = els[id], r = el._region; - var c = el.ddScrollConfig ? el.ddScrollConfig : dds; - if(r && r.contains(pt) && el.isScrollable()){ - if(r.bottom - pt.y <= c.vthresh){ - if(proc.el != el){ - startProc(el, "down"); - } - return; - }else if(r.right - pt.x <= c.hthresh){ - if(proc.el != el){ - startProc(el, "left"); - } - return; - }else if(pt.y - r.top <= c.vthresh){ - if(proc.el != el){ - startProc(el, "up"); - } - return; - }else if(pt.x - r.left <= c.hthresh){ - if(proc.el != el){ - startProc(el, "right"); - } - return; - } - } - } - clearProc(); - }; - - ddm.fireEvents = ddm.fireEvents.createSequence(onFire, ddm); - ddm.stopDrag = ddm.stopDrag.createSequence(onStop, ddm); - - return { - - register : function(el){ - if(Ext.isArray(el)){ - for(var i = 0, len = el.length; i < len; i++) { - this.register(el[i]); - } - }else{ - el = Ext.get(el); - els[el.id] = el; - } - }, - - - unregister : function(el){ - if(Ext.isArray(el)){ - for(var i = 0, len = el.length; i < len; i++) { - this.unregister(el[i]); - } - }else{ - el = Ext.get(el); - delete els[el.id]; - } - }, - - - vthresh : 25, - - hthresh : 25, - - - increment : 100, - - - frequency : 500, - - - animate: true, - - - animDuration: .4, - - - ddGroup: undefined, - - - refreshCache : function(){ - for(var id in els){ - if(typeof els[id] == 'object'){ - els[id]._region = els[id].getRegion(); - } - } - } - }; -}(); -Ext.dd.Registry = function(){ - var elements = {}; - var handles = {}; - var autoIdSeed = 0; - - var getId = function(el, autogen){ - if(typeof el == "string"){ - return el; - } - var id = el.id; - if(!id && autogen !== false){ - id = "extdd-" + (++autoIdSeed); - el.id = id; - } - return id; - }; - - return { - - register : function(el, data){ - data = data || {}; - if(typeof el == "string"){ - el = document.getElementById(el); - } - data.ddel = el; - elements[getId(el)] = data; - if(data.isHandle !== false){ - handles[data.ddel.id] = data; - } - if(data.handles){ - var hs = data.handles; - for(var i = 0, len = hs.length; i < len; i++){ - handles[getId(hs[i])] = data; - } - } - }, - - - unregister : function(el){ - var id = getId(el, false); - var data = elements[id]; - if(data){ - delete elements[id]; - if(data.handles){ - var hs = data.handles; - for(var i = 0, len = hs.length; i < len; i++){ - delete handles[getId(hs[i], false)]; - } - } - } - }, - - - getHandle : function(id){ - if(typeof id != "string"){ - id = id.id; - } - return handles[id]; - }, - - - getHandleFromEvent : function(e){ - var t = Ext.lib.Event.getTarget(e); - return t ? handles[t.id] : null; - }, - - - getTarget : function(id){ - if(typeof id != "string"){ - id = id.id; - } - return elements[id]; - }, - - - getTargetFromEvent : function(e){ - var t = Ext.lib.Event.getTarget(e); - return t ? elements[t.id] || handles[t.id] : null; - } - }; -}(); -Ext.dd.StatusProxy = function(config){ - Ext.apply(this, config); - this.id = this.id || Ext.id(); - this.el = new Ext.Layer({ - dh: { - id: this.id, tag: "div", cls: "x-dd-drag-proxy "+this.dropNotAllowed, children: [ - {tag: "div", cls: "x-dd-drop-icon"}, - {tag: "div", cls: "x-dd-drag-ghost"} - ] - }, - shadow: !config || config.shadow !== false - }); - this.ghost = Ext.get(this.el.dom.childNodes[1]); - this.dropStatus = this.dropNotAllowed; -}; - -Ext.dd.StatusProxy.prototype = { - - dropAllowed : "x-dd-drop-ok", - - dropNotAllowed : "x-dd-drop-nodrop", - - - setStatus : function(cssClass){ - cssClass = cssClass || this.dropNotAllowed; - if(this.dropStatus != cssClass){ - this.el.replaceClass(this.dropStatus, cssClass); - this.dropStatus = cssClass; - } - }, - - - reset : function(clearGhost){ - this.el.dom.className = "x-dd-drag-proxy " + this.dropNotAllowed; - this.dropStatus = this.dropNotAllowed; - if(clearGhost){ - this.ghost.update(""); - } - }, - - - update : function(html){ - if(typeof html == "string"){ - this.ghost.update(html); - }else{ - this.ghost.update(""); - html.style.margin = "0"; - this.ghost.dom.appendChild(html); - } - var el = this.ghost.dom.firstChild; - if(el){ - Ext.fly(el).setStyle('float', 'none'); - } - }, - - - getEl : function(){ - return this.el; - }, - - - getGhost : function(){ - return this.ghost; - }, - - - hide : function(clear){ - this.el.hide(); - if(clear){ - this.reset(true); - } - }, - - - stop : function(){ - if(this.anim && this.anim.isAnimated && this.anim.isAnimated()){ - this.anim.stop(); - } - }, - - - show : function(){ - this.el.show(); - }, - - - sync : function(){ - this.el.sync(); - }, - - - repair : function(xy, callback, scope){ - this.callback = callback; - this.scope = scope; - if(xy && this.animRepair !== false){ - this.el.addClass("x-dd-drag-repair"); - this.el.hideUnders(true); - this.anim = this.el.shift({ - duration: this.repairDuration || .5, - easing: 'easeOut', - xy: xy, - stopFx: true, - callback: this.afterRepair, - scope: this - }); - }else{ - this.afterRepair(); - } - }, - - - afterRepair : function(){ - this.hide(true); - if(typeof this.callback == "function"){ - this.callback.call(this.scope || this); - } - this.callback = null; - this.scope = null; - }, - - destroy: function(){ - Ext.destroy(this.ghost, this.el); - } -}; -Ext.dd.DragSource = function(el, config){ - this.el = Ext.get(el); - if(!this.dragData){ - this.dragData = {}; - } - - Ext.apply(this, config); - - if(!this.proxy){ - this.proxy = new Ext.dd.StatusProxy(); - } - Ext.dd.DragSource.superclass.constructor.call(this, this.el.dom, this.ddGroup || this.group, - {dragElId : this.proxy.id, resizeFrame: false, isTarget: false, scroll: this.scroll === true}); - - this.dragging = false; -}; - -Ext.extend(Ext.dd.DragSource, Ext.dd.DDProxy, { - - - dropAllowed : "x-dd-drop-ok", - - dropNotAllowed : "x-dd-drop-nodrop", - - - getDragData : function(e){ - return this.dragData; - }, - - - onDragEnter : function(e, id){ - var target = Ext.dd.DragDropMgr.getDDById(id); - this.cachedTarget = target; - if(this.beforeDragEnter(target, e, id) !== false){ - if(target.isNotifyTarget){ - var status = target.notifyEnter(this, e, this.dragData); - this.proxy.setStatus(status); - }else{ - this.proxy.setStatus(this.dropAllowed); - } - - if(this.afterDragEnter){ - - this.afterDragEnter(target, e, id); - } - } - }, - - - beforeDragEnter : function(target, e, id){ - return true; - }, - - - alignElWithMouse: function() { - Ext.dd.DragSource.superclass.alignElWithMouse.apply(this, arguments); - this.proxy.sync(); - }, - - - onDragOver : function(e, id){ - var target = this.cachedTarget || Ext.dd.DragDropMgr.getDDById(id); - if(this.beforeDragOver(target, e, id) !== false){ - if(target.isNotifyTarget){ - var status = target.notifyOver(this, e, this.dragData); - this.proxy.setStatus(status); - } - - if(this.afterDragOver){ - - this.afterDragOver(target, e, id); - } - } - }, - - - beforeDragOver : function(target, e, id){ - return true; - }, - - - onDragOut : function(e, id){ - var target = this.cachedTarget || Ext.dd.DragDropMgr.getDDById(id); - if(this.beforeDragOut(target, e, id) !== false){ - if(target.isNotifyTarget){ - target.notifyOut(this, e, this.dragData); - } - this.proxy.reset(); - if(this.afterDragOut){ - - this.afterDragOut(target, e, id); - } - } - this.cachedTarget = null; - }, - - - beforeDragOut : function(target, e, id){ - return true; - }, - - - onDragDrop : function(e, id){ - var target = this.cachedTarget || Ext.dd.DragDropMgr.getDDById(id); - if(this.beforeDragDrop(target, e, id) !== false){ - if(target.isNotifyTarget){ - if(target.notifyDrop(this, e, this.dragData)){ - this.onValidDrop(target, e, id); - }else{ - this.onInvalidDrop(target, e, id); - } - }else{ - this.onValidDrop(target, e, id); - } - - if(this.afterDragDrop){ - - this.afterDragDrop(target, e, id); - } - } - delete this.cachedTarget; - }, - - - beforeDragDrop : function(target, e, id){ - return true; - }, - - - onValidDrop : function(target, e, id){ - this.hideProxy(); - if(this.afterValidDrop){ - - this.afterValidDrop(target, e, id); - } - }, - - - getRepairXY : function(e, data){ - return this.el.getXY(); - }, - - - onInvalidDrop : function(target, e, id){ - this.beforeInvalidDrop(target, e, id); - if(this.cachedTarget){ - if(this.cachedTarget.isNotifyTarget){ - this.cachedTarget.notifyOut(this, e, this.dragData); - } - this.cacheTarget = null; - } - this.proxy.repair(this.getRepairXY(e, this.dragData), this.afterRepair, this); - - if(this.afterInvalidDrop){ - - this.afterInvalidDrop(e, id); - } - }, - - - afterRepair : function(){ - if(Ext.enableFx){ - this.el.highlight(this.hlColor || "c3daf9"); - } - this.dragging = false; - }, - - - beforeInvalidDrop : function(target, e, id){ - return true; - }, - - - handleMouseDown : function(e){ - if(this.dragging) { - return; - } - var data = this.getDragData(e); - if(data && this.onBeforeDrag(data, e) !== false){ - this.dragData = data; - this.proxy.stop(); - Ext.dd.DragSource.superclass.handleMouseDown.apply(this, arguments); - } - }, - - - onBeforeDrag : function(data, e){ - return true; - }, - - - onStartDrag : Ext.emptyFn, - - - startDrag : function(x, y){ - this.proxy.reset(); - this.dragging = true; - this.proxy.update(""); - this.onInitDrag(x, y); - this.proxy.show(); - }, - - - onInitDrag : function(x, y){ - var clone = this.el.dom.cloneNode(true); - clone.id = Ext.id(); - this.proxy.update(clone); - this.onStartDrag(x, y); - return true; - }, - - - getProxy : function(){ - return this.proxy; - }, - - - hideProxy : function(){ - this.proxy.hide(); - this.proxy.reset(true); - this.dragging = false; - }, - - - triggerCacheRefresh : function(){ - Ext.dd.DDM.refreshCache(this.groups); - }, - - - b4EndDrag: function(e) { - }, - - - endDrag : function(e){ - this.onEndDrag(this.dragData, e); - }, - - - onEndDrag : function(data, e){ - }, - - - autoOffset : function(x, y) { - this.setDelta(-12, -20); - }, - - destroy: function(){ - Ext.dd.DragSource.superclass.destroy.call(this); - Ext.destroy(this.proxy); - } -}); -Ext.dd.DropTarget = Ext.extend(Ext.dd.DDTarget, { - - constructor : function(el, config){ - this.el = Ext.get(el); - - Ext.apply(this, config); - - if(this.containerScroll){ - Ext.dd.ScrollManager.register(this.el); - } - - Ext.dd.DropTarget.superclass.constructor.call(this, this.el.dom, this.ddGroup || this.group, - {isTarget: true}); - }, - - - - - dropAllowed : "x-dd-drop-ok", - - dropNotAllowed : "x-dd-drop-nodrop", - - - isTarget : true, - - - isNotifyTarget : true, - - - notifyEnter : function(dd, e, data){ - if(this.overClass){ - this.el.addClass(this.overClass); - } - return this.dropAllowed; - }, - - - notifyOver : function(dd, e, data){ - return this.dropAllowed; - }, - - - notifyOut : function(dd, e, data){ - if(this.overClass){ - this.el.removeClass(this.overClass); - } - }, - - - notifyDrop : function(dd, e, data){ - return false; - }, - - destroy : function(){ - Ext.dd.DropTarget.superclass.destroy.call(this); - if(this.containerScroll){ - Ext.dd.ScrollManager.unregister(this.el); - } - } -}); -Ext.dd.DragZone = Ext.extend(Ext.dd.DragSource, { - - constructor : function(el, config){ - Ext.dd.DragZone.superclass.constructor.call(this, el, config); - if(this.containerScroll){ - Ext.dd.ScrollManager.register(this.el); - } - }, - - - - - - - getDragData : function(e){ - return Ext.dd.Registry.getHandleFromEvent(e); - }, - - - onInitDrag : function(x, y){ - this.proxy.update(this.dragData.ddel.cloneNode(true)); - this.onStartDrag(x, y); - return true; - }, - - - afterRepair : function(){ - if(Ext.enableFx){ - Ext.Element.fly(this.dragData.ddel).highlight(this.hlColor || "c3daf9"); - } - this.dragging = false; - }, - - - getRepairXY : function(e){ - return Ext.Element.fly(this.dragData.ddel).getXY(); - }, - - destroy : function(){ - Ext.dd.DragZone.superclass.destroy.call(this); - if(this.containerScroll){ - Ext.dd.ScrollManager.unregister(this.el); - } - } -}); -Ext.dd.DropZone = function(el, config){ - Ext.dd.DropZone.superclass.constructor.call(this, el, config); -}; - -Ext.extend(Ext.dd.DropZone, Ext.dd.DropTarget, { - - getTargetFromEvent : function(e){ - return Ext.dd.Registry.getTargetFromEvent(e); - }, - - - onNodeEnter : function(n, dd, e, data){ - - }, - - - onNodeOver : function(n, dd, e, data){ - return this.dropAllowed; - }, - - - onNodeOut : function(n, dd, e, data){ - - }, - - - onNodeDrop : function(n, dd, e, data){ - return false; - }, - - - onContainerOver : function(dd, e, data){ - return this.dropNotAllowed; - }, - - - onContainerDrop : function(dd, e, data){ - return false; - }, - - - notifyEnter : function(dd, e, data){ - return this.dropNotAllowed; - }, - - - notifyOver : function(dd, e, data){ - var n = this.getTargetFromEvent(e); - if(!n){ - if(this.lastOverNode){ - this.onNodeOut(this.lastOverNode, dd, e, data); - this.lastOverNode = null; - } - return this.onContainerOver(dd, e, data); - } - if(this.lastOverNode != n){ - if(this.lastOverNode){ - this.onNodeOut(this.lastOverNode, dd, e, data); - } - this.onNodeEnter(n, dd, e, data); - this.lastOverNode = n; - } - return this.onNodeOver(n, dd, e, data); - }, - - - notifyOut : function(dd, e, data){ - if(this.lastOverNode){ - this.onNodeOut(this.lastOverNode, dd, e, data); - this.lastOverNode = null; - } - }, - - - notifyDrop : function(dd, e, data){ - if(this.lastOverNode){ - this.onNodeOut(this.lastOverNode, dd, e, data); - this.lastOverNode = null; - } - var n = this.getTargetFromEvent(e); - return n ? - this.onNodeDrop(n, dd, e, data) : - this.onContainerDrop(dd, e, data); - }, - - - triggerCacheRefresh : function(){ - Ext.dd.DDM.refreshCache(this.groups); - } -}); -Ext.Element.addMethods({ - - initDD : function(group, config, overrides){ - var dd = new Ext.dd.DD(Ext.id(this.dom), group, config); - return Ext.apply(dd, overrides); - }, - - - initDDProxy : function(group, config, overrides){ - var dd = new Ext.dd.DDProxy(Ext.id(this.dom), group, config); - return Ext.apply(dd, overrides); - }, - - - initDDTarget : function(group, config, overrides){ - var dd = new Ext.dd.DDTarget(Ext.id(this.dom), group, config); - return Ext.apply(dd, overrides); - } -}); - -Ext.data.Api = (function() { - - - - - - var validActions = {}; - - return { - - actions : { - create : 'create', - read : 'read', - update : 'update', - destroy : 'destroy' - }, - - - restActions : { - create : 'POST', - read : 'GET', - update : 'PUT', - destroy : 'DELETE' - }, - - - isAction : function(action) { - return (Ext.data.Api.actions[action]) ? true : false; - }, - - - getVerb : function(name) { - if (validActions[name]) { - return validActions[name]; - } - for (var verb in this.actions) { - if (this.actions[verb] === name) { - validActions[name] = verb; - break; - } - } - return (validActions[name] !== undefined) ? validActions[name] : null; - }, - - - isValid : function(api){ - var invalid = []; - var crud = this.actions; - for (var action in api) { - if (!(action in crud)) { - invalid.push(action); - } - } - return (!invalid.length) ? true : invalid; - }, - - - hasUniqueUrl : function(proxy, verb) { - var url = (proxy.api[verb]) ? proxy.api[verb].url : null; - var unique = true; - for (var action in proxy.api) { - if ((unique = (action === verb) ? true : (proxy.api[action].url != url) ? true : false) === false) { - break; - } - } - return unique; - }, - - - prepare : function(proxy) { - if (!proxy.api) { - proxy.api = {}; - } - for (var verb in this.actions) { - var action = this.actions[verb]; - proxy.api[action] = proxy.api[action] || proxy.url || proxy.directFn; - if (typeof(proxy.api[action]) == 'string') { - proxy.api[action] = { - url: proxy.api[action], - method: (proxy.restful === true) ? Ext.data.Api.restActions[action] : undefined - }; - } - } - }, - - - restify : function(proxy) { - proxy.restful = true; - for (var verb in this.restActions) { - proxy.api[this.actions[verb]].method || - (proxy.api[this.actions[verb]].method = this.restActions[verb]); - } - - - proxy.onWrite = proxy.onWrite.createInterceptor(function(action, o, response, rs) { - var reader = o.reader; - var res = new Ext.data.Response({ - action: action, - raw: response - }); - - switch (response.status) { - case 200: - return true; - break; - case 201: - if (Ext.isEmpty(res.raw.responseText)) { - res.success = true; - } else { - - return true; - } - break; - case 204: - res.success = true; - res.data = null; - break; - default: - return true; - break; - } - if (res.success === true) { - this.fireEvent("write", this, action, res.data, res, rs, o.request.arg); - } else { - this.fireEvent('exception', this, 'remote', action, o, res, rs); - } - o.request.callback.call(o.request.scope, res.data, res, res.success); - - return false; - }, proxy); - } - }; -})(); - - -Ext.data.Response = function(params, response) { - Ext.apply(this, params, { - raw: response - }); -}; -Ext.data.Response.prototype = { - message : null, - success : false, - status : null, - root : null, - raw : null, - - getMessage : function() { - return this.message; - }, - getSuccess : function() { - return this.success; - }, - getStatus : function() { - return this.status; - }, - getRoot : function() { - return this.root; - }, - getRawResponse : function() { - return this.raw; - } -}; - - -Ext.data.Api.Error = Ext.extend(Ext.Error, { - constructor : function(message, arg) { - this.arg = arg; - Ext.Error.call(this, message); - }, - name: 'Ext.data.Api' -}); -Ext.apply(Ext.data.Api.Error.prototype, { - lang: { - 'action-url-undefined': 'No fallback url defined for this action. When defining a DataProxy api, please be sure to define an url for each CRUD action in Ext.data.Api.actions or define a default url in addition to your api-configuration.', - 'invalid': 'received an invalid API-configuration. Please ensure your proxy API-configuration contains only the actions defined in Ext.data.Api.actions', - 'invalid-url': 'Invalid url. Please review your proxy configuration.', - 'execute': 'Attempted to execute an unknown action. Valid API actions are defined in Ext.data.Api.actions"' - } -}); - - - - -Ext.data.SortTypes = { - - none : function(s){ - return s; - }, - - - stripTagsRE : /<\/?[^>]+>/gi, - - - asText : function(s){ - return String(s).replace(this.stripTagsRE, ""); - }, - - - asUCText : function(s){ - return String(s).toUpperCase().replace(this.stripTagsRE, ""); - }, - - - asUCString : function(s) { - return String(s).toUpperCase(); - }, - - - asDate : function(s) { - if(!s){ - return 0; - } - if(Ext.isDate(s)){ - return s.getTime(); - } - return Date.parse(String(s)); - }, - - - asFloat : function(s) { - var val = parseFloat(String(s).replace(/,/g, "")); - return isNaN(val) ? 0 : val; - }, - - - asInt : function(s) { - var val = parseInt(String(s).replace(/,/g, ""), 10); - return isNaN(val) ? 0 : val; - } -}; -Ext.data.Record = function(data, id){ - - this.id = (id || id === 0) ? id : Ext.data.Record.id(this); - this.data = data || {}; -}; - - -Ext.data.Record.create = function(o){ - var f = Ext.extend(Ext.data.Record, {}); - var p = f.prototype; - p.fields = new Ext.util.MixedCollection(false, function(field){ - return field.name; - }); - for(var i = 0, len = o.length; i < len; i++){ - p.fields.add(new Ext.data.Field(o[i])); - } - f.getField = function(name){ - return p.fields.get(name); - }; - return f; -}; - -Ext.data.Record.PREFIX = 'ext-record'; -Ext.data.Record.AUTO_ID = 1; -Ext.data.Record.EDIT = 'edit'; -Ext.data.Record.REJECT = 'reject'; -Ext.data.Record.COMMIT = 'commit'; - - - -Ext.data.Record.id = function(rec) { - rec.phantom = true; - return [Ext.data.Record.PREFIX, '-', Ext.data.Record.AUTO_ID++].join(''); -}; - -Ext.data.Record.prototype = { - - - - - - - dirty : false, - editing : false, - error : null, - - modified : null, - - phantom : false, - - - join : function(store){ - - this.store = store; - }, - - - set : function(name, value){ - var encode = Ext.isPrimitive(value) ? String : Ext.encode; - if(encode(this.data[name]) == encode(value)) { - return; - } - this.dirty = true; - if(!this.modified){ - this.modified = {}; - } - if(this.modified[name] === undefined){ - this.modified[name] = this.data[name]; - } - this.data[name] = value; - if(!this.editing){ - this.afterEdit(); - } - }, - - - afterEdit : function(){ - if (this.store != undefined && typeof this.store.afterEdit == "function") { - this.store.afterEdit(this); - } - }, - - - afterReject : function(){ - if(this.store){ - this.store.afterReject(this); - } - }, - - - afterCommit : function(){ - if(this.store){ - this.store.afterCommit(this); - } - }, - - - get : function(name){ - return this.data[name]; - }, - - - beginEdit : function(){ - this.editing = true; - this.modified = this.modified || {}; - }, - - - cancelEdit : function(){ - this.editing = false; - delete this.modified; - }, - - - endEdit : function(){ - this.editing = false; - if(this.dirty){ - this.afterEdit(); - } - }, - - - reject : function(silent){ - var m = this.modified; - for(var n in m){ - if(typeof m[n] != "function"){ - this.data[n] = m[n]; - } - } - this.dirty = false; - delete this.modified; - this.editing = false; - if(silent !== true){ - this.afterReject(); - } - }, - - - commit : function(silent){ - this.dirty = false; - delete this.modified; - this.editing = false; - if(silent !== true){ - this.afterCommit(); - } - }, - - - getChanges : function(){ - var m = this.modified, cs = {}; - for(var n in m){ - if(m.hasOwnProperty(n)){ - cs[n] = this.data[n]; - } - } - return cs; - }, - - - hasError : function(){ - return this.error !== null; - }, - - - clearError : function(){ - this.error = null; - }, - - - copy : function(newId) { - return new this.constructor(Ext.apply({}, this.data), newId || this.id); - }, - - - isModified : function(fieldName){ - return !!(this.modified && this.modified.hasOwnProperty(fieldName)); - }, - - - isValid : function() { - return this.fields.find(function(f) { - return (f.allowBlank === false && Ext.isEmpty(this.data[f.name])) ? true : false; - },this) ? false : true; - }, - - - markDirty : function(){ - this.dirty = true; - if(!this.modified){ - this.modified = {}; - } - this.fields.each(function(f) { - this.modified[f.name] = this.data[f.name]; - },this); - } -}; - -Ext.StoreMgr = Ext.apply(new Ext.util.MixedCollection(), { - - - - register : function(){ - for(var i = 0, s; (s = arguments[i]); i++){ - this.add(s); - } - }, - - - unregister : function(){ - for(var i = 0, s; (s = arguments[i]); i++){ - this.remove(this.lookup(s)); - } - }, - - - lookup : function(id){ - if(Ext.isArray(id)){ - var fields = ['field1'], expand = !Ext.isArray(id[0]); - if(!expand){ - for(var i = 2, len = id[0].length; i <= len; ++i){ - fields.push('field' + i); - } - } - return new Ext.data.ArrayStore({ - fields: fields, - data: id, - expandData: expand, - autoDestroy: true, - autoCreated: true - - }); - } - return Ext.isObject(id) ? (id.events ? id : Ext.create(id, 'store')) : this.get(id); - }, - - - getKey : function(o){ - return o.storeId; - } -}); -Ext.data.Store = Ext.extend(Ext.util.Observable, { - - - - - - - - writer : undefined, - - - - remoteSort : false, - - - autoDestroy : false, - - - pruneModifiedRecords : false, - - - lastOptions : null, - - - autoSave : true, - - - batch : true, - - - restful: false, - - - paramNames : undefined, - - - defaultParamNames : { - start : 'start', - limit : 'limit', - sort : 'sort', - dir : 'dir' - }, - - isDestroyed: false, - hasMultiSort: false, - - - batchKey : '_ext_batch_', - - constructor : function(config){ - - - - - this.data = new Ext.util.MixedCollection(false); - this.data.getKey = function(o){ - return o.id; - }; - - - - this.removed = []; - - if(config && config.data){ - this.inlineData = config.data; - delete config.data; - } - - Ext.apply(this, config); - - - this.baseParams = Ext.isObject(this.baseParams) ? this.baseParams : {}; - - this.paramNames = Ext.applyIf(this.paramNames || {}, this.defaultParamNames); - - if((this.url || this.api) && !this.proxy){ - this.proxy = new Ext.data.HttpProxy({url: this.url, api: this.api}); - } - - if (this.restful === true && this.proxy) { - - - this.batch = false; - Ext.data.Api.restify(this.proxy); - } - - if(this.reader){ - if(!this.recordType){ - this.recordType = this.reader.recordType; - } - if(this.reader.onMetaChange){ - this.reader.onMetaChange = this.reader.onMetaChange.createSequence(this.onMetaChange, this); - } - if (this.writer) { - if (this.writer instanceof(Ext.data.DataWriter) === false) { - this.writer = this.buildWriter(this.writer); - } - this.writer.meta = this.reader.meta; - this.pruneModifiedRecords = true; - } - } - - - - if(this.recordType){ - - this.fields = this.recordType.prototype.fields; - } - this.modified = []; - - this.addEvents( - - 'datachanged', - - 'metachange', - - 'add', - - 'remove', - - 'update', - - 'clear', - - 'exception', - - 'beforeload', - - 'load', - - 'loadexception', - - 'beforewrite', - - 'write', - - 'beforesave', - - 'save' - - ); - - if(this.proxy){ - - this.relayEvents(this.proxy, ['loadexception', 'exception']); - } - - if (this.writer) { - this.on({ - scope: this, - add: this.createRecords, - remove: this.destroyRecord, - update: this.updateRecord, - clear: this.onClear - }); - } - - this.sortToggle = {}; - if(this.sortField){ - this.setDefaultSort(this.sortField, this.sortDir); - }else if(this.sortInfo){ - this.setDefaultSort(this.sortInfo.field, this.sortInfo.direction); - } - - Ext.data.Store.superclass.constructor.call(this); - - if(this.id){ - this.storeId = this.id; - delete this.id; - } - if(this.storeId){ - Ext.StoreMgr.register(this); - } - if(this.inlineData){ - this.loadData(this.inlineData); - delete this.inlineData; - }else if(this.autoLoad){ - this.load.defer(10, this, [ - typeof this.autoLoad == 'object' ? - this.autoLoad : undefined]); - } - - this.batchCounter = 0; - this.batches = {}; - }, - - - buildWriter : function(config) { - var klass = undefined, - type = (config.format || 'json').toLowerCase(); - switch (type) { - case 'json': - klass = Ext.data.JsonWriter; - break; - case 'xml': - klass = Ext.data.XmlWriter; - break; - default: - klass = Ext.data.JsonWriter; - } - return new klass(config); - }, - - - destroy : function(){ - if(!this.isDestroyed){ - if(this.storeId){ - Ext.StoreMgr.unregister(this); - } - this.clearData(); - this.data = null; - Ext.destroy(this.proxy); - this.reader = this.writer = null; - this.purgeListeners(); - this.isDestroyed = true; - } - }, - - - add : function(records) { - var i, len, record, index; - - records = [].concat(records); - if (records.length < 1) { - return; - } - - for (i = 0, len = records.length; i < len; i++) { - record = records[i]; - - record.join(this); - - if (record.dirty || record.phantom) { - this.modified.push(record); - } - } - - index = this.data.length; - this.data.addAll(records); - - if (this.snapshot) { - this.snapshot.addAll(records); - } - - this.fireEvent('add', this, records, index); - }, - - - addSorted : function(record){ - var index = this.findInsertIndex(record); - this.insert(index, record); - }, - - - doUpdate: function(rec){ - var id = rec.id; - - this.getById(id).join(null); - - this.data.replace(id, rec); - if (this.snapshot) { - this.snapshot.replace(id, rec); - } - rec.join(this); - this.fireEvent('update', this, rec, Ext.data.Record.COMMIT); - }, - - - remove : function(record){ - if(Ext.isArray(record)){ - Ext.each(record, function(r){ - this.remove(r); - }, this); - return; - } - var index = this.data.indexOf(record); - if(index > -1){ - record.join(null); - this.data.removeAt(index); - } - if(this.pruneModifiedRecords){ - this.modified.remove(record); - } - if(this.snapshot){ - this.snapshot.remove(record); - } - if(index > -1){ - this.fireEvent('remove', this, record, index); - } - }, - - - removeAt : function(index){ - this.remove(this.getAt(index)); - }, - - - removeAll : function(silent){ - var items = []; - this.each(function(rec){ - items.push(rec); - }); - this.clearData(); - if(this.snapshot){ - this.snapshot.clear(); - } - if(this.pruneModifiedRecords){ - this.modified = []; - } - if (silent !== true) { - this.fireEvent('clear', this, items); - } - }, - - - onClear: function(store, records){ - Ext.each(records, function(rec, index){ - this.destroyRecord(this, rec, index); - }, this); - }, - - - insert : function(index, records) { - var i, len, record; - - records = [].concat(records); - for (i = 0, len = records.length; i < len; i++) { - record = records[i]; - - this.data.insert(index + i, record); - record.join(this); - - if (record.dirty || record.phantom) { - this.modified.push(record); - } - } - - if (this.snapshot) { - this.snapshot.addAll(records); - } - - this.fireEvent('add', this, records, index); - }, - - - indexOf : function(record){ - return this.data.indexOf(record); - }, - - - indexOfId : function(id){ - return this.data.indexOfKey(id); - }, - - - getById : function(id){ - return (this.snapshot || this.data).key(id); - }, - - - getAt : function(index){ - return this.data.itemAt(index); - }, - - - getRange : function(start, end){ - return this.data.getRange(start, end); - }, - - - storeOptions : function(o){ - o = Ext.apply({}, o); - delete o.callback; - delete o.scope; - this.lastOptions = o; - }, - - - clearData: function(){ - this.data.each(function(rec) { - rec.join(null); - }); - this.data.clear(); - }, - - - load : function(options) { - options = Ext.apply({}, options); - this.storeOptions(options); - if(this.sortInfo && this.remoteSort){ - var pn = this.paramNames; - options.params = Ext.apply({}, options.params); - options.params[pn.sort] = this.sortInfo.field; - options.params[pn.dir] = this.sortInfo.direction; - } - try { - return this.execute('read', null, options); - } catch(e) { - this.handleException(e); - return false; - } - }, - - - updateRecord : function(store, record, action) { - if (action == Ext.data.Record.EDIT && this.autoSave === true && (!record.phantom || (record.phantom && record.isValid()))) { - this.save(); - } - }, - - - createRecords : function(store, records, index) { - var modified = this.modified, - length = records.length, - record, i; - - for (i = 0; i < length; i++) { - record = records[i]; - - if (record.phantom && record.isValid()) { - record.markDirty(); - - if (modified.indexOf(record) == -1) { - modified.push(record); - } - } - } - if (this.autoSave === true) { - this.save(); - } - }, - - - destroyRecord : function(store, record, index) { - if (this.modified.indexOf(record) != -1) { - this.modified.remove(record); - } - if (!record.phantom) { - this.removed.push(record); - - - - - record.lastIndex = index; - - if (this.autoSave === true) { - this.save(); - } - } - }, - - - execute : function(action, rs, options, batch) { - - if (!Ext.data.Api.isAction(action)) { - throw new Ext.data.Api.Error('execute', action); - } - - options = Ext.applyIf(options||{}, { - params: {} - }); - if(batch !== undefined){ - this.addToBatch(batch); - } - - - var doRequest = true; - - if (action === 'read') { - doRequest = this.fireEvent('beforeload', this, options); - Ext.applyIf(options.params, this.baseParams); - } - else { - - - if (this.writer.listful === true && this.restful !== true) { - rs = (Ext.isArray(rs)) ? rs : [rs]; - } - - else if (Ext.isArray(rs) && rs.length == 1) { - rs = rs.shift(); - } - - if ((doRequest = this.fireEvent('beforewrite', this, action, rs, options)) !== false) { - this.writer.apply(options.params, this.baseParams, action, rs); - } - } - if (doRequest !== false) { - - if (this.writer && this.proxy.url && !this.proxy.restful && !Ext.data.Api.hasUniqueUrl(this.proxy, action)) { - options.params.xaction = action; - } - - - - - - this.proxy.request(Ext.data.Api.actions[action], rs, options.params, this.reader, this.createCallback(action, rs, batch), this, options); - } - return doRequest; - }, - - - save : function() { - if (!this.writer) { - throw new Ext.data.Store.Error('writer-undefined'); - } - - var queue = [], - len, - trans, - batch, - data = {}, - i; - - if(this.removed.length){ - queue.push(['destroy', this.removed]); - } - - - var rs = [].concat(this.getModifiedRecords()); - if(rs.length){ - - var phantoms = []; - for(i = rs.length-1; i >= 0; i--){ - if(rs[i].phantom === true){ - var rec = rs.splice(i, 1).shift(); - if(rec.isValid()){ - phantoms.push(rec); - } - }else if(!rs[i].isValid()){ - rs.splice(i,1); - } - } - - if(phantoms.length){ - queue.push(['create', phantoms]); - } - - - if(rs.length){ - queue.push(['update', rs]); - } - } - len = queue.length; - if(len){ - batch = ++this.batchCounter; - for(i = 0; i < len; ++i){ - trans = queue[i]; - data[trans[0]] = trans[1]; - } - if(this.fireEvent('beforesave', this, data) !== false){ - for(i = 0; i < len; ++i){ - trans = queue[i]; - this.doTransaction(trans[0], trans[1], batch); - } - return batch; - } - } - return -1; - }, - - - doTransaction : function(action, rs, batch) { - function transaction(records) { - try{ - this.execute(action, records, undefined, batch); - }catch (e){ - this.handleException(e); - } - } - if(this.batch === false){ - for(var i = 0, len = rs.length; i < len; i++){ - transaction.call(this, rs[i]); - } - }else{ - transaction.call(this, rs); - } - }, - - - addToBatch : function(batch){ - var b = this.batches, - key = this.batchKey + batch, - o = b[key]; - - if(!o){ - b[key] = o = { - id: batch, - count: 0, - data: {} - }; - } - ++o.count; - }, - - removeFromBatch : function(batch, action, data){ - var b = this.batches, - key = this.batchKey + batch, - o = b[key], - arr; - - - if(o){ - arr = o.data[action] || []; - o.data[action] = arr.concat(data); - if(o.count === 1){ - data = o.data; - delete b[key]; - this.fireEvent('save', this, batch, data); - }else{ - --o.count; - } - } - }, - - - - createCallback : function(action, rs, batch) { - var actions = Ext.data.Api.actions; - return (action == 'read') ? this.loadRecords : function(data, response, success) { - - this['on' + Ext.util.Format.capitalize(action) + 'Records'](success, rs, [].concat(data)); - - if (success === true) { - this.fireEvent('write', this, action, data, response, rs); - } - this.removeFromBatch(batch, action, data); - }; - }, - - - - - clearModified : function(rs) { - if (Ext.isArray(rs)) { - for (var n=rs.length-1;n>=0;n--) { - this.modified.splice(this.modified.indexOf(rs[n]), 1); - } - } else { - this.modified.splice(this.modified.indexOf(rs), 1); - } - }, - - - reMap : function(record) { - if (Ext.isArray(record)) { - for (var i = 0, len = record.length; i < len; i++) { - this.reMap(record[i]); - } - } else { - delete this.data.map[record._phid]; - this.data.map[record.id] = record; - var index = this.data.keys.indexOf(record._phid); - this.data.keys.splice(index, 1, record.id); - delete record._phid; - } - }, - - - onCreateRecords : function(success, rs, data) { - if (success === true) { - try { - this.reader.realize(rs, data); - } - catch (e) { - this.handleException(e); - if (Ext.isArray(rs)) { - - this.onCreateRecords(success, rs, data); - } - } - } - }, - - - onUpdateRecords : function(success, rs, data) { - if (success === true) { - try { - this.reader.update(rs, data); - } catch (e) { - this.handleException(e); - if (Ext.isArray(rs)) { - - this.onUpdateRecords(success, rs, data); - } - } - } - }, - - - onDestroyRecords : function(success, rs, data) { - - rs = (rs instanceof Ext.data.Record) ? [rs] : [].concat(rs); - for (var i=0,len=rs.length;i=0;i--) { - this.insert(rs[i].lastIndex, rs[i]); - } - } - }, - - - handleException : function(e) { - - Ext.handleError(e); - }, - - - reload : function(options){ - this.load(Ext.applyIf(options||{}, this.lastOptions)); - }, - - - - loadRecords : function(o, options, success){ - var i, len; - - if (this.isDestroyed === true) { - return; - } - if(!o || success === false){ - if(success !== false){ - this.fireEvent('load', this, [], options); - } - if(options.callback){ - options.callback.call(options.scope || this, [], options, false, o); - } - return; - } - var r = o.records, t = o.totalRecords || r.length; - if(!options || options.add !== true){ - if(this.pruneModifiedRecords){ - this.modified = []; - } - for(i = 0, len = r.length; i < len; i++){ - r[i].join(this); - } - if(this.snapshot){ - this.data = this.snapshot; - delete this.snapshot; - } - this.clearData(); - this.data.addAll(r); - this.totalLength = t; - this.applySort(); - this.fireEvent('datachanged', this); - }else{ - var toAdd = [], - rec, - cnt = 0; - for(i = 0, len = r.length; i < len; ++i){ - rec = r[i]; - if(this.indexOfId(rec.id) > -1){ - this.doUpdate(rec); - }else{ - toAdd.push(rec); - ++cnt; - } - } - this.totalLength = Math.max(t, this.data.length + cnt); - this.add(toAdd); - } - this.fireEvent('load', this, r, options); - if(options.callback){ - options.callback.call(options.scope || this, r, options, true); - } - }, - - - loadData : function(o, append){ - var r = this.reader.readRecords(o); - this.loadRecords(r, {add: append}, true); - }, - - - getCount : function(){ - return this.data.length || 0; - }, - - - getTotalCount : function(){ - return this.totalLength || 0; - }, - - - getSortState : function(){ - return this.sortInfo; - }, - - - applySort : function(){ - if ((this.sortInfo || this.multiSortInfo) && !this.remoteSort) { - this.sortData(); - } - }, - - - sortData : function() { - var sortInfo = this.hasMultiSort ? this.multiSortInfo : this.sortInfo, - direction = sortInfo.direction || "ASC", - sorters = sortInfo.sorters, - sortFns = []; - - - if (!this.hasMultiSort) { - sorters = [{direction: direction, field: sortInfo.field}]; - } - - - for (var i=0, j = sorters.length; i < j; i++) { - sortFns.push(this.createSortFunction(sorters[i].field, sorters[i].direction)); - } - - if (sortFns.length == 0) { - return; - } - - - - var directionModifier = direction.toUpperCase() == "DESC" ? -1 : 1; - - - var fn = function(r1, r2) { - var result = sortFns[0].call(this, r1, r2); - - - if (sortFns.length > 1) { - for (var i=1, j = sortFns.length; i < j; i++) { - result = result || sortFns[i].call(this, r1, r2); - } - } - - return directionModifier * result; - }; - - - this.data.sort(direction, fn); - if (this.snapshot && this.snapshot != this.data) { - this.snapshot.sort(direction, fn); - } - }, - - - createSortFunction: function(field, direction) { - direction = direction || "ASC"; - var directionModifier = direction.toUpperCase() == "DESC" ? -1 : 1; - - var sortType = this.fields.get(field).sortType; - - - - return function(r1, r2) { - var v1 = sortType(r1.data[field]), - v2 = sortType(r2.data[field]); - - return directionModifier * (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)); - }; - }, - - - setDefaultSort : function(field, dir) { - dir = dir ? dir.toUpperCase() : 'ASC'; - this.sortInfo = {field: field, direction: dir}; - this.sortToggle[field] = dir; - }, - - - sort : function(fieldName, dir) { - if (Ext.isArray(arguments[0])) { - return this.multiSort.call(this, fieldName, dir); - } else { - return this.singleSort(fieldName, dir); - } - }, - - - singleSort: function(fieldName, dir) { - var field = this.fields.get(fieldName); - if (!field) { - return false; - } - - var name = field.name, - sortInfo = this.sortInfo || null, - sortToggle = this.sortToggle ? this.sortToggle[name] : null; - - if (!dir) { - if (sortInfo && sortInfo.field == name) { - dir = (this.sortToggle[name] || 'ASC').toggle('ASC', 'DESC'); - } else { - dir = field.sortDir; - } - } - - this.sortToggle[name] = dir; - this.sortInfo = {field: name, direction: dir}; - this.hasMultiSort = false; - - if (this.remoteSort) { - if (!this.load(this.lastOptions)) { - if (sortToggle) { - this.sortToggle[name] = sortToggle; - } - if (sortInfo) { - this.sortInfo = sortInfo; - } - } - } else { - this.applySort(); - this.fireEvent('datachanged', this); - } - return true; - }, - - - multiSort: function(sorters, direction) { - this.hasMultiSort = true; - direction = direction || "ASC"; - - - if (this.multiSortInfo && direction == this.multiSortInfo.direction) { - direction = direction.toggle("ASC", "DESC"); - } - - - this.multiSortInfo = { - sorters : sorters, - direction: direction - }; - - if (this.remoteSort) { - this.singleSort(sorters[0].field, sorters[0].direction); - - } else { - this.applySort(); - this.fireEvent('datachanged', this); - } - }, - - - each : function(fn, scope){ - this.data.each(fn, scope); - }, - - - getModifiedRecords : function(){ - return this.modified; - }, - - - sum : function(property, start, end){ - var rs = this.data.items, v = 0; - start = start || 0; - end = (end || end === 0) ? end : rs.length-1; - - for(var i = start; i <= end; i++){ - v += (rs[i].data[property] || 0); - } - return v; - }, - - - createFilterFn : function(property, value, anyMatch, caseSensitive, exactMatch){ - if(Ext.isEmpty(value, false)){ - return false; - } - value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch); - return function(r) { - return value.test(r.data[property]); - }; - }, - - - createMultipleFilterFn: function(filters) { - return function(record) { - var isMatch = true; - - for (var i=0, j = filters.length; i < j; i++) { - var filter = filters[i], - fn = filter.fn, - scope = filter.scope; - - isMatch = isMatch && fn.call(scope, record); - } - - return isMatch; - }; - }, - - - filter : function(property, value, anyMatch, caseSensitive, exactMatch){ - var fn; - - if (Ext.isObject(property)) { - property = [property]; - } - - if (Ext.isArray(property)) { - var filters = []; - - - for (var i=0, j = property.length; i < j; i++) { - var filter = property[i], - func = filter.fn, - scope = filter.scope || this; - - - if (!Ext.isFunction(func)) { - func = this.createFilterFn(filter.property, filter.value, filter.anyMatch, filter.caseSensitive, filter.exactMatch); - } - - filters.push({fn: func, scope: scope}); - } - - fn = this.createMultipleFilterFn(filters); - } else { - - fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch); - } - - return fn ? this.filterBy(fn) : this.clearFilter(); - }, - - - filterBy : function(fn, scope){ - this.snapshot = this.snapshot || this.data; - this.data = this.queryBy(fn, scope || this); - this.fireEvent('datachanged', this); - }, - - - clearFilter : function(suppressEvent){ - if(this.isFiltered()){ - this.data = this.snapshot; - delete this.snapshot; - if(suppressEvent !== true){ - this.fireEvent('datachanged', this); - } - } - }, - - - isFiltered : function(){ - return !!this.snapshot && this.snapshot != this.data; - }, - - - query : function(property, value, anyMatch, caseSensitive){ - var fn = this.createFilterFn(property, value, anyMatch, caseSensitive); - return fn ? this.queryBy(fn) : this.data.clone(); - }, - - - queryBy : function(fn, scope){ - var data = this.snapshot || this.data; - return data.filterBy(fn, scope||this); - }, - - - find : function(property, value, start, anyMatch, caseSensitive){ - var fn = this.createFilterFn(property, value, anyMatch, caseSensitive); - return fn ? this.data.findIndexBy(fn, null, start) : -1; - }, - - - findExact: function(property, value, start){ - return this.data.findIndexBy(function(rec){ - return rec.get(property) === value; - }, this, start); - }, - - - findBy : function(fn, scope, start){ - return this.data.findIndexBy(fn, scope, start); - }, - - - collect : function(dataIndex, allowNull, bypassFilter){ - var d = (bypassFilter === true && this.snapshot) ? - this.snapshot.items : this.data.items; - var v, sv, r = [], l = {}; - for(var i = 0, len = d.length; i < len; i++){ - v = d[i].data[dataIndex]; - sv = String(v); - if((allowNull || !Ext.isEmpty(v)) && !l[sv]){ - l[sv] = true; - r[r.length] = v; - } - } - return r; - }, - - - afterEdit : function(record){ - if(this.modified.indexOf(record) == -1){ - this.modified.push(record); - } - this.fireEvent('update', this, record, Ext.data.Record.EDIT); - }, - - - afterReject : function(record){ - this.modified.remove(record); - this.fireEvent('update', this, record, Ext.data.Record.REJECT); - }, - - - afterCommit : function(record){ - this.modified.remove(record); - this.fireEvent('update', this, record, Ext.data.Record.COMMIT); - }, - - - commitChanges : function(){ - var modified = this.modified.slice(0), - length = modified.length, - i; - - for (i = 0; i < length; i++){ - modified[i].commit(); - } - - this.modified = []; - this.removed = []; - }, - - - rejectChanges : function() { - var modified = this.modified.slice(0), - removed = this.removed.slice(0).reverse(), - mLength = modified.length, - rLength = removed.length, - i; - - for (i = 0; i < mLength; i++) { - modified[i].reject(); - } - - for (i = 0; i < rLength; i++) { - this.insert(removed[i].lastIndex || 0, removed[i]); - removed[i].reject(); - } - - this.modified = []; - this.removed = []; - }, - - - onMetaChange : function(meta){ - this.recordType = this.reader.recordType; - this.fields = this.recordType.prototype.fields; - delete this.snapshot; - if(this.reader.meta.sortInfo){ - this.sortInfo = this.reader.meta.sortInfo; - }else if(this.sortInfo && !this.fields.get(this.sortInfo.field)){ - delete this.sortInfo; - } - if(this.writer){ - this.writer.meta = this.reader.meta; - } - this.modified = []; - this.fireEvent('metachange', this, this.reader.meta); - }, - - - findInsertIndex : function(record){ - this.suspendEvents(); - var data = this.data.clone(); - this.data.add(record); - this.applySort(); - var index = this.data.indexOf(record); - this.data = data; - this.resumeEvents(); - return index; - }, - - - setBaseParam : function (name, value){ - this.baseParams = this.baseParams || {}; - this.baseParams[name] = value; - } -}); - -Ext.reg('store', Ext.data.Store); - - -Ext.data.Store.Error = Ext.extend(Ext.Error, { - name: 'Ext.data.Store' -}); -Ext.apply(Ext.data.Store.Error.prototype, { - lang: { - 'writer-undefined' : 'Attempted to execute a write-action without a DataWriter installed.' - } -}); - -Ext.data.Field = Ext.extend(Object, { - - constructor : function(config){ - if(Ext.isString(config)){ - config = {name: config}; - } - Ext.apply(this, config); - - var types = Ext.data.Types, - st = this.sortType, - t; - - if(this.type){ - if(Ext.isString(this.type)){ - this.type = Ext.data.Types[this.type.toUpperCase()] || types.AUTO; - } - }else{ - this.type = types.AUTO; - } - - - if(Ext.isString(st)){ - this.sortType = Ext.data.SortTypes[st]; - }else if(Ext.isEmpty(st)){ - this.sortType = this.type.sortType; - } - - if(!this.convert){ - this.convert = this.type.convert; - } - }, - - - - - - dateFormat: null, - - - useNull: false, - - - defaultValue: "", - - mapping: null, - - sortType : null, - - sortDir : "ASC", - - allowBlank : true -}); - -Ext.data.DataReader = function(meta, recordType){ - - this.meta = meta; - - this.recordType = Ext.isArray(recordType) ? - Ext.data.Record.create(recordType) : recordType; - - - if (this.recordType){ - this.buildExtractors(); - } -}; - -Ext.data.DataReader.prototype = { - - - getTotal: Ext.emptyFn, - - getRoot: Ext.emptyFn, - - getMessage: Ext.emptyFn, - - getSuccess: Ext.emptyFn, - - getId: Ext.emptyFn, - - buildExtractors : Ext.emptyFn, - - extractValues : Ext.emptyFn, - - - realize: function(rs, data){ - if (Ext.isArray(rs)) { - for (var i = rs.length - 1; i >= 0; i--) { - - if (Ext.isArray(data)) { - this.realize(rs.splice(i,1).shift(), data.splice(i,1).shift()); - } - else { - - - this.realize(rs.splice(i,1).shift(), data); - } - } - } - else { - - if (Ext.isArray(data) && data.length == 1) { - data = data.shift(); - } - if (!this.isData(data)) { - - - throw new Ext.data.DataReader.Error('realize', rs); - } - rs.phantom = false; - rs._phid = rs.id; - rs.id = this.getId(data); - rs.data = data; - - rs.commit(); - rs.store.reMap(rs); - } - }, - - - update : function(rs, data) { - if (Ext.isArray(rs)) { - for (var i=rs.length-1; i >= 0; i--) { - if (Ext.isArray(data)) { - this.update(rs.splice(i,1).shift(), data.splice(i,1).shift()); - } - else { - - - this.update(rs.splice(i,1).shift(), data); - } - } - } - else { - - if (Ext.isArray(data) && data.length == 1) { - data = data.shift(); - } - if (this.isData(data)) { - rs.data = Ext.apply(rs.data, data); - } - rs.commit(); - } - }, - - - extractData : function(root, returnRecords) { - - var rawName = (this instanceof Ext.data.JsonReader) ? 'json' : 'node'; - - var rs = []; - - - - if (this.isData(root) && !(this instanceof Ext.data.XmlReader)) { - root = [root]; - } - var f = this.recordType.prototype.fields, - fi = f.items, - fl = f.length, - rs = []; - if (returnRecords === true) { - var Record = this.recordType; - for (var i = 0; i < root.length; i++) { - var n = root[i]; - var record = new Record(this.extractValues(n, fi, fl), this.getId(n)); - record[rawName] = n; - rs.push(record); - } - } - else { - for (var i = 0; i < root.length; i++) { - var data = this.extractValues(root[i], fi, fl); - data[this.meta.idProperty] = this.getId(root[i]); - rs.push(data); - } - } - return rs; - }, - - - isData : function(data) { - return (data && Ext.isObject(data) && !Ext.isEmpty(this.getId(data))) ? true : false; - }, - - - onMetaChange : function(meta){ - delete this.ef; - this.meta = meta; - this.recordType = Ext.data.Record.create(meta.fields); - this.buildExtractors(); - } -}; - - -Ext.data.DataReader.Error = Ext.extend(Ext.Error, { - constructor : function(message, arg) { - this.arg = arg; - Ext.Error.call(this, message); - }, - name: 'Ext.data.DataReader' -}); -Ext.apply(Ext.data.DataReader.Error.prototype, { - lang : { - 'update': "#update received invalid data from server. Please see docs for DataReader#update and review your DataReader configuration.", - 'realize': "#realize was called with invalid remote-data. Please see the docs for DataReader#realize and review your DataReader configuration.", - 'invalid-response': "#readResponse received an invalid response from the server." - } -}); - -Ext.data.DataWriter = function(config){ - Ext.apply(this, config); -}; -Ext.data.DataWriter.prototype = { - - - writeAllFields : false, - - listful : false, - - - apply : function(params, baseParams, action, rs) { - var data = [], - renderer = action + 'Record'; - - if (Ext.isArray(rs)) { - Ext.each(rs, function(rec){ - data.push(this[renderer](rec)); - }, this); - } - else if (rs instanceof Ext.data.Record) { - data = this[renderer](rs); - } - this.render(params, baseParams, data); - }, - - - render : Ext.emptyFn, - - - updateRecord : Ext.emptyFn, - - - createRecord : Ext.emptyFn, - - - destroyRecord : Ext.emptyFn, - - - toHash : function(rec, config) { - var map = rec.fields.map, - data = {}, - raw = (this.writeAllFields === false && rec.phantom === false) ? rec.getChanges() : rec.data, - m; - Ext.iterate(raw, function(prop, value){ - if((m = map[prop])){ - data[m.mapping ? m.mapping : m.name] = value; - } - }); - - - - if (rec.phantom) { - if (rec.fields.containsKey(this.meta.idProperty) && Ext.isEmpty(rec.data[this.meta.idProperty])) { - delete data[this.meta.idProperty]; - } - } else { - data[this.meta.idProperty] = rec.id; - } - return data; - }, - - - toArray : function(data) { - var fields = []; - Ext.iterate(data, function(k, v) {fields.push({name: k, value: v});},this); - return fields; - } -}; -Ext.data.DataProxy = function(conn){ - - - conn = conn || {}; - - - - - - this.api = conn.api; - this.url = conn.url; - this.restful = conn.restful; - this.listeners = conn.listeners; - - - this.prettyUrls = conn.prettyUrls; - - - - this.addEvents( - - 'exception', - - 'beforeload', - - 'load', - - 'loadexception', - - 'beforewrite', - - 'write' - ); - Ext.data.DataProxy.superclass.constructor.call(this); - - - try { - Ext.data.Api.prepare(this); - } catch (e) { - if (e instanceof Ext.data.Api.Error) { - e.toConsole(); - } - } - - Ext.data.DataProxy.relayEvents(this, ['beforewrite', 'write', 'exception']); -}; - -Ext.extend(Ext.data.DataProxy, Ext.util.Observable, { - - restful: false, - - - setApi : function() { - if (arguments.length == 1) { - var valid = Ext.data.Api.isValid(arguments[0]); - if (valid === true) { - this.api = arguments[0]; - } - else { - throw new Ext.data.Api.Error('invalid', valid); - } - } - else if (arguments.length == 2) { - if (!Ext.data.Api.isAction(arguments[0])) { - throw new Ext.data.Api.Error('invalid', arguments[0]); - } - this.api[arguments[0]] = arguments[1]; - } - Ext.data.Api.prepare(this); - }, - - - isApiAction : function(action) { - return (this.api[action]) ? true : false; - }, - - - request : function(action, rs, params, reader, callback, scope, options) { - if (!this.api[action] && !this.load) { - throw new Ext.data.DataProxy.Error('action-undefined', action); - } - params = params || {}; - if ((action === Ext.data.Api.actions.read) ? this.fireEvent("beforeload", this, params) : this.fireEvent("beforewrite", this, action, rs, params) !== false) { - this.doRequest.apply(this, arguments); - } - else { - callback.call(scope || this, null, options, false); - } - }, - - - - load : null, - - - doRequest : function(action, rs, params, reader, callback, scope, options) { - - - - this.load(params, reader, callback, scope, options); - }, - - - onRead : Ext.emptyFn, - - onWrite : Ext.emptyFn, - - buildUrl : function(action, record) { - record = record || null; - - - - - var url = (this.conn && this.conn.url) ? this.conn.url : (this.api[action]) ? this.api[action].url : this.url; - if (!url) { - throw new Ext.data.Api.Error('invalid-url', action); - } - - - - - - - - var provides = null; - var m = url.match(/(.*)(\.json|\.xml|\.html)$/); - if (m) { - provides = m[2]; - url = m[1]; - } - - if ((this.restful === true || this.prettyUrls === true) && record instanceof Ext.data.Record && !record.phantom) { - url += '/' + record.id; - } - return (provides === null) ? url : url + provides; - }, - - - destroy: function(){ - this.purgeListeners(); - } -}); - - - -Ext.apply(Ext.data.DataProxy, Ext.util.Observable.prototype); -Ext.util.Observable.call(Ext.data.DataProxy); - - -Ext.data.DataProxy.Error = Ext.extend(Ext.Error, { - constructor : function(message, arg) { - this.arg = arg; - Ext.Error.call(this, message); - }, - name: 'Ext.data.DataProxy' -}); -Ext.apply(Ext.data.DataProxy.Error.prototype, { - lang: { - 'action-undefined': "DataProxy attempted to execute an API-action but found an undefined url / function. Please review your Proxy url/api-configuration.", - 'api-invalid': 'Recieved an invalid API-configuration. Please ensure your proxy API-configuration contains only the actions from Ext.data.Api.actions.' - } -}); - - - -Ext.data.Request = function(params) { - Ext.apply(this, params); -}; -Ext.data.Request.prototype = { - - action : undefined, - - rs : undefined, - - params: undefined, - - callback : Ext.emptyFn, - - scope : undefined, - - reader : undefined -}; - -Ext.data.Response = function(params) { - Ext.apply(this, params); -}; -Ext.data.Response.prototype = { - - action: undefined, - - success : undefined, - - message : undefined, - - data: undefined, - - raw: undefined, - - records: undefined -}; - -Ext.data.ScriptTagProxy = function(config){ - Ext.apply(this, config); - - Ext.data.ScriptTagProxy.superclass.constructor.call(this, config); - - this.head = document.getElementsByTagName("head")[0]; - - -}; - -Ext.data.ScriptTagProxy.TRANS_ID = 1000; - -Ext.extend(Ext.data.ScriptTagProxy, Ext.data.DataProxy, { - - - timeout : 30000, - - callbackParam : "callback", - - nocache : true, - - - doRequest : function(action, rs, params, reader, callback, scope, arg) { - var p = Ext.urlEncode(Ext.apply(params, this.extraParams)); - - var url = this.buildUrl(action, rs); - if (!url) { - throw new Ext.data.Api.Error('invalid-url', url); - } - url = Ext.urlAppend(url, p); - - if(this.nocache){ - url = Ext.urlAppend(url, '_dc=' + (new Date().getTime())); - } - var transId = ++Ext.data.ScriptTagProxy.TRANS_ID; - var trans = { - id : transId, - action: action, - cb : "stcCallback"+transId, - scriptId : "stcScript"+transId, - params : params, - arg : arg, - url : url, - callback : callback, - scope : scope, - reader : reader - }; - window[trans.cb] = this.createCallback(action, rs, trans); - url += String.format("&{0}={1}", this.callbackParam, trans.cb); - if(this.autoAbort !== false){ - this.abort(); - } - - trans.timeoutId = this.handleFailure.defer(this.timeout, this, [trans]); - - var script = document.createElement("script"); - script.setAttribute("src", url); - script.setAttribute("type", "text/javascript"); - script.setAttribute("id", trans.scriptId); - this.head.appendChild(script); - - this.trans = trans; - }, - - - createCallback : function(action, rs, trans) { - var self = this; - return function(res) { - self.trans = false; - self.destroyTrans(trans, true); - if (action === Ext.data.Api.actions.read) { - self.onRead.call(self, action, trans, res); - } else { - self.onWrite.call(self, action, trans, res, rs); - } - }; - }, - - onRead : function(action, trans, res) { - var result; - try { - result = trans.reader.readRecords(res); - }catch(e){ - - this.fireEvent("loadexception", this, trans, res, e); - - this.fireEvent('exception', this, 'response', action, trans, res, e); - trans.callback.call(trans.scope||window, null, trans.arg, false); - return; - } - if (result.success === false) { - - this.fireEvent('loadexception', this, trans, res); - - this.fireEvent('exception', this, 'remote', action, trans, res, null); - } else { - this.fireEvent("load", this, res, trans.arg); - } - trans.callback.call(trans.scope||window, result, trans.arg, result.success); - }, - - onWrite : function(action, trans, response, rs) { - var reader = trans.reader; - try { - - var res = reader.readResponse(action, response); - } catch (e) { - this.fireEvent('exception', this, 'response', action, trans, res, e); - trans.callback.call(trans.scope||window, null, res, false); - return; - } - if(!res.success === true){ - this.fireEvent('exception', this, 'remote', action, trans, res, rs); - trans.callback.call(trans.scope||window, null, res, false); - return; - } - this.fireEvent("write", this, action, res.data, res, rs, trans.arg ); - trans.callback.call(trans.scope||window, res.data, res, true); - }, - - - isLoading : function(){ - return this.trans ? true : false; - }, - - - abort : function(){ - if(this.isLoading()){ - this.destroyTrans(this.trans); - } - }, - - - destroyTrans : function(trans, isLoaded){ - this.head.removeChild(document.getElementById(trans.scriptId)); - clearTimeout(trans.timeoutId); - if(isLoaded){ - window[trans.cb] = undefined; - try{ - delete window[trans.cb]; - }catch(e){} - }else{ - - window[trans.cb] = function(){ - window[trans.cb] = undefined; - try{ - delete window[trans.cb]; - }catch(e){} - }; - } - }, - - - handleFailure : function(trans){ - this.trans = false; - this.destroyTrans(trans, false); - if (trans.action === Ext.data.Api.actions.read) { - - this.fireEvent("loadexception", this, null, trans.arg); - } - - this.fireEvent('exception', this, 'response', trans.action, { - response: null, - options: trans.arg - }); - trans.callback.call(trans.scope||window, null, trans.arg, false); - }, - - - destroy: function(){ - this.abort(); - Ext.data.ScriptTagProxy.superclass.destroy.call(this); - } -}); -Ext.data.HttpProxy = function(conn){ - Ext.data.HttpProxy.superclass.constructor.call(this, conn); - - - this.conn = conn; - - - - - - this.conn.url = null; - - this.useAjax = !conn || !conn.events; - - - var actions = Ext.data.Api.actions; - this.activeRequest = {}; - for (var verb in actions) { - this.activeRequest[actions[verb]] = undefined; - } -}; - -Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { - - getConnection : function() { - return this.useAjax ? Ext.Ajax : this.conn; - }, - - - setUrl : function(url, makePermanent) { - this.conn.url = url; - if (makePermanent === true) { - this.url = url; - this.api = null; - Ext.data.Api.prepare(this); - } - }, - - - doRequest : function(action, rs, params, reader, cb, scope, arg) { - var o = { - method: (this.api[action]) ? this.api[action]['method'] : undefined, - request: { - callback : cb, - scope : scope, - arg : arg - }, - reader: reader, - callback : this.createCallback(action, rs), - scope: this - }; - - - - if (params.jsonData) { - o.jsonData = params.jsonData; - } else if (params.xmlData) { - o.xmlData = params.xmlData; - } else { - o.params = params || {}; - } - - - - this.conn.url = this.buildUrl(action, rs); - - if(this.useAjax){ - - Ext.applyIf(o, this.conn); - - - if (this.activeRequest[action]) { - - - - - - } - this.activeRequest[action] = Ext.Ajax.request(o); - }else{ - this.conn.request(o); - } - - this.conn.url = null; - }, - - - createCallback : function(action, rs) { - return function(o, success, response) { - this.activeRequest[action] = undefined; - if (!success) { - if (action === Ext.data.Api.actions.read) { - - - this.fireEvent('loadexception', this, o, response); - } - this.fireEvent('exception', this, 'response', action, o, response); - o.request.callback.call(o.request.scope, null, o.request.arg, false); - return; - } - if (action === Ext.data.Api.actions.read) { - this.onRead(action, o, response); - } else { - this.onWrite(action, o, response, rs); - } - }; - }, - - - onRead : function(action, o, response) { - var result; - try { - result = o.reader.read(response); - }catch(e){ - - - this.fireEvent('loadexception', this, o, response, e); - - this.fireEvent('exception', this, 'response', action, o, response, e); - o.request.callback.call(o.request.scope, null, o.request.arg, false); - return; - } - if (result.success === false) { - - - this.fireEvent('loadexception', this, o, response); - - - var res = o.reader.readResponse(action, response); - this.fireEvent('exception', this, 'remote', action, o, res, null); - } - else { - this.fireEvent('load', this, o, o.request.arg); - } - - - - o.request.callback.call(o.request.scope, result, o.request.arg, result.success); - }, - - onWrite : function(action, o, response, rs) { - var reader = o.reader; - var res; - try { - res = reader.readResponse(action, response); - } catch (e) { - this.fireEvent('exception', this, 'response', action, o, response, e); - o.request.callback.call(o.request.scope, null, o.request.arg, false); - return; - } - if (res.success === true) { - this.fireEvent('write', this, action, res.data, res, rs, o.request.arg); - } else { - this.fireEvent('exception', this, 'remote', action, o, res, rs); - } - - - - o.request.callback.call(o.request.scope, res.data, res, res.success); - }, - - - destroy: function(){ - if(!this.useAjax){ - this.conn.abort(); - }else if(this.activeRequest){ - var actions = Ext.data.Api.actions; - for (var verb in actions) { - if(this.activeRequest[actions[verb]]){ - Ext.Ajax.abort(this.activeRequest[actions[verb]]); - } - } - } - Ext.data.HttpProxy.superclass.destroy.call(this); - } -}); -Ext.data.MemoryProxy = function(data){ - - var api = {}; - api[Ext.data.Api.actions.read] = true; - Ext.data.MemoryProxy.superclass.constructor.call(this, { - api: api - }); - this.data = data; -}; - -Ext.extend(Ext.data.MemoryProxy, Ext.data.DataProxy, { - - - - doRequest : function(action, rs, params, reader, callback, scope, arg) { - - params = params || {}; - var result; - try { - result = reader.readRecords(this.data); - }catch(e){ - - this.fireEvent("loadexception", this, null, arg, e); - - this.fireEvent('exception', this, 'response', action, arg, null, e); - callback.call(scope, null, arg, false); - return; - } - callback.call(scope, result, arg, true); - } -}); -Ext.data.Types = new function(){ - var st = Ext.data.SortTypes; - Ext.apply(this, { - - stripRe: /[\$,%]/g, - - - AUTO: { - convert: function(v){ return v; }, - sortType: st.none, - type: 'auto' - }, - - - STRING: { - convert: function(v){ return (v === undefined || v === null) ? '' : String(v); }, - sortType: st.asUCString, - type: 'string' - }, - - - INT: { - convert: function(v){ - return v !== undefined && v !== null && v !== '' ? - parseInt(String(v).replace(Ext.data.Types.stripRe, ''), 10) : (this.useNull ? null : 0); - }, - sortType: st.none, - type: 'int' - }, - - - FLOAT: { - convert: function(v){ - return v !== undefined && v !== null && v !== '' ? - parseFloat(String(v).replace(Ext.data.Types.stripRe, ''), 10) : (this.useNull ? null : 0); - }, - sortType: st.none, - type: 'float' - }, - - - BOOL: { - convert: function(v){ return v === true || v === 'true' || v == 1; }, - sortType: st.none, - type: 'bool' - }, - - - DATE: { - convert: function(v){ - var df = this.dateFormat; - if(!v){ - return null; - } - if(Ext.isDate(v)){ - return v; - } - if(df){ - if(df == 'timestamp'){ - return new Date(v*1000); - } - if(df == 'time'){ - return new Date(parseInt(v, 10)); - } - return Date.parseDate(v, df); - } - var parsed = Date.parse(v); - return parsed ? new Date(parsed) : null; - }, - sortType: st.asDate, - type: 'date' - } - }); - - Ext.apply(this, { - - BOOLEAN: this.BOOL, - - INTEGER: this.INT, - - NUMBER: this.FLOAT - }); -}; -Ext.data.JsonWriter = Ext.extend(Ext.data.DataWriter, { - - encode : true, - - encodeDelete: false, - - constructor : function(config){ - Ext.data.JsonWriter.superclass.constructor.call(this, config); - }, - - - render : function(params, baseParams, data) { - if (this.encode === true) { - - Ext.apply(params, baseParams); - params[this.meta.root] = Ext.encode(data); - } else { - - var jdata = Ext.apply({}, baseParams); - jdata[this.meta.root] = data; - params.jsonData = jdata; - } - }, - - createRecord : function(rec) { - return this.toHash(rec); - }, - - updateRecord : function(rec) { - return this.toHash(rec); - - }, - - destroyRecord : function(rec){ - if(this.encodeDelete){ - var data = {}; - data[this.meta.idProperty] = rec.id; - return data; - }else{ - return rec.id; - } - } -}); -Ext.data.JsonReader = function(meta, recordType){ - meta = meta || {}; - - - - - Ext.applyIf(meta, { - idProperty: 'id', - successProperty: 'success', - totalProperty: 'total' - }); - - Ext.data.JsonReader.superclass.constructor.call(this, meta, recordType || meta.fields); -}; -Ext.extend(Ext.data.JsonReader, Ext.data.DataReader, { - - - read : function(response){ - var json = response.responseText; - var o = Ext.decode(json); - if(!o) { - throw {message: 'JsonReader.read: Json object not found'}; - } - return this.readRecords(o); - }, - - - - readResponse : function(action, response) { - var o = (response.responseText !== undefined) ? Ext.decode(response.responseText) : response; - if(!o) { - throw new Ext.data.JsonReader.Error('response'); - } - - var root = this.getRoot(o), - success = this.getSuccess(o); - if (success && action === Ext.data.Api.actions.create) { - var def = Ext.isDefined(root); - if (def && Ext.isEmpty(root)) { - throw new Ext.data.JsonReader.Error('root-empty', this.meta.root); - } - else if (!def) { - throw new Ext.data.JsonReader.Error('root-undefined-response', this.meta.root); - } - } - - - var res = new Ext.data.Response({ - action: action, - success: success, - data: (root) ? this.extractData(root, false) : [], - message: this.getMessage(o), - raw: o - }); - - - if (Ext.isEmpty(res.success)) { - throw new Ext.data.JsonReader.Error('successProperty-response', this.meta.successProperty); - } - return res; - }, - - - readRecords : function(o){ - - this.jsonData = o; - if(o.metaData){ - this.onMetaChange(o.metaData); - } - var s = this.meta, Record = this.recordType, - f = Record.prototype.fields, fi = f.items, fl = f.length, v; - - var root = this.getRoot(o), c = root.length, totalRecords = c, success = true; - if(s.totalProperty){ - v = parseInt(this.getTotal(o), 10); - if(!isNaN(v)){ - totalRecords = v; - } - } - if(s.successProperty){ - v = this.getSuccess(o); - if(v === false || v === 'false'){ - success = false; - } - } - - - return { - success : success, - records : this.extractData(root, true), - totalRecords : totalRecords - }; - }, - - - buildExtractors : function() { - if(this.ef){ - return; - } - var s = this.meta, Record = this.recordType, - f = Record.prototype.fields, fi = f.items, fl = f.length; - - if(s.totalProperty) { - this.getTotal = this.createAccessor(s.totalProperty); - } - if(s.successProperty) { - this.getSuccess = this.createAccessor(s.successProperty); - } - if (s.messageProperty) { - this.getMessage = this.createAccessor(s.messageProperty); - } - this.getRoot = s.root ? this.createAccessor(s.root) : function(p){return p;}; - if (s.id || s.idProperty) { - var g = this.createAccessor(s.id || s.idProperty); - this.getId = function(rec) { - var r = g(rec); - return (r === undefined || r === '') ? null : r; - }; - } else { - this.getId = function(){return null;}; - } - var ef = []; - for(var i = 0; i < fl; i++){ - f = fi[i]; - var map = (f.mapping !== undefined && f.mapping !== null) ? f.mapping : f.name; - ef.push(this.createAccessor(map)); - } - this.ef = ef; - }, - - - simpleAccess : function(obj, subsc) { - return obj[subsc]; - }, - - - createAccessor : function(){ - var re = /[\[\.]/; - return function(expr) { - if(Ext.isEmpty(expr)){ - return Ext.emptyFn; - } - if(Ext.isFunction(expr)){ - return expr; - } - var i = String(expr).search(re); - if(i >= 0){ - return new Function('obj', 'return obj' + (i > 0 ? '.' : '') + expr); - } - return function(obj){ - return obj[expr]; - }; - - }; - }(), - - - extractValues : function(data, items, len) { - var f, values = {}; - for(var j = 0; j < len; j++){ - f = items[j]; - var v = this.ef[j](data); - values[f.name] = f.convert((v !== undefined) ? v : f.defaultValue, data); - } - return values; - } -}); - - -Ext.data.JsonReader.Error = Ext.extend(Ext.Error, { - constructor : function(message, arg) { - this.arg = arg; - Ext.Error.call(this, message); - }, - name : 'Ext.data.JsonReader' -}); -Ext.apply(Ext.data.JsonReader.Error.prototype, { - lang: { - 'response': 'An error occurred while json-decoding your server response', - 'successProperty-response': 'Could not locate your "successProperty" in your server response. Please review your JsonReader config to ensure the config-property "successProperty" matches the property in your server-response. See the JsonReader docs.', - 'root-undefined-config': 'Your JsonReader was configured without a "root" property. Please review your JsonReader config and make sure to define the root property. See the JsonReader docs.', - 'idProperty-undefined' : 'Your JsonReader was configured without an "idProperty" Please review your JsonReader configuration and ensure the "idProperty" is set (e.g.: "id"). See the JsonReader docs.', - 'root-empty': 'Data was expected to be returned by the server in the "root" property of the response. Please review your JsonReader configuration to ensure the "root" property matches that returned in the server-response. See JsonReader docs.' - } -}); - -Ext.data.ArrayReader = Ext.extend(Ext.data.JsonReader, { - - - - - readRecords : function(o){ - this.arrayData = o; - var s = this.meta, - sid = s ? Ext.num(s.idIndex, s.id) : null, - recordType = this.recordType, - fields = recordType.prototype.fields, - records = [], - success = true, - v; - - var root = this.getRoot(o); - - for(var i = 0, len = root.length; i < len; i++) { - var n = root[i], - values = {}, - id = ((sid || sid === 0) && n[sid] !== undefined && n[sid] !== "" ? n[sid] : null); - for(var j = 0, jlen = fields.length; j < jlen; j++) { - var f = fields.items[j], - k = f.mapping !== undefined && f.mapping !== null ? f.mapping : j; - v = n[k] !== undefined ? n[k] : f.defaultValue; - v = f.convert(v, n); - values[f.name] = v; - } - var record = new recordType(values, id); - record.json = n; - records[records.length] = record; - } - - var totalRecords = records.length; - - if(s.totalProperty) { - v = parseInt(this.getTotal(o), 10); - if(!isNaN(v)) { - totalRecords = v; - } - } - if(s.successProperty){ - v = this.getSuccess(o); - if(v === false || v === 'false'){ - success = false; - } - } - - return { - success : success, - records : records, - totalRecords : totalRecords - }; - } -}); -Ext.data.ArrayStore = Ext.extend(Ext.data.Store, { - - constructor: function(config){ - Ext.data.ArrayStore.superclass.constructor.call(this, Ext.apply(config, { - reader: new Ext.data.ArrayReader(config) - })); - }, - - loadData : function(data, append){ - if(this.expandData === true){ - var r = []; - for(var i = 0, len = data.length; i < len; i++){ - r[r.length] = [data[i]]; - } - data = r; - } - Ext.data.ArrayStore.superclass.loadData.call(this, data, append); - } -}); -Ext.reg('arraystore', Ext.data.ArrayStore); - - -Ext.data.SimpleStore = Ext.data.ArrayStore; -Ext.reg('simplestore', Ext.data.SimpleStore); -Ext.data.JsonStore = Ext.extend(Ext.data.Store, { - - constructor: function(config){ - Ext.data.JsonStore.superclass.constructor.call(this, Ext.apply(config, { - reader: new Ext.data.JsonReader(config) - })); - } -}); -Ext.reg('jsonstore', Ext.data.JsonStore); -Ext.data.XmlWriter = function(params) { - Ext.data.XmlWriter.superclass.constructor.apply(this, arguments); - - this.tpl = (typeof(this.tpl) === 'string') ? new Ext.XTemplate(this.tpl).compile() : this.tpl.compile(); -}; -Ext.extend(Ext.data.XmlWriter, Ext.data.DataWriter, { - - documentRoot: 'xrequest', - - forceDocumentRoot: false, - - root: 'records', - - xmlVersion : '1.0', - - xmlEncoding: 'ISO-8859-15', - - - tpl: '<\u003fxml version="{version}" encoding="{encoding}"\u003f><{documentRoot}><{name}>{value}<{root}><{parent.record}><{name}>{value}', - - - - render : function(params, baseParams, data) { - baseParams = this.toArray(baseParams); - params.xmlData = this.tpl.applyTemplate({ - version: this.xmlVersion, - encoding: this.xmlEncoding, - documentRoot: (baseParams.length > 0 || this.forceDocumentRoot === true) ? this.documentRoot : false, - record: this.meta.record, - root: this.root, - baseParams: baseParams, - records: (Ext.isArray(data[0])) ? data : [data] - }); - }, - - - createRecord : function(rec) { - return this.toArray(this.toHash(rec)); - }, - - - updateRecord : function(rec) { - return this.toArray(this.toHash(rec)); - - }, - - destroyRecord : function(rec) { - var data = {}; - data[this.meta.idProperty] = rec.id; - return this.toArray(data); - } -}); - -Ext.data.XmlReader = function(meta, recordType){ - meta = meta || {}; - - - Ext.applyIf(meta, { - idProperty: meta.idProperty || meta.idPath || meta.id, - successProperty: meta.successProperty || meta.success - }); - - Ext.data.XmlReader.superclass.constructor.call(this, meta, recordType || meta.fields); -}; -Ext.extend(Ext.data.XmlReader, Ext.data.DataReader, { - - read : function(response){ - var doc = response.responseXML; - if(!doc) { - throw {message: "XmlReader.read: XML Document not available"}; - } - return this.readRecords(doc); - }, - - - readRecords : function(doc){ - - this.xmlData = doc; - - var root = doc.documentElement || doc, - q = Ext.DomQuery, - totalRecords = 0, - success = true; - - if(this.meta.totalProperty){ - totalRecords = this.getTotal(root, 0); - } - if(this.meta.successProperty){ - success = this.getSuccess(root); - } - - var records = this.extractData(q.select(this.meta.record, root), true); - - - return { - success : success, - records : records, - totalRecords : totalRecords || records.length - }; - }, - - - readResponse : function(action, response) { - var q = Ext.DomQuery, - doc = response.responseXML, - root = doc.documentElement || doc; - - - var res = new Ext.data.Response({ - action: action, - success : this.getSuccess(root), - message: this.getMessage(root), - data: this.extractData(q.select(this.meta.record, root) || q.select(this.meta.root, root), false), - raw: doc - }); - - if (Ext.isEmpty(res.success)) { - throw new Ext.data.DataReader.Error('successProperty-response', this.meta.successProperty); - } - - - if (action === Ext.data.Api.actions.create) { - var def = Ext.isDefined(res.data); - if (def && Ext.isEmpty(res.data)) { - throw new Ext.data.JsonReader.Error('root-empty', this.meta.root); - } - else if (!def) { - throw new Ext.data.JsonReader.Error('root-undefined-response', this.meta.root); - } - } - return res; - }, - - getSuccess : function() { - return true; - }, - - - buildExtractors : function() { - if(this.ef){ - return; - } - var s = this.meta, - Record = this.recordType, - f = Record.prototype.fields, - fi = f.items, - fl = f.length; - - if(s.totalProperty) { - this.getTotal = this.createAccessor(s.totalProperty); - } - if(s.successProperty) { - this.getSuccess = this.createAccessor(s.successProperty); - } - if (s.messageProperty) { - this.getMessage = this.createAccessor(s.messageProperty); - } - this.getRoot = function(res) { - return (!Ext.isEmpty(res[this.meta.record])) ? res[this.meta.record] : res[this.meta.root]; - }; - if (s.idPath || s.idProperty) { - var g = this.createAccessor(s.idPath || s.idProperty); - this.getId = function(rec) { - var id = g(rec) || rec.id; - return (id === undefined || id === '') ? null : id; - }; - } else { - this.getId = function(){return null;}; - } - var ef = []; - for(var i = 0; i < fl; i++){ - f = fi[i]; - var map = (f.mapping !== undefined && f.mapping !== null) ? f.mapping : f.name; - ef.push(this.createAccessor(map)); - } - this.ef = ef; - }, - - - createAccessor : function(){ - var q = Ext.DomQuery; - return function(key) { - if (Ext.isFunction(key)) { - return key; - } - switch(key) { - case this.meta.totalProperty: - return function(root, def){ - return q.selectNumber(key, root, def); - }; - break; - case this.meta.successProperty: - return function(root, def) { - var sv = q.selectValue(key, root, true); - var success = sv !== false && sv !== 'false'; - return success; - }; - break; - default: - return function(root, def) { - return q.selectValue(key, root, def); - }; - break; - } - }; - }(), - - - extractValues : function(data, items, len) { - var f, values = {}; - for(var j = 0; j < len; j++){ - f = items[j]; - var v = this.ef[j](data); - values[f.name] = f.convert((v !== undefined) ? v : f.defaultValue, data); - } - return values; - } -}); -Ext.data.XmlStore = Ext.extend(Ext.data.Store, { - - constructor: function(config){ - Ext.data.XmlStore.superclass.constructor.call(this, Ext.apply(config, { - reader: new Ext.data.XmlReader(config) - })); - } -}); -Ext.reg('xmlstore', Ext.data.XmlStore); -Ext.data.GroupingStore = Ext.extend(Ext.data.Store, { - - - constructor: function(config) { - config = config || {}; - - - - - - this.hasMultiSort = true; - this.multiSortInfo = this.multiSortInfo || {sorters: []}; - - var sorters = this.multiSortInfo.sorters, - groupField = config.groupField || this.groupField, - sortInfo = config.sortInfo || this.sortInfo, - groupDir = config.groupDir || this.groupDir; - - - if(groupField){ - sorters.push({ - field : groupField, - direction: groupDir - }); - } - - - if (sortInfo) { - sorters.push(sortInfo); - } - - Ext.data.GroupingStore.superclass.constructor.call(this, config); - - this.addEvents( - - 'groupchange' - ); - - this.applyGroupField(); - }, - - - - remoteGroup : false, - - groupOnSort:false, - - - groupDir : 'ASC', - - - clearGrouping : function(){ - this.groupField = false; - - if(this.remoteGroup){ - if(this.baseParams){ - delete this.baseParams.groupBy; - delete this.baseParams.groupDir; - } - var lo = this.lastOptions; - if(lo && lo.params){ - delete lo.params.groupBy; - delete lo.params.groupDir; - } - - this.reload(); - }else{ - this.sort(); - this.fireEvent('datachanged', this); - } - }, - - - groupBy : function(field, forceRegroup, direction) { - direction = direction ? (String(direction).toUpperCase() == 'DESC' ? 'DESC' : 'ASC') : this.groupDir; - - if (this.groupField == field && this.groupDir == direction && !forceRegroup) { - return; - } - - - - var sorters = this.multiSortInfo.sorters; - if (sorters.length > 0 && sorters[0].field == this.groupField) { - sorters.shift(); - } - - this.groupField = field; - this.groupDir = direction; - this.applyGroupField(); - - var fireGroupEvent = function() { - this.fireEvent('groupchange', this, this.getGroupState()); - }; - - if (this.groupOnSort) { - this.sort(field, direction); - fireGroupEvent.call(this); - return; - } - - if (this.remoteGroup) { - this.on('load', fireGroupEvent, this, {single: true}); - this.reload(); - } else { - this.sort(sorters); - fireGroupEvent.call(this); - } - }, - - - - sort : function(fieldName, dir) { - if (this.remoteSort) { - return Ext.data.GroupingStore.superclass.sort.call(this, fieldName, dir); - } - - var sorters = []; - - - if (Ext.isArray(arguments[0])) { - sorters = arguments[0]; - } else if (fieldName == undefined) { - - - sorters = this.sortInfo ? [this.sortInfo] : []; - } else { - - - var field = this.fields.get(fieldName); - if (!field) return false; - - var name = field.name, - sortInfo = this.sortInfo || null, - sortToggle = this.sortToggle ? this.sortToggle[name] : null; - - if (!dir) { - if (sortInfo && sortInfo.field == name) { - dir = (this.sortToggle[name] || 'ASC').toggle('ASC', 'DESC'); - } else { - dir = field.sortDir; - } - } - - this.sortToggle[name] = dir; - this.sortInfo = {field: name, direction: dir}; - - sorters = [this.sortInfo]; - } - - - if (this.groupField) { - sorters.unshift({direction: this.groupDir, field: this.groupField}); - } - - return this.multiSort.call(this, sorters, dir); - }, - - - applyGroupField: function(){ - if (this.remoteGroup) { - if(!this.baseParams){ - this.baseParams = {}; - } - - Ext.apply(this.baseParams, { - groupBy : this.groupField, - groupDir: this.groupDir - }); - - var lo = this.lastOptions; - if (lo && lo.params) { - lo.params.groupDir = this.groupDir; - - - delete lo.params.groupBy; - } - } - }, - - - applyGrouping : function(alwaysFireChange){ - if(this.groupField !== false){ - this.groupBy(this.groupField, true, this.groupDir); - return true; - }else{ - if(alwaysFireChange === true){ - this.fireEvent('datachanged', this); - } - return false; - } - }, - - - getGroupState : function(){ - return this.groupOnSort && this.groupField !== false ? - (this.sortInfo ? this.sortInfo.field : undefined) : this.groupField; - } -}); -Ext.reg('groupingstore', Ext.data.GroupingStore); - -Ext.data.DirectProxy = function(config){ - Ext.apply(this, config); - if(typeof this.paramOrder == 'string'){ - this.paramOrder = this.paramOrder.split(/[\s,|]/); - } - Ext.data.DirectProxy.superclass.constructor.call(this, config); -}; - -Ext.extend(Ext.data.DirectProxy, Ext.data.DataProxy, { - - paramOrder: undefined, - - - paramsAsHash: true, - - - directFn : undefined, - - - doRequest : function(action, rs, params, reader, callback, scope, options) { - var args = [], - directFn = this.api[action] || this.directFn; - - switch (action) { - case Ext.data.Api.actions.create: - args.push(params.jsonData); - break; - case Ext.data.Api.actions.read: - - if(directFn.directCfg.method.len > 0){ - if(this.paramOrder){ - for(var i = 0, len = this.paramOrder.length; i < len; i++){ - args.push(params[this.paramOrder[i]]); - } - }else if(this.paramsAsHash){ - args.push(params); - } - } - break; - case Ext.data.Api.actions.update: - args.push(params.jsonData); - break; - case Ext.data.Api.actions.destroy: - args.push(params.jsonData); - break; - } - - var trans = { - params : params || {}, - request: { - callback : callback, - scope : scope, - arg : options - }, - reader: reader - }; - - args.push(this.createCallback(action, rs, trans), this); - directFn.apply(window, args); - }, - - - createCallback : function(action, rs, trans) { - var me = this; - return function(result, res) { - if (!res.status) { - - if (action === Ext.data.Api.actions.read) { - me.fireEvent("loadexception", me, trans, res, null); - } - me.fireEvent('exception', me, 'remote', action, trans, res, null); - trans.request.callback.call(trans.request.scope, null, trans.request.arg, false); - return; - } - if (action === Ext.data.Api.actions.read) { - me.onRead(action, trans, result, res); - } else { - me.onWrite(action, trans, result, res, rs); - } - }; - }, - - - onRead : function(action, trans, result, res) { - var records; - try { - records = trans.reader.readRecords(result); - } - catch (ex) { - - this.fireEvent("loadexception", this, trans, res, ex); - - this.fireEvent('exception', this, 'response', action, trans, res, ex); - trans.request.callback.call(trans.request.scope, null, trans.request.arg, false); - return; - } - this.fireEvent("load", this, res, trans.request.arg); - trans.request.callback.call(trans.request.scope, records, trans.request.arg, true); - }, - - onWrite : function(action, trans, result, res, rs) { - var data = trans.reader.extractData(trans.reader.getRoot(result), false); - var success = trans.reader.getSuccess(result); - success = (success !== false); - if (success){ - this.fireEvent("write", this, action, data, res, rs, trans.request.arg); - }else{ - this.fireEvent('exception', this, 'remote', action, trans, result, rs); - } - trans.request.callback.call(trans.request.scope, data, res, success); - } -}); - -Ext.data.DirectStore = Ext.extend(Ext.data.Store, { - constructor : function(config){ - - var c = Ext.apply({}, { - batchTransactions: false - }, config); - Ext.data.DirectStore.superclass.constructor.call(this, Ext.apply(c, { - proxy: Ext.isDefined(c.proxy) ? c.proxy : new Ext.data.DirectProxy(Ext.copyTo({}, c, 'paramOrder,paramsAsHash,directFn,api')), - reader: (!Ext.isDefined(c.reader) && c.fields) ? new Ext.data.JsonReader(Ext.copyTo({}, c, 'totalProperty,root,idProperty'), c.fields) : c.reader - })); - } -}); -Ext.reg('directstore', Ext.data.DirectStore); - -Ext.Direct = Ext.extend(Ext.util.Observable, { - - - - exceptions: { - TRANSPORT: 'xhr', - PARSE: 'parse', - LOGIN: 'login', - SERVER: 'exception' - }, - - - constructor: function(){ - this.addEvents( - - 'event', - - 'exception' - ); - this.transactions = {}; - this.providers = {}; - }, - - - addProvider : function(provider){ - var a = arguments; - if(a.length > 1){ - for(var i = 0, len = a.length; i < len; i++){ - this.addProvider(a[i]); - } - return; - } - - - if(!provider.events){ - provider = new Ext.Direct.PROVIDERS[provider.type](provider); - } - provider.id = provider.id || Ext.id(); - this.providers[provider.id] = provider; - - provider.on('data', this.onProviderData, this); - provider.on('exception', this.onProviderException, this); - - - if(!provider.isConnected()){ - provider.connect(); - } - - return provider; - }, - - - getProvider : function(id){ - return this.providers[id]; - }, - - removeProvider : function(id){ - var provider = id.id ? id : this.providers[id]; - provider.un('data', this.onProviderData, this); - provider.un('exception', this.onProviderException, this); - delete this.providers[provider.id]; - return provider; - }, - - addTransaction: function(t){ - this.transactions[t.tid] = t; - return t; - }, - - removeTransaction: function(t){ - delete this.transactions[t.tid || t]; - return t; - }, - - getTransaction: function(tid){ - return this.transactions[tid.tid || tid]; - }, - - onProviderData : function(provider, e){ - if(Ext.isArray(e)){ - for(var i = 0, len = e.length; i < len; i++){ - this.onProviderData(provider, e[i]); - } - return; - } - if(e.name && e.name != 'event' && e.name != 'exception'){ - this.fireEvent(e.name, e); - }else if(e.type == 'exception'){ - this.fireEvent('exception', e); - } - this.fireEvent('event', e, provider); - }, - - createEvent : function(response, extraProps){ - return new Ext.Direct.eventTypes[response.type](Ext.apply(response, extraProps)); - } -}); - -Ext.Direct = new Ext.Direct(); - -Ext.Direct.TID = 1; -Ext.Direct.PROVIDERS = {}; -Ext.Direct.Transaction = function(config){ - Ext.apply(this, config); - this.tid = ++Ext.Direct.TID; - this.retryCount = 0; -}; -Ext.Direct.Transaction.prototype = { - send: function(){ - this.provider.queueTransaction(this); - }, - - retry: function(){ - this.retryCount++; - this.send(); - }, - - getProvider: function(){ - return this.provider; - } -};Ext.Direct.Event = function(config){ - Ext.apply(this, config); -}; - -Ext.Direct.Event.prototype = { - status: true, - getData: function(){ - return this.data; - } -}; - -Ext.Direct.RemotingEvent = Ext.extend(Ext.Direct.Event, { - type: 'rpc', - getTransaction: function(){ - return this.transaction || Ext.Direct.getTransaction(this.tid); - } -}); - -Ext.Direct.ExceptionEvent = Ext.extend(Ext.Direct.RemotingEvent, { - status: false, - type: 'exception' -}); - -Ext.Direct.eventTypes = { - 'rpc': Ext.Direct.RemotingEvent, - 'event': Ext.Direct.Event, - 'exception': Ext.Direct.ExceptionEvent -}; - -Ext.direct.Provider = Ext.extend(Ext.util.Observable, { - - - - priority: 1, - - - - - constructor : function(config){ - Ext.apply(this, config); - this.addEvents( - - 'connect', - - 'disconnect', - - 'data', - - 'exception' - ); - Ext.direct.Provider.superclass.constructor.call(this, config); - }, - - - isConnected: function(){ - return false; - }, - - - connect: Ext.emptyFn, - - - disconnect: Ext.emptyFn -}); - -Ext.direct.JsonProvider = Ext.extend(Ext.direct.Provider, { - parseResponse: function(xhr){ - if(!Ext.isEmpty(xhr.responseText)){ - if(typeof xhr.responseText == 'object'){ - return xhr.responseText; - } - return Ext.decode(xhr.responseText); - } - return null; - }, - - getEvents: function(xhr){ - var data = null; - try{ - data = this.parseResponse(xhr); - }catch(e){ - var event = new Ext.Direct.ExceptionEvent({ - data: e, - xhr: xhr, - code: Ext.Direct.exceptions.PARSE, - message: 'Error parsing json response: \n\n ' + data - }); - return [event]; - } - var events = []; - if(Ext.isArray(data)){ - for(var i = 0, len = data.length; i < len; i++){ - events.push(Ext.Direct.createEvent(data[i])); - } - }else{ - events.push(Ext.Direct.createEvent(data)); - } - return events; - } -}); -Ext.direct.PollingProvider = Ext.extend(Ext.direct.JsonProvider, { - - - priority: 3, - - - interval: 3000, - - - - - - - constructor : function(config){ - Ext.direct.PollingProvider.superclass.constructor.call(this, config); - this.addEvents( - - 'beforepoll', - - 'poll' - ); - }, - - - isConnected: function(){ - return !!this.pollTask; - }, - - - connect: function(){ - if(this.url && !this.pollTask){ - this.pollTask = Ext.TaskMgr.start({ - run: function(){ - if(this.fireEvent('beforepoll', this) !== false){ - if(typeof this.url == 'function'){ - this.url(this.baseParams); - }else{ - Ext.Ajax.request({ - url: this.url, - callback: this.onData, - scope: this, - params: this.baseParams - }); - } - } - }, - interval: this.interval, - scope: this - }); - this.fireEvent('connect', this); - }else if(!this.url){ - throw 'Error initializing PollingProvider, no url configured.'; - } - }, - - - disconnect: function(){ - if(this.pollTask){ - Ext.TaskMgr.stop(this.pollTask); - delete this.pollTask; - this.fireEvent('disconnect', this); - } - }, - - - onData: function(opt, success, xhr){ - if(success){ - var events = this.getEvents(xhr); - for(var i = 0, len = events.length; i < len; i++){ - var e = events[i]; - this.fireEvent('data', this, e); - } - }else{ - var e = new Ext.Direct.ExceptionEvent({ - data: e, - code: Ext.Direct.exceptions.TRANSPORT, - message: 'Unable to connect to the server.', - xhr: xhr - }); - this.fireEvent('data', this, e); - } - } -}); - -Ext.Direct.PROVIDERS['polling'] = Ext.direct.PollingProvider; -Ext.direct.RemotingProvider = Ext.extend(Ext.direct.JsonProvider, { - - - - - - - - - - enableBuffer: 10, - - - maxRetries: 1, - - - timeout: undefined, - - constructor : function(config){ - Ext.direct.RemotingProvider.superclass.constructor.call(this, config); - this.addEvents( - - 'beforecall', - - 'call' - ); - this.namespace = (Ext.isString(this.namespace)) ? Ext.ns(this.namespace) : this.namespace || window; - this.transactions = {}; - this.callBuffer = []; - }, - - - initAPI : function(){ - var o = this.actions; - for(var c in o){ - var cls = this.namespace[c] || (this.namespace[c] = {}), - ms = o[c]; - for(var i = 0, len = ms.length; i < len; i++){ - var m = ms[i]; - cls[m.name] = this.createMethod(c, m); - } - } - }, - - - isConnected: function(){ - return !!this.connected; - }, - - connect: function(){ - if(this.url){ - this.initAPI(); - this.connected = true; - this.fireEvent('connect', this); - }else if(!this.url){ - throw 'Error initializing RemotingProvider, no url configured.'; - } - }, - - disconnect: function(){ - if(this.connected){ - this.connected = false; - this.fireEvent('disconnect', this); - } - }, - - onData: function(opt, success, xhr){ - if(success){ - var events = this.getEvents(xhr); - for(var i = 0, len = events.length; i < len; i++){ - var e = events[i], - t = this.getTransaction(e); - this.fireEvent('data', this, e); - if(t){ - this.doCallback(t, e, true); - Ext.Direct.removeTransaction(t); - } - } - }else{ - var ts = [].concat(opt.ts); - for(var i = 0, len = ts.length; i < len; i++){ - var t = this.getTransaction(ts[i]); - if(t && t.retryCount < this.maxRetries){ - t.retry(); - }else{ - var e = new Ext.Direct.ExceptionEvent({ - data: e, - transaction: t, - code: Ext.Direct.exceptions.TRANSPORT, - message: 'Unable to connect to the server.', - xhr: xhr - }); - this.fireEvent('data', this, e); - if(t){ - this.doCallback(t, e, false); - Ext.Direct.removeTransaction(t); - } - } - } - } - }, - - getCallData: function(t){ - return { - action: t.action, - method: t.method, - data: t.data, - type: 'rpc', - tid: t.tid - }; - }, - - doSend : function(data){ - var o = { - url: this.url, - callback: this.onData, - scope: this, - ts: data, - timeout: this.timeout - }, callData; - - if(Ext.isArray(data)){ - callData = []; - for(var i = 0, len = data.length; i < len; i++){ - callData.push(this.getCallData(data[i])); - } - }else{ - callData = this.getCallData(data); - } - - if(this.enableUrlEncode){ - var params = {}; - params[Ext.isString(this.enableUrlEncode) ? this.enableUrlEncode : 'data'] = Ext.encode(callData); - o.params = params; - }else{ - o.jsonData = callData; - } - Ext.Ajax.request(o); - }, - - combineAndSend : function(){ - var len = this.callBuffer.length; - if(len > 0){ - this.doSend(len == 1 ? this.callBuffer[0] : this.callBuffer); - this.callBuffer = []; - } - }, - - queueTransaction: function(t){ - if(t.form){ - this.processForm(t); - return; - } - this.callBuffer.push(t); - if(this.enableBuffer){ - if(!this.callTask){ - this.callTask = new Ext.util.DelayedTask(this.combineAndSend, this); - } - this.callTask.delay(Ext.isNumber(this.enableBuffer) ? this.enableBuffer : 10); - }else{ - this.combineAndSend(); - } - }, - - doCall : function(c, m, args){ - var data = null, hs = args[m.len], scope = args[m.len+1]; - - if(m.len !== 0){ - data = args.slice(0, m.len); - } - - var t = new Ext.Direct.Transaction({ - provider: this, - args: args, - action: c, - method: m.name, - data: data, - cb: scope && Ext.isFunction(hs) ? hs.createDelegate(scope) : hs - }); - - if(this.fireEvent('beforecall', this, t, m) !== false){ - Ext.Direct.addTransaction(t); - this.queueTransaction(t); - this.fireEvent('call', this, t, m); - } - }, - - doForm : function(c, m, form, callback, scope){ - var t = new Ext.Direct.Transaction({ - provider: this, - action: c, - method: m.name, - args:[form, callback, scope], - cb: scope && Ext.isFunction(callback) ? callback.createDelegate(scope) : callback, - isForm: true - }); - - if(this.fireEvent('beforecall', this, t, m) !== false){ - Ext.Direct.addTransaction(t); - var isUpload = String(form.getAttribute("enctype")).toLowerCase() == 'multipart/form-data', - params = { - extTID: t.tid, - extAction: c, - extMethod: m.name, - extType: 'rpc', - extUpload: String(isUpload) - }; - - - - Ext.apply(t, { - form: Ext.getDom(form), - isUpload: isUpload, - params: callback && Ext.isObject(callback.params) ? Ext.apply(params, callback.params) : params - }); - this.fireEvent('call', this, t, m); - this.processForm(t); - } - }, - - processForm: function(t){ - Ext.Ajax.request({ - url: this.url, - params: t.params, - callback: this.onData, - scope: this, - form: t.form, - isUpload: t.isUpload, - ts: t - }); - }, - - createMethod : function(c, m){ - var f; - if(!m.formHandler){ - f = function(){ - this.doCall(c, m, Array.prototype.slice.call(arguments, 0)); - }.createDelegate(this); - }else{ - f = function(form, callback, scope){ - this.doForm(c, m, form, callback, scope); - }.createDelegate(this); - } - f.directCfg = { - action: c, - method: m - }; - return f; - }, - - getTransaction: function(opt){ - return opt && opt.tid ? Ext.Direct.getTransaction(opt.tid) : null; - }, - - doCallback: function(t, e){ - var fn = e.status ? 'success' : 'failure'; - if(t && t.cb){ - var hs = t.cb, - result = Ext.isDefined(e.result) ? e.result : e.data; - if(Ext.isFunction(hs)){ - hs(result, e); - } else{ - Ext.callback(hs[fn], hs.scope, [result, e]); - Ext.callback(hs.callback, hs.scope, [result, e]); - } - } - } -}); -Ext.Direct.PROVIDERS['remoting'] = Ext.direct.RemotingProvider; -Ext.Resizable = Ext.extend(Ext.util.Observable, { - - constructor: function(el, config){ - this.el = Ext.get(el); - if(config && config.wrap){ - config.resizeChild = this.el; - this.el = this.el.wrap(typeof config.wrap == 'object' ? config.wrap : {cls:'xresizable-wrap'}); - this.el.id = this.el.dom.id = config.resizeChild.id + '-rzwrap'; - this.el.setStyle('overflow', 'hidden'); - this.el.setPositioning(config.resizeChild.getPositioning()); - config.resizeChild.clearPositioning(); - if(!config.width || !config.height){ - var csize = config.resizeChild.getSize(); - this.el.setSize(csize.width, csize.height); - } - if(config.pinned && !config.adjustments){ - config.adjustments = 'auto'; - } - } - - - this.proxy = this.el.createProxy({tag: 'div', cls: 'x-resizable-proxy', id: this.el.id + '-rzproxy'}, Ext.getBody()); - this.proxy.unselectable(); - this.proxy.enableDisplayMode('block'); - - Ext.apply(this, config); - - if(this.pinned){ - this.disableTrackOver = true; - this.el.addClass('x-resizable-pinned'); - } - - var position = this.el.getStyle('position'); - if(position != 'absolute' && position != 'fixed'){ - this.el.setStyle('position', 'relative'); - } - if(!this.handles){ - this.handles = 's,e,se'; - if(this.multiDirectional){ - this.handles += ',n,w'; - } - } - if(this.handles == 'all'){ - this.handles = 'n s e w ne nw se sw'; - } - var hs = this.handles.split(/\s*?[,;]\s*?| /); - var ps = Ext.Resizable.positions; - for(var i = 0, len = hs.length; i < len; i++){ - if(hs[i] && ps[hs[i]]){ - var pos = ps[hs[i]]; - this[pos] = new Ext.Resizable.Handle(this, pos, this.disableTrackOver, this.transparent, this.handleCls); - } - } - - this.corner = this.southeast; - - if(this.handles.indexOf('n') != -1 || this.handles.indexOf('w') != -1){ - this.updateBox = true; - } - - this.activeHandle = null; - - if(this.resizeChild){ - if(typeof this.resizeChild == 'boolean'){ - this.resizeChild = Ext.get(this.el.dom.firstChild, true); - }else{ - this.resizeChild = Ext.get(this.resizeChild, true); - } - } - - if(this.adjustments == 'auto'){ - var rc = this.resizeChild; - var hw = this.west, he = this.east, hn = this.north, hs = this.south; - if(rc && (hw || hn)){ - rc.position('relative'); - rc.setLeft(hw ? hw.el.getWidth() : 0); - rc.setTop(hn ? hn.el.getHeight() : 0); - } - this.adjustments = [ - (he ? -he.el.getWidth() : 0) + (hw ? -hw.el.getWidth() : 0), - (hn ? -hn.el.getHeight() : 0) + (hs ? -hs.el.getHeight() : 0) -1 - ]; - } - - if(this.draggable){ - this.dd = this.dynamic ? - this.el.initDD(null) : this.el.initDDProxy(null, {dragElId: this.proxy.id}); - this.dd.setHandleElId(this.resizeChild ? this.resizeChild.id : this.el.id); - if(this.constrainTo){ - this.dd.constrainTo(this.constrainTo); - } - } - - this.addEvents( - - 'beforeresize', - - 'resize' - ); - - if(this.width !== null && this.height !== null){ - this.resizeTo(this.width, this.height); - }else{ - this.updateChildSize(); - } - if(Ext.isIE){ - this.el.dom.style.zoom = 1; - } - Ext.Resizable.superclass.constructor.call(this); - }, - - - adjustments : [0, 0], - - animate : false, - - - disableTrackOver : false, - - draggable: false, - - duration : 0.35, - - dynamic : false, - - easing : 'easeOutStrong', - - enabled : true, - - - handles : false, - - multiDirectional : false, - - height : null, - - width : null, - - heightIncrement : 0, - - widthIncrement : 0, - - minHeight : 5, - - minWidth : 5, - - maxHeight : 10000, - - maxWidth : 10000, - - minX: 0, - - minY: 0, - - pinned : false, - - preserveRatio : false, - - resizeChild : false, - - transparent: false, - - - - - - - resizeTo : function(width, height){ - this.el.setSize(width, height); - this.updateChildSize(); - this.fireEvent('resize', this, width, height, null); - }, - - - startSizing : function(e, handle){ - this.fireEvent('beforeresize', this, e); - if(this.enabled){ - - if(!this.overlay){ - this.overlay = this.el.createProxy({tag: 'div', cls: 'x-resizable-overlay', html: ' '}, Ext.getBody()); - this.overlay.unselectable(); - this.overlay.enableDisplayMode('block'); - this.overlay.on({ - scope: this, - mousemove: this.onMouseMove, - mouseup: this.onMouseUp - }); - } - this.overlay.setStyle('cursor', handle.el.getStyle('cursor')); - - this.resizing = true; - this.startBox = this.el.getBox(); - this.startPoint = e.getXY(); - this.offsets = [(this.startBox.x + this.startBox.width) - this.startPoint[0], - (this.startBox.y + this.startBox.height) - this.startPoint[1]]; - - this.overlay.setSize(Ext.lib.Dom.getViewWidth(true), Ext.lib.Dom.getViewHeight(true)); - this.overlay.show(); - - if(this.constrainTo) { - var ct = Ext.get(this.constrainTo); - this.resizeRegion = ct.getRegion().adjust( - ct.getFrameWidth('t'), - ct.getFrameWidth('l'), - -ct.getFrameWidth('b'), - -ct.getFrameWidth('r') - ); - } - - this.proxy.setStyle('visibility', 'hidden'); - this.proxy.show(); - this.proxy.setBox(this.startBox); - if(!this.dynamic){ - this.proxy.setStyle('visibility', 'visible'); - } - } - }, - - - onMouseDown : function(handle, e){ - if(this.enabled){ - e.stopEvent(); - this.activeHandle = handle; - this.startSizing(e, handle); - } - }, - - - onMouseUp : function(e){ - this.activeHandle = null; - var size = this.resizeElement(); - this.resizing = false; - this.handleOut(); - this.overlay.hide(); - this.proxy.hide(); - this.fireEvent('resize', this, size.width, size.height, e); - }, - - - updateChildSize : function(){ - if(this.resizeChild){ - var el = this.el; - var child = this.resizeChild; - var adj = this.adjustments; - if(el.dom.offsetWidth){ - var b = el.getSize(true); - child.setSize(b.width+adj[0], b.height+adj[1]); - } - - - - - if(Ext.isIE){ - setTimeout(function(){ - if(el.dom.offsetWidth){ - var b = el.getSize(true); - child.setSize(b.width+adj[0], b.height+adj[1]); - } - }, 10); - } - } - }, - - - snap : function(value, inc, min){ - if(!inc || !value){ - return value; - } - var newValue = value; - var m = value % inc; - if(m > 0){ - if(m > (inc/2)){ - newValue = value + (inc-m); - }else{ - newValue = value - m; - } - } - return Math.max(min, newValue); - }, - - - resizeElement : function(){ - var box = this.proxy.getBox(); - if(this.updateBox){ - this.el.setBox(box, false, this.animate, this.duration, null, this.easing); - }else{ - this.el.setSize(box.width, box.height, this.animate, this.duration, null, this.easing); - } - this.updateChildSize(); - if(!this.dynamic){ - this.proxy.hide(); - } - if(this.draggable && this.constrainTo){ - this.dd.resetConstraints(); - this.dd.constrainTo(this.constrainTo); - } - return box; - }, - - - constrain : function(v, diff, m, mx){ - if(v - diff < m){ - diff = v - m; - }else if(v - diff > mx){ - diff = v - mx; - } - return diff; - }, - - - onMouseMove : function(e){ - if(this.enabled && this.activeHandle){ - try{ - - if(this.resizeRegion && !this.resizeRegion.contains(e.getPoint())) { - return; - } - - - var curSize = this.curSize || this.startBox, - x = this.startBox.x, y = this.startBox.y, - ox = x, - oy = y, - w = curSize.width, - h = curSize.height, - ow = w, - oh = h, - mw = this.minWidth, - mh = this.minHeight, - mxw = this.maxWidth, - mxh = this.maxHeight, - wi = this.widthIncrement, - hi = this.heightIncrement, - eventXY = e.getXY(), - diffX = -(this.startPoint[0] - Math.max(this.minX, eventXY[0])), - diffY = -(this.startPoint[1] - Math.max(this.minY, eventXY[1])), - pos = this.activeHandle.position, - tw, - th; - - switch(pos){ - case 'east': - w += diffX; - w = Math.min(Math.max(mw, w), mxw); - break; - case 'south': - h += diffY; - h = Math.min(Math.max(mh, h), mxh); - break; - case 'southeast': - w += diffX; - h += diffY; - w = Math.min(Math.max(mw, w), mxw); - h = Math.min(Math.max(mh, h), mxh); - break; - case 'north': - diffY = this.constrain(h, diffY, mh, mxh); - y += diffY; - h -= diffY; - break; - case 'west': - diffX = this.constrain(w, diffX, mw, mxw); - x += diffX; - w -= diffX; - break; - case 'northeast': - w += diffX; - w = Math.min(Math.max(mw, w), mxw); - diffY = this.constrain(h, diffY, mh, mxh); - y += diffY; - h -= diffY; - break; - case 'northwest': - diffX = this.constrain(w, diffX, mw, mxw); - diffY = this.constrain(h, diffY, mh, mxh); - y += diffY; - h -= diffY; - x += diffX; - w -= diffX; - break; - case 'southwest': - diffX = this.constrain(w, diffX, mw, mxw); - h += diffY; - h = Math.min(Math.max(mh, h), mxh); - x += diffX; - w -= diffX; - break; - } - - var sw = this.snap(w, wi, mw); - var sh = this.snap(h, hi, mh); - if(sw != w || sh != h){ - switch(pos){ - case 'northeast': - y -= sh - h; - break; - case 'north': - y -= sh - h; - break; - case 'southwest': - x -= sw - w; - break; - case 'west': - x -= sw - w; - break; - case 'northwest': - x -= sw - w; - y -= sh - h; - break; - } - w = sw; - h = sh; - } - - if(this.preserveRatio){ - switch(pos){ - case 'southeast': - case 'east': - h = oh * (w/ow); - h = Math.min(Math.max(mh, h), mxh); - w = ow * (h/oh); - break; - case 'south': - w = ow * (h/oh); - w = Math.min(Math.max(mw, w), mxw); - h = oh * (w/ow); - break; - case 'northeast': - w = ow * (h/oh); - w = Math.min(Math.max(mw, w), mxw); - h = oh * (w/ow); - break; - case 'north': - tw = w; - w = ow * (h/oh); - w = Math.min(Math.max(mw, w), mxw); - h = oh * (w/ow); - x += (tw - w) / 2; - break; - case 'southwest': - h = oh * (w/ow); - h = Math.min(Math.max(mh, h), mxh); - tw = w; - w = ow * (h/oh); - x += tw - w; - break; - case 'west': - th = h; - h = oh * (w/ow); - h = Math.min(Math.max(mh, h), mxh); - y += (th - h) / 2; - tw = w; - w = ow * (h/oh); - x += tw - w; - break; - case 'northwest': - tw = w; - th = h; - h = oh * (w/ow); - h = Math.min(Math.max(mh, h), mxh); - w = ow * (h/oh); - y += th - h; - x += tw - w; - break; - - } - } - this.proxy.setBounds(x, y, w, h); - if(this.dynamic){ - this.resizeElement(); - } - }catch(ex){} - } - }, - - - handleOver : function(){ - if(this.enabled){ - this.el.addClass('x-resizable-over'); - } - }, - - - handleOut : function(){ - if(!this.resizing){ - this.el.removeClass('x-resizable-over'); - } - }, - - - getEl : function(){ - return this.el; - }, - - - getResizeChild : function(){ - return this.resizeChild; - }, - - - destroy : function(removeEl){ - Ext.destroy(this.dd, this.overlay, this.proxy); - this.overlay = null; - this.proxy = null; - - var ps = Ext.Resizable.positions; - for(var k in ps){ - if(typeof ps[k] != 'function' && this[ps[k]]){ - this[ps[k]].destroy(); - } - } - if(removeEl){ - this.el.update(''); - Ext.destroy(this.el); - this.el = null; - } - this.purgeListeners(); - }, - - syncHandleHeight : function(){ - var h = this.el.getHeight(true); - if(this.west){ - this.west.el.setHeight(h); - } - if(this.east){ - this.east.el.setHeight(h); - } - } -}); - - - -Ext.Resizable.positions = { - n: 'north', s: 'south', e: 'east', w: 'west', se: 'southeast', sw: 'southwest', nw: 'northwest', ne: 'northeast' -}; - -Ext.Resizable.Handle = Ext.extend(Object, { - constructor : function(rz, pos, disableTrackOver, transparent, cls){ - if(!this.tpl){ - - var tpl = Ext.DomHelper.createTemplate( - {tag: 'div', cls: 'x-resizable-handle x-resizable-handle-{0}'} - ); - tpl.compile(); - Ext.Resizable.Handle.prototype.tpl = tpl; - } - this.position = pos; - this.rz = rz; - this.el = this.tpl.append(rz.el.dom, [this.position], true); - this.el.unselectable(); - if(transparent){ - this.el.setOpacity(0); - } - if(!Ext.isEmpty(cls)){ - this.el.addClass(cls); - } - this.el.on('mousedown', this.onMouseDown, this); - if(!disableTrackOver){ - this.el.on({ - scope: this, - mouseover: this.onMouseOver, - mouseout: this.onMouseOut - }); - } - }, - - - afterResize : function(rz){ - - }, - - onMouseDown : function(e){ - this.rz.onMouseDown(this, e); - }, - - onMouseOver : function(e){ - this.rz.handleOver(this, e); - }, - - onMouseOut : function(e){ - this.rz.handleOut(this, e); - }, - - destroy : function(){ - Ext.destroy(this.el); - this.el = null; - } -}); - -Ext.Window = Ext.extend(Ext.Panel, { - - - - - - - - - - - - - baseCls : 'x-window', - - resizable : true, - - draggable : true, - - closable : true, - - closeAction : 'close', - - constrain : false, - - constrainHeader : false, - - plain : false, - - minimizable : false, - - maximizable : false, - - minHeight : 100, - - minWidth : 200, - - expandOnShow : true, - - - showAnimDuration: 0.25, - - - hideAnimDuration: 0.25, - - - collapsible : false, - - - initHidden : undefined, - - - hidden : true, - - - - - - - elements : 'header,body', - - frame : true, - - floating : true, - - - initComponent : function(){ - this.initTools(); - Ext.Window.superclass.initComponent.call(this); - this.addEvents( - - - - 'resize', - - 'maximize', - - 'minimize', - - 'restore' - ); - - if(Ext.isDefined(this.initHidden)){ - this.hidden = this.initHidden; - } - if(this.hidden === false){ - this.hidden = true; - this.show(); - } - }, - - - getState : function(){ - return Ext.apply(Ext.Window.superclass.getState.call(this) || {}, this.getBox(true)); - }, - - - onRender : function(ct, position){ - Ext.Window.superclass.onRender.call(this, ct, position); - - if(this.plain){ - this.el.addClass('x-window-plain'); - } - - - this.focusEl = this.el.createChild({ - tag: 'a', href:'#', cls:'x-dlg-focus', - tabIndex:'-1', html: ' '}); - this.focusEl.swallowEvent('click', true); - - this.proxy = this.el.createProxy('x-window-proxy'); - this.proxy.enableDisplayMode('block'); - - if(this.modal){ - this.mask = this.container.createChild({cls:'ext-el-mask'}, this.el.dom); - this.mask.enableDisplayMode('block'); - this.mask.hide(); - this.mon(this.mask, 'click', this.focus, this); - } - if(this.maximizable){ - this.mon(this.header, 'dblclick', this.toggleMaximize, this); - } - }, - - - initEvents : function(){ - Ext.Window.superclass.initEvents.call(this); - if(this.animateTarget){ - this.setAnimateTarget(this.animateTarget); - } - - if(this.resizable){ - this.resizer = new Ext.Resizable(this.el, { - minWidth: this.minWidth, - minHeight:this.minHeight, - handles: this.resizeHandles || 'all', - pinned: true, - resizeElement : this.resizerAction, - handleCls: 'x-window-handle' - }); - this.resizer.window = this; - this.mon(this.resizer, 'beforeresize', this.beforeResize, this); - } - - if(this.draggable){ - this.header.addClass('x-window-draggable'); - } - this.mon(this.el, 'mousedown', this.toFront, this); - this.manager = this.manager || Ext.WindowMgr; - this.manager.register(this); - if(this.maximized){ - this.maximized = false; - this.maximize(); - } - if(this.closable){ - var km = this.getKeyMap(); - km.on(27, this.onEsc, this); - km.disable(); - } - }, - - initDraggable : function(){ - - this.dd = new Ext.Window.DD(this); - }, - - - onEsc : function(k, e){ - if (this.activeGhost) { - this.unghost(); - } - e.stopEvent(); - this[this.closeAction](); - }, - - - beforeDestroy : function(){ - if(this.rendered){ - this.hide(); - this.clearAnchor(); - Ext.destroy( - this.focusEl, - this.resizer, - this.dd, - this.proxy, - this.mask - ); - } - Ext.Window.superclass.beforeDestroy.call(this); - }, - - - onDestroy : function(){ - if(this.manager){ - this.manager.unregister(this); - } - Ext.Window.superclass.onDestroy.call(this); - }, - - - initTools : function(){ - if(this.minimizable){ - this.addTool({ - id: 'minimize', - handler: this.minimize.createDelegate(this, []) - }); - } - if(this.maximizable){ - this.addTool({ - id: 'maximize', - handler: this.maximize.createDelegate(this, []) - }); - this.addTool({ - id: 'restore', - handler: this.restore.createDelegate(this, []), - hidden:true - }); - } - if(this.closable){ - this.addTool({ - id: 'close', - handler: this[this.closeAction].createDelegate(this, []) - }); - } - }, - - - resizerAction : function(){ - var box = this.proxy.getBox(); - this.proxy.hide(); - this.window.handleResize(box); - return box; - }, - - - beforeResize : function(){ - this.resizer.minHeight = Math.max(this.minHeight, this.getFrameHeight() + 40); - this.resizer.minWidth = Math.max(this.minWidth, this.getFrameWidth() + 40); - this.resizeBox = this.el.getBox(); - }, - - - updateHandles : function(){ - if(Ext.isIE && this.resizer){ - this.resizer.syncHandleHeight(); - this.el.repaint(); - } - }, - - - handleResize : function(box){ - var rz = this.resizeBox; - if(rz.x != box.x || rz.y != box.y){ - this.updateBox(box); - }else{ - this.setSize(box); - if (Ext.isIE6 && Ext.isStrict) { - this.doLayout(); - } - } - this.focus(); - this.updateHandles(); - this.saveState(); - }, - - - focus : function(){ - var f = this.focusEl, - db = this.defaultButton, - t = typeof db, - el, - ct; - if(Ext.isDefined(db)){ - if(Ext.isNumber(db) && this.fbar){ - f = this.fbar.items.get(db); - }else if(Ext.isString(db)){ - f = Ext.getCmp(db); - }else{ - f = db; - } - el = f.getEl(); - ct = Ext.getDom(this.container); - if (el && ct) { - if (ct != document.body && !Ext.lib.Region.getRegion(ct).contains(Ext.lib.Region.getRegion(el.dom))){ - return; - } - } - } - f = f || this.focusEl; - f.focus.defer(10, f); - }, - - - setAnimateTarget : function(el){ - el = Ext.get(el); - this.animateTarget = el; - }, - - - beforeShow : function(){ - delete this.el.lastXY; - delete this.el.lastLT; - if(this.x === undefined || this.y === undefined){ - var xy = this.el.getAlignToXY(this.container, 'c-c'); - var pos = this.el.translatePoints(xy[0], xy[1]); - this.x = this.x === undefined? pos.left : this.x; - this.y = this.y === undefined? pos.top : this.y; - } - this.el.setLeftTop(this.x, this.y); - - if(this.expandOnShow){ - this.expand(false); - } - - if(this.modal){ - Ext.getBody().addClass('x-body-masked'); - this.mask.setSize(Ext.lib.Dom.getViewWidth(true), Ext.lib.Dom.getViewHeight(true)); - this.mask.show(); - } - }, - - - show : function(animateTarget, cb, scope){ - if(!this.rendered){ - this.render(Ext.getBody()); - } - if(this.hidden === false){ - this.toFront(); - return this; - } - if(this.fireEvent('beforeshow', this) === false){ - return this; - } - if(cb){ - this.on('show', cb, scope, {single:true}); - } - this.hidden = false; - if(Ext.isDefined(animateTarget)){ - this.setAnimateTarget(animateTarget); - } - this.beforeShow(); - if(this.animateTarget){ - this.animShow(); - }else{ - this.afterShow(); - } - return this; - }, - - - afterShow : function(isAnim){ - if (this.isDestroyed){ - return false; - } - this.proxy.hide(); - this.el.setStyle('display', 'block'); - this.el.show(); - if(this.maximized){ - this.fitContainer(); - } - if(Ext.isMac && Ext.isGecko2){ - this.cascade(this.setAutoScroll); - } - - if(this.monitorResize || this.modal || this.constrain || this.constrainHeader){ - Ext.EventManager.onWindowResize(this.onWindowResize, this); - } - this.doConstrain(); - this.doLayout(); - if(this.keyMap){ - this.keyMap.enable(); - } - this.toFront(); - this.updateHandles(); - if(isAnim && (Ext.isIE || Ext.isWebKit)){ - var sz = this.getSize(); - this.onResize(sz.width, sz.height); - } - this.onShow(); - this.fireEvent('show', this); - }, - - - animShow : function(){ - this.proxy.show(); - this.proxy.setBox(this.animateTarget.getBox()); - this.proxy.setOpacity(0); - var b = this.getBox(); - this.el.setStyle('display', 'none'); - this.proxy.shift(Ext.apply(b, { - callback: this.afterShow.createDelegate(this, [true], false), - scope: this, - easing: 'easeNone', - duration: this.showAnimDuration, - opacity: 0.5 - })); - }, - - - hide : function(animateTarget, cb, scope){ - if(this.hidden || this.fireEvent('beforehide', this) === false){ - return this; - } - if(cb){ - this.on('hide', cb, scope, {single:true}); - } - this.hidden = true; - if(animateTarget !== undefined){ - this.setAnimateTarget(animateTarget); - } - if(this.modal){ - this.mask.hide(); - Ext.getBody().removeClass('x-body-masked'); - } - if(this.animateTarget){ - this.animHide(); - }else{ - this.el.hide(); - this.afterHide(); - } - return this; - }, - - - afterHide : function(){ - this.proxy.hide(); - if(this.monitorResize || this.modal || this.constrain || this.constrainHeader){ - Ext.EventManager.removeResizeListener(this.onWindowResize, this); - } - if(this.keyMap){ - this.keyMap.disable(); - } - this.onHide(); - this.fireEvent('hide', this); - }, - - - animHide : function(){ - this.proxy.setOpacity(0.5); - this.proxy.show(); - var tb = this.getBox(false); - this.proxy.setBox(tb); - this.el.hide(); - this.proxy.shift(Ext.apply(this.animateTarget.getBox(), { - callback: this.afterHide, - scope: this, - duration: this.hideAnimDuration, - easing: 'easeNone', - opacity: 0 - })); - }, - - - onShow : Ext.emptyFn, - - - onHide : Ext.emptyFn, - - - onWindowResize : function(){ - if(this.maximized){ - this.fitContainer(); - } - if(this.modal){ - this.mask.setSize('100%', '100%'); - var force = this.mask.dom.offsetHeight; - this.mask.setSize(Ext.lib.Dom.getViewWidth(true), Ext.lib.Dom.getViewHeight(true)); - } - this.doConstrain(); - }, - - - doConstrain : function(){ - if(this.constrain || this.constrainHeader){ - var offsets; - if(this.constrain){ - offsets = { - right:this.el.shadowOffset, - left:this.el.shadowOffset, - bottom:this.el.shadowOffset - }; - }else { - var s = this.getSize(); - offsets = { - right:-(s.width - 100), - bottom:-(s.height - 25 + this.el.getConstrainOffset()) - }; - } - - var xy = this.el.getConstrainToXY(this.container, true, offsets); - if(xy){ - this.setPosition(xy[0], xy[1]); - } - } - }, - - - ghost : function(cls){ - var ghost = this.createGhost(cls); - var box = this.getBox(true); - ghost.setLeftTop(box.x, box.y); - ghost.setWidth(box.width); - this.el.hide(); - this.activeGhost = ghost; - return ghost; - }, - - - unghost : function(show, matchPosition){ - if(!this.activeGhost) { - return; - } - if(show !== false){ - this.el.show(); - this.focus.defer(10, this); - if(Ext.isMac && Ext.isGecko2){ - this.cascade(this.setAutoScroll); - } - } - if(matchPosition !== false){ - this.setPosition(this.activeGhost.getLeft(true), this.activeGhost.getTop(true)); - } - this.activeGhost.hide(); - this.activeGhost.remove(); - delete this.activeGhost; - }, - - - minimize : function(){ - this.fireEvent('minimize', this); - return this; - }, - - - close : function(){ - if(this.fireEvent('beforeclose', this) !== false){ - if(this.hidden){ - this.doClose(); - }else{ - this.hide(null, this.doClose, this); - } - } - }, - - - doClose : function(){ - this.fireEvent('close', this); - this.destroy(); - }, - - - maximize : function(){ - if(!this.maximized){ - this.expand(false); - this.restoreSize = this.getSize(); - this.restorePos = this.getPosition(true); - if (this.maximizable){ - this.tools.maximize.hide(); - this.tools.restore.show(); - } - this.maximized = true; - this.el.disableShadow(); - - if(this.dd){ - this.dd.lock(); - } - if(this.collapsible){ - this.tools.toggle.hide(); - } - this.el.addClass('x-window-maximized'); - this.container.addClass('x-window-maximized-ct'); - - this.setPosition(0, 0); - this.fitContainer(); - this.fireEvent('maximize', this); - } - return this; - }, - - - restore : function(){ - if(this.maximized){ - var t = this.tools; - this.el.removeClass('x-window-maximized'); - if(t.restore){ - t.restore.hide(); - } - if(t.maximize){ - t.maximize.show(); - } - this.setPosition(this.restorePos[0], this.restorePos[1]); - this.setSize(this.restoreSize.width, this.restoreSize.height); - delete this.restorePos; - delete this.restoreSize; - this.maximized = false; - this.el.enableShadow(true); - - if(this.dd){ - this.dd.unlock(); - } - if(this.collapsible && t.toggle){ - t.toggle.show(); - } - this.container.removeClass('x-window-maximized-ct'); - - this.doConstrain(); - this.fireEvent('restore', this); - } - return this; - }, - - - toggleMaximize : function(){ - return this[this.maximized ? 'restore' : 'maximize'](); - }, - - - fitContainer : function(){ - var vs = this.container.getViewSize(false); - this.setSize(vs.width, vs.height); - }, - - - - setZIndex : function(index){ - if(this.modal){ - this.mask.setStyle('z-index', index); - } - this.el.setZIndex(++index); - index += 5; - - if(this.resizer){ - this.resizer.proxy.setStyle('z-index', ++index); - } - - this.lastZIndex = index; - }, - - - alignTo : function(element, position, offsets){ - var xy = this.el.getAlignToXY(element, position, offsets); - this.setPagePosition(xy[0], xy[1]); - return this; - }, - - - anchorTo : function(el, alignment, offsets, monitorScroll){ - this.clearAnchor(); - this.anchorTarget = { - el: el, - alignment: alignment, - offsets: offsets - }; - - Ext.EventManager.onWindowResize(this.doAnchor, this); - var tm = typeof monitorScroll; - if(tm != 'undefined'){ - Ext.EventManager.on(window, 'scroll', this.doAnchor, this, - {buffer: tm == 'number' ? monitorScroll : 50}); - } - return this.doAnchor(); - }, - - - doAnchor : function(){ - var o = this.anchorTarget; - this.alignTo(o.el, o.alignment, o.offsets); - return this; - }, - - - clearAnchor : function(){ - if(this.anchorTarget){ - Ext.EventManager.removeResizeListener(this.doAnchor, this); - Ext.EventManager.un(window, 'scroll', this.doAnchor, this); - delete this.anchorTarget; - } - return this; - }, - - - toFront : function(e){ - if(this.manager.bringToFront(this)){ - if(!e || !e.getTarget().focus){ - this.focus(); - } - } - return this; - }, - - - setActive : function(active){ - if(active){ - if(!this.maximized){ - this.el.enableShadow(true); - } - this.fireEvent('activate', this); - }else{ - this.el.disableShadow(); - this.fireEvent('deactivate', this); - } - }, - - - toBack : function(){ - this.manager.sendToBack(this); - return this; - }, - - - center : function(){ - var xy = this.el.getAlignToXY(this.container, 'c-c'); - this.setPagePosition(xy[0], xy[1]); - return this; - } - - -}); -Ext.reg('window', Ext.Window); - - -Ext.Window.DD = Ext.extend(Ext.dd.DD, { - - constructor : function(win){ - this.win = win; - Ext.Window.DD.superclass.constructor.call(this, win.el.id, 'WindowDD-'+win.id); - this.setHandleElId(win.header.id); - this.scroll = false; - }, - - moveOnly:true, - headerOffsets:[100, 25], - startDrag : function(){ - var w = this.win; - this.proxy = w.ghost(w.initialConfig.cls); - if(w.constrain !== false){ - var so = w.el.shadowOffset; - this.constrainTo(w.container, {right: so, left: so, bottom: so}); - }else if(w.constrainHeader !== false){ - var s = this.proxy.getSize(); - this.constrainTo(w.container, {right: -(s.width-this.headerOffsets[0]), bottom: -(s.height-this.headerOffsets[1])}); - } - }, - b4Drag : Ext.emptyFn, - - onDrag : function(e){ - this.alignElWithMouse(this.proxy, e.getPageX(), e.getPageY()); - }, - - endDrag : function(e){ - this.win.unghost(); - this.win.saveState(); - } -}); - -Ext.WindowGroup = function(){ - var list = {}; - var accessList = []; - var front = null; - - - var sortWindows = function(d1, d2){ - return (!d1._lastAccess || d1._lastAccess < d2._lastAccess) ? -1 : 1; - }; - - - var orderWindows = function(){ - var a = accessList, len = a.length; - if(len > 0){ - a.sort(sortWindows); - var seed = a[0].manager.zseed; - for(var i = 0; i < len; i++){ - var win = a[i]; - if(win && !win.hidden){ - win.setZIndex(seed + (i*10)); - } - } - } - activateLast(); - }; - - - var setActiveWin = function(win){ - if(win != front){ - if(front){ - front.setActive(false); - } - front = win; - if(win){ - win.setActive(true); - } - } - }; - - - var activateLast = function(){ - for(var i = accessList.length-1; i >=0; --i) { - if(!accessList[i].hidden){ - setActiveWin(accessList[i]); - return; - } - } - - setActiveWin(null); - }; - - return { - - zseed : 9000, - - - register : function(win){ - if(win.manager){ - win.manager.unregister(win); - } - win.manager = this; - - list[win.id] = win; - accessList.push(win); - win.on('hide', activateLast); - }, - - - unregister : function(win){ - delete win.manager; - delete list[win.id]; - win.un('hide', activateLast); - accessList.remove(win); - }, - - - get : function(id){ - return typeof id == "object" ? id : list[id]; - }, - - - bringToFront : function(win){ - win = this.get(win); - if(win != front){ - win._lastAccess = new Date().getTime(); - orderWindows(); - return true; - } - return false; - }, - - - sendToBack : function(win){ - win = this.get(win); - win._lastAccess = -(new Date().getTime()); - orderWindows(); - return win; - }, - - - hideAll : function(){ - for(var id in list){ - if(list[id] && typeof list[id] != "function" && list[id].isVisible()){ - list[id].hide(); - } - } - }, - - - getActive : function(){ - return front; - }, - - - getBy : function(fn, scope){ - var r = []; - for(var i = accessList.length-1; i >=0; --i) { - var win = accessList[i]; - if(fn.call(scope||win, win) !== false){ - r.push(win); - } - } - return r; - }, - - - each : function(fn, scope){ - for(var id in list){ - if(list[id] && typeof list[id] != "function"){ - if(fn.call(scope || list[id], list[id]) === false){ - return; - } - } - } - } - }; -}; - - - -Ext.WindowMgr = new Ext.WindowGroup(); -Ext.MessageBox = function(){ - var dlg, opt, mask, waitTimer, - bodyEl, msgEl, textboxEl, textareaEl, progressBar, pp, iconEl, spacerEl, - buttons, activeTextEl, bwidth, bufferIcon = '', iconCls = '', - buttonNames = ['ok', 'yes', 'no', 'cancel']; - - - var handleButton = function(button){ - buttons[button].blur(); - if(dlg.isVisible()){ - dlg.hide(); - handleHide(); - Ext.callback(opt.fn, opt.scope||window, [button, activeTextEl.dom.value, opt], 1); - } - }; - - - var handleHide = function(){ - if(opt && opt.cls){ - dlg.el.removeClass(opt.cls); - } - progressBar.reset(); - }; - - - var handleEsc = function(d, k, e){ - if(opt && opt.closable !== false){ - dlg.hide(); - handleHide(); - } - if(e){ - e.stopEvent(); - } - }; - - - var updateButtons = function(b){ - var width = 0, - cfg; - if(!b){ - Ext.each(buttonNames, function(name){ - buttons[name].hide(); - }); - return width; - } - dlg.footer.dom.style.display = ''; - Ext.iterate(buttons, function(name, btn){ - cfg = b[name]; - if(cfg){ - btn.show(); - btn.setText(Ext.isString(cfg) ? cfg : Ext.MessageBox.buttonText[name]); - width += btn.getEl().getWidth() + 15; - }else{ - btn.hide(); - } - }); - return width; - }; - - return { - - getDialog : function(titleText){ - if(!dlg){ - var btns = []; - - buttons = {}; - Ext.each(buttonNames, function(name){ - btns.push(buttons[name] = new Ext.Button({ - text: this.buttonText[name], - handler: handleButton.createCallback(name), - hideMode: 'offsets' - })); - }, this); - dlg = new Ext.Window({ - autoCreate : true, - title:titleText, - resizable:false, - constrain:true, - constrainHeader:true, - minimizable : false, - maximizable : false, - stateful: false, - modal: true, - shim:true, - buttonAlign:"center", - width:400, - height:100, - minHeight: 80, - plain:true, - footer:true, - closable:true, - close : function(){ - if(opt && opt.buttons && opt.buttons.no && !opt.buttons.cancel){ - handleButton("no"); - }else{ - handleButton("cancel"); - } - }, - fbar: new Ext.Toolbar({ - items: btns, - enableOverflow: false - }) - }); - dlg.render(document.body); - dlg.getEl().addClass('x-window-dlg'); - mask = dlg.mask; - bodyEl = dlg.body.createChild({ - html:'

        ' - }); - iconEl = Ext.get(bodyEl.dom.firstChild); - var contentEl = bodyEl.dom.childNodes[1]; - msgEl = Ext.get(contentEl.firstChild); - textboxEl = Ext.get(contentEl.childNodes[2].firstChild); - textboxEl.enableDisplayMode(); - textboxEl.addKeyListener([10,13], function(){ - if(dlg.isVisible() && opt && opt.buttons){ - if(opt.buttons.ok){ - handleButton("ok"); - }else if(opt.buttons.yes){ - handleButton("yes"); - } - } - }); - textareaEl = Ext.get(contentEl.childNodes[2].childNodes[1]); - textareaEl.enableDisplayMode(); - progressBar = new Ext.ProgressBar({ - renderTo:bodyEl - }); - bodyEl.createChild({cls:'x-clear'}); - } - return dlg; - }, - - - updateText : function(text){ - if(!dlg.isVisible() && !opt.width){ - dlg.setSize(this.maxWidth, 100); - } - - msgEl.update(text ? text + ' ' : ' '); - - var iw = iconCls != '' ? (iconEl.getWidth() + iconEl.getMargins('lr')) : 0, - mw = msgEl.getWidth() + msgEl.getMargins('lr'), - fw = dlg.getFrameWidth('lr'), - bw = dlg.body.getFrameWidth('lr'), - w; - - w = Math.max(Math.min(opt.width || iw+mw+fw+bw, opt.maxWidth || this.maxWidth), - Math.max(opt.minWidth || this.minWidth, bwidth || 0)); - - if(opt.prompt === true){ - activeTextEl.setWidth(w-iw-fw-bw); - } - if(opt.progress === true || opt.wait === true){ - progressBar.setSize(w-iw-fw-bw); - } - if(Ext.isIE && w == bwidth){ - w += 4; - } - msgEl.update(text || ' '); - dlg.setSize(w, 'auto').center(); - return this; - }, - - - updateProgress : function(value, progressText, msg){ - progressBar.updateProgress(value, progressText); - if(msg){ - this.updateText(msg); - } - return this; - }, - - - isVisible : function(){ - return dlg && dlg.isVisible(); - }, - - - hide : function(){ - var proxy = dlg ? dlg.activeGhost : null; - if(this.isVisible() || proxy){ - dlg.hide(); - handleHide(); - if (proxy){ - - - dlg.unghost(false, false); - } - } - return this; - }, - - - show : function(options){ - if(this.isVisible()){ - this.hide(); - } - opt = options; - var d = this.getDialog(opt.title || " "); - - d.setTitle(opt.title || " "); - var allowClose = (opt.closable !== false && opt.progress !== true && opt.wait !== true); - d.tools.close.setDisplayed(allowClose); - activeTextEl = textboxEl; - opt.prompt = opt.prompt || (opt.multiline ? true : false); - if(opt.prompt){ - if(opt.multiline){ - textboxEl.hide(); - textareaEl.show(); - textareaEl.setHeight(Ext.isNumber(opt.multiline) ? opt.multiline : this.defaultTextHeight); - activeTextEl = textareaEl; - }else{ - textboxEl.show(); - textareaEl.hide(); - } - }else{ - textboxEl.hide(); - textareaEl.hide(); - } - activeTextEl.dom.value = opt.value || ""; - if(opt.prompt){ - d.focusEl = activeTextEl; - }else{ - var bs = opt.buttons; - var db = null; - if(bs && bs.ok){ - db = buttons["ok"]; - }else if(bs && bs.yes){ - db = buttons["yes"]; - } - if (db){ - d.focusEl = db; - } - } - if(Ext.isDefined(opt.iconCls)){ - d.setIconClass(opt.iconCls); - } - this.setIcon(Ext.isDefined(opt.icon) ? opt.icon : bufferIcon); - bwidth = updateButtons(opt.buttons); - progressBar.setVisible(opt.progress === true || opt.wait === true); - this.updateProgress(0, opt.progressText); - this.updateText(opt.msg); - if(opt.cls){ - d.el.addClass(opt.cls); - } - d.proxyDrag = opt.proxyDrag === true; - d.modal = opt.modal !== false; - d.mask = opt.modal !== false ? mask : false; - if(!d.isVisible()){ - - document.body.appendChild(dlg.el.dom); - d.setAnimateTarget(opt.animEl); - - d.on('show', function(){ - if(allowClose === true){ - d.keyMap.enable(); - }else{ - d.keyMap.disable(); - } - }, this, {single:true}); - d.show(opt.animEl); - } - if(opt.wait === true){ - progressBar.wait(opt.waitConfig); - } - return this; - }, - - - setIcon : function(icon){ - if(!dlg){ - bufferIcon = icon; - return; - } - bufferIcon = undefined; - if(icon && icon != ''){ - iconEl.removeClass('x-hidden'); - iconEl.replaceClass(iconCls, icon); - bodyEl.addClass('x-dlg-icon'); - iconCls = icon; - }else{ - iconEl.replaceClass(iconCls, 'x-hidden'); - bodyEl.removeClass('x-dlg-icon'); - iconCls = ''; - } - return this; - }, - - - progress : function(title, msg, progressText){ - this.show({ - title : title, - msg : msg, - buttons: false, - progress:true, - closable:false, - minWidth: this.minProgressWidth, - progressText: progressText - }); - return this; - }, - - - wait : function(msg, title, config){ - this.show({ - title : title, - msg : msg, - buttons: false, - closable:false, - wait:true, - modal:true, - minWidth: this.minProgressWidth, - waitConfig: config - }); - return this; - }, - - - alert : function(title, msg, fn, scope){ - this.show({ - title : title, - msg : msg, - buttons: this.OK, - fn: fn, - scope : scope, - minWidth: this.minWidth - }); - return this; - }, - - - confirm : function(title, msg, fn, scope){ - this.show({ - title : title, - msg : msg, - buttons: this.YESNO, - fn: fn, - scope : scope, - icon: this.QUESTION, - minWidth: this.minWidth - }); - return this; - }, - - - prompt : function(title, msg, fn, scope, multiline, value){ - this.show({ - title : title, - msg : msg, - buttons: this.OKCANCEL, - fn: fn, - minWidth: this.minPromptWidth, - scope : scope, - prompt:true, - multiline: multiline, - value: value - }); - return this; - }, - - - OK : {ok:true}, - - CANCEL : {cancel:true}, - - OKCANCEL : {ok:true, cancel:true}, - - YESNO : {yes:true, no:true}, - - YESNOCANCEL : {yes:true, no:true, cancel:true}, - - INFO : 'ext-mb-info', - - WARNING : 'ext-mb-warning', - - QUESTION : 'ext-mb-question', - - ERROR : 'ext-mb-error', - - - defaultTextHeight : 75, - - maxWidth : 600, - - minWidth : 100, - - minProgressWidth : 250, - - minPromptWidth: 250, - - buttonText : { - ok : "OK", - cancel : "Cancel", - yes : "Yes", - no : "No" - } - }; -}(); - - -Ext.Msg = Ext.MessageBox; -Ext.dd.PanelProxy = Ext.extend(Object, { - - constructor : function(panel, config){ - this.panel = panel; - this.id = this.panel.id +'-ddproxy'; - Ext.apply(this, config); - }, - - - insertProxy : true, - - - setStatus : Ext.emptyFn, - reset : Ext.emptyFn, - update : Ext.emptyFn, - stop : Ext.emptyFn, - sync: Ext.emptyFn, - - - getEl : function(){ - return this.ghost; - }, - - - getGhost : function(){ - return this.ghost; - }, - - - getProxy : function(){ - return this.proxy; - }, - - - hide : function(){ - if(this.ghost){ - if(this.proxy){ - this.proxy.remove(); - delete this.proxy; - } - this.panel.el.dom.style.display = ''; - this.ghost.remove(); - delete this.ghost; - } - }, - - - show : function(){ - if(!this.ghost){ - this.ghost = this.panel.createGhost(this.panel.initialConfig.cls, undefined, Ext.getBody()); - this.ghost.setXY(this.panel.el.getXY()); - if(this.insertProxy){ - this.proxy = this.panel.el.insertSibling({cls:'x-panel-dd-spacer'}); - this.proxy.setSize(this.panel.getSize()); - } - this.panel.el.dom.style.display = 'none'; - } - }, - - - repair : function(xy, callback, scope){ - this.hide(); - if(typeof callback == "function"){ - callback.call(scope || this); - } - }, - - - moveProxy : function(parentNode, before){ - if(this.proxy){ - parentNode.insertBefore(this.proxy.dom, before); - } - } -}); - - -Ext.Panel.DD = Ext.extend(Ext.dd.DragSource, { - - constructor : function(panel, cfg){ - this.panel = panel; - this.dragData = {panel: panel}; - this.proxy = new Ext.dd.PanelProxy(panel, cfg); - Ext.Panel.DD.superclass.constructor.call(this, panel.el, cfg); - var h = panel.header, - el = panel.body; - if(h){ - this.setHandleElId(h.id); - el = panel.header; - } - el.setStyle('cursor', 'move'); - this.scroll = false; - }, - - showFrame: Ext.emptyFn, - startDrag: Ext.emptyFn, - b4StartDrag: function(x, y) { - this.proxy.show(); - }, - b4MouseDown: function(e) { - var x = e.getPageX(), - y = e.getPageY(); - this.autoOffset(x, y); - }, - onInitDrag : function(x, y){ - this.onStartDrag(x, y); - return true; - }, - createFrame : Ext.emptyFn, - getDragEl : function(e){ - return this.proxy.ghost.dom; - }, - endDrag : function(e){ - this.proxy.hide(); - this.panel.saveState(); - }, - - autoOffset : function(x, y) { - x -= this.startPageX; - y -= this.startPageY; - this.setDelta(x, y); - } -}); -Ext.state.Provider = Ext.extend(Ext.util.Observable, { - - constructor : function(){ - - this.addEvents("statechange"); - this.state = {}; - Ext.state.Provider.superclass.constructor.call(this); - }, - - - get : function(name, defaultValue){ - return typeof this.state[name] == "undefined" ? - defaultValue : this.state[name]; - }, - - - clear : function(name){ - delete this.state[name]; - this.fireEvent("statechange", this, name, null); - }, - - - set : function(name, value){ - this.state[name] = value; - this.fireEvent("statechange", this, name, value); - }, - - - decodeValue : function(cookie){ - - var re = /^(a|n|d|b|s|o|e)\:(.*)$/, - matches = re.exec(unescape(cookie)), - all, - type, - v, - kv; - if(!matches || !matches[1]){ - return; - } - type = matches[1]; - v = matches[2]; - switch(type){ - case 'e': - return null; - case 'n': - return parseFloat(v); - case 'd': - return new Date(Date.parse(v)); - case 'b': - return (v == '1'); - case 'a': - all = []; - if(v != ''){ - Ext.each(v.split('^'), function(val){ - all.push(this.decodeValue(val)); - }, this); - } - return all; - case 'o': - all = {}; - if(v != ''){ - Ext.each(v.split('^'), function(val){ - kv = val.split('='); - all[kv[0]] = this.decodeValue(kv[1]); - }, this); - } - return all; - default: - return v; - } - }, - - - encodeValue : function(v){ - var enc, - flat = '', - i = 0, - len, - key; - if(v == null){ - return 'e:1'; - }else if(typeof v == 'number'){ - enc = 'n:' + v; - }else if(typeof v == 'boolean'){ - enc = 'b:' + (v ? '1' : '0'); - }else if(Ext.isDate(v)){ - enc = 'd:' + v.toGMTString(); - }else if(Ext.isArray(v)){ - for(len = v.length; i < len; i++){ - flat += this.encodeValue(v[i]); - if(i != len - 1){ - flat += '^'; - } - } - enc = 'a:' + flat; - }else if(typeof v == 'object'){ - for(key in v){ - if(typeof v[key] != 'function' && v[key] !== undefined){ - flat += key + '=' + this.encodeValue(v[key]) + '^'; - } - } - enc = 'o:' + flat.substring(0, flat.length-1); - }else{ - enc = 's:' + v; - } - return escape(enc); - } -}); - -Ext.state.Manager = function(){ - var provider = new Ext.state.Provider(); - - return { - - setProvider : function(stateProvider){ - provider = stateProvider; - }, - - - get : function(key, defaultValue){ - return provider.get(key, defaultValue); - }, - - - set : function(key, value){ - provider.set(key, value); - }, - - - clear : function(key){ - provider.clear(key); - }, - - - getProvider : function(){ - return provider; - } - }; -}(); - -Ext.state.CookieProvider = Ext.extend(Ext.state.Provider, { - - constructor : function(config){ - Ext.state.CookieProvider.superclass.constructor.call(this); - this.path = "/"; - this.expires = new Date(new Date().getTime()+(1000*60*60*24*7)); - this.domain = null; - this.secure = false; - Ext.apply(this, config); - this.state = this.readCookies(); - }, - - - set : function(name, value){ - if(typeof value == "undefined" || value === null){ - this.clear(name); - return; - } - this.setCookie(name, value); - Ext.state.CookieProvider.superclass.set.call(this, name, value); - }, - - - clear : function(name){ - this.clearCookie(name); - Ext.state.CookieProvider.superclass.clear.call(this, name); - }, - - - readCookies : function(){ - var cookies = {}, - c = document.cookie + ";", - re = /\s?(.*?)=(.*?);/g, - matches, - name, - value; - while((matches = re.exec(c)) != null){ - name = matches[1]; - value = matches[2]; - if(name && name.substring(0,3) == "ys-"){ - cookies[name.substr(3)] = this.decodeValue(value); - } - } - return cookies; - }, - - - setCookie : function(name, value){ - document.cookie = "ys-"+ name + "=" + this.encodeValue(value) + - ((this.expires == null) ? "" : ("; expires=" + this.expires.toGMTString())) + - ((this.path == null) ? "" : ("; path=" + this.path)) + - ((this.domain == null) ? "" : ("; domain=" + this.domain)) + - ((this.secure == true) ? "; secure" : ""); - }, - - - clearCookie : function(name){ - document.cookie = "ys-" + name + "=null; expires=Thu, 01-Jan-70 00:00:01 GMT" + - ((this.path == null) ? "" : ("; path=" + this.path)) + - ((this.domain == null) ? "" : ("; domain=" + this.domain)) + - ((this.secure == true) ? "; secure" : ""); - } -}); -Ext.DataView = Ext.extend(Ext.BoxComponent, { - - - - - - - - - - selectedClass : "x-view-selected", - - emptyText : "", - - - deferEmptyText: true, - - trackOver: false, - - - blockRefresh: false, - - - last: false, - - - initComponent : function(){ - Ext.DataView.superclass.initComponent.call(this); - if(Ext.isString(this.tpl) || Ext.isArray(this.tpl)){ - this.tpl = new Ext.XTemplate(this.tpl); - } - - this.addEvents( - - "beforeclick", - - "click", - - "mouseenter", - - "mouseleave", - - "containerclick", - - "dblclick", - - "contextmenu", - - "containercontextmenu", - - "selectionchange", - - - "beforeselect" - ); - - this.store = Ext.StoreMgr.lookup(this.store); - this.all = new Ext.CompositeElementLite(); - this.selected = new Ext.CompositeElementLite(); - }, - - - afterRender : function(){ - Ext.DataView.superclass.afterRender.call(this); - - this.mon(this.getTemplateTarget(), { - "click": this.onClick, - "dblclick": this.onDblClick, - "contextmenu": this.onContextMenu, - scope:this - }); - - if(this.overClass || this.trackOver){ - this.mon(this.getTemplateTarget(), { - "mouseover": this.onMouseOver, - "mouseout": this.onMouseOut, - scope:this - }); - } - - if(this.store){ - this.bindStore(this.store, true); - } - }, - - - refresh : function() { - this.clearSelections(false, true); - var el = this.getTemplateTarget(), - records = this.store.getRange(); - - el.update(''); - if(records.length < 1){ - if(!this.deferEmptyText || this.hasSkippedEmptyText){ - el.update(this.emptyText); - } - this.all.clear(); - }else{ - this.tpl.overwrite(el, this.collectData(records, 0)); - this.all.fill(Ext.query(this.itemSelector, el.dom)); - this.updateIndexes(0); - } - this.hasSkippedEmptyText = true; - }, - - getTemplateTarget: function(){ - return this.el; - }, - - - prepareData : function(data){ - return data; - }, - - - collectData : function(records, startIndex){ - var r = [], - i = 0, - len = records.length; - for(; i < len; i++){ - r[r.length] = this.prepareData(records[i].data, startIndex + i, records[i]); - } - return r; - }, - - - bufferRender : function(records, index){ - var div = document.createElement('div'); - this.tpl.overwrite(div, this.collectData(records, index)); - return Ext.query(this.itemSelector, div); - }, - - - onUpdate : function(ds, record){ - var index = this.store.indexOf(record); - if(index > -1){ - var sel = this.isSelected(index), - original = this.all.elements[index], - node = this.bufferRender([record], index)[0]; - - this.all.replaceElement(index, node, true); - if(sel){ - this.selected.replaceElement(original, node); - this.all.item(index).addClass(this.selectedClass); - } - this.updateIndexes(index, index); - } - }, - - - onAdd : function(ds, records, index){ - if(this.all.getCount() === 0){ - this.refresh(); - return; - } - var nodes = this.bufferRender(records, index), n, a = this.all.elements; - if(index < this.all.getCount()){ - n = this.all.item(index).insertSibling(nodes, 'before', true); - a.splice.apply(a, [index, 0].concat(nodes)); - }else{ - n = this.all.last().insertSibling(nodes, 'after', true); - a.push.apply(a, nodes); - } - this.updateIndexes(index); - }, - - - onRemove : function(ds, record, index){ - this.deselect(index); - this.all.removeElement(index, true); - this.updateIndexes(index); - if (this.store.getCount() === 0){ - this.refresh(); - } - }, - - - refreshNode : function(index){ - this.onUpdate(this.store, this.store.getAt(index)); - }, - - - updateIndexes : function(startIndex, endIndex){ - var ns = this.all.elements; - startIndex = startIndex || 0; - endIndex = endIndex || ((endIndex === 0) ? 0 : (ns.length - 1)); - for(var i = startIndex; i <= endIndex; i++){ - ns[i].viewIndex = i; - } - }, - - - getStore : function(){ - return this.store; - }, - - - bindStore : function(store, initial){ - if(!initial && this.store){ - if(store !== this.store && this.store.autoDestroy){ - this.store.destroy(); - }else{ - this.store.un("beforeload", this.onBeforeLoad, this); - this.store.un("datachanged", this.onDataChanged, this); - this.store.un("add", this.onAdd, this); - this.store.un("remove", this.onRemove, this); - this.store.un("update", this.onUpdate, this); - this.store.un("clear", this.refresh, this); - } - if(!store){ - this.store = null; - } - } - if(store){ - store = Ext.StoreMgr.lookup(store); - store.on({ - scope: this, - beforeload: this.onBeforeLoad, - datachanged: this.onDataChanged, - add: this.onAdd, - remove: this.onRemove, - update: this.onUpdate, - clear: this.refresh - }); - } - this.store = store; - if(store){ - this.refresh(); - } - }, - - - onDataChanged: function() { - if (this.blockRefresh !== true) { - this.refresh.apply(this, arguments); - } - }, - - - findItemFromChild : function(node){ - return Ext.fly(node).findParent(this.itemSelector, this.getTemplateTarget()); - }, - - - onClick : function(e){ - var item = e.getTarget(this.itemSelector, this.getTemplateTarget()), - index; - if(item){ - index = this.indexOf(item); - if(this.onItemClick(item, index, e) !== false){ - this.fireEvent("click", this, index, item, e); - } - }else{ - if(this.fireEvent("containerclick", this, e) !== false){ - this.onContainerClick(e); - } - } - }, - - onContainerClick : function(e){ - this.clearSelections(); - }, - - - onContextMenu : function(e){ - var item = e.getTarget(this.itemSelector, this.getTemplateTarget()); - if(item){ - this.fireEvent("contextmenu", this, this.indexOf(item), item, e); - }else{ - this.fireEvent("containercontextmenu", this, e); - } - }, - - - onDblClick : function(e){ - var item = e.getTarget(this.itemSelector, this.getTemplateTarget()); - if(item){ - this.fireEvent("dblclick", this, this.indexOf(item), item, e); - } - }, - - - onMouseOver : function(e){ - var item = e.getTarget(this.itemSelector, this.getTemplateTarget()); - if(item && item !== this.lastItem){ - this.lastItem = item; - Ext.fly(item).addClass(this.overClass); - this.fireEvent("mouseenter", this, this.indexOf(item), item, e); - } - }, - - - onMouseOut : function(e){ - if(this.lastItem){ - if(!e.within(this.lastItem, true, true)){ - Ext.fly(this.lastItem).removeClass(this.overClass); - this.fireEvent("mouseleave", this, this.indexOf(this.lastItem), this.lastItem, e); - delete this.lastItem; - } - } - }, - - - onItemClick : function(item, index, e){ - if(this.fireEvent("beforeclick", this, index, item, e) === false){ - return false; - } - if(this.multiSelect){ - this.doMultiSelection(item, index, e); - e.preventDefault(); - }else if(this.singleSelect){ - this.doSingleSelection(item, index, e); - e.preventDefault(); - } - return true; - }, - - - doSingleSelection : function(item, index, e){ - if(e.ctrlKey && this.isSelected(index)){ - this.deselect(index); - }else{ - this.select(index, false); - } - }, - - - doMultiSelection : function(item, index, e){ - if(e.shiftKey && this.last !== false){ - var last = this.last; - this.selectRange(last, index, e.ctrlKey); - this.last = last; - }else{ - if((e.ctrlKey||this.simpleSelect) && this.isSelected(index)){ - this.deselect(index); - }else{ - this.select(index, e.ctrlKey || e.shiftKey || this.simpleSelect); - } - } - }, - - - getSelectionCount : function(){ - return this.selected.getCount(); - }, - - - getSelectedNodes : function(){ - return this.selected.elements; - }, - - - getSelectedIndexes : function(){ - var indexes = [], - selected = this.selected.elements, - i = 0, - len = selected.length; - - for(; i < len; i++){ - indexes.push(selected[i].viewIndex); - } - return indexes; - }, - - - getSelectedRecords : function(){ - return this.getRecords(this.selected.elements); - }, - - - getRecords : function(nodes){ - var records = [], - i = 0, - len = nodes.length; - - for(; i < len; i++){ - records[records.length] = this.store.getAt(nodes[i].viewIndex); - } - return records; - }, - - - getRecord : function(node){ - return this.store.getAt(node.viewIndex); - }, - - - clearSelections : function(suppressEvent, skipUpdate){ - if((this.multiSelect || this.singleSelect) && this.selected.getCount() > 0){ - if(!skipUpdate){ - this.selected.removeClass(this.selectedClass); - } - this.selected.clear(); - this.last = false; - if(!suppressEvent){ - this.fireEvent("selectionchange", this, this.selected.elements); - } - } - }, - - - isSelected : function(node){ - return this.selected.contains(this.getNode(node)); - }, - - - deselect : function(node){ - if(this.isSelected(node)){ - node = this.getNode(node); - this.selected.removeElement(node); - if(this.last == node.viewIndex){ - this.last = false; - } - Ext.fly(node).removeClass(this.selectedClass); - this.fireEvent("selectionchange", this, this.selected.elements); - } - }, - - - select : function(nodeInfo, keepExisting, suppressEvent){ - if(Ext.isArray(nodeInfo)){ - if(!keepExisting){ - this.clearSelections(true); - } - for(var i = 0, len = nodeInfo.length; i < len; i++){ - this.select(nodeInfo[i], true, true); - } - if(!suppressEvent){ - this.fireEvent("selectionchange", this, this.selected.elements); - } - } else{ - var node = this.getNode(nodeInfo); - if(!keepExisting){ - this.clearSelections(true); - } - if(node && !this.isSelected(node)){ - if(this.fireEvent("beforeselect", this, node, this.selected.elements) !== false){ - Ext.fly(node).addClass(this.selectedClass); - this.selected.add(node); - this.last = node.viewIndex; - if(!suppressEvent){ - this.fireEvent("selectionchange", this, this.selected.elements); - } - } - } - } - }, - - - selectRange : function(start, end, keepExisting){ - if(!keepExisting){ - this.clearSelections(true); - } - this.select(this.getNodes(start, end), true); - }, - - - getNode : function(nodeInfo){ - if(Ext.isString(nodeInfo)){ - return document.getElementById(nodeInfo); - }else if(Ext.isNumber(nodeInfo)){ - return this.all.elements[nodeInfo]; - }else if(nodeInfo instanceof Ext.data.Record){ - var idx = this.store.indexOf(nodeInfo); - return this.all.elements[idx]; - } - return nodeInfo; - }, - - - getNodes : function(start, end){ - var ns = this.all.elements, - nodes = [], - i; - - start = start || 0; - end = !Ext.isDefined(end) ? Math.max(ns.length - 1, 0) : end; - if(start <= end){ - for(i = start; i <= end && ns[i]; i++){ - nodes.push(ns[i]); - } - } else{ - for(i = start; i >= end && ns[i]; i--){ - nodes.push(ns[i]); - } - } - return nodes; - }, - - - indexOf : function(node){ - node = this.getNode(node); - if(Ext.isNumber(node.viewIndex)){ - return node.viewIndex; - } - return this.all.indexOf(node); - }, - - - onBeforeLoad : function(){ - if(this.loadingText){ - this.clearSelections(false, true); - this.getTemplateTarget().update('
        '+this.loadingText+'
        '); - this.all.clear(); - } - }, - - onDestroy : function(){ - this.all.clear(); - this.selected.clear(); - Ext.DataView.superclass.onDestroy.call(this); - this.bindStore(null); - } -}); - - -Ext.DataView.prototype.setStore = Ext.DataView.prototype.bindStore; - -Ext.reg('dataview', Ext.DataView); - -Ext.list.ListView = Ext.extend(Ext.DataView, { - - - - itemSelector: 'dl', - - selectedClass:'x-list-selected', - - overClass:'x-list-over', - - - scrollOffset : undefined, - - columnResize: true, - - - columnSort: true, - - - - maxColumnWidth: Ext.isIE ? 99 : 100, - - initComponent : function(){ - if(this.columnResize){ - this.colResizer = new Ext.list.ColumnResizer(this.colResizer); - this.colResizer.init(this); - } - if(this.columnSort){ - this.colSorter = new Ext.list.Sorter(this.columnSort); - this.colSorter.init(this); - } - if(!this.internalTpl){ - this.internalTpl = new Ext.XTemplate( - '
        ', - '', - '
        ', - '{header}', - '
        ', - '
        ', - '
        ', - '
        ', - '
        ', - '
        ' - ); - } - if(!this.tpl){ - this.tpl = new Ext.XTemplate( - '', - '
        ', - '', - '
        ', - ' class="{cls}">', - '{[values.tpl.apply(parent)]}', - '
        ', - '
        ', - '
        ', - '
        ', - '
        ' - ); - }; - - var cs = this.columns, - allocatedWidth = 0, - colsWithWidth = 0, - len = cs.length, - columns = []; - - for(var i = 0; i < len; i++){ - var c = cs[i]; - if(!c.isColumn) { - c.xtype = c.xtype ? (/^lv/.test(c.xtype) ? c.xtype : 'lv' + c.xtype) : 'lvcolumn'; - c = Ext.create(c); - } - if(c.width) { - allocatedWidth += c.width*100; - if(allocatedWidth > this.maxColumnWidth){ - c.width -= (allocatedWidth - this.maxColumnWidth) / 100; - } - colsWithWidth++; - } - columns.push(c); - } - - cs = this.columns = columns; - - - if(colsWithWidth < len){ - var remaining = len - colsWithWidth; - if(allocatedWidth < this.maxColumnWidth){ - var perCol = ((this.maxColumnWidth-allocatedWidth) / remaining)/100; - for(var j = 0; j < len; j++){ - var c = cs[j]; - if(!c.width){ - c.width = perCol; - } - } - } - } - Ext.list.ListView.superclass.initComponent.call(this); - }, - - onRender : function(){ - this.autoEl = { - cls: 'x-list-wrap' - }; - Ext.list.ListView.superclass.onRender.apply(this, arguments); - - this.internalTpl.overwrite(this.el, {columns: this.columns}); - - this.innerBody = Ext.get(this.el.dom.childNodes[1].firstChild); - this.innerHd = Ext.get(this.el.dom.firstChild.firstChild); - - if(this.hideHeaders){ - this.el.dom.firstChild.style.display = 'none'; - } - }, - - getTemplateTarget : function(){ - return this.innerBody; - }, - - - collectData : function(){ - var rs = Ext.list.ListView.superclass.collectData.apply(this, arguments); - return { - columns: this.columns, - rows: rs - }; - }, - - verifyInternalSize : function(){ - if(this.lastSize){ - this.onResize(this.lastSize.width, this.lastSize.height); - } - }, - - - onResize : function(w, h){ - var body = this.innerBody.dom, - header = this.innerHd.dom, - scrollWidth = w - Ext.num(this.scrollOffset, Ext.getScrollBarWidth()) + 'px', - parentNode; - - if(!body){ - return; - } - parentNode = body.parentNode; - if(Ext.isNumber(w)){ - if(this.reserveScrollOffset || ((parentNode.offsetWidth - parentNode.clientWidth) > 10)){ - body.style.width = scrollWidth; - header.style.width = scrollWidth; - }else{ - body.style.width = w + 'px'; - header.style.width = w + 'px'; - setTimeout(function(){ - if((parentNode.offsetWidth - parentNode.clientWidth) > 10){ - body.style.width = scrollWidth; - header.style.width = scrollWidth; - } - }, 10); - } - } - if(Ext.isNumber(h)){ - parentNode.style.height = Math.max(0, h - header.parentNode.offsetHeight) + 'px'; - } - }, - - updateIndexes : function(){ - Ext.list.ListView.superclass.updateIndexes.apply(this, arguments); - this.verifyInternalSize(); - }, - - findHeaderIndex : function(header){ - header = header.dom || header; - var parentNode = header.parentNode, - children = parentNode.parentNode.childNodes, - i = 0, - c; - for(; c = children[i]; i++){ - if(c == parentNode){ - return i; - } - } - return -1; - }, - - setHdWidths : function(){ - var els = this.innerHd.dom.getElementsByTagName('div'), - i = 0, - columns = this.columns, - len = columns.length; - - for(; i < len; i++){ - els[i].style.width = (columns[i].width*100) + '%'; - } - } -}); - -Ext.reg('listview', Ext.list.ListView); - - -Ext.ListView = Ext.list.ListView; -Ext.list.Column = Ext.extend(Object, { - - isColumn: true, - - - align: 'left', - - header: '', - - - width: null, - - - cls: '', - - - - - - constructor : function(c){ - if(!c.tpl){ - c.tpl = new Ext.XTemplate('{' + c.dataIndex + '}'); - } - else if(Ext.isString(c.tpl)){ - c.tpl = new Ext.XTemplate(c.tpl); - } - - Ext.apply(this, c); - } -}); - -Ext.reg('lvcolumn', Ext.list.Column); - - -Ext.list.NumberColumn = Ext.extend(Ext.list.Column, { - - format: '0,000.00', - - constructor : function(c) { - c.tpl = c.tpl || new Ext.XTemplate('{' + c.dataIndex + ':number("' + (c.format || this.format) + '")}'); - Ext.list.NumberColumn.superclass.constructor.call(this, c); - } -}); - -Ext.reg('lvnumbercolumn', Ext.list.NumberColumn); - - -Ext.list.DateColumn = Ext.extend(Ext.list.Column, { - format: 'm/d/Y', - constructor : function(c) { - c.tpl = c.tpl || new Ext.XTemplate('{' + c.dataIndex + ':date("' + (c.format || this.format) + '")}'); - Ext.list.DateColumn.superclass.constructor.call(this, c); - } -}); -Ext.reg('lvdatecolumn', Ext.list.DateColumn); - - -Ext.list.BooleanColumn = Ext.extend(Ext.list.Column, { - - trueText: 'true', - - falseText: 'false', - - undefinedText: ' ', - - constructor : function(c) { - c.tpl = c.tpl || new Ext.XTemplate('{' + c.dataIndex + ':this.format}'); - - var t = this.trueText, f = this.falseText, u = this.undefinedText; - c.tpl.format = function(v){ - if(v === undefined){ - return u; - } - if(!v || v === 'false'){ - return f; - } - return t; - }; - - Ext.list.DateColumn.superclass.constructor.call(this, c); - } -}); - -Ext.reg('lvbooleancolumn', Ext.list.BooleanColumn); -Ext.list.ColumnResizer = Ext.extend(Ext.util.Observable, { - - minPct: .05, - - constructor: function(config){ - Ext.apply(this, config); - Ext.list.ColumnResizer.superclass.constructor.call(this); - }, - init : function(listView){ - this.view = listView; - listView.on('render', this.initEvents, this); - }, - - initEvents : function(view){ - view.mon(view.innerHd, 'mousemove', this.handleHdMove, this); - this.tracker = new Ext.dd.DragTracker({ - onBeforeStart: this.onBeforeStart.createDelegate(this), - onStart: this.onStart.createDelegate(this), - onDrag: this.onDrag.createDelegate(this), - onEnd: this.onEnd.createDelegate(this), - tolerance: 3, - autoStart: 300 - }); - this.tracker.initEl(view.innerHd); - view.on('beforedestroy', this.tracker.destroy, this.tracker); - }, - - handleHdMove : function(e, t){ - var handleWidth = 5, - x = e.getPageX(), - header = e.getTarget('em', 3, true); - if(header){ - var region = header.getRegion(), - style = header.dom.style, - parentNode = header.dom.parentNode; - - if(x - region.left <= handleWidth && parentNode != parentNode.parentNode.firstChild){ - this.activeHd = Ext.get(parentNode.previousSibling.firstChild); - style.cursor = Ext.isWebKit ? 'e-resize' : 'col-resize'; - } else if(region.right - x <= handleWidth && parentNode != parentNode.parentNode.lastChild.previousSibling){ - this.activeHd = header; - style.cursor = Ext.isWebKit ? 'w-resize' : 'col-resize'; - } else{ - delete this.activeHd; - style.cursor = ''; - } - } - }, - - onBeforeStart : function(e){ - this.dragHd = this.activeHd; - return !!this.dragHd; - }, - - onStart: function(e){ - - var me = this, - view = me.view, - dragHeader = me.dragHd, - x = me.tracker.getXY()[0]; - - me.proxy = view.el.createChild({cls:'x-list-resizer'}); - me.dragX = dragHeader.getX(); - me.headerIndex = view.findHeaderIndex(dragHeader); - - me.headersDisabled = view.disableHeaders; - view.disableHeaders = true; - - me.proxy.setHeight(view.el.getHeight()); - me.proxy.setX(me.dragX); - me.proxy.setWidth(x - me.dragX); - - this.setBoundaries(); - - }, - - - setBoundaries: function(relativeX){ - var view = this.view, - headerIndex = this.headerIndex, - width = view.innerHd.getWidth(), - relativeX = view.innerHd.getX(), - minWidth = Math.ceil(width * this.minPct), - maxWidth = width - minWidth, - numColumns = view.columns.length, - headers = view.innerHd.select('em', true), - minX = minWidth + relativeX, - maxX = maxWidth + relativeX, - header; - - if (numColumns == 2) { - this.minX = minX; - this.maxX = maxX; - }else{ - header = headers.item(headerIndex + 2); - this.minX = headers.item(headerIndex).getX() + minWidth; - this.maxX = header ? header.getX() - minWidth : maxX; - if (headerIndex == 0) { - - this.minX = minX; - } else if (headerIndex == numColumns - 2) { - - this.maxX = maxX; - } - } - }, - - onDrag: function(e){ - var me = this, - cursorX = me.tracker.getXY()[0].constrain(me.minX, me.maxX); - - me.proxy.setWidth(cursorX - this.dragX); - }, - - onEnd: function(e){ - - var newWidth = this.proxy.getWidth(), - index = this.headerIndex, - view = this.view, - columns = view.columns, - width = view.innerHd.getWidth(), - newPercent = Math.ceil(newWidth * view.maxColumnWidth / width) / 100, - disabled = this.headersDisabled, - headerCol = columns[index], - otherCol = columns[index + 1], - totalPercent = headerCol.width + otherCol.width; - - this.proxy.remove(); - - headerCol.width = newPercent; - otherCol.width = totalPercent - newPercent; - - delete this.dragHd; - view.setHdWidths(); - view.refresh(); - - setTimeout(function(){ - view.disableHeaders = disabled; - }, 100); - } -}); - - -Ext.ListView.ColumnResizer = Ext.list.ColumnResizer; -Ext.list.Sorter = Ext.extend(Ext.util.Observable, { - - sortClasses : ["sort-asc", "sort-desc"], - - constructor: function(config){ - Ext.apply(this, config); - Ext.list.Sorter.superclass.constructor.call(this); - }, - - init : function(listView){ - this.view = listView; - listView.on('render', this.initEvents, this); - }, - - initEvents : function(view){ - view.mon(view.innerHd, 'click', this.onHdClick, this); - view.innerHd.setStyle('cursor', 'pointer'); - view.mon(view.store, 'datachanged', this.updateSortState, this); - this.updateSortState.defer(10, this, [view.store]); - }, - - updateSortState : function(store){ - var state = store.getSortState(); - if(!state){ - return; - } - this.sortState = state; - var cs = this.view.columns, sortColumn = -1; - for(var i = 0, len = cs.length; i < len; i++){ - if(cs[i].dataIndex == state.field){ - sortColumn = i; - break; - } - } - if(sortColumn != -1){ - var sortDir = state.direction; - this.updateSortIcon(sortColumn, sortDir); - } - }, - - updateSortIcon : function(col, dir){ - var sc = this.sortClasses; - var hds = this.view.innerHd.select('em').removeClass(sc); - hds.item(col).addClass(sc[dir == "DESC" ? 1 : 0]); - }, - - onHdClick : function(e){ - var hd = e.getTarget('em', 3); - if(hd && !this.view.disableHeaders){ - var index = this.view.findHeaderIndex(hd); - this.view.store.sort(this.view.columns[index].dataIndex); - } - } -}); - - -Ext.ListView.Sorter = Ext.list.Sorter; -Ext.TabPanel = Ext.extend(Ext.Panel, { - - - - deferredRender : true, - - tabWidth : 120, - - minTabWidth : 30, - - resizeTabs : false, - - enableTabScroll : false, - - scrollIncrement : 0, - - scrollRepeatInterval : 400, - - scrollDuration : 0.35, - - animScroll : true, - - tabPosition : 'top', - - baseCls : 'x-tab-panel', - - autoTabs : false, - - autoTabSelector : 'div.x-tab', - - activeTab : undefined, - - tabMargin : 2, - - plain : false, - - wheelIncrement : 20, - - - idDelimiter : '__', - - - itemCls : 'x-tab-item', - - - elements : 'body', - headerAsText : false, - frame : false, - hideBorders :true, - - - initComponent : function(){ - this.frame = false; - Ext.TabPanel.superclass.initComponent.call(this); - this.addEvents( - - 'beforetabchange', - - 'tabchange', - - 'contextmenu' - ); - - this.setLayout(new Ext.layout.CardLayout(Ext.apply({ - layoutOnCardChange: this.layoutOnTabChange, - deferredRender: this.deferredRender - }, this.layoutConfig))); - - if(this.tabPosition == 'top'){ - this.elements += ',header'; - this.stripTarget = 'header'; - }else { - this.elements += ',footer'; - this.stripTarget = 'footer'; - } - if(!this.stack){ - this.stack = Ext.TabPanel.AccessStack(); - } - this.initItems(); - }, - - - onRender : function(ct, position){ - Ext.TabPanel.superclass.onRender.call(this, ct, position); - - if(this.plain){ - var pos = this.tabPosition == 'top' ? 'header' : 'footer'; - this[pos].addClass('x-tab-panel-'+pos+'-plain'); - } - - var st = this[this.stripTarget]; - - this.stripWrap = st.createChild({cls:'x-tab-strip-wrap', cn:{ - tag:'ul', cls:'x-tab-strip x-tab-strip-'+this.tabPosition}}); - - var beforeEl = (this.tabPosition=='bottom' ? this.stripWrap : null); - st.createChild({cls:'x-tab-strip-spacer'}, beforeEl); - this.strip = new Ext.Element(this.stripWrap.dom.firstChild); - - - this.edge = this.strip.createChild({tag:'li', cls:'x-tab-edge', cn: [{tag: 'span', cls: 'x-tab-strip-text', cn: ' '}]}); - this.strip.createChild({cls:'x-clear'}); - - this.body.addClass('x-tab-panel-body-'+this.tabPosition); - - - if(!this.itemTpl){ - var tt = new Ext.Template( - '
      • ', - '', - '{text}', - '
      • ' - ); - tt.disableFormats = true; - tt.compile(); - Ext.TabPanel.prototype.itemTpl = tt; - } - - this.items.each(this.initTab, this); - }, - - - afterRender : function(){ - Ext.TabPanel.superclass.afterRender.call(this); - if(this.autoTabs){ - this.readTabs(false); - } - if(this.activeTab !== undefined){ - var item = Ext.isObject(this.activeTab) ? this.activeTab : this.items.get(this.activeTab); - delete this.activeTab; - this.setActiveTab(item); - } - }, - - - initEvents : function(){ - Ext.TabPanel.superclass.initEvents.call(this); - this.mon(this.strip, { - scope: this, - mousedown: this.onStripMouseDown, - contextmenu: this.onStripContextMenu - }); - if(this.enableTabScroll){ - this.mon(this.strip, 'mousewheel', this.onWheel, this); - } - }, - - - findTargets : function(e){ - var item = null, - itemEl = e.getTarget('li:not(.x-tab-edge)', this.strip); - - if(itemEl){ - item = this.getComponent(itemEl.id.split(this.idDelimiter)[1]); - if(item.disabled){ - return { - close : null, - item : null, - el : null - }; - } - } - return { - close : e.getTarget('.x-tab-strip-close', this.strip), - item : item, - el : itemEl - }; - }, - - - onStripMouseDown : function(e){ - if(e.button !== 0){ - return; - } - e.preventDefault(); - var t = this.findTargets(e); - if(t.close){ - if (t.item.fireEvent('beforeclose', t.item) !== false) { - t.item.fireEvent('close', t.item); - this.remove(t.item); - } - return; - } - if(t.item && t.item != this.activeTab){ - this.setActiveTab(t.item); - } - }, - - - onStripContextMenu : function(e){ - e.preventDefault(); - var t = this.findTargets(e); - if(t.item){ - this.fireEvent('contextmenu', this, t.item, e); - } - }, - - - readTabs : function(removeExisting){ - if(removeExisting === true){ - this.items.each(function(item){ - this.remove(item); - }, this); - } - var tabs = this.el.query(this.autoTabSelector); - for(var i = 0, len = tabs.length; i < len; i++){ - var tab = tabs[i], - title = tab.getAttribute('title'); - tab.removeAttribute('title'); - this.add({ - title: title, - contentEl: tab - }); - } - }, - - - initTab : function(item, index){ - var before = this.strip.dom.childNodes[index], - p = this.getTemplateArgs(item), - el = before ? - this.itemTpl.insertBefore(before, p) : - this.itemTpl.append(this.strip, p), - cls = 'x-tab-strip-over', - tabEl = Ext.get(el); - - tabEl.hover(function(){ - if(!item.disabled){ - tabEl.addClass(cls); - } - }, function(){ - tabEl.removeClass(cls); - }); - - if(item.tabTip){ - tabEl.child('span.x-tab-strip-text', true).qtip = item.tabTip; - } - item.tabEl = el; - - - tabEl.select('a').on('click', function(e){ - if(!e.getPageX()){ - this.onStripMouseDown(e); - } - }, this, {preventDefault: true}); - - item.on({ - scope: this, - disable: this.onItemDisabled, - enable: this.onItemEnabled, - titlechange: this.onItemTitleChanged, - iconchange: this.onItemIconChanged, - beforeshow: this.onBeforeShowItem - }); - }, - - - - - getTemplateArgs : function(item) { - var cls = item.closable ? 'x-tab-strip-closable' : ''; - if(item.disabled){ - cls += ' x-item-disabled'; - } - if(item.iconCls){ - cls += ' x-tab-with-icon'; - } - if(item.tabCls){ - cls += ' ' + item.tabCls; - } - - return { - id: this.id + this.idDelimiter + item.getItemId(), - text: item.title, - cls: cls, - iconCls: item.iconCls || '' - }; - }, - - - onAdd : function(c){ - Ext.TabPanel.superclass.onAdd.call(this, c); - if(this.rendered){ - var items = this.items; - this.initTab(c, items.indexOf(c)); - this.delegateUpdates(); - } - }, - - - onBeforeAdd : function(item){ - var existing = item.events ? (this.items.containsKey(item.getItemId()) ? item : null) : this.items.get(item); - if(existing){ - this.setActiveTab(item); - return false; - } - Ext.TabPanel.superclass.onBeforeAdd.apply(this, arguments); - var es = item.elements; - item.elements = es ? es.replace(',header', '') : es; - item.border = (item.border === true); - }, - - - onRemove : function(c){ - var te = Ext.get(c.tabEl); - - if(te){ - te.select('a').removeAllListeners(); - Ext.destroy(te); - } - Ext.TabPanel.superclass.onRemove.call(this, c); - this.stack.remove(c); - delete c.tabEl; - c.un('disable', this.onItemDisabled, this); - c.un('enable', this.onItemEnabled, this); - c.un('titlechange', this.onItemTitleChanged, this); - c.un('iconchange', this.onItemIconChanged, this); - c.un('beforeshow', this.onBeforeShowItem, this); - if(c == this.activeTab){ - var next = this.stack.next(); - if(next){ - this.setActiveTab(next); - }else if(this.items.getCount() > 0){ - this.setActiveTab(0); - }else{ - this.setActiveTab(null); - } - } - if(!this.destroying){ - this.delegateUpdates(); - } - }, - - - onBeforeShowItem : function(item){ - if(item != this.activeTab){ - this.setActiveTab(item); - return false; - } - }, - - - onItemDisabled : function(item){ - var el = this.getTabEl(item); - if(el){ - Ext.fly(el).addClass('x-item-disabled'); - } - this.stack.remove(item); - }, - - - onItemEnabled : function(item){ - var el = this.getTabEl(item); - if(el){ - Ext.fly(el).removeClass('x-item-disabled'); - } - }, - - - onItemTitleChanged : function(item){ - var el = this.getTabEl(item); - if(el){ - Ext.fly(el).child('span.x-tab-strip-text', true).innerHTML = item.title; - } - }, - - - onItemIconChanged : function(item, iconCls, oldCls){ - var el = this.getTabEl(item); - if(el){ - el = Ext.get(el); - el.child('span.x-tab-strip-text').replaceClass(oldCls, iconCls); - el[Ext.isEmpty(iconCls) ? 'removeClass' : 'addClass']('x-tab-with-icon'); - } - }, - - - getTabEl : function(item){ - var c = this.getComponent(item); - return c ? c.tabEl : null; - }, - - - onResize : function(){ - Ext.TabPanel.superclass.onResize.apply(this, arguments); - this.delegateUpdates(); - }, - - - beginUpdate : function(){ - this.suspendUpdates = true; - }, - - - endUpdate : function(){ - this.suspendUpdates = false; - this.delegateUpdates(); - }, - - - hideTabStripItem : function(item){ - item = this.getComponent(item); - var el = this.getTabEl(item); - if(el){ - el.style.display = 'none'; - this.delegateUpdates(); - } - this.stack.remove(item); - }, - - - unhideTabStripItem : function(item){ - item = this.getComponent(item); - var el = this.getTabEl(item); - if(el){ - el.style.display = ''; - this.delegateUpdates(); - } - }, - - - delegateUpdates : function(){ - var rendered = this.rendered; - if(this.suspendUpdates){ - return; - } - if(this.resizeTabs && rendered){ - this.autoSizeTabs(); - } - if(this.enableTabScroll && rendered){ - this.autoScrollTabs(); - } - }, - - - autoSizeTabs : function(){ - var count = this.items.length, - ce = this.tabPosition != 'bottom' ? 'header' : 'footer', - ow = this[ce].dom.offsetWidth, - aw = this[ce].dom.clientWidth; - - if(!this.resizeTabs || count < 1 || !aw){ - return; - } - - var each = Math.max(Math.min(Math.floor((aw-4) / count) - this.tabMargin, this.tabWidth), this.minTabWidth); - this.lastTabWidth = each; - var lis = this.strip.query('li:not(.x-tab-edge)'); - for(var i = 0, len = lis.length; i < len; i++) { - var li = lis[i], - inner = Ext.fly(li).child('.x-tab-strip-inner', true), - tw = li.offsetWidth, - iw = inner.offsetWidth; - inner.style.width = (each - (tw-iw)) + 'px'; - } - }, - - - adjustBodyWidth : function(w){ - if(this.header){ - this.header.setWidth(w); - } - if(this.footer){ - this.footer.setWidth(w); - } - return w; - }, - - - setActiveTab : function(item){ - item = this.getComponent(item); - if(this.fireEvent('beforetabchange', this, item, this.activeTab) === false){ - return; - } - if(!this.rendered){ - this.activeTab = item; - return; - } - if(this.activeTab != item){ - if(this.activeTab){ - var oldEl = this.getTabEl(this.activeTab); - if(oldEl){ - Ext.fly(oldEl).removeClass('x-tab-strip-active'); - } - } - this.activeTab = item; - if(item){ - var el = this.getTabEl(item); - Ext.fly(el).addClass('x-tab-strip-active'); - this.stack.add(item); - - this.layout.setActiveItem(item); - - this.delegateUpdates(); - if(this.scrolling){ - this.scrollToTab(item, this.animScroll); - } - } - this.fireEvent('tabchange', this, item); - } - }, - - - getActiveTab : function(){ - return this.activeTab || null; - }, - - - getItem : function(item){ - return this.getComponent(item); - }, - - - autoScrollTabs : function(){ - this.pos = this.tabPosition=='bottom' ? this.footer : this.header; - var count = this.items.length, - ow = this.pos.dom.offsetWidth, - tw = this.pos.dom.clientWidth, - wrap = this.stripWrap, - wd = wrap.dom, - cw = wd.offsetWidth, - pos = this.getScrollPos(), - l = this.edge.getOffsetsTo(this.stripWrap)[0] + pos; - - if(!this.enableTabScroll || cw < 20){ - return; - } - if(count == 0 || l <= tw){ - - wd.scrollLeft = 0; - wrap.setWidth(tw); - if(this.scrolling){ - this.scrolling = false; - this.pos.removeClass('x-tab-scrolling'); - this.scrollLeft.hide(); - this.scrollRight.hide(); - - if(Ext.isAir || Ext.isWebKit){ - wd.style.marginLeft = ''; - wd.style.marginRight = ''; - } - } - }else{ - if(!this.scrolling){ - this.pos.addClass('x-tab-scrolling'); - - if(Ext.isAir || Ext.isWebKit){ - wd.style.marginLeft = '18px'; - wd.style.marginRight = '18px'; - } - } - tw -= wrap.getMargins('lr'); - wrap.setWidth(tw > 20 ? tw : 20); - if(!this.scrolling){ - if(!this.scrollLeft){ - this.createScrollers(); - }else{ - this.scrollLeft.show(); - this.scrollRight.show(); - } - } - this.scrolling = true; - if(pos > (l-tw)){ - wd.scrollLeft = l-tw; - }else{ - this.scrollToTab(this.activeTab, false); - } - this.updateScrollButtons(); - } - }, - - - createScrollers : function(){ - this.pos.addClass('x-tab-scrolling-' + this.tabPosition); - var h = this.stripWrap.dom.offsetHeight; - - - var sl = this.pos.insertFirst({ - cls:'x-tab-scroller-left' - }); - sl.setHeight(h); - sl.addClassOnOver('x-tab-scroller-left-over'); - this.leftRepeater = new Ext.util.ClickRepeater(sl, { - interval : this.scrollRepeatInterval, - handler: this.onScrollLeft, - scope: this - }); - this.scrollLeft = sl; - - - var sr = this.pos.insertFirst({ - cls:'x-tab-scroller-right' - }); - sr.setHeight(h); - sr.addClassOnOver('x-tab-scroller-right-over'); - this.rightRepeater = new Ext.util.ClickRepeater(sr, { - interval : this.scrollRepeatInterval, - handler: this.onScrollRight, - scope: this - }); - this.scrollRight = sr; - }, - - - getScrollWidth : function(){ - return this.edge.getOffsetsTo(this.stripWrap)[0] + this.getScrollPos(); - }, - - - getScrollPos : function(){ - return parseInt(this.stripWrap.dom.scrollLeft, 10) || 0; - }, - - - getScrollArea : function(){ - return parseInt(this.stripWrap.dom.clientWidth, 10) || 0; - }, - - - getScrollAnim : function(){ - return {duration:this.scrollDuration, callback: this.updateScrollButtons, scope: this}; - }, - - - getScrollIncrement : function(){ - return this.scrollIncrement || (this.resizeTabs ? this.lastTabWidth+2 : 100); - }, - - - - scrollToTab : function(item, animate){ - if(!item){ - return; - } - var el = this.getTabEl(item), - pos = this.getScrollPos(), - area = this.getScrollArea(), - left = Ext.fly(el).getOffsetsTo(this.stripWrap)[0] + pos, - right = left + el.offsetWidth; - if(left < pos){ - this.scrollTo(left, animate); - }else if(right > (pos + area)){ - this.scrollTo(right - area, animate); - } - }, - - - scrollTo : function(pos, animate){ - this.stripWrap.scrollTo('left', pos, animate ? this.getScrollAnim() : false); - if(!animate){ - this.updateScrollButtons(); - } - }, - - onWheel : function(e){ - var d = e.getWheelDelta()*this.wheelIncrement*-1; - e.stopEvent(); - - var pos = this.getScrollPos(), - newpos = pos + d, - sw = this.getScrollWidth()-this.getScrollArea(); - - var s = Math.max(0, Math.min(sw, newpos)); - if(s != pos){ - this.scrollTo(s, false); - } - }, - - - onScrollRight : function(){ - var sw = this.getScrollWidth()-this.getScrollArea(), - pos = this.getScrollPos(), - s = Math.min(sw, pos + this.getScrollIncrement()); - if(s != pos){ - this.scrollTo(s, this.animScroll); - } - }, - - - onScrollLeft : function(){ - var pos = this.getScrollPos(), - s = Math.max(0, pos - this.getScrollIncrement()); - if(s != pos){ - this.scrollTo(s, this.animScroll); - } - }, - - - updateScrollButtons : function(){ - var pos = this.getScrollPos(); - this.scrollLeft[pos === 0 ? 'addClass' : 'removeClass']('x-tab-scroller-left-disabled'); - this.scrollRight[pos >= (this.getScrollWidth()-this.getScrollArea()) ? 'addClass' : 'removeClass']('x-tab-scroller-right-disabled'); - }, - - - beforeDestroy : function() { - Ext.destroy(this.leftRepeater, this.rightRepeater); - this.deleteMembers('strip', 'edge', 'scrollLeft', 'scrollRight', 'stripWrap'); - this.activeTab = null; - Ext.TabPanel.superclass.beforeDestroy.apply(this); - } - - - - - - - - - - - - - -}); -Ext.reg('tabpanel', Ext.TabPanel); - - -Ext.TabPanel.prototype.activate = Ext.TabPanel.prototype.setActiveTab; - - -Ext.TabPanel.AccessStack = function(){ - var items = []; - return { - add : function(item){ - items.push(item); - if(items.length > 10){ - items.shift(); - } - }, - - remove : function(item){ - var s = []; - for(var i = 0, len = items.length; i < len; i++) { - if(items[i] != item){ - s.push(items[i]); - } - } - items = s; - }, - - next : function(){ - return items.pop(); - } - }; -}; - -Ext.Button = Ext.extend(Ext.BoxComponent, { - - hidden : false, - - disabled : false, - - pressed : false, - - - - - - - enableToggle : false, - - - - menuAlign : 'tl-bl?', - - - - - type : 'button', - - - menuClassTarget : 'tr:nth(2)', - - - clickEvent : 'click', - - - handleMouseEvents : true, - - - tooltipType : 'qtip', - - - buttonSelector : 'button:first-child', - - - scale : 'small', - - - - - iconAlign : 'left', - - - arrowAlign : 'right', - - - - - - - initComponent : function(){ - if(this.menu){ - - - if (Ext.isArray(this.menu)){ - this.menu = { items: this.menu }; - } - - - - if (Ext.isObject(this.menu)){ - this.menu.ownerCt = this; - } - - this.menu = Ext.menu.MenuMgr.get(this.menu); - this.menu.ownerCt = undefined; - } - - Ext.Button.superclass.initComponent.call(this); - - this.addEvents( - - 'click', - - 'toggle', - - 'mouseover', - - 'mouseout', - - 'menushow', - - 'menuhide', - - 'menutriggerover', - - 'menutriggerout' - ); - - if(Ext.isString(this.toggleGroup)){ - this.enableToggle = true; - } - }, - - - getTemplateArgs : function(){ - return [this.type, 'x-btn-' + this.scale + ' x-btn-icon-' + this.scale + '-' + this.iconAlign, this.getMenuClass(), this.cls, this.id]; - }, - - - setButtonClass : function(){ - if(this.useSetClass){ - if(!Ext.isEmpty(this.oldCls)){ - this.el.removeClass([this.oldCls, 'x-btn-pressed']); - } - this.oldCls = (this.iconCls || this.icon) ? (this.text ? 'x-btn-text-icon' : 'x-btn-icon') : 'x-btn-noicon'; - this.el.addClass([this.oldCls, this.pressed ? 'x-btn-pressed' : null]); - } - }, - - - getMenuClass : function(){ - return this.menu ? (this.arrowAlign != 'bottom' ? 'x-btn-arrow' : 'x-btn-arrow-bottom') : ''; - }, - - - onRender : function(ct, position){ - if(!this.template){ - if(!Ext.Button.buttonTemplate){ - - Ext.Button.buttonTemplate = new Ext.Template( - '', - '', - '', - '', - '
          
          
          
        '); - Ext.Button.buttonTemplate.compile(); - } - this.template = Ext.Button.buttonTemplate; - } - - var btn, targs = this.getTemplateArgs(); - - if(position){ - btn = this.template.insertBefore(position, targs, true); - }else{ - btn = this.template.append(ct, targs, true); - } - - this.btnEl = btn.child(this.buttonSelector); - this.mon(this.btnEl, { - scope: this, - focus: this.onFocus, - blur: this.onBlur - }); - - this.initButtonEl(btn, this.btnEl); - - Ext.ButtonToggleMgr.register(this); - }, - - - initButtonEl : function(btn, btnEl){ - this.el = btn; - this.setIcon(this.icon); - this.setText(this.text); - this.setIconClass(this.iconCls); - if(Ext.isDefined(this.tabIndex)){ - btnEl.dom.tabIndex = this.tabIndex; - } - if(this.tooltip){ - this.setTooltip(this.tooltip, true); - } - - if(this.handleMouseEvents){ - this.mon(btn, { - scope: this, - mouseover: this.onMouseOver, - mousedown: this.onMouseDown - }); - - - - } - - if(this.menu){ - this.mon(this.menu, { - scope: this, - show: this.onMenuShow, - hide: this.onMenuHide - }); - } - - if(this.repeat){ - var repeater = new Ext.util.ClickRepeater(btn, Ext.isObject(this.repeat) ? this.repeat : {}); - this.mon(repeater, 'click', this.onRepeatClick, this); - }else{ - this.mon(btn, this.clickEvent, this.onClick, this); - } - }, - - - afterRender : function(){ - Ext.Button.superclass.afterRender.call(this); - this.useSetClass = true; - this.setButtonClass(); - this.doc = Ext.getDoc(); - this.doAutoWidth(); - }, - - - setIconClass : function(cls){ - this.iconCls = cls; - if(this.el){ - this.btnEl.dom.className = ''; - this.btnEl.addClass(['x-btn-text', cls || '']); - this.setButtonClass(); - } - return this; - }, - - - setTooltip : function(tooltip, initial){ - if(this.rendered){ - if(!initial){ - this.clearTip(); - } - if(Ext.isObject(tooltip)){ - Ext.QuickTips.register(Ext.apply({ - target: this.btnEl.id - }, tooltip)); - this.tooltip = tooltip; - }else{ - this.btnEl.dom[this.tooltipType] = tooltip; - } - }else{ - this.tooltip = tooltip; - } - return this; - }, - - - clearTip : function(){ - if(Ext.isObject(this.tooltip)){ - Ext.QuickTips.unregister(this.btnEl); - } - }, - - - beforeDestroy : function(){ - if(this.rendered){ - this.clearTip(); - } - if(this.menu && this.destroyMenu !== false) { - Ext.destroy(this.btnEl, this.menu); - } - Ext.destroy(this.repeater); - }, - - - onDestroy : function(){ - if(this.rendered){ - this.doc.un('mouseover', this.monitorMouseOver, this); - this.doc.un('mouseup', this.onMouseUp, this); - delete this.doc; - delete this.btnEl; - Ext.ButtonToggleMgr.unregister(this); - } - Ext.Button.superclass.onDestroy.call(this); - }, - - - doAutoWidth : function(){ - if(this.autoWidth !== false && this.el && this.text && this.width === undefined){ - this.el.setWidth('auto'); - if(Ext.isIE7 && Ext.isStrict){ - var ib = this.btnEl; - if(ib && ib.getWidth() > 20){ - ib.clip(); - ib.setWidth(Ext.util.TextMetrics.measure(ib, this.text).width+ib.getFrameWidth('lr')); - } - } - if(this.minWidth){ - if(this.el.getWidth() < this.minWidth){ - this.el.setWidth(this.minWidth); - } - } - } - }, - - - setHandler : function(handler, scope){ - this.handler = handler; - this.scope = scope; - return this; - }, - - - setText : function(text){ - this.text = text; - if(this.el){ - this.btnEl.update(text || ' '); - this.setButtonClass(); - } - this.doAutoWidth(); - return this; - }, - - - setIcon : function(icon){ - this.icon = icon; - if(this.el){ - this.btnEl.setStyle('background-image', icon ? 'url(' + icon + ')' : ''); - this.setButtonClass(); - } - return this; - }, - - - getText : function(){ - return this.text; - }, - - - toggle : function(state, suppressEvent){ - state = state === undefined ? !this.pressed : !!state; - if(state != this.pressed){ - if(this.rendered){ - this.el[state ? 'addClass' : 'removeClass']('x-btn-pressed'); - } - this.pressed = state; - if(!suppressEvent){ - this.fireEvent('toggle', this, state); - if(this.toggleHandler){ - this.toggleHandler.call(this.scope || this, this, state); - } - } - } - return this; - }, - - - onDisable : function(){ - this.onDisableChange(true); - }, - - - onEnable : function(){ - this.onDisableChange(false); - }, - - onDisableChange : function(disabled){ - if(this.el){ - if(!Ext.isIE6 || !this.text){ - this.el[disabled ? 'addClass' : 'removeClass'](this.disabledClass); - } - this.el.dom.disabled = disabled; - } - this.disabled = disabled; - }, - - - showMenu : function(){ - if(this.rendered && this.menu){ - if(this.tooltip){ - Ext.QuickTips.getQuickTip().cancelShow(this.btnEl); - } - if(this.menu.isVisible()){ - this.menu.hide(); - } - this.menu.ownerCt = this; - this.menu.show(this.el, this.menuAlign); - } - return this; - }, - - - hideMenu : function(){ - if(this.hasVisibleMenu()){ - this.menu.hide(); - } - return this; - }, - - - hasVisibleMenu : function(){ - return this.menu && this.menu.ownerCt == this && this.menu.isVisible(); - }, - - - onRepeatClick : function(repeat, e){ - this.onClick(e); - }, - - - onClick : function(e){ - if(e){ - e.preventDefault(); - } - if(e.button !== 0){ - return; - } - if(!this.disabled){ - this.doToggle(); - if(this.menu && !this.hasVisibleMenu() && !this.ignoreNextClick){ - this.showMenu(); - } - this.fireEvent('click', this, e); - if(this.handler){ - - this.handler.call(this.scope || this, this, e); - } - } - }, - - - doToggle: function(){ - if (this.enableToggle && (this.allowDepress !== false || !this.pressed)) { - this.toggle(); - } - }, - - - isMenuTriggerOver : function(e, internal){ - return this.menu && !internal; - }, - - - isMenuTriggerOut : function(e, internal){ - return this.menu && !internal; - }, - - - onMouseOver : function(e){ - if(!this.disabled){ - var internal = e.within(this.el, true); - if(!internal){ - this.el.addClass('x-btn-over'); - if(!this.monitoringMouseOver){ - this.doc.on('mouseover', this.monitorMouseOver, this); - this.monitoringMouseOver = true; - } - this.fireEvent('mouseover', this, e); - } - if(this.isMenuTriggerOver(e, internal)){ - this.fireEvent('menutriggerover', this, this.menu, e); - } - } - }, - - - monitorMouseOver : function(e){ - if(e.target != this.el.dom && !e.within(this.el)){ - if(this.monitoringMouseOver){ - this.doc.un('mouseover', this.monitorMouseOver, this); - this.monitoringMouseOver = false; - } - this.onMouseOut(e); - } - }, - - - onMouseOut : function(e){ - var internal = e.within(this.el) && e.target != this.el.dom; - this.el.removeClass('x-btn-over'); - this.fireEvent('mouseout', this, e); - if(this.isMenuTriggerOut(e, internal)){ - this.fireEvent('menutriggerout', this, this.menu, e); - } - }, - - focus : function() { - this.btnEl.focus(); - }, - - blur : function() { - this.btnEl.blur(); - }, - - - onFocus : function(e){ - if(!this.disabled){ - this.el.addClass('x-btn-focus'); - } - }, - - onBlur : function(e){ - this.el.removeClass('x-btn-focus'); - }, - - - getClickEl : function(e, isUp){ - return this.el; - }, - - - onMouseDown : function(e){ - if(!this.disabled && e.button === 0){ - this.getClickEl(e).addClass('x-btn-click'); - this.doc.on('mouseup', this.onMouseUp, this); - } - }, - - onMouseUp : function(e){ - if(e.button === 0){ - this.getClickEl(e, true).removeClass('x-btn-click'); - this.doc.un('mouseup', this.onMouseUp, this); - } - }, - - onMenuShow : function(e){ - if(this.menu.ownerCt == this){ - this.menu.ownerCt = this; - this.ignoreNextClick = 0; - this.el.addClass('x-btn-menu-active'); - this.fireEvent('menushow', this, this.menu); - } - }, - - onMenuHide : function(e){ - if(this.menu.ownerCt == this){ - this.el.removeClass('x-btn-menu-active'); - this.ignoreNextClick = this.restoreClick.defer(250, this); - this.fireEvent('menuhide', this, this.menu); - delete this.menu.ownerCt; - } - }, - - - restoreClick : function(){ - this.ignoreNextClick = 0; - } - - - - - - - -}); -Ext.reg('button', Ext.Button); - - -Ext.ButtonToggleMgr = function(){ - var groups = {}; - - function toggleGroup(btn, state){ - if(state){ - var g = groups[btn.toggleGroup]; - for(var i = 0, l = g.length; i < l; i++){ - if(g[i] != btn){ - g[i].toggle(false); - } - } - } - } - - return { - register : function(btn){ - if(!btn.toggleGroup){ - return; - } - var g = groups[btn.toggleGroup]; - if(!g){ - g = groups[btn.toggleGroup] = []; - } - g.push(btn); - btn.on('toggle', toggleGroup); - }, - - unregister : function(btn){ - if(!btn.toggleGroup){ - return; - } - var g = groups[btn.toggleGroup]; - if(g){ - g.remove(btn); - btn.un('toggle', toggleGroup); - } - }, - - - getPressed : function(group){ - var g = groups[group]; - if(g){ - for(var i = 0, len = g.length; i < len; i++){ - if(g[i].pressed === true){ - return g[i]; - } - } - } - return null; - } - }; -}(); - -Ext.SplitButton = Ext.extend(Ext.Button, { - - arrowSelector : 'em', - split: true, - - - initComponent : function(){ - Ext.SplitButton.superclass.initComponent.call(this); - - this.addEvents("arrowclick"); - }, - - - onRender : function(){ - Ext.SplitButton.superclass.onRender.apply(this, arguments); - if(this.arrowTooltip){ - this.el.child(this.arrowSelector).dom[this.tooltipType] = this.arrowTooltip; - } - }, - - - setArrowHandler : function(handler, scope){ - this.arrowHandler = handler; - this.scope = scope; - }, - - getMenuClass : function(){ - return 'x-btn-split' + (this.arrowAlign == 'bottom' ? '-bottom' : ''); - }, - - isClickOnArrow : function(e){ - if (this.arrowAlign != 'bottom') { - var visBtn = this.el.child('em.x-btn-split'); - var right = visBtn.getRegion().right - visBtn.getPadding('r'); - return e.getPageX() > right; - } else { - return e.getPageY() > this.btnEl.getRegion().bottom; - } - }, - - - onClick : function(e, t){ - e.preventDefault(); - if(!this.disabled){ - if(this.isClickOnArrow(e)){ - if(this.menu && !this.menu.isVisible() && !this.ignoreNextClick){ - this.showMenu(); - } - this.fireEvent("arrowclick", this, e); - if(this.arrowHandler){ - this.arrowHandler.call(this.scope || this, this, e); - } - }else{ - this.doToggle(); - this.fireEvent("click", this, e); - if(this.handler){ - this.handler.call(this.scope || this, this, e); - } - } - } - }, - - - isMenuTriggerOver : function(e){ - return this.menu && e.target.tagName == this.arrowSelector; - }, - - - isMenuTriggerOut : function(e, internal){ - return this.menu && e.target.tagName != this.arrowSelector; - } -}); - -Ext.reg('splitbutton', Ext.SplitButton); -Ext.CycleButton = Ext.extend(Ext.SplitButton, { - - - - - - - - - getItemText : function(item){ - if(item && this.showText === true){ - var text = ''; - if(this.prependText){ - text += this.prependText; - } - text += item.text; - return text; - } - return undefined; - }, - - - setActiveItem : function(item, suppressEvent){ - if(!Ext.isObject(item)){ - item = this.menu.getComponent(item); - } - if(item){ - if(!this.rendered){ - this.text = this.getItemText(item); - this.iconCls = item.iconCls; - }else{ - var t = this.getItemText(item); - if(t){ - this.setText(t); - } - this.setIconClass(item.iconCls); - } - this.activeItem = item; - if(!item.checked){ - item.setChecked(true, suppressEvent); - } - if(this.forceIcon){ - this.setIconClass(this.forceIcon); - } - if(!suppressEvent){ - this.fireEvent('change', this, item); - } - } - }, - - - getActiveItem : function(){ - return this.activeItem; - }, - - - initComponent : function(){ - this.addEvents( - - "change" - ); - - if(this.changeHandler){ - this.on('change', this.changeHandler, this.scope||this); - delete this.changeHandler; - } - - this.itemCount = this.items.length; - - this.menu = {cls:'x-cycle-menu', items:[]}; - var checked = 0; - Ext.each(this.items, function(item, i){ - Ext.apply(item, { - group: item.group || this.id, - itemIndex: i, - checkHandler: this.checkHandler, - scope: this, - checked: item.checked || false - }); - this.menu.items.push(item); - if(item.checked){ - checked = i; - } - }, this); - Ext.CycleButton.superclass.initComponent.call(this); - this.on('click', this.toggleSelected, this); - this.setActiveItem(checked, true); - }, - - - checkHandler : function(item, pressed){ - if(pressed){ - this.setActiveItem(item); - } - }, - - - toggleSelected : function(){ - var m = this.menu; - m.render(); - - if(!m.hasLayout){ - m.doLayout(); - } - - var nextIdx, checkItem; - for (var i = 1; i < this.itemCount; i++) { - nextIdx = (this.activeItem.itemIndex + i) % this.itemCount; - - checkItem = m.items.itemAt(nextIdx); - - if (!checkItem.disabled) { - checkItem.setChecked(true); - break; - } - } - } -}); -Ext.reg('cycle', Ext.CycleButton); -Ext.Toolbar = function(config){ - if(Ext.isArray(config)){ - config = {items: config, layout: 'toolbar'}; - } else { - config = Ext.apply({ - layout: 'toolbar' - }, config); - if(config.buttons) { - config.items = config.buttons; - } - } - Ext.Toolbar.superclass.constructor.call(this, config); -}; - -(function(){ - -var T = Ext.Toolbar; - -Ext.extend(T, Ext.Container, { - - defaultType: 'button', - - - - enableOverflow : false, - - - - - trackMenus : true, - internalDefaults: {removeMode: 'container', hideParent: true}, - toolbarCls: 'x-toolbar', - - initComponent : function(){ - T.superclass.initComponent.call(this); - - - this.addEvents('overflowchange'); - }, - - - onRender : function(ct, position){ - if(!this.el){ - if(!this.autoCreate){ - this.autoCreate = { - cls: this.toolbarCls + ' x-small-editor' - }; - } - this.el = ct.createChild(Ext.apply({ id: this.id },this.autoCreate), position); - Ext.Toolbar.superclass.onRender.apply(this, arguments); - } - }, - - - - - lookupComponent : function(c){ - if(Ext.isString(c)){ - if(c == '-'){ - c = new T.Separator(); - }else if(c == ' '){ - c = new T.Spacer(); - }else if(c == '->'){ - c = new T.Fill(); - }else{ - c = new T.TextItem(c); - } - this.applyDefaults(c); - }else{ - if(c.isFormField || c.render){ - c = this.createComponent(c); - }else if(c.tag){ - c = new T.Item({autoEl: c}); - }else if(c.tagName){ - c = new T.Item({el:c}); - }else if(Ext.isObject(c)){ - c = c.xtype ? this.createComponent(c) : this.constructButton(c); - } - } - return c; - }, - - - applyDefaults : function(c){ - if(!Ext.isString(c)){ - c = Ext.Toolbar.superclass.applyDefaults.call(this, c); - var d = this.internalDefaults; - if(c.events){ - Ext.applyIf(c.initialConfig, d); - Ext.apply(c, d); - }else{ - Ext.applyIf(c, d); - } - } - return c; - }, - - - addSeparator : function(){ - return this.add(new T.Separator()); - }, - - - addSpacer : function(){ - return this.add(new T.Spacer()); - }, - - - addFill : function(){ - this.add(new T.Fill()); - }, - - - addElement : function(el){ - return this.addItem(new T.Item({el:el})); - }, - - - addItem : function(item){ - return this.add.apply(this, arguments); - }, - - - addButton : function(config){ - if(Ext.isArray(config)){ - var buttons = []; - for(var i = 0, len = config.length; i < len; i++) { - buttons.push(this.addButton(config[i])); - } - return buttons; - } - return this.add(this.constructButton(config)); - }, - - - addText : function(text){ - return this.addItem(new T.TextItem(text)); - }, - - - addDom : function(config){ - return this.add(new T.Item({autoEl: config})); - }, - - - addField : function(field){ - return this.add(field); - }, - - - insertButton : function(index, item){ - if(Ext.isArray(item)){ - var buttons = []; - for(var i = 0, len = item.length; i < len; i++) { - buttons.push(this.insertButton(index + i, item[i])); - } - return buttons; - } - return Ext.Toolbar.superclass.insert.call(this, index, item); - }, - - - trackMenu : function(item, remove){ - if(this.trackMenus && item.menu){ - var method = remove ? 'mun' : 'mon'; - this[method](item, 'menutriggerover', this.onButtonTriggerOver, this); - this[method](item, 'menushow', this.onButtonMenuShow, this); - this[method](item, 'menuhide', this.onButtonMenuHide, this); - } - }, - - - constructButton : function(item){ - var b = item.events ? item : this.createComponent(item, item.split ? 'splitbutton' : this.defaultType); - return b; - }, - - - onAdd : function(c){ - Ext.Toolbar.superclass.onAdd.call(this); - this.trackMenu(c); - if(this.disabled){ - c.disable(); - } - }, - - - onRemove : function(c){ - Ext.Toolbar.superclass.onRemove.call(this); - if (c == this.activeMenuBtn) { - delete this.activeMenuBtn; - } - this.trackMenu(c, true); - }, - - - onDisable : function(){ - this.items.each(function(item){ - if(item.disable){ - item.disable(); - } - }); - }, - - - onEnable : function(){ - this.items.each(function(item){ - if(item.enable){ - item.enable(); - } - }); - }, - - - onButtonTriggerOver : function(btn){ - if(this.activeMenuBtn && this.activeMenuBtn != btn){ - this.activeMenuBtn.hideMenu(); - btn.showMenu(); - this.activeMenuBtn = btn; - } - }, - - - onButtonMenuShow : function(btn){ - this.activeMenuBtn = btn; - }, - - - onButtonMenuHide : function(btn){ - delete this.activeMenuBtn; - } -}); -Ext.reg('toolbar', Ext.Toolbar); - - -T.Item = Ext.extend(Ext.BoxComponent, { - hideParent: true, - enable:Ext.emptyFn, - disable:Ext.emptyFn, - focus:Ext.emptyFn - -}); -Ext.reg('tbitem', T.Item); - - -T.Separator = Ext.extend(T.Item, { - onRender : function(ct, position){ - this.el = ct.createChild({tag:'span', cls:'xtb-sep'}, position); - } -}); -Ext.reg('tbseparator', T.Separator); - - -T.Spacer = Ext.extend(T.Item, { - - - onRender : function(ct, position){ - this.el = ct.createChild({tag:'div', cls:'xtb-spacer', style: this.width?'width:'+this.width+'px':''}, position); - } -}); -Ext.reg('tbspacer', T.Spacer); - - -T.Fill = Ext.extend(T.Item, { - - render : Ext.emptyFn, - isFill : true -}); -Ext.reg('tbfill', T.Fill); - - -T.TextItem = Ext.extend(T.Item, { - - - constructor: function(config){ - T.TextItem.superclass.constructor.call(this, Ext.isString(config) ? {text: config} : config); - }, - - - onRender : function(ct, position) { - this.autoEl = {cls: 'xtb-text', html: this.text || ''}; - T.TextItem.superclass.onRender.call(this, ct, position); - }, - - - setText : function(t) { - if(this.rendered){ - this.el.update(t); - }else{ - this.text = t; - } - } -}); -Ext.reg('tbtext', T.TextItem); - - -T.Button = Ext.extend(Ext.Button, {}); -T.SplitButton = Ext.extend(Ext.SplitButton, {}); -Ext.reg('tbbutton', T.Button); -Ext.reg('tbsplit', T.SplitButton); - -})(); - -Ext.ButtonGroup = Ext.extend(Ext.Panel, { - - - baseCls: 'x-btn-group', - - layout:'table', - defaultType: 'button', - - frame: true, - internalDefaults: {removeMode: 'container', hideParent: true}, - - initComponent : function(){ - this.layoutConfig = this.layoutConfig || {}; - Ext.applyIf(this.layoutConfig, { - columns : this.columns - }); - if(!this.title){ - this.addClass('x-btn-group-notitle'); - } - this.on('afterlayout', this.onAfterLayout, this); - Ext.ButtonGroup.superclass.initComponent.call(this); - }, - - applyDefaults : function(c){ - c = Ext.ButtonGroup.superclass.applyDefaults.call(this, c); - var d = this.internalDefaults; - if(c.events){ - Ext.applyIf(c.initialConfig, d); - Ext.apply(c, d); - }else{ - Ext.applyIf(c, d); - } - return c; - }, - - onAfterLayout : function(){ - var bodyWidth = this.body.getFrameWidth('lr') + this.body.dom.firstChild.offsetWidth; - this.body.setWidth(bodyWidth); - this.el.setWidth(bodyWidth + this.getFrameWidth()); - } - -}); - -Ext.reg('buttongroup', Ext.ButtonGroup); - -(function() { - -var T = Ext.Toolbar; - -Ext.PagingToolbar = Ext.extend(Ext.Toolbar, { - - - - pageSize : 20, - - - displayMsg : 'Displaying {0} - {1} of {2}', - - emptyMsg : 'No data to display', - - beforePageText : 'Page', - - afterPageText : 'of {0}', - - firstText : 'First Page', - - prevText : 'Previous Page', - - nextText : 'Next Page', - - lastText : 'Last Page', - - refreshText : 'Refresh', - - - - - - - - initComponent : function(){ - var pagingItems = [this.first = new T.Button({ - tooltip: this.firstText, - overflowText: this.firstText, - iconCls: 'x-tbar-page-first', - disabled: true, - handler: this.moveFirst, - scope: this - }), this.prev = new T.Button({ - tooltip: this.prevText, - overflowText: this.prevText, - iconCls: 'x-tbar-page-prev', - disabled: true, - handler: this.movePrevious, - scope: this - }), '-', this.beforePageText, - this.inputItem = new Ext.form.NumberField({ - cls: 'x-tbar-page-number', - allowDecimals: false, - allowNegative: false, - enableKeyEvents: true, - selectOnFocus: true, - submitValue: false, - listeners: { - scope: this, - keydown: this.onPagingKeyDown, - blur: this.onPagingBlur - } - }), this.afterTextItem = new T.TextItem({ - text: String.format(this.afterPageText, 1) - }), '-', this.next = new T.Button({ - tooltip: this.nextText, - overflowText: this.nextText, - iconCls: 'x-tbar-page-next', - disabled: true, - handler: this.moveNext, - scope: this - }), this.last = new T.Button({ - tooltip: this.lastText, - overflowText: this.lastText, - iconCls: 'x-tbar-page-last', - disabled: true, - handler: this.moveLast, - scope: this - }), '-', this.refresh = new T.Button({ - tooltip: this.refreshText, - overflowText: this.refreshText, - iconCls: 'x-tbar-loading', - handler: this.doRefresh, - scope: this - })]; - - - var userItems = this.items || this.buttons || []; - if (this.prependButtons) { - this.items = userItems.concat(pagingItems); - }else{ - this.items = pagingItems.concat(userItems); - } - delete this.buttons; - if(this.displayInfo){ - this.items.push('->'); - this.items.push(this.displayItem = new T.TextItem({})); - } - Ext.PagingToolbar.superclass.initComponent.call(this); - this.addEvents( - - 'change', - - 'beforechange' - ); - this.on('afterlayout', this.onFirstLayout, this, {single: true}); - this.cursor = 0; - this.bindStore(this.store, true); - }, - - - onFirstLayout : function(){ - if(this.dsLoaded){ - this.onLoad.apply(this, this.dsLoaded); - } - }, - - - updateInfo : function(){ - if(this.displayItem){ - var count = this.store.getCount(); - var msg = count == 0 ? - this.emptyMsg : - String.format( - this.displayMsg, - this.cursor+1, this.cursor+count, this.store.getTotalCount() - ); - this.displayItem.setText(msg); - } - }, - - - onLoad : function(store, r, o){ - if(!this.rendered){ - this.dsLoaded = [store, r, o]; - return; - } - var p = this.getParams(); - this.cursor = (o.params && o.params[p.start]) ? o.params[p.start] : 0; - var d = this.getPageData(), ap = d.activePage, ps = d.pages; - - this.afterTextItem.setText(String.format(this.afterPageText, d.pages)); - this.inputItem.setValue(ap); - this.first.setDisabled(ap == 1); - this.prev.setDisabled(ap == 1); - this.next.setDisabled(ap == ps); - this.last.setDisabled(ap == ps); - this.refresh.enable(); - this.updateInfo(); - this.fireEvent('change', this, d); - }, - - - getPageData : function(){ - var total = this.store.getTotalCount(); - return { - total : total, - activePage : Math.ceil((this.cursor+this.pageSize)/this.pageSize), - pages : total < this.pageSize ? 1 : Math.ceil(total/this.pageSize) - }; - }, - - - changePage : function(page){ - this.doLoad(((page-1) * this.pageSize).constrain(0, this.store.getTotalCount())); - }, - - - onLoadError : function(){ - if(!this.rendered){ - return; - } - this.refresh.enable(); - }, - - - readPage : function(d){ - var v = this.inputItem.getValue(), pageNum; - if (!v || isNaN(pageNum = parseInt(v, 10))) { - this.inputItem.setValue(d.activePage); - return false; - } - return pageNum; - }, - - onPagingFocus : function(){ - this.inputItem.select(); - }, - - - onPagingBlur : function(e){ - this.inputItem.setValue(this.getPageData().activePage); - }, - - - onPagingKeyDown : function(field, e){ - var k = e.getKey(), d = this.getPageData(), pageNum; - if (k == e.RETURN) { - e.stopEvent(); - pageNum = this.readPage(d); - if(pageNum !== false){ - pageNum = Math.min(Math.max(1, pageNum), d.pages) - 1; - this.doLoad(pageNum * this.pageSize); - } - }else if (k == e.HOME || k == e.END){ - e.stopEvent(); - pageNum = k == e.HOME ? 1 : d.pages; - field.setValue(pageNum); - }else if (k == e.UP || k == e.PAGEUP || k == e.DOWN || k == e.PAGEDOWN){ - e.stopEvent(); - if((pageNum = this.readPage(d))){ - var increment = e.shiftKey ? 10 : 1; - if(k == e.DOWN || k == e.PAGEDOWN){ - increment *= -1; - } - pageNum += increment; - if(pageNum >= 1 & pageNum <= d.pages){ - field.setValue(pageNum); - } - } - } - }, - - - getParams : function(){ - - return this.paramNames || this.store.paramNames; - }, - - - beforeLoad : function(){ - if(this.rendered && this.refresh){ - this.refresh.disable(); - } - }, - - - doLoad : function(start){ - var o = {}, pn = this.getParams(); - o[pn.start] = start; - o[pn.limit] = this.pageSize; - if(this.fireEvent('beforechange', this, o) !== false){ - this.store.load({params:o}); - } - }, - - - moveFirst : function(){ - this.doLoad(0); - }, - - - movePrevious : function(){ - this.doLoad(Math.max(0, this.cursor-this.pageSize)); - }, - - - moveNext : function(){ - this.doLoad(this.cursor+this.pageSize); - }, - - - moveLast : function(){ - var total = this.store.getTotalCount(), - extra = total % this.pageSize; - - this.doLoad(extra ? (total - extra) : total - this.pageSize); - }, - - - doRefresh : function(){ - this.doLoad(this.cursor); - }, - - - bindStore : function(store, initial){ - var doLoad; - if(!initial && this.store){ - if(store !== this.store && this.store.autoDestroy){ - this.store.destroy(); - }else{ - this.store.un('beforeload', this.beforeLoad, this); - this.store.un('load', this.onLoad, this); - this.store.un('exception', this.onLoadError, this); - } - if(!store){ - this.store = null; - } - } - if(store){ - store = Ext.StoreMgr.lookup(store); - store.on({ - scope: this, - beforeload: this.beforeLoad, - load: this.onLoad, - exception: this.onLoadError - }); - doLoad = true; - } - this.store = store; - if(doLoad){ - this.onLoad(store, null, {}); - } - }, - - - unbind : function(store){ - this.bindStore(null); - }, - - - bind : function(store){ - this.bindStore(store); - }, - - - onDestroy : function(){ - this.bindStore(null); - Ext.PagingToolbar.superclass.onDestroy.call(this); - } -}); - -})(); -Ext.reg('paging', Ext.PagingToolbar); -Ext.History = (function () { - var iframe, hiddenField; - var ready = false; - var currentToken; - - function getHash() { - var href = location.href, i = href.indexOf("#"), - hash = i >= 0 ? href.substr(i + 1) : null; - - if (Ext.isGecko) { - hash = decodeURIComponent(hash); - } - return hash; - } - - function doSave() { - hiddenField.value = currentToken; - } - - function handleStateChange(token) { - currentToken = token; - Ext.History.fireEvent('change', token); - } - - function updateIFrame (token) { - var html = ['
        ',Ext.util.Format.htmlEncode(token),'
        '].join(''); - try { - var doc = iframe.contentWindow.document; - doc.open(); - doc.write(html); - doc.close(); - return true; - } catch (e) { - return false; - } - } - - function checkIFrame() { - if (!iframe.contentWindow || !iframe.contentWindow.document) { - setTimeout(checkIFrame, 10); - return; - } - - var doc = iframe.contentWindow.document; - var elem = doc.getElementById("state"); - var token = elem ? elem.innerText : null; - - var hash = getHash(); - - setInterval(function () { - - doc = iframe.contentWindow.document; - elem = doc.getElementById("state"); - - var newtoken = elem ? elem.innerText : null; - - var newHash = getHash(); - - if (newtoken !== token) { - token = newtoken; - handleStateChange(token); - location.hash = token; - hash = token; - doSave(); - } else if (newHash !== hash) { - hash = newHash; - updateIFrame(newHash); - } - - }, 50); - - ready = true; - - Ext.History.fireEvent('ready', Ext.History); - } - - function startUp() { - currentToken = hiddenField.value ? hiddenField.value : getHash(); - - if (Ext.isIE) { - checkIFrame(); - } else { - var hash = getHash(); - setInterval(function () { - var newHash = getHash(); - if (newHash !== hash) { - hash = newHash; - handleStateChange(hash); - doSave(); - } - }, 50); - ready = true; - Ext.History.fireEvent('ready', Ext.History); - } - } - - return { - - fieldId: 'x-history-field', - - iframeId: 'x-history-frame', - - events:{}, - - - init: function (onReady, scope) { - if(ready) { - Ext.callback(onReady, scope, [this]); - return; - } - if(!Ext.isReady){ - Ext.onReady(function(){ - Ext.History.init(onReady, scope); - }); - return; - } - hiddenField = Ext.getDom(Ext.History.fieldId); - if (Ext.isIE) { - iframe = Ext.getDom(Ext.History.iframeId); - } - this.addEvents( - - 'ready', - - 'change' - ); - if(onReady){ - this.on('ready', onReady, scope, {single:true}); - } - startUp(); - }, - - - add: function (token, preventDup) { - if(preventDup !== false){ - if(this.getToken() == token){ - return true; - } - } - if (Ext.isIE) { - return updateIFrame(token); - } else { - location.hash = token; - return true; - } - }, - - - back: function(){ - history.go(-1); - }, - - - forward: function(){ - history.go(1); - }, - - - getToken: function() { - return ready ? currentToken : getHash(); - } - }; -})(); -Ext.apply(Ext.History, new Ext.util.Observable()); -Ext.Tip = Ext.extend(Ext.Panel, { - - - - minWidth : 40, - - maxWidth : 300, - - shadow : "sides", - - defaultAlign : "tl-bl?", - autoRender: true, - quickShowInterval : 250, - - - frame:true, - hidden:true, - baseCls: 'x-tip', - floating:{shadow:true,shim:true,useDisplay:true,constrain:false}, - autoHeight:true, - - closeAction: 'hide', - - - initComponent : function(){ - Ext.Tip.superclass.initComponent.call(this); - if(this.closable && !this.title){ - this.elements += ',header'; - } - }, - - - afterRender : function(){ - Ext.Tip.superclass.afterRender.call(this); - if(this.closable){ - this.addTool({ - id: 'close', - handler: this[this.closeAction], - scope: this - }); - } - }, - - - showAt : function(xy){ - Ext.Tip.superclass.show.call(this); - if(this.measureWidth !== false && (!this.initialConfig || typeof this.initialConfig.width != 'number')){ - this.doAutoWidth(); - } - if(this.constrainPosition){ - xy = this.el.adjustForConstraints(xy); - } - this.setPagePosition(xy[0], xy[1]); - }, - - - doAutoWidth : function(adjust){ - adjust = adjust || 0; - var bw = this.body.getTextWidth(); - if(this.title){ - bw = Math.max(bw, this.header.child('span').getTextWidth(this.title)); - } - bw += this.getFrameWidth() + (this.closable ? 20 : 0) + this.body.getPadding("lr") + adjust; - this.setWidth(bw.constrain(this.minWidth, this.maxWidth)); - - - if(Ext.isIE7 && !this.repainted){ - this.el.repaint(); - this.repainted = true; - } - }, - - - showBy : function(el, pos){ - if(!this.rendered){ - this.render(Ext.getBody()); - } - this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign)); - }, - - initDraggable : function(){ - this.dd = new Ext.Tip.DD(this, typeof this.draggable == 'boolean' ? null : this.draggable); - this.header.addClass('x-tip-draggable'); - } -}); - -Ext.reg('tip', Ext.Tip); - - -Ext.Tip.DD = function(tip, config){ - Ext.apply(this, config); - this.tip = tip; - Ext.Tip.DD.superclass.constructor.call(this, tip.el.id, 'WindowDD-'+tip.id); - this.setHandleElId(tip.header.id); - this.scroll = false; -}; - -Ext.extend(Ext.Tip.DD, Ext.dd.DD, { - moveOnly:true, - scroll:false, - headerOffsets:[100, 25], - startDrag : function(){ - this.tip.el.disableShadow(); - }, - endDrag : function(e){ - this.tip.el.enableShadow(true); - } -}); -Ext.ToolTip = Ext.extend(Ext.Tip, { - - - - - showDelay : 500, - - hideDelay : 200, - - dismissDelay : 5000, - - - trackMouse : false, - - anchorToTarget : true, - - anchorOffset : 0, - - - - targetCounter : 0, - - constrainPosition : false, - - - initComponent : function(){ - Ext.ToolTip.superclass.initComponent.call(this); - this.lastActive = new Date(); - this.initTarget(this.target); - this.origAnchor = this.anchor; - }, - - - onRender : function(ct, position){ - Ext.ToolTip.superclass.onRender.call(this, ct, position); - this.anchorCls = 'x-tip-anchor-' + this.getAnchorPosition(); - this.anchorEl = this.el.createChild({ - cls: 'x-tip-anchor ' + this.anchorCls - }); - }, - - - afterRender : function(){ - Ext.ToolTip.superclass.afterRender.call(this); - this.anchorEl.setStyle('z-index', this.el.getZIndex() + 1).setVisibilityMode(Ext.Element.DISPLAY); - }, - - - initTarget : function(target){ - var t; - if((t = Ext.get(target))){ - if(this.target){ - var tg = Ext.get(this.target); - this.mun(tg, 'mouseover', this.onTargetOver, this); - this.mun(tg, 'mouseout', this.onTargetOut, this); - this.mun(tg, 'mousemove', this.onMouseMove, this); - } - this.mon(t, { - mouseover: this.onTargetOver, - mouseout: this.onTargetOut, - mousemove: this.onMouseMove, - scope: this - }); - this.target = t; - } - if(this.anchor){ - this.anchorTarget = this.target; - } - }, - - - onMouseMove : function(e){ - var t = this.delegate ? e.getTarget(this.delegate) : this.triggerElement = true; - if (t) { - this.targetXY = e.getXY(); - if (t === this.triggerElement) { - if(!this.hidden && this.trackMouse){ - this.setPagePosition(this.getTargetXY()); - } - } else { - this.hide(); - this.lastActive = new Date(0); - this.onTargetOver(e); - } - } else if (!this.closable && this.isVisible()) { - this.hide(); - } - }, - - - getTargetXY : function(){ - if(this.delegate){ - this.anchorTarget = this.triggerElement; - } - if(this.anchor){ - this.targetCounter++; - var offsets = this.getOffsets(), - xy = (this.anchorToTarget && !this.trackMouse) ? this.el.getAlignToXY(this.anchorTarget, this.getAnchorAlign()) : this.targetXY, - dw = Ext.lib.Dom.getViewWidth() - 5, - dh = Ext.lib.Dom.getViewHeight() - 5, - de = document.documentElement, - bd = document.body, - scrollX = (de.scrollLeft || bd.scrollLeft || 0) + 5, - scrollY = (de.scrollTop || bd.scrollTop || 0) + 5, - axy = [xy[0] + offsets[0], xy[1] + offsets[1]], - sz = this.getSize(); - - this.anchorEl.removeClass(this.anchorCls); - - if(this.targetCounter < 2){ - if(axy[0] < scrollX){ - if(this.anchorToTarget){ - this.defaultAlign = 'l-r'; - if(this.mouseOffset){this.mouseOffset[0] *= -1;} - } - this.anchor = 'left'; - return this.getTargetXY(); - } - if(axy[0]+sz.width > dw){ - if(this.anchorToTarget){ - this.defaultAlign = 'r-l'; - if(this.mouseOffset){this.mouseOffset[0] *= -1;} - } - this.anchor = 'right'; - return this.getTargetXY(); - } - if(axy[1] < scrollY){ - if(this.anchorToTarget){ - this.defaultAlign = 't-b'; - if(this.mouseOffset){this.mouseOffset[1] *= -1;} - } - this.anchor = 'top'; - return this.getTargetXY(); - } - if(axy[1]+sz.height > dh){ - if(this.anchorToTarget){ - this.defaultAlign = 'b-t'; - if(this.mouseOffset){this.mouseOffset[1] *= -1;} - } - this.anchor = 'bottom'; - return this.getTargetXY(); - } - } - - this.anchorCls = 'x-tip-anchor-'+this.getAnchorPosition(); - this.anchorEl.addClass(this.anchorCls); - this.targetCounter = 0; - return axy; - }else{ - var mouseOffset = this.getMouseOffset(); - return [this.targetXY[0]+mouseOffset[0], this.targetXY[1]+mouseOffset[1]]; - } - }, - - getMouseOffset : function(){ - var offset = this.anchor ? [0,0] : [15,18]; - if(this.mouseOffset){ - offset[0] += this.mouseOffset[0]; - offset[1] += this.mouseOffset[1]; - } - return offset; - }, - - - getAnchorPosition : function(){ - if(this.anchor){ - this.tipAnchor = this.anchor.charAt(0); - }else{ - var m = this.defaultAlign.match(/^([a-z]+)-([a-z]+)(\?)?$/); - if(!m){ - throw 'AnchorTip.defaultAlign is invalid'; - } - this.tipAnchor = m[1].charAt(0); - } - - switch(this.tipAnchor){ - case 't': return 'top'; - case 'b': return 'bottom'; - case 'r': return 'right'; - } - return 'left'; - }, - - - getAnchorAlign : function(){ - switch(this.anchor){ - case 'top' : return 'tl-bl'; - case 'left' : return 'tl-tr'; - case 'right': return 'tr-tl'; - default : return 'bl-tl'; - } - }, - - - getOffsets : function(){ - var offsets, - ap = this.getAnchorPosition().charAt(0); - if(this.anchorToTarget && !this.trackMouse){ - switch(ap){ - case 't': - offsets = [0, 9]; - break; - case 'b': - offsets = [0, -13]; - break; - case 'r': - offsets = [-13, 0]; - break; - default: - offsets = [9, 0]; - break; - } - }else{ - switch(ap){ - case 't': - offsets = [-15-this.anchorOffset, 30]; - break; - case 'b': - offsets = [-19-this.anchorOffset, -13-this.el.dom.offsetHeight]; - break; - case 'r': - offsets = [-15-this.el.dom.offsetWidth, -13-this.anchorOffset]; - break; - default: - offsets = [25, -13-this.anchorOffset]; - break; - } - } - var mouseOffset = this.getMouseOffset(); - offsets[0] += mouseOffset[0]; - offsets[1] += mouseOffset[1]; - - return offsets; - }, - - - onTargetOver : function(e){ - if(this.disabled || e.within(this.target.dom, true)){ - return; - } - var t = e.getTarget(this.delegate); - if (t) { - this.triggerElement = t; - this.clearTimer('hide'); - this.targetXY = e.getXY(); - this.delayShow(); - } - }, - - - delayShow : function(){ - if(this.hidden && !this.showTimer){ - if(this.lastActive.getElapsed() < this.quickShowInterval){ - this.show(); - }else{ - this.showTimer = this.show.defer(this.showDelay, this); - } - }else if(!this.hidden && this.autoHide !== false){ - this.show(); - } - }, - - - onTargetOut : function(e){ - if(this.disabled || e.within(this.target.dom, true)){ - return; - } - this.clearTimer('show'); - if(this.autoHide !== false){ - this.delayHide(); - } - }, - - - delayHide : function(){ - if(!this.hidden && !this.hideTimer){ - this.hideTimer = this.hide.defer(this.hideDelay, this); - } - }, - - - hide: function(){ - this.clearTimer('dismiss'); - this.lastActive = new Date(); - if(this.anchorEl){ - this.anchorEl.hide(); - } - Ext.ToolTip.superclass.hide.call(this); - delete this.triggerElement; - }, - - - show : function(){ - if(this.anchor){ - - - this.showAt([-1000,-1000]); - this.origConstrainPosition = this.constrainPosition; - this.constrainPosition = false; - this.anchor = this.origAnchor; - } - this.showAt(this.getTargetXY()); - - if(this.anchor){ - this.anchorEl.show(); - this.syncAnchor(); - this.constrainPosition = this.origConstrainPosition; - }else{ - this.anchorEl.hide(); - } - }, - - - showAt : function(xy){ - this.lastActive = new Date(); - this.clearTimers(); - Ext.ToolTip.superclass.showAt.call(this, xy); - if(this.dismissDelay && this.autoHide !== false){ - this.dismissTimer = this.hide.defer(this.dismissDelay, this); - } - if(this.anchor && !this.anchorEl.isVisible()){ - this.syncAnchor(); - this.anchorEl.show(); - }else{ - this.anchorEl.hide(); - } - }, - - - syncAnchor : function(){ - var anchorPos, targetPos, offset; - switch(this.tipAnchor.charAt(0)){ - case 't': - anchorPos = 'b'; - targetPos = 'tl'; - offset = [20+this.anchorOffset, 2]; - break; - case 'r': - anchorPos = 'l'; - targetPos = 'tr'; - offset = [-2, 11+this.anchorOffset]; - break; - case 'b': - anchorPos = 't'; - targetPos = 'bl'; - offset = [20+this.anchorOffset, -2]; - break; - default: - anchorPos = 'r'; - targetPos = 'tl'; - offset = [2, 11+this.anchorOffset]; - break; - } - this.anchorEl.alignTo(this.el, anchorPos+'-'+targetPos, offset); - }, - - - setPagePosition : function(x, y){ - Ext.ToolTip.superclass.setPagePosition.call(this, x, y); - if(this.anchor){ - this.syncAnchor(); - } - }, - - - clearTimer : function(name){ - name = name + 'Timer'; - clearTimeout(this[name]); - delete this[name]; - }, - - - clearTimers : function(){ - this.clearTimer('show'); - this.clearTimer('dismiss'); - this.clearTimer('hide'); - }, - - - onShow : function(){ - Ext.ToolTip.superclass.onShow.call(this); - Ext.getDoc().on('mousedown', this.onDocMouseDown, this); - }, - - - onHide : function(){ - Ext.ToolTip.superclass.onHide.call(this); - Ext.getDoc().un('mousedown', this.onDocMouseDown, this); - }, - - - onDocMouseDown : function(e){ - if(this.autoHide !== true && !this.closable && !e.within(this.el.dom)){ - this.disable(); - this.doEnable.defer(100, this); - } - }, - - - doEnable : function(){ - if(!this.isDestroyed){ - this.enable(); - } - }, - - - onDisable : function(){ - this.clearTimers(); - this.hide(); - }, - - - adjustPosition : function(x, y){ - if(this.constrainPosition){ - var ay = this.targetXY[1], h = this.getSize().height; - if(y <= ay && (y+h) >= ay){ - y = ay-h-5; - } - } - return {x : x, y: y}; - }, - - beforeDestroy : function(){ - this.clearTimers(); - Ext.destroy(this.anchorEl); - delete this.anchorEl; - delete this.target; - delete this.anchorTarget; - delete this.triggerElement; - Ext.ToolTip.superclass.beforeDestroy.call(this); - }, - - - onDestroy : function(){ - Ext.getDoc().un('mousedown', this.onDocMouseDown, this); - Ext.ToolTip.superclass.onDestroy.call(this); - } -}); - -Ext.reg('tooltip', Ext.ToolTip); -Ext.QuickTip = Ext.extend(Ext.ToolTip, { - - - interceptTitles : false, - - - tagConfig : { - namespace : "ext", - attribute : "qtip", - width : "qwidth", - target : "target", - title : "qtitle", - hide : "hide", - cls : "qclass", - align : "qalign", - anchor : "anchor" - }, - - - initComponent : function(){ - this.target = this.target || Ext.getDoc(); - this.targets = this.targets || {}; - Ext.QuickTip.superclass.initComponent.call(this); - }, - - - register : function(config){ - var cs = Ext.isArray(config) ? config : arguments; - for(var i = 0, len = cs.length; i < len; i++){ - var c = cs[i]; - var target = c.target; - if(target){ - if(Ext.isArray(target)){ - for(var j = 0, jlen = target.length; j < jlen; j++){ - this.targets[Ext.id(target[j])] = c; - } - } else{ - this.targets[Ext.id(target)] = c; - } - } - } - }, - - - unregister : function(el){ - delete this.targets[Ext.id(el)]; - }, - - - cancelShow: function(el){ - var at = this.activeTarget; - el = Ext.get(el).dom; - if(this.isVisible()){ - if(at && at.el == el){ - this.hide(); - } - }else if(at && at.el == el){ - this.clearTimer('show'); - } - }, - - getTipCfg: function(e) { - var t = e.getTarget(), - ttp, - cfg; - if(this.interceptTitles && t.title && Ext.isString(t.title)){ - ttp = t.title; - t.qtip = ttp; - t.removeAttribute("title"); - e.preventDefault(); - }else{ - cfg = this.tagConfig; - ttp = t.qtip || Ext.fly(t).getAttribute(cfg.attribute, cfg.namespace); - } - return ttp; - }, - - - onTargetOver : function(e){ - if(this.disabled){ - return; - } - this.targetXY = e.getXY(); - var t = e.getTarget(); - if(!t || t.nodeType !== 1 || t == document || t == document.body){ - return; - } - if(this.activeTarget && ((t == this.activeTarget.el) || Ext.fly(this.activeTarget.el).contains(t))){ - this.clearTimer('hide'); - this.show(); - return; - } - if(t && this.targets[t.id]){ - this.activeTarget = this.targets[t.id]; - this.activeTarget.el = t; - this.anchor = this.activeTarget.anchor; - if(this.anchor){ - this.anchorTarget = t; - } - this.delayShow(); - return; - } - var ttp, et = Ext.fly(t), cfg = this.tagConfig, ns = cfg.namespace; - if(ttp = this.getTipCfg(e)){ - var autoHide = et.getAttribute(cfg.hide, ns); - this.activeTarget = { - el: t, - text: ttp, - width: et.getAttribute(cfg.width, ns), - autoHide: autoHide != "user" && autoHide !== 'false', - title: et.getAttribute(cfg.title, ns), - cls: et.getAttribute(cfg.cls, ns), - align: et.getAttribute(cfg.align, ns) - - }; - this.anchor = et.getAttribute(cfg.anchor, ns); - if(this.anchor){ - this.anchorTarget = t; - } - this.delayShow(); - } - }, - - - onTargetOut : function(e){ - - - if (this.activeTarget && e.within(this.activeTarget.el) && !this.getTipCfg(e)) { - return; - } - - this.clearTimer('show'); - if(this.autoHide !== false){ - this.delayHide(); - } - }, - - - showAt : function(xy){ - var t = this.activeTarget; - if(t){ - if(!this.rendered){ - this.render(Ext.getBody()); - this.activeTarget = t; - } - if(t.width){ - this.setWidth(t.width); - this.body.setWidth(this.adjustBodyWidth(t.width - this.getFrameWidth())); - this.measureWidth = false; - } else{ - this.measureWidth = true; - } - this.setTitle(t.title || ''); - this.body.update(t.text); - this.autoHide = t.autoHide; - this.dismissDelay = t.dismissDelay || this.dismissDelay; - if(this.lastCls){ - this.el.removeClass(this.lastCls); - delete this.lastCls; - } - if(t.cls){ - this.el.addClass(t.cls); - this.lastCls = t.cls; - } - if(this.anchor){ - this.constrainPosition = false; - }else if(t.align){ - xy = this.el.getAlignToXY(t.el, t.align); - this.constrainPosition = false; - }else{ - this.constrainPosition = true; - } - } - Ext.QuickTip.superclass.showAt.call(this, xy); - }, - - - hide: function(){ - delete this.activeTarget; - Ext.QuickTip.superclass.hide.call(this); - } -}); -Ext.reg('quicktip', Ext.QuickTip); -Ext.QuickTips = function(){ - var tip, - disabled = false; - - return { - - init : function(autoRender){ - if(!tip){ - if(!Ext.isReady){ - Ext.onReady(function(){ - Ext.QuickTips.init(autoRender); - }); - return; - } - tip = new Ext.QuickTip({ - elements:'header,body', - disabled: disabled - }); - if(autoRender !== false){ - tip.render(Ext.getBody()); - } - } - }, - - - ddDisable : function(){ - - if(tip && !disabled){ - tip.disable(); - } - }, - - - ddEnable : function(){ - - if(tip && !disabled){ - tip.enable(); - } - }, - - - enable : function(){ - if(tip){ - tip.enable(); - } - disabled = false; - }, - - - disable : function(){ - if(tip){ - tip.disable(); - } - disabled = true; - }, - - - isEnabled : function(){ - return tip !== undefined && !tip.disabled; - }, - - - getQuickTip : function(){ - return tip; - }, - - - register : function(){ - tip.register.apply(tip, arguments); - }, - - - unregister : function(){ - tip.unregister.apply(tip, arguments); - }, - - - tips : function(){ - tip.register.apply(tip, arguments); - } - }; -}(); -Ext.slider.Tip = Ext.extend(Ext.Tip, { - minWidth: 10, - offsets : [0, -10], - - init: function(slider) { - slider.on({ - scope : this, - dragstart: this.onSlide, - drag : this.onSlide, - dragend : this.hide, - destroy : this.destroy - }); - }, - - - onSlide : function(slider, e, thumb) { - this.show(); - this.body.update(this.getText(thumb)); - this.doAutoWidth(); - this.el.alignTo(thumb.el, 'b-t?', this.offsets); - }, - - - getText : function(thumb) { - return String(thumb.value); - } -}); - - -Ext.ux.SliderTip = Ext.slider.Tip; -Ext.tree.TreePanel = Ext.extend(Ext.Panel, { - rootVisible : true, - animate : Ext.enableFx, - lines : true, - enableDD : false, - hlDrop : Ext.enableFx, - pathSeparator : '/', - - - bubbleEvents : [], - - initComponent : function(){ - Ext.tree.TreePanel.superclass.initComponent.call(this); - - if(!this.eventModel){ - this.eventModel = new Ext.tree.TreeEventModel(this); - } - - - var l = this.loader; - if(!l){ - l = new Ext.tree.TreeLoader({ - dataUrl: this.dataUrl, - requestMethod: this.requestMethod - }); - }else if(Ext.isObject(l) && !l.load){ - l = new Ext.tree.TreeLoader(l); - } - this.loader = l; - - this.nodeHash = {}; - - - if(this.root){ - var r = this.root; - delete this.root; - this.setRootNode(r); - } - - - this.addEvents( - - - 'append', - - 'remove', - - 'movenode', - - 'insert', - - 'beforeappend', - - 'beforeremove', - - 'beforemovenode', - - 'beforeinsert', - - - 'beforeload', - - 'load', - - 'textchange', - - 'beforeexpandnode', - - 'beforecollapsenode', - - 'expandnode', - - 'disabledchange', - - 'collapsenode', - - 'beforeclick', - - 'click', - - 'containerclick', - - 'checkchange', - - 'beforedblclick', - - 'dblclick', - - 'containerdblclick', - - 'contextmenu', - - 'containercontextmenu', - - 'beforechildrenrendered', - - 'startdrag', - - 'enddrag', - - 'dragdrop', - - 'beforenodedrop', - - 'nodedrop', - - 'nodedragover' - ); - if(this.singleExpand){ - this.on('beforeexpandnode', this.restrictExpand, this); - } - }, - - - proxyNodeEvent : function(ename, a1, a2, a3, a4, a5, a6){ - if(ename == 'collapse' || ename == 'expand' || ename == 'beforecollapse' || ename == 'beforeexpand' || ename == 'move' || ename == 'beforemove'){ - ename = ename+'node'; - } - - return this.fireEvent(ename, a1, a2, a3, a4, a5, a6); - }, - - - - getRootNode : function(){ - return this.root; - }, - - - setRootNode : function(node){ - this.destroyRoot(); - if(!node.render){ - node = this.loader.createNode(node); - } - this.root = node; - node.ownerTree = this; - node.isRoot = true; - this.registerNode(node); - if(!this.rootVisible){ - var uiP = node.attributes.uiProvider; - node.ui = uiP ? new uiP(node) : new Ext.tree.RootTreeNodeUI(node); - } - if(this.innerCt){ - this.clearInnerCt(); - this.renderRoot(); - } - return node; - }, - - clearInnerCt : function(){ - this.innerCt.update(''); - }, - - - renderRoot : function(){ - this.root.render(); - if(!this.rootVisible){ - this.root.renderChildren(); - } - }, - - - getNodeById : function(id){ - return this.nodeHash[id]; - }, - - - registerNode : function(node){ - this.nodeHash[node.id] = node; - }, - - - unregisterNode : function(node){ - delete this.nodeHash[node.id]; - }, - - - toString : function(){ - return '[Tree'+(this.id?' '+this.id:'')+']'; - }, - - - restrictExpand : function(node){ - var p = node.parentNode; - if(p){ - if(p.expandedChild && p.expandedChild.parentNode == p){ - p.expandedChild.collapse(); - } - p.expandedChild = node; - } - }, - - - getChecked : function(a, startNode){ - startNode = startNode || this.root; - var r = []; - var f = function(){ - if(this.attributes.checked){ - r.push(!a ? this : (a == 'id' ? this.id : this.attributes[a])); - } - }; - startNode.cascade(f); - return r; - }, - - - getLoader : function(){ - return this.loader; - }, - - - expandAll : function(){ - this.root.expand(true); - }, - - - collapseAll : function(){ - this.root.collapse(true); - }, - - - getSelectionModel : function(){ - if(!this.selModel){ - this.selModel = new Ext.tree.DefaultSelectionModel(); - } - return this.selModel; - }, - - - expandPath : function(path, attr, callback){ - if(Ext.isEmpty(path)){ - if(callback){ - callback(false, undefined); - } - return; - } - attr = attr || 'id'; - var keys = path.split(this.pathSeparator); - var curNode = this.root; - if(curNode.attributes[attr] != keys[1]){ - if(callback){ - callback(false, null); - } - return; - } - var index = 1; - var f = function(){ - if(++index == keys.length){ - if(callback){ - callback(true, curNode); - } - return; - } - var c = curNode.findChild(attr, keys[index]); - if(!c){ - if(callback){ - callback(false, curNode); - } - return; - } - curNode = c; - c.expand(false, false, f); - }; - curNode.expand(false, false, f); - }, - - - selectPath : function(path, attr, callback){ - if(Ext.isEmpty(path)){ - if(callback){ - callback(false, undefined); - } - return; - } - attr = attr || 'id'; - var keys = path.split(this.pathSeparator), - v = keys.pop(); - if(keys.length > 1){ - var f = function(success, node){ - if(success && node){ - var n = node.findChild(attr, v); - if(n){ - n.select(); - if(callback){ - callback(true, n); - } - }else if(callback){ - callback(false, n); - } - }else{ - if(callback){ - callback(false, n); - } - } - }; - this.expandPath(keys.join(this.pathSeparator), attr, f); - }else{ - this.root.select(); - if(callback){ - callback(true, this.root); - } - } - }, - - - getTreeEl : function(){ - return this.body; - }, - - - onRender : function(ct, position){ - Ext.tree.TreePanel.superclass.onRender.call(this, ct, position); - this.el.addClass('x-tree'); - this.innerCt = this.body.createChild({tag:'ul', - cls:'x-tree-root-ct ' + - (this.useArrows ? 'x-tree-arrows' : this.lines ? 'x-tree-lines' : 'x-tree-no-lines')}); - }, - - - initEvents : function(){ - Ext.tree.TreePanel.superclass.initEvents.call(this); - - if(this.containerScroll){ - Ext.dd.ScrollManager.register(this.body); - } - if((this.enableDD || this.enableDrop) && !this.dropZone){ - - this.dropZone = new Ext.tree.TreeDropZone(this, this.dropConfig || { - ddGroup: this.ddGroup || 'TreeDD', appendOnly: this.ddAppendOnly === true - }); - } - if((this.enableDD || this.enableDrag) && !this.dragZone){ - - this.dragZone = new Ext.tree.TreeDragZone(this, this.dragConfig || { - ddGroup: this.ddGroup || 'TreeDD', - scroll: this.ddScroll - }); - } - this.getSelectionModel().init(this); - }, - - - afterRender : function(){ - Ext.tree.TreePanel.superclass.afterRender.call(this); - this.renderRoot(); - }, - - beforeDestroy : function(){ - if(this.rendered){ - Ext.dd.ScrollManager.unregister(this.body); - Ext.destroy(this.dropZone, this.dragZone); - } - this.destroyRoot(); - Ext.destroy(this.loader); - this.nodeHash = this.root = this.loader = null; - Ext.tree.TreePanel.superclass.beforeDestroy.call(this); - }, - - - destroyRoot : function(){ - if(this.root && this.root.destroy){ - this.root.destroy(true); - } - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -}); - -Ext.tree.TreePanel.nodeTypes = {}; - -Ext.reg('treepanel', Ext.tree.TreePanel);Ext.tree.TreeEventModel = function(tree){ - this.tree = tree; - this.tree.on('render', this.initEvents, this); -}; - -Ext.tree.TreeEventModel.prototype = { - initEvents : function(){ - var t = this.tree; - - if(t.trackMouseOver !== false){ - t.mon(t.innerCt, { - scope: this, - mouseover: this.delegateOver, - mouseout: this.delegateOut - }); - } - t.mon(t.getTreeEl(), { - scope: this, - click: this.delegateClick, - dblclick: this.delegateDblClick, - contextmenu: this.delegateContextMenu - }); - }, - - getNode : function(e){ - var t; - if(t = e.getTarget('.x-tree-node-el', 10)){ - var id = Ext.fly(t, '_treeEvents').getAttribute('tree-node-id', 'ext'); - if(id){ - return this.tree.getNodeById(id); - } - } - return null; - }, - - getNodeTarget : function(e){ - var t = e.getTarget('.x-tree-node-icon', 1); - if(!t){ - t = e.getTarget('.x-tree-node-el', 6); - } - return t; - }, - - delegateOut : function(e, t){ - if(!this.beforeEvent(e)){ - return; - } - if(e.getTarget('.x-tree-ec-icon', 1)){ - var n = this.getNode(e); - this.onIconOut(e, n); - if(n == this.lastEcOver){ - delete this.lastEcOver; - } - } - if((t = this.getNodeTarget(e)) && !e.within(t, true)){ - this.onNodeOut(e, this.getNode(e)); - } - }, - - delegateOver : function(e, t){ - if(!this.beforeEvent(e)){ - return; - } - if(Ext.isGecko && !this.trackingDoc){ - Ext.getBody().on('mouseover', this.trackExit, this); - this.trackingDoc = true; - } - if(this.lastEcOver){ - this.onIconOut(e, this.lastEcOver); - delete this.lastEcOver; - } - if(e.getTarget('.x-tree-ec-icon', 1)){ - this.lastEcOver = this.getNode(e); - this.onIconOver(e, this.lastEcOver); - } - if(t = this.getNodeTarget(e)){ - this.onNodeOver(e, this.getNode(e)); - } - }, - - trackExit : function(e){ - if(this.lastOverNode){ - if(this.lastOverNode.ui && !e.within(this.lastOverNode.ui.getEl())){ - this.onNodeOut(e, this.lastOverNode); - } - delete this.lastOverNode; - Ext.getBody().un('mouseover', this.trackExit, this); - this.trackingDoc = false; - } - - }, - - delegateClick : function(e, t){ - if(this.beforeEvent(e)){ - if(e.getTarget('input[type=checkbox]', 1)){ - this.onCheckboxClick(e, this.getNode(e)); - }else if(e.getTarget('.x-tree-ec-icon', 1)){ - this.onIconClick(e, this.getNode(e)); - }else if(this.getNodeTarget(e)){ - this.onNodeClick(e, this.getNode(e)); - } - }else{ - this.checkContainerEvent(e, 'click'); - } - }, - - delegateDblClick : function(e, t){ - if(this.beforeEvent(e)){ - if(this.getNodeTarget(e)){ - this.onNodeDblClick(e, this.getNode(e)); - } - }else{ - this.checkContainerEvent(e, 'dblclick'); - } - }, - - delegateContextMenu : function(e, t){ - if(this.beforeEvent(e)){ - if(this.getNodeTarget(e)){ - this.onNodeContextMenu(e, this.getNode(e)); - } - }else{ - this.checkContainerEvent(e, 'contextmenu'); - } - }, - - checkContainerEvent: function(e, type){ - if(this.disabled){ - e.stopEvent(); - return false; - } - this.onContainerEvent(e, type); - }, - - onContainerEvent: function(e, type){ - this.tree.fireEvent('container' + type, this.tree, e); - }, - - onNodeClick : function(e, node){ - node.ui.onClick(e); - }, - - onNodeOver : function(e, node){ - this.lastOverNode = node; - node.ui.onOver(e); - }, - - onNodeOut : function(e, node){ - node.ui.onOut(e); - }, - - onIconOver : function(e, node){ - node.ui.addClass('x-tree-ec-over'); - }, - - onIconOut : function(e, node){ - node.ui.removeClass('x-tree-ec-over'); - }, - - onIconClick : function(e, node){ - node.ui.ecClick(e); - }, - - onCheckboxClick : function(e, node){ - node.ui.onCheckChange(e); - }, - - onNodeDblClick : function(e, node){ - node.ui.onDblClick(e); - }, - - onNodeContextMenu : function(e, node){ - node.ui.onContextMenu(e); - }, - - beforeEvent : function(e){ - var node = this.getNode(e); - if(this.disabled || !node || !node.ui){ - e.stopEvent(); - return false; - } - return true; - }, - - disable: function(){ - this.disabled = true; - }, - - enable: function(){ - this.disabled = false; - } -}; -Ext.tree.DefaultSelectionModel = Ext.extend(Ext.util.Observable, { - - constructor : function(config){ - this.selNode = null; - - this.addEvents( - - 'selectionchange', - - - 'beforeselect' - ); - - Ext.apply(this, config); - Ext.tree.DefaultSelectionModel.superclass.constructor.call(this); - }, - - init : function(tree){ - this.tree = tree; - tree.mon(tree.getTreeEl(), 'keydown', this.onKeyDown, this); - tree.on('click', this.onNodeClick, this); - }, - - onNodeClick : function(node, e){ - this.select(node); - }, - - - select : function(node, selectNextNode){ - - if (!Ext.fly(node.ui.wrap).isVisible() && selectNextNode) { - return selectNextNode.call(this, node); - } - var last = this.selNode; - if(node == last){ - node.ui.onSelectedChange(true); - }else if(this.fireEvent('beforeselect', this, node, last) !== false){ - if(last && last.ui){ - last.ui.onSelectedChange(false); - } - this.selNode = node; - node.ui.onSelectedChange(true); - this.fireEvent('selectionchange', this, node, last); - } - return node; - }, - - - unselect : function(node, silent){ - if(this.selNode == node){ - this.clearSelections(silent); - } - }, - - - clearSelections : function(silent){ - var n = this.selNode; - if(n){ - n.ui.onSelectedChange(false); - this.selNode = null; - if(silent !== true){ - this.fireEvent('selectionchange', this, null); - } - } - return n; - }, - - - getSelectedNode : function(){ - return this.selNode; - }, - - - isSelected : function(node){ - return this.selNode == node; - }, - - - selectPrevious : function( s){ - if(!(s = s || this.selNode || this.lastSelNode)){ - return null; - } - - var ps = s.previousSibling; - if(ps){ - if(!ps.isExpanded() || ps.childNodes.length < 1){ - return this.select(ps, this.selectPrevious); - } else{ - var lc = ps.lastChild; - while(lc && lc.isExpanded() && Ext.fly(lc.ui.wrap).isVisible() && lc.childNodes.length > 0){ - lc = lc.lastChild; - } - return this.select(lc, this.selectPrevious); - } - } else if(s.parentNode && (this.tree.rootVisible || !s.parentNode.isRoot)){ - return this.select(s.parentNode, this.selectPrevious); - } - return null; - }, - - - selectNext : function( s){ - if(!(s = s || this.selNode || this.lastSelNode)){ - return null; - } - - if(s.firstChild && s.isExpanded() && Ext.fly(s.ui.wrap).isVisible()){ - return this.select(s.firstChild, this.selectNext); - }else if(s.nextSibling){ - return this.select(s.nextSibling, this.selectNext); - }else if(s.parentNode){ - var newS = null; - s.parentNode.bubble(function(){ - if(this.nextSibling){ - newS = this.getOwnerTree().selModel.select(this.nextSibling, this.selectNext); - return false; - } - }); - return newS; - } - return null; - }, - - onKeyDown : function(e){ - var s = this.selNode || this.lastSelNode; - - var sm = this; - if(!s){ - return; - } - var k = e.getKey(); - switch(k){ - case e.DOWN: - e.stopEvent(); - this.selectNext(); - break; - case e.UP: - e.stopEvent(); - this.selectPrevious(); - break; - case e.RIGHT: - e.preventDefault(); - if(s.hasChildNodes()){ - if(!s.isExpanded()){ - s.expand(); - }else if(s.firstChild){ - this.select(s.firstChild, e); - } - } - break; - case e.LEFT: - e.preventDefault(); - if(s.hasChildNodes() && s.isExpanded()){ - s.collapse(); - }else if(s.parentNode && (this.tree.rootVisible || s.parentNode != this.tree.getRootNode())){ - this.select(s.parentNode, e); - } - break; - }; - } -}); - - -Ext.tree.MultiSelectionModel = Ext.extend(Ext.util.Observable, { - - constructor : function(config){ - this.selNodes = []; - this.selMap = {}; - this.addEvents( - - 'selectionchange' - ); - Ext.apply(this, config); - Ext.tree.MultiSelectionModel.superclass.constructor.call(this); - }, - - init : function(tree){ - this.tree = tree; - tree.mon(tree.getTreeEl(), 'keydown', this.onKeyDown, this); - tree.on('click', this.onNodeClick, this); - }, - - onNodeClick : function(node, e){ - if(e.ctrlKey && this.isSelected(node)){ - this.unselect(node); - }else{ - this.select(node, e, e.ctrlKey); - } - }, - - - select : function(node, e, keepExisting){ - if(keepExisting !== true){ - this.clearSelections(true); - } - if(this.isSelected(node)){ - this.lastSelNode = node; - return node; - } - this.selNodes.push(node); - this.selMap[node.id] = node; - this.lastSelNode = node; - node.ui.onSelectedChange(true); - this.fireEvent('selectionchange', this, this.selNodes); - return node; - }, - - - unselect : function(node){ - if(this.selMap[node.id]){ - node.ui.onSelectedChange(false); - var sn = this.selNodes; - var index = sn.indexOf(node); - if(index != -1){ - this.selNodes.splice(index, 1); - } - delete this.selMap[node.id]; - this.fireEvent('selectionchange', this, this.selNodes); - } - }, - - - clearSelections : function(suppressEvent){ - var sn = this.selNodes; - if(sn.length > 0){ - for(var i = 0, len = sn.length; i < len; i++){ - sn[i].ui.onSelectedChange(false); - } - this.selNodes = []; - this.selMap = {}; - if(suppressEvent !== true){ - this.fireEvent('selectionchange', this, this.selNodes); - } - } - }, - - - isSelected : function(node){ - return this.selMap[node.id] ? true : false; - }, - - - getSelectedNodes : function(){ - return this.selNodes.concat([]); - }, - - onKeyDown : Ext.tree.DefaultSelectionModel.prototype.onKeyDown, - - selectNext : Ext.tree.DefaultSelectionModel.prototype.selectNext, - - selectPrevious : Ext.tree.DefaultSelectionModel.prototype.selectPrevious -}); -Ext.data.Tree = Ext.extend(Ext.util.Observable, { - - constructor: function(root){ - this.nodeHash = {}; - - this.root = null; - if(root){ - this.setRootNode(root); - } - this.addEvents( - - "append", - - "remove", - - "move", - - "insert", - - "beforeappend", - - "beforeremove", - - "beforemove", - - "beforeinsert" - ); - Ext.data.Tree.superclass.constructor.call(this); - }, - - - pathSeparator: "/", - - - proxyNodeEvent : function(){ - return this.fireEvent.apply(this, arguments); - }, - - - getRootNode : function(){ - return this.root; - }, - - - setRootNode : function(node){ - this.root = node; - node.ownerTree = this; - node.isRoot = true; - this.registerNode(node); - return node; - }, - - - getNodeById : function(id){ - return this.nodeHash[id]; - }, - - - registerNode : function(node){ - this.nodeHash[node.id] = node; - }, - - - unregisterNode : function(node){ - delete this.nodeHash[node.id]; - }, - - toString : function(){ - return "[Tree"+(this.id?" "+this.id:"")+"]"; - } -}); - - -Ext.data.Node = Ext.extend(Ext.util.Observable, { - - constructor: function(attributes){ - - this.attributes = attributes || {}; - this.leaf = this.attributes.leaf; - - this.id = this.attributes.id; - if(!this.id){ - this.id = Ext.id(null, "xnode-"); - this.attributes.id = this.id; - } - - this.childNodes = []; - - this.parentNode = null; - - this.firstChild = null; - - this.lastChild = null; - - this.previousSibling = null; - - this.nextSibling = null; - - this.addEvents({ - - "append" : true, - - "remove" : true, - - "move" : true, - - "insert" : true, - - "beforeappend" : true, - - "beforeremove" : true, - - "beforemove" : true, - - "beforeinsert" : true - }); - this.listeners = this.attributes.listeners; - Ext.data.Node.superclass.constructor.call(this); - }, - - - fireEvent : function(evtName){ - - if(Ext.data.Node.superclass.fireEvent.apply(this, arguments) === false){ - return false; - } - - var ot = this.getOwnerTree(); - if(ot){ - if(ot.proxyNodeEvent.apply(ot, arguments) === false){ - return false; - } - } - return true; - }, - - - isLeaf : function(){ - return this.leaf === true; - }, - - - setFirstChild : function(node){ - this.firstChild = node; - }, - - - setLastChild : function(node){ - this.lastChild = node; - }, - - - - isLast : function(){ - return (!this.parentNode ? true : this.parentNode.lastChild == this); - }, - - - isFirst : function(){ - return (!this.parentNode ? true : this.parentNode.firstChild == this); - }, - - - hasChildNodes : function(){ - return !this.isLeaf() && this.childNodes.length > 0; - }, - - - isExpandable : function(){ - return this.attributes.expandable || this.hasChildNodes(); - }, - - - appendChild : function(node){ - var multi = false; - if(Ext.isArray(node)){ - multi = node; - }else if(arguments.length > 1){ - multi = arguments; - } - - if(multi){ - for(var i = 0, len = multi.length; i < len; i++) { - this.appendChild(multi[i]); - } - }else{ - if(this.fireEvent("beforeappend", this.ownerTree, this, node) === false){ - return false; - } - var index = this.childNodes.length; - var oldParent = node.parentNode; - - if(oldParent){ - if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index) === false){ - return false; - } - oldParent.removeChild(node); - } - index = this.childNodes.length; - if(index === 0){ - this.setFirstChild(node); - } - this.childNodes.push(node); - node.parentNode = this; - var ps = this.childNodes[index-1]; - if(ps){ - node.previousSibling = ps; - ps.nextSibling = node; - }else{ - node.previousSibling = null; - } - node.nextSibling = null; - this.setLastChild(node); - node.setOwnerTree(this.getOwnerTree()); - this.fireEvent("append", this.ownerTree, this, node, index); - if(oldParent){ - node.fireEvent("move", this.ownerTree, node, oldParent, this, index); - } - return node; - } - }, - - - removeChild : function(node, destroy){ - var index = this.childNodes.indexOf(node); - if(index == -1){ - return false; - } - if(this.fireEvent("beforeremove", this.ownerTree, this, node) === false){ - return false; - } - - - this.childNodes.splice(index, 1); - - - if(node.previousSibling){ - node.previousSibling.nextSibling = node.nextSibling; - } - if(node.nextSibling){ - node.nextSibling.previousSibling = node.previousSibling; - } - - - if(this.firstChild == node){ - this.setFirstChild(node.nextSibling); - } - if(this.lastChild == node){ - this.setLastChild(node.previousSibling); - } - - this.fireEvent("remove", this.ownerTree, this, node); - if(destroy){ - node.destroy(true); - }else{ - node.clear(); - } - return node; - }, - - - clear : function(destroy){ - - this.setOwnerTree(null, destroy); - this.parentNode = this.previousSibling = this.nextSibling = null; - if(destroy){ - this.firstChild = this.lastChild = null; - } - }, - - - destroy : function( silent){ - - if(silent === true){ - this.purgeListeners(); - this.clear(true); - Ext.each(this.childNodes, function(n){ - n.destroy(true); - }); - this.childNodes = null; - }else{ - this.remove(true); - } - }, - - - insertBefore : function(node, refNode){ - if(!refNode){ - return this.appendChild(node); - } - - if(node == refNode){ - return false; - } - - if(this.fireEvent("beforeinsert", this.ownerTree, this, node, refNode) === false){ - return false; - } - var index = this.childNodes.indexOf(refNode); - var oldParent = node.parentNode; - var refIndex = index; - - - if(oldParent == this && this.childNodes.indexOf(node) < index){ - refIndex--; - } - - - if(oldParent){ - if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index, refNode) === false){ - return false; - } - oldParent.removeChild(node); - } - if(refIndex === 0){ - this.setFirstChild(node); - } - this.childNodes.splice(refIndex, 0, node); - node.parentNode = this; - var ps = this.childNodes[refIndex-1]; - if(ps){ - node.previousSibling = ps; - ps.nextSibling = node; - }else{ - node.previousSibling = null; - } - node.nextSibling = refNode; - refNode.previousSibling = node; - node.setOwnerTree(this.getOwnerTree()); - this.fireEvent("insert", this.ownerTree, this, node, refNode); - if(oldParent){ - node.fireEvent("move", this.ownerTree, node, oldParent, this, refIndex, refNode); - } - return node; - }, - - - remove : function(destroy){ - if (this.parentNode) { - this.parentNode.removeChild(this, destroy); - } - return this; - }, - - - removeAll : function(destroy){ - var cn = this.childNodes, - n; - while((n = cn[0])){ - this.removeChild(n, destroy); - } - return this; - }, - - - item : function(index){ - return this.childNodes[index]; - }, - - - replaceChild : function(newChild, oldChild){ - var s = oldChild ? oldChild.nextSibling : null; - this.removeChild(oldChild); - this.insertBefore(newChild, s); - return oldChild; - }, - - - indexOf : function(child){ - return this.childNodes.indexOf(child); - }, - - - getOwnerTree : function(){ - - if(!this.ownerTree){ - var p = this; - while(p){ - if(p.ownerTree){ - this.ownerTree = p.ownerTree; - break; - } - p = p.parentNode; - } - } - return this.ownerTree; - }, - - - getDepth : function(){ - var depth = 0; - var p = this; - while(p.parentNode){ - ++depth; - p = p.parentNode; - } - return depth; - }, - - - setOwnerTree : function(tree, destroy){ - - if(tree != this.ownerTree){ - if(this.ownerTree){ - this.ownerTree.unregisterNode(this); - } - this.ownerTree = tree; - - if(destroy !== true){ - Ext.each(this.childNodes, function(n){ - n.setOwnerTree(tree); - }); - } - if(tree){ - tree.registerNode(this); - } - } - }, - - - setId: function(id){ - if(id !== this.id){ - var t = this.ownerTree; - if(t){ - t.unregisterNode(this); - } - this.id = this.attributes.id = id; - if(t){ - t.registerNode(this); - } - this.onIdChange(id); - } - }, - - - onIdChange: Ext.emptyFn, - - - getPath : function(attr){ - attr = attr || "id"; - var p = this.parentNode; - var b = [this.attributes[attr]]; - while(p){ - b.unshift(p.attributes[attr]); - p = p.parentNode; - } - var sep = this.getOwnerTree().pathSeparator; - return sep + b.join(sep); - }, - - - bubble : function(fn, scope, args){ - var p = this; - while(p){ - if(fn.apply(scope || p, args || [p]) === false){ - break; - } - p = p.parentNode; - } - }, - - - cascade : function(fn, scope, args){ - if(fn.apply(scope || this, args || [this]) !== false){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].cascade(fn, scope, args); - } - } - }, - - - eachChild : function(fn, scope, args){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - if(fn.apply(scope || cs[i], args || [cs[i]]) === false){ - break; - } - } - }, - - - findChild : function(attribute, value, deep){ - return this.findChildBy(function(){ - return this.attributes[attribute] == value; - }, null, deep); - }, - - - findChildBy : function(fn, scope, deep){ - var cs = this.childNodes, - len = cs.length, - i = 0, - n, - res; - for(; i < len; i++){ - n = cs[i]; - if(fn.call(scope || n, n) === true){ - return n; - }else if (deep){ - res = n.findChildBy(fn, scope, deep); - if(res != null){ - return res; - } - } - - } - return null; - }, - - - sort : function(fn, scope){ - var cs = this.childNodes; - var len = cs.length; - if(len > 0){ - var sortFn = scope ? function(){fn.apply(scope, arguments);} : fn; - cs.sort(sortFn); - for(var i = 0; i < len; i++){ - var n = cs[i]; - n.previousSibling = cs[i-1]; - n.nextSibling = cs[i+1]; - if(i === 0){ - this.setFirstChild(n); - } - if(i == len-1){ - this.setLastChild(n); - } - } - } - }, - - - contains : function(node){ - return node.isAncestor(this); - }, - - - isAncestor : function(node){ - var p = this.parentNode; - while(p){ - if(p == node){ - return true; - } - p = p.parentNode; - } - return false; - }, - - toString : function(){ - return "[Node"+(this.id?" "+this.id:"")+"]"; - } -}); -Ext.tree.TreeNode = Ext.extend(Ext.data.Node, { - - constructor : function(attributes){ - attributes = attributes || {}; - if(Ext.isString(attributes)){ - attributes = {text: attributes}; - } - this.childrenRendered = false; - this.rendered = false; - Ext.tree.TreeNode.superclass.constructor.call(this, attributes); - this.expanded = attributes.expanded === true; - this.isTarget = attributes.isTarget !== false; - this.draggable = attributes.draggable !== false && attributes.allowDrag !== false; - this.allowChildren = attributes.allowChildren !== false && attributes.allowDrop !== false; - - - this.text = attributes.text; - - this.disabled = attributes.disabled === true; - - this.hidden = attributes.hidden === true; - - this.addEvents( - - 'textchange', - - 'beforeexpand', - - 'beforecollapse', - - 'expand', - - 'disabledchange', - - 'collapse', - - 'beforeclick', - - 'click', - - 'checkchange', - - 'beforedblclick', - - 'dblclick', - - 'contextmenu', - - 'beforechildrenrendered' - ); - - var uiClass = this.attributes.uiProvider || this.defaultUI || Ext.tree.TreeNodeUI; - - - this.ui = new uiClass(this); - }, - - preventHScroll : true, - - isExpanded : function(){ - return this.expanded; - }, - - - getUI : function(){ - return this.ui; - }, - - getLoader : function(){ - var owner; - return this.loader || ((owner = this.getOwnerTree()) && owner.loader ? owner.loader : (this.loader = new Ext.tree.TreeLoader())); - }, - - - setFirstChild : function(node){ - var of = this.firstChild; - Ext.tree.TreeNode.superclass.setFirstChild.call(this, node); - if(this.childrenRendered && of && node != of){ - of.renderIndent(true, true); - } - if(this.rendered){ - this.renderIndent(true, true); - } - }, - - - setLastChild : function(node){ - var ol = this.lastChild; - Ext.tree.TreeNode.superclass.setLastChild.call(this, node); - if(this.childrenRendered && ol && node != ol){ - ol.renderIndent(true, true); - } - if(this.rendered){ - this.renderIndent(true, true); - } - }, - - - - appendChild : function(n){ - if(!n.render && !Ext.isArray(n)){ - n = this.getLoader().createNode(n); - } - var node = Ext.tree.TreeNode.superclass.appendChild.call(this, n); - if(node && this.childrenRendered){ - node.render(); - } - this.ui.updateExpandIcon(); - return node; - }, - - - removeChild : function(node, destroy){ - this.ownerTree.getSelectionModel().unselect(node); - Ext.tree.TreeNode.superclass.removeChild.apply(this, arguments); - - if(!destroy){ - var rendered = node.ui.rendered; - - if(rendered){ - node.ui.remove(); - } - if(rendered && this.childNodes.length < 1){ - this.collapse(false, false); - }else{ - this.ui.updateExpandIcon(); - } - if(!this.firstChild && !this.isHiddenRoot()){ - this.childrenRendered = false; - } - } - return node; - }, - - - insertBefore : function(node, refNode){ - if(!node.render){ - node = this.getLoader().createNode(node); - } - var newNode = Ext.tree.TreeNode.superclass.insertBefore.call(this, node, refNode); - if(newNode && refNode && this.childrenRendered){ - node.render(); - } - this.ui.updateExpandIcon(); - return newNode; - }, - - - setText : function(text){ - var oldText = this.text; - this.text = this.attributes.text = text; - if(this.rendered){ - this.ui.onTextChange(this, text, oldText); - } - this.fireEvent('textchange', this, text, oldText); - }, - - - setIconCls : function(cls){ - var old = this.attributes.iconCls; - this.attributes.iconCls = cls; - if(this.rendered){ - this.ui.onIconClsChange(this, cls, old); - } - }, - - - setTooltip : function(tip, title){ - this.attributes.qtip = tip; - this.attributes.qtipTitle = title; - if(this.rendered){ - this.ui.onTipChange(this, tip, title); - } - }, - - - setIcon : function(icon){ - this.attributes.icon = icon; - if(this.rendered){ - this.ui.onIconChange(this, icon); - } - }, - - - setHref : function(href, target){ - this.attributes.href = href; - this.attributes.hrefTarget = target; - if(this.rendered){ - this.ui.onHrefChange(this, href, target); - } - }, - - - setCls : function(cls){ - var old = this.attributes.cls; - this.attributes.cls = cls; - if(this.rendered){ - this.ui.onClsChange(this, cls, old); - } - }, - - - select : function(){ - var t = this.getOwnerTree(); - if(t){ - t.getSelectionModel().select(this); - } - }, - - - unselect : function(silent){ - var t = this.getOwnerTree(); - if(t){ - t.getSelectionModel().unselect(this, silent); - } - }, - - - isSelected : function(){ - var t = this.getOwnerTree(); - return t ? t.getSelectionModel().isSelected(this) : false; - }, - - - expand : function(deep, anim, callback, scope){ - if(!this.expanded){ - if(this.fireEvent('beforeexpand', this, deep, anim) === false){ - return; - } - if(!this.childrenRendered){ - this.renderChildren(); - } - this.expanded = true; - if(!this.isHiddenRoot() && (this.getOwnerTree().animate && anim !== false) || anim){ - this.ui.animExpand(function(){ - this.fireEvent('expand', this); - this.runCallback(callback, scope || this, [this]); - if(deep === true){ - this.expandChildNodes(true, true); - } - }.createDelegate(this)); - return; - }else{ - this.ui.expand(); - this.fireEvent('expand', this); - this.runCallback(callback, scope || this, [this]); - } - }else{ - this.runCallback(callback, scope || this, [this]); - } - if(deep === true){ - this.expandChildNodes(true); - } - }, - - runCallback : function(cb, scope, args){ - if(Ext.isFunction(cb)){ - cb.apply(scope, args); - } - }, - - isHiddenRoot : function(){ - return this.isRoot && !this.getOwnerTree().rootVisible; - }, - - - collapse : function(deep, anim, callback, scope){ - if(this.expanded && !this.isHiddenRoot()){ - if(this.fireEvent('beforecollapse', this, deep, anim) === false){ - return; - } - this.expanded = false; - if((this.getOwnerTree().animate && anim !== false) || anim){ - this.ui.animCollapse(function(){ - this.fireEvent('collapse', this); - this.runCallback(callback, scope || this, [this]); - if(deep === true){ - this.collapseChildNodes(true); - } - }.createDelegate(this)); - return; - }else{ - this.ui.collapse(); - this.fireEvent('collapse', this); - this.runCallback(callback, scope || this, [this]); - } - }else if(!this.expanded){ - this.runCallback(callback, scope || this, [this]); - } - if(deep === true){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].collapse(true, false); - } - } - }, - - - delayedExpand : function(delay){ - if(!this.expandProcId){ - this.expandProcId = this.expand.defer(delay, this); - } - }, - - - cancelExpand : function(){ - if(this.expandProcId){ - clearTimeout(this.expandProcId); - } - this.expandProcId = false; - }, - - - toggle : function(){ - if(this.expanded){ - this.collapse(); - }else{ - this.expand(); - } - }, - - - ensureVisible : function(callback, scope){ - var tree = this.getOwnerTree(); - tree.expandPath(this.parentNode ? this.parentNode.getPath() : this.getPath(), false, function(){ - var node = tree.getNodeById(this.id); - tree.getTreeEl().scrollChildIntoView(node.ui.anchor); - this.runCallback(callback, scope || this, [this]); - }.createDelegate(this)); - }, - - - expandChildNodes : function(deep, anim) { - var cs = this.childNodes, - i, - len = cs.length; - for (i = 0; i < len; i++) { - cs[i].expand(deep, anim); - } - }, - - - collapseChildNodes : function(deep){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].collapse(deep); - } - }, - - - disable : function(){ - this.disabled = true; - this.unselect(); - if(this.rendered && this.ui.onDisableChange){ - this.ui.onDisableChange(this, true); - } - this.fireEvent('disabledchange', this, true); - }, - - - enable : function(){ - this.disabled = false; - if(this.rendered && this.ui.onDisableChange){ - this.ui.onDisableChange(this, false); - } - this.fireEvent('disabledchange', this, false); - }, - - - renderChildren : function(suppressEvent){ - if(suppressEvent !== false){ - this.fireEvent('beforechildrenrendered', this); - } - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].render(true); - } - this.childrenRendered = true; - }, - - - sort : function(fn, scope){ - Ext.tree.TreeNode.superclass.sort.apply(this, arguments); - if(this.childrenRendered){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].render(true); - } - } - }, - - - render : function(bulkRender){ - this.ui.render(bulkRender); - if(!this.rendered){ - - this.getOwnerTree().registerNode(this); - this.rendered = true; - if(this.expanded){ - this.expanded = false; - this.expand(false, false); - } - } - }, - - - renderIndent : function(deep, refresh){ - if(refresh){ - this.ui.childIndent = null; - } - this.ui.renderIndent(); - if(deep === true && this.childrenRendered){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].renderIndent(true, refresh); - } - } - }, - - beginUpdate : function(){ - this.childrenRendered = false; - }, - - endUpdate : function(){ - if(this.expanded && this.rendered){ - this.renderChildren(); - } - }, - - - destroy : function(silent){ - if(silent === true){ - this.unselect(true); - } - Ext.tree.TreeNode.superclass.destroy.call(this, silent); - Ext.destroy(this.ui, this.loader); - this.ui = this.loader = null; - }, - - - onIdChange : function(id){ - this.ui.onIdChange(id); - } -}); - -Ext.tree.TreePanel.nodeTypes.node = Ext.tree.TreeNode; - Ext.tree.AsyncTreeNode = function(config){ - this.loaded = config && config.loaded === true; - this.loading = false; - Ext.tree.AsyncTreeNode.superclass.constructor.apply(this, arguments); - - this.addEvents('beforeload', 'load'); - - -}; -Ext.extend(Ext.tree.AsyncTreeNode, Ext.tree.TreeNode, { - expand : function(deep, anim, callback, scope){ - if(this.loading){ - var timer; - var f = function(){ - if(!this.loading){ - clearInterval(timer); - this.expand(deep, anim, callback, scope); - } - }.createDelegate(this); - timer = setInterval(f, 200); - return; - } - if(!this.loaded){ - if(this.fireEvent("beforeload", this) === false){ - return; - } - this.loading = true; - this.ui.beforeLoad(this); - var loader = this.loader || this.attributes.loader || this.getOwnerTree().getLoader(); - if(loader){ - loader.load(this, this.loadComplete.createDelegate(this, [deep, anim, callback, scope]), this); - return; - } - } - Ext.tree.AsyncTreeNode.superclass.expand.call(this, deep, anim, callback, scope); - }, - - - isLoading : function(){ - return this.loading; - }, - - loadComplete : function(deep, anim, callback, scope){ - this.loading = false; - this.loaded = true; - this.ui.afterLoad(this); - this.fireEvent("load", this); - this.expand(deep, anim, callback, scope); - }, - - - isLoaded : function(){ - return this.loaded; - }, - - hasChildNodes : function(){ - if(!this.isLeaf() && !this.loaded){ - return true; - }else{ - return Ext.tree.AsyncTreeNode.superclass.hasChildNodes.call(this); - } - }, - - - reload : function(callback, scope){ - this.collapse(false, false); - while(this.firstChild){ - this.removeChild(this.firstChild).destroy(); - } - this.childrenRendered = false; - this.loaded = false; - if(this.isHiddenRoot()){ - this.expanded = false; - } - this.expand(false, false, callback, scope); - } -}); - -Ext.tree.TreePanel.nodeTypes.async = Ext.tree.AsyncTreeNode; -Ext.tree.TreeNodeUI = Ext.extend(Object, { - - constructor : function(node){ - Ext.apply(this, { - node: node, - rendered: false, - animating: false, - wasLeaf: true, - ecc: 'x-tree-ec-icon x-tree-elbow', - emptyIcon: Ext.BLANK_IMAGE_URL - }); - }, - - - removeChild : function(node){ - if(this.rendered){ - this.ctNode.removeChild(node.ui.getEl()); - } - }, - - - beforeLoad : function(){ - this.addClass("x-tree-node-loading"); - }, - - - afterLoad : function(){ - this.removeClass("x-tree-node-loading"); - }, - - - onTextChange : function(node, text, oldText){ - if(this.rendered){ - this.textNode.innerHTML = text; - } - }, - - - onIconClsChange : function(node, cls, oldCls){ - if(this.rendered){ - Ext.fly(this.iconNode).replaceClass(oldCls, cls); - } - }, - - - onIconChange : function(node, icon){ - if(this.rendered){ - - var empty = Ext.isEmpty(icon); - this.iconNode.src = empty ? this.emptyIcon : icon; - Ext.fly(this.iconNode)[empty ? 'removeClass' : 'addClass']('x-tree-node-inline-icon'); - } - }, - - - onTipChange : function(node, tip, title){ - if(this.rendered){ - var hasTitle = Ext.isDefined(title); - if(this.textNode.setAttributeNS){ - this.textNode.setAttributeNS("ext", "qtip", tip); - if(hasTitle){ - this.textNode.setAttributeNS("ext", "qtitle", title); - } - }else{ - this.textNode.setAttribute("ext:qtip", tip); - if(hasTitle){ - this.textNode.setAttribute("ext:qtitle", title); - } - } - } - }, - - - onHrefChange : function(node, href, target){ - if(this.rendered){ - this.anchor.href = this.getHref(href); - if(Ext.isDefined(target)){ - this.anchor.target = target; - } - } - }, - - - onClsChange : function(node, cls, oldCls){ - if(this.rendered){ - Ext.fly(this.elNode).replaceClass(oldCls, cls); - } - }, - - - onDisableChange : function(node, state){ - this.disabled = state; - if (this.checkbox) { - this.checkbox.disabled = state; - } - this[state ? 'addClass' : 'removeClass']('x-tree-node-disabled'); - }, - - - onSelectedChange : function(state){ - if(state){ - this.focus(); - this.addClass("x-tree-selected"); - }else{ - - this.removeClass("x-tree-selected"); - } - }, - - - onMove : function(tree, node, oldParent, newParent, index, refNode){ - this.childIndent = null; - if(this.rendered){ - var targetNode = newParent.ui.getContainer(); - if(!targetNode){ - this.holder = document.createElement("div"); - this.holder.appendChild(this.wrap); - return; - } - var insertBefore = refNode ? refNode.ui.getEl() : null; - if(insertBefore){ - targetNode.insertBefore(this.wrap, insertBefore); - }else{ - targetNode.appendChild(this.wrap); - } - this.node.renderIndent(true, oldParent != newParent); - } - }, - - - addClass : function(cls){ - if(this.elNode){ - Ext.fly(this.elNode).addClass(cls); - } - }, - - - removeClass : function(cls){ - if(this.elNode){ - Ext.fly(this.elNode).removeClass(cls); - } - }, - - - remove : function(){ - if(this.rendered){ - this.holder = document.createElement("div"); - this.holder.appendChild(this.wrap); - } - }, - - - fireEvent : function(){ - return this.node.fireEvent.apply(this.node, arguments); - }, - - - initEvents : function(){ - this.node.on("move", this.onMove, this); - - if(this.node.disabled){ - this.onDisableChange(this.node, true); - } - if(this.node.hidden){ - this.hide(); - } - var ot = this.node.getOwnerTree(); - var dd = ot.enableDD || ot.enableDrag || ot.enableDrop; - if(dd && (!this.node.isRoot || ot.rootVisible)){ - Ext.dd.Registry.register(this.elNode, { - node: this.node, - handles: this.getDDHandles(), - isHandle: false - }); - } - }, - - - getDDHandles : function(){ - return [this.iconNode, this.textNode, this.elNode]; - }, - - - hide : function(){ - this.node.hidden = true; - if(this.wrap){ - this.wrap.style.display = "none"; - } - }, - - - show : function(){ - this.node.hidden = false; - if(this.wrap){ - this.wrap.style.display = ""; - } - }, - - - onContextMenu : function(e){ - if (this.node.hasListener("contextmenu") || this.node.getOwnerTree().hasListener("contextmenu")) { - e.preventDefault(); - this.focus(); - this.fireEvent("contextmenu", this.node, e); - } - }, - - - onClick : function(e){ - if(this.dropping){ - e.stopEvent(); - return; - } - if(this.fireEvent("beforeclick", this.node, e) !== false){ - var a = e.getTarget('a'); - if(!this.disabled && this.node.attributes.href && a){ - this.fireEvent("click", this.node, e); - return; - }else if(a && e.ctrlKey){ - e.stopEvent(); - } - e.preventDefault(); - if(this.disabled){ - return; - } - - if(this.node.attributes.singleClickExpand && !this.animating && this.node.isExpandable()){ - this.node.toggle(); - } - - this.fireEvent("click", this.node, e); - }else{ - e.stopEvent(); - } - }, - - - onDblClick : function(e){ - e.preventDefault(); - if(this.disabled){ - return; - } - if(this.fireEvent("beforedblclick", this.node, e) !== false){ - if(this.checkbox){ - this.toggleCheck(); - } - if(!this.animating && this.node.isExpandable()){ - this.node.toggle(); - } - this.fireEvent("dblclick", this.node, e); - } - }, - - onOver : function(e){ - this.addClass('x-tree-node-over'); - }, - - onOut : function(e){ - this.removeClass('x-tree-node-over'); - }, - - - onCheckChange : function(){ - var checked = this.checkbox.checked; - - this.checkbox.defaultChecked = checked; - this.node.attributes.checked = checked; - this.fireEvent('checkchange', this.node, checked); - }, - - - ecClick : function(e){ - if(!this.animating && this.node.isExpandable()){ - this.node.toggle(); - } - }, - - - startDrop : function(){ - this.dropping = true; - }, - - - endDrop : function(){ - setTimeout(function(){ - this.dropping = false; - }.createDelegate(this), 50); - }, - - - expand : function(){ - this.updateExpandIcon(); - this.ctNode.style.display = ""; - }, - - - focus : function(){ - if(!this.node.preventHScroll){ - try{this.anchor.focus(); - }catch(e){} - }else{ - try{ - var noscroll = this.node.getOwnerTree().getTreeEl().dom; - var l = noscroll.scrollLeft; - this.anchor.focus(); - noscroll.scrollLeft = l; - }catch(e){} - } - }, - - - toggleCheck : function(value){ - var cb = this.checkbox; - if(cb){ - cb.checked = (value === undefined ? !cb.checked : value); - this.onCheckChange(); - } - }, - - - blur : function(){ - try{ - this.anchor.blur(); - }catch(e){} - }, - - - animExpand : function(callback){ - var ct = Ext.get(this.ctNode); - ct.stopFx(); - if(!this.node.isExpandable()){ - this.updateExpandIcon(); - this.ctNode.style.display = ""; - Ext.callback(callback); - return; - } - this.animating = true; - this.updateExpandIcon(); - - ct.slideIn('t', { - callback : function(){ - this.animating = false; - Ext.callback(callback); - }, - scope: this, - duration: this.node.ownerTree.duration || .25 - }); - }, - - - highlight : function(){ - var tree = this.node.getOwnerTree(); - Ext.fly(this.wrap).highlight( - tree.hlColor || "C3DAF9", - {endColor: tree.hlBaseColor} - ); - }, - - - collapse : function(){ - this.updateExpandIcon(); - this.ctNode.style.display = "none"; - }, - - - animCollapse : function(callback){ - var ct = Ext.get(this.ctNode); - ct.enableDisplayMode('block'); - ct.stopFx(); - - this.animating = true; - this.updateExpandIcon(); - - ct.slideOut('t', { - callback : function(){ - this.animating = false; - Ext.callback(callback); - }, - scope: this, - duration: this.node.ownerTree.duration || .25 - }); - }, - - - getContainer : function(){ - return this.ctNode; - }, - - - getEl : function(){ - return this.wrap; - }, - - - appendDDGhost : function(ghostNode){ - ghostNode.appendChild(this.elNode.cloneNode(true)); - }, - - - getDDRepairXY : function(){ - return Ext.lib.Dom.getXY(this.iconNode); - }, - - - onRender : function(){ - this.render(); - }, - - - render : function(bulkRender){ - var n = this.node, a = n.attributes; - var targetNode = n.parentNode ? - n.parentNode.ui.getContainer() : n.ownerTree.innerCt.dom; - - if(!this.rendered){ - this.rendered = true; - - this.renderElements(n, a, targetNode, bulkRender); - - if(a.qtip){ - this.onTipChange(n, a.qtip, a.qtipTitle); - }else if(a.qtipCfg){ - a.qtipCfg.target = Ext.id(this.textNode); - Ext.QuickTips.register(a.qtipCfg); - } - this.initEvents(); - if(!this.node.expanded){ - this.updateExpandIcon(true); - } - }else{ - if(bulkRender === true) { - targetNode.appendChild(this.wrap); - } - } - }, - - - renderElements : function(n, a, targetNode, bulkRender){ - - this.indentMarkup = n.parentNode ? n.parentNode.ui.getChildIndent() : ''; - - var cb = Ext.isBoolean(a.checked), - nel, - href = this.getHref(a.href), - buf = ['
      • ', - '',this.indentMarkup,"", - '', - '', - cb ? ('' : '/>')) : '', - '',n.text,"
        ", - '', - "
      • "].join(''); - - if(bulkRender !== true && n.nextSibling && (nel = n.nextSibling.ui.getEl())){ - this.wrap = Ext.DomHelper.insertHtml("beforeBegin", nel, buf); - }else{ - this.wrap = Ext.DomHelper.insertHtml("beforeEnd", targetNode, buf); - } - - this.elNode = this.wrap.childNodes[0]; - this.ctNode = this.wrap.childNodes[1]; - var cs = this.elNode.childNodes; - this.indentNode = cs[0]; - this.ecNode = cs[1]; - this.iconNode = cs[2]; - var index = 3; - if(cb){ - this.checkbox = cs[3]; - - this.checkbox.defaultChecked = this.checkbox.checked; - index++; - } - this.anchor = cs[index]; - this.textNode = cs[index].firstChild; - }, - - - getHref : function(href){ - return Ext.isEmpty(href) ? (Ext.isGecko ? '' : '#') : href; - }, - - - getAnchor : function(){ - return this.anchor; - }, - - - getTextEl : function(){ - return this.textNode; - }, - - - getIconEl : function(){ - return this.iconNode; - }, - - - isChecked : function(){ - return this.checkbox ? this.checkbox.checked : false; - }, - - - updateExpandIcon : function(){ - if(this.rendered){ - var n = this.node, - c1, - c2, - cls = n.isLast() ? "x-tree-elbow-end" : "x-tree-elbow", - hasChild = n.hasChildNodes(); - if(hasChild || n.attributes.expandable){ - if(n.expanded){ - cls += "-minus"; - c1 = "x-tree-node-collapsed"; - c2 = "x-tree-node-expanded"; - }else{ - cls += "-plus"; - c1 = "x-tree-node-expanded"; - c2 = "x-tree-node-collapsed"; - } - if(this.wasLeaf){ - this.removeClass("x-tree-node-leaf"); - this.wasLeaf = false; - } - if(this.c1 != c1 || this.c2 != c2){ - Ext.fly(this.elNode).replaceClass(c1, c2); - this.c1 = c1; this.c2 = c2; - } - }else{ - if(!this.wasLeaf){ - Ext.fly(this.elNode).replaceClass("x-tree-node-expanded", "x-tree-node-collapsed"); - delete this.c1; - delete this.c2; - this.wasLeaf = true; - } - } - var ecc = "x-tree-ec-icon "+cls; - if(this.ecc != ecc){ - this.ecNode.className = ecc; - this.ecc = ecc; - } - } - }, - - - onIdChange: function(id){ - if(this.rendered){ - this.elNode.setAttribute('ext:tree-node-id', id); - } - }, - - - getChildIndent : function(){ - if(!this.childIndent){ - var buf = [], - p = this.node; - while(p){ - if(!p.isRoot || (p.isRoot && p.ownerTree.rootVisible)){ - if(!p.isLast()) { - buf.unshift(''); - } else { - buf.unshift(''); - } - } - p = p.parentNode; - } - this.childIndent = buf.join(""); - } - return this.childIndent; - }, - - - renderIndent : function(){ - if(this.rendered){ - var indent = "", - p = this.node.parentNode; - if(p){ - indent = p.ui.getChildIndent(); - } - if(this.indentMarkup != indent){ - this.indentNode.innerHTML = indent; - this.indentMarkup = indent; - } - this.updateExpandIcon(); - } - }, - - destroy : function(){ - if(this.elNode){ - Ext.dd.Registry.unregister(this.elNode.id); - } - - Ext.each(['textnode', 'anchor', 'checkbox', 'indentNode', 'ecNode', 'iconNode', 'elNode', 'ctNode', 'wrap', 'holder'], function(el){ - if(this[el]){ - Ext.fly(this[el]).remove(); - delete this[el]; - } - }, this); - delete this.node; - } -}); - - -Ext.tree.RootTreeNodeUI = Ext.extend(Ext.tree.TreeNodeUI, { - - render : function(){ - if(!this.rendered){ - var targetNode = this.node.ownerTree.innerCt.dom; - this.node.expanded = true; - targetNode.innerHTML = '
        '; - this.wrap = this.ctNode = targetNode.firstChild; - } - }, - collapse : Ext.emptyFn, - expand : Ext.emptyFn -}); -Ext.tree.TreeLoader = function(config){ - this.baseParams = {}; - Ext.apply(this, config); - - this.addEvents( - - "beforeload", - - "load", - - "loadexception" - ); - Ext.tree.TreeLoader.superclass.constructor.call(this); - if(Ext.isString(this.paramOrder)){ - this.paramOrder = this.paramOrder.split(/[\s,|]/); - } -}; - -Ext.extend(Ext.tree.TreeLoader, Ext.util.Observable, { - - - - - - - - uiProviders : {}, - - - clearOnLoad : true, - - - paramOrder: undefined, - - - paramsAsHash: false, - - - nodeParameter: 'node', - - - directFn : undefined, - - - load : function(node, callback, scope){ - if(this.clearOnLoad){ - while(node.firstChild){ - node.removeChild(node.firstChild); - } - } - if(this.doPreload(node)){ - this.runCallback(callback, scope || node, [node]); - }else if(this.directFn || this.dataUrl || this.url){ - this.requestData(node, callback, scope || node); - } - }, - - doPreload : function(node){ - if(node.attributes.children){ - if(node.childNodes.length < 1){ - var cs = node.attributes.children; - node.beginUpdate(); - for(var i = 0, len = cs.length; i < len; i++){ - var cn = node.appendChild(this.createNode(cs[i])); - if(this.preloadChildren){ - this.doPreload(cn); - } - } - node.endUpdate(); - } - return true; - } - return false; - }, - - getParams: function(node){ - var bp = Ext.apply({}, this.baseParams), - np = this.nodeParameter, - po = this.paramOrder; - - np && (bp[ np ] = node.id); - - if(this.directFn){ - var buf = [node.id]; - if(po){ - - if(np && po.indexOf(np) > -1){ - buf = []; - } - - for(var i = 0, len = po.length; i < len; i++){ - buf.push(bp[ po[i] ]); - } - }else if(this.paramsAsHash){ - buf = [bp]; - } - return buf; - }else{ - return bp; - } - }, - - requestData : function(node, callback, scope){ - if(this.fireEvent("beforeload", this, node, callback) !== false){ - if(this.directFn){ - var args = this.getParams(node); - args.push(this.processDirectResponse.createDelegate(this, [{callback: callback, node: node, scope: scope}], true)); - this.directFn.apply(window, args); - }else{ - this.transId = Ext.Ajax.request({ - method:this.requestMethod, - url: this.dataUrl||this.url, - success: this.handleResponse, - failure: this.handleFailure, - scope: this, - argument: {callback: callback, node: node, scope: scope}, - params: this.getParams(node) - }); - } - }else{ - - - this.runCallback(callback, scope || node, []); - } - }, - - processDirectResponse: function(result, response, args){ - if(response.status){ - this.handleResponse({ - responseData: Ext.isArray(result) ? result : null, - responseText: result, - argument: args - }); - }else{ - this.handleFailure({ - argument: args - }); - } - }, - - - runCallback: function(cb, scope, args){ - if(Ext.isFunction(cb)){ - cb.apply(scope, args); - } - }, - - isLoading : function(){ - return !!this.transId; - }, - - abort : function(){ - if(this.isLoading()){ - Ext.Ajax.abort(this.transId); - } - }, - - - createNode : function(attr){ - - if(this.baseAttrs){ - Ext.applyIf(attr, this.baseAttrs); - } - if(this.applyLoader !== false && !attr.loader){ - attr.loader = this; - } - if(Ext.isString(attr.uiProvider)){ - attr.uiProvider = this.uiProviders[attr.uiProvider] || eval(attr.uiProvider); - } - if(attr.nodeType){ - return new Ext.tree.TreePanel.nodeTypes[attr.nodeType](attr); - }else{ - return attr.leaf ? - new Ext.tree.TreeNode(attr) : - new Ext.tree.AsyncTreeNode(attr); - } - }, - - processResponse : function(response, node, callback, scope){ - var json = response.responseText; - try { - var o = response.responseData || Ext.decode(json); - node.beginUpdate(); - for(var i = 0, len = o.length; i < len; i++){ - var n = this.createNode(o[i]); - if(n){ - node.appendChild(n); - } - } - node.endUpdate(); - this.runCallback(callback, scope || node, [node]); - }catch(e){ - this.handleFailure(response); - } - }, - - handleResponse : function(response){ - this.transId = false; - var a = response.argument; - this.processResponse(response, a.node, a.callback, a.scope); - this.fireEvent("load", this, a.node, response); - }, - - handleFailure : function(response){ - this.transId = false; - var a = response.argument; - this.fireEvent("loadexception", this, a.node, response); - this.runCallback(a.callback, a.scope || a.node, [a.node]); - }, - - destroy : function(){ - this.abort(); - this.purgeListeners(); - } -}); -Ext.tree.TreeFilter = function(tree, config){ - this.tree = tree; - this.filtered = {}; - Ext.apply(this, config); -}; - -Ext.tree.TreeFilter.prototype = { - clearBlank:false, - reverse:false, - autoClear:false, - remove:false, - - - filter : function(value, attr, startNode){ - attr = attr || "text"; - var f; - if(typeof value == "string"){ - var vlen = value.length; - - if(vlen == 0 && this.clearBlank){ - this.clear(); - return; - } - value = value.toLowerCase(); - f = function(n){ - return n.attributes[attr].substr(0, vlen).toLowerCase() == value; - }; - }else if(value.exec){ - f = function(n){ - return value.test(n.attributes[attr]); - }; - }else{ - throw 'Illegal filter type, must be string or regex'; - } - this.filterBy(f, null, startNode); - }, - - - filterBy : function(fn, scope, startNode){ - startNode = startNode || this.tree.root; - if(this.autoClear){ - this.clear(); - } - var af = this.filtered, rv = this.reverse; - var f = function(n){ - if(n == startNode){ - return true; - } - if(af[n.id]){ - return false; - } - var m = fn.call(scope || n, n); - if(!m || rv){ - af[n.id] = n; - n.ui.hide(); - return false; - } - return true; - }; - startNode.cascade(f); - if(this.remove){ - for(var id in af){ - if(typeof id != "function"){ - var n = af[id]; - if(n && n.parentNode){ - n.parentNode.removeChild(n); - } - } - } - } - }, - - - clear : function(){ - var t = this.tree; - var af = this.filtered; - for(var id in af){ - if(typeof id != "function"){ - var n = af[id]; - if(n){ - n.ui.show(); - } - } - } - this.filtered = {}; - } -}; - -Ext.tree.TreeSorter = Ext.extend(Object, { - - constructor: function(tree, config){ - - - - - - - - Ext.apply(this, config); - tree.on({ - scope: this, - beforechildrenrendered: this.doSort, - append: this.updateSort, - insert: this.updateSort, - textchange: this.updateSortParent - }); - - var desc = this.dir && this.dir.toLowerCase() == 'desc', - prop = this.property || 'text', - sortType = this.sortType, - folderSort = this.folderSort, - caseSensitive = this.caseSensitive === true, - leafAttr = this.leafAttr || 'leaf'; - - if(Ext.isString(sortType)){ - sortType = Ext.data.SortTypes[sortType]; - } - this.sortFn = function(n1, n2){ - var attr1 = n1.attributes, - attr2 = n2.attributes; - - if(folderSort){ - if(attr1[leafAttr] && !attr2[leafAttr]){ - return 1; - } - if(!attr1[leafAttr] && attr2[leafAttr]){ - return -1; - } - } - var prop1 = attr1[prop], - prop2 = attr2[prop], - v1 = sortType ? sortType(prop1) : (caseSensitive ? prop1 : prop1.toUpperCase()), - v2 = sortType ? sortType(prop2) : (caseSensitive ? prop2 : prop2.toUpperCase()); - - if(v1 < v2){ - return desc ? 1 : -1; - }else if(v1 > v2){ - return desc ? -1 : 1; - } - return 0; - }; - }, - - doSort : function(node){ - node.sort(this.sortFn); - }, - - updateSort : function(tree, node){ - if(node.childrenRendered){ - this.doSort.defer(1, this, [node]); - } - }, - - updateSortParent : function(node){ - var p = node.parentNode; - if(p && p.childrenRendered){ - this.doSort.defer(1, this, [p]); - } - } -}); - -if(Ext.dd.DropZone){ - -Ext.tree.TreeDropZone = function(tree, config){ - - this.allowParentInsert = config.allowParentInsert || false; - - this.allowContainerDrop = config.allowContainerDrop || false; - - this.appendOnly = config.appendOnly || false; - - Ext.tree.TreeDropZone.superclass.constructor.call(this, tree.getTreeEl(), config); - - this.tree = tree; - - this.dragOverData = {}; - - this.lastInsertClass = "x-tree-no-status"; -}; - -Ext.extend(Ext.tree.TreeDropZone, Ext.dd.DropZone, { - - ddGroup : "TreeDD", - - - expandDelay : 1000, - - - expandNode : function(node){ - if(node.hasChildNodes() && !node.isExpanded()){ - node.expand(false, null, this.triggerCacheRefresh.createDelegate(this)); - } - }, - - - queueExpand : function(node){ - this.expandProcId = this.expandNode.defer(this.expandDelay, this, [node]); - }, - - - cancelExpand : function(){ - if(this.expandProcId){ - clearTimeout(this.expandProcId); - this.expandProcId = false; - } - }, - - - isValidDropPoint : function(n, pt, dd, e, data){ - if(!n || !data){ return false; } - var targetNode = n.node; - var dropNode = data.node; - - if(!(targetNode && targetNode.isTarget && pt)){ - return false; - } - if(pt == "append" && targetNode.allowChildren === false){ - return false; - } - if((pt == "above" || pt == "below") && (targetNode.parentNode && targetNode.parentNode.allowChildren === false)){ - return false; - } - if(dropNode && (targetNode == dropNode || dropNode.contains(targetNode))){ - return false; - } - - var overEvent = this.dragOverData; - overEvent.tree = this.tree; - overEvent.target = targetNode; - overEvent.data = data; - overEvent.point = pt; - overEvent.source = dd; - overEvent.rawEvent = e; - overEvent.dropNode = dropNode; - overEvent.cancel = false; - var result = this.tree.fireEvent("nodedragover", overEvent); - return overEvent.cancel === false && result !== false; - }, - - - getDropPoint : function(e, n, dd){ - var tn = n.node; - if(tn.isRoot){ - return tn.allowChildren !== false ? "append" : false; - } - var dragEl = n.ddel; - var t = Ext.lib.Dom.getY(dragEl), b = t + dragEl.offsetHeight; - var y = Ext.lib.Event.getPageY(e); - var noAppend = tn.allowChildren === false || tn.isLeaf(); - if(this.appendOnly || tn.parentNode.allowChildren === false){ - return noAppend ? false : "append"; - } - var noBelow = false; - if(!this.allowParentInsert){ - noBelow = tn.hasChildNodes() && tn.isExpanded(); - } - var q = (b - t) / (noAppend ? 2 : 3); - if(y >= t && y < (t + q)){ - return "above"; - }else if(!noBelow && (noAppend || y >= b-q && y <= b)){ - return "below"; - }else{ - return "append"; - } - }, - - - onNodeEnter : function(n, dd, e, data){ - this.cancelExpand(); - }, - - onContainerOver : function(dd, e, data) { - if (this.allowContainerDrop && this.isValidDropPoint({ ddel: this.tree.getRootNode().ui.elNode, node: this.tree.getRootNode() }, "append", dd, e, data)) { - return this.dropAllowed; - } - return this.dropNotAllowed; - }, - - - onNodeOver : function(n, dd, e, data){ - var pt = this.getDropPoint(e, n, dd); - var node = n.node; - - - if(!this.expandProcId && pt == "append" && node.hasChildNodes() && !n.node.isExpanded()){ - this.queueExpand(node); - }else if(pt != "append"){ - this.cancelExpand(); - } - - - var returnCls = this.dropNotAllowed; - if(this.isValidDropPoint(n, pt, dd, e, data)){ - if(pt){ - var el = n.ddel; - var cls; - if(pt == "above"){ - returnCls = n.node.isFirst() ? "x-tree-drop-ok-above" : "x-tree-drop-ok-between"; - cls = "x-tree-drag-insert-above"; - }else if(pt == "below"){ - returnCls = n.node.isLast() ? "x-tree-drop-ok-below" : "x-tree-drop-ok-between"; - cls = "x-tree-drag-insert-below"; - }else{ - returnCls = "x-tree-drop-ok-append"; - cls = "x-tree-drag-append"; - } - if(this.lastInsertClass != cls){ - Ext.fly(el).replaceClass(this.lastInsertClass, cls); - this.lastInsertClass = cls; - } - } - } - return returnCls; - }, - - - onNodeOut : function(n, dd, e, data){ - this.cancelExpand(); - this.removeDropIndicators(n); - }, - - - onNodeDrop : function(n, dd, e, data){ - var point = this.getDropPoint(e, n, dd); - var targetNode = n.node; - targetNode.ui.startDrop(); - if(!this.isValidDropPoint(n, point, dd, e, data)){ - targetNode.ui.endDrop(); - return false; - } - - var dropNode = data.node || (dd.getTreeNode ? dd.getTreeNode(data, targetNode, point, e) : null); - return this.processDrop(targetNode, data, point, dd, e, dropNode); - }, - - onContainerDrop : function(dd, e, data){ - if (this.allowContainerDrop && this.isValidDropPoint({ ddel: this.tree.getRootNode().ui.elNode, node: this.tree.getRootNode() }, "append", dd, e, data)) { - var targetNode = this.tree.getRootNode(); - targetNode.ui.startDrop(); - var dropNode = data.node || (dd.getTreeNode ? dd.getTreeNode(data, targetNode, 'append', e) : null); - return this.processDrop(targetNode, data, 'append', dd, e, dropNode); - } - return false; - }, - - - processDrop: function(target, data, point, dd, e, dropNode){ - var dropEvent = { - tree : this.tree, - target: target, - data: data, - point: point, - source: dd, - rawEvent: e, - dropNode: dropNode, - cancel: !dropNode, - dropStatus: false - }; - var retval = this.tree.fireEvent("beforenodedrop", dropEvent); - if(retval === false || dropEvent.cancel === true || !dropEvent.dropNode){ - target.ui.endDrop(); - return dropEvent.dropStatus; - } - - target = dropEvent.target; - if(point == 'append' && !target.isExpanded()){ - target.expand(false, null, function(){ - this.completeDrop(dropEvent); - }.createDelegate(this)); - }else{ - this.completeDrop(dropEvent); - } - return true; - }, - - - completeDrop : function(de){ - var ns = de.dropNode, p = de.point, t = de.target; - if(!Ext.isArray(ns)){ - ns = [ns]; - } - var n; - for(var i = 0, len = ns.length; i < len; i++){ - n = ns[i]; - if(p == "above"){ - t.parentNode.insertBefore(n, t); - }else if(p == "below"){ - t.parentNode.insertBefore(n, t.nextSibling); - }else{ - t.appendChild(n); - } - } - n.ui.focus(); - if(Ext.enableFx && this.tree.hlDrop){ - n.ui.highlight(); - } - t.ui.endDrop(); - this.tree.fireEvent("nodedrop", de); - }, - - - afterNodeMoved : function(dd, data, e, targetNode, dropNode){ - if(Ext.enableFx && this.tree.hlDrop){ - dropNode.ui.focus(); - dropNode.ui.highlight(); - } - this.tree.fireEvent("nodedrop", this.tree, targetNode, data, dd, e); - }, - - - getTree : function(){ - return this.tree; - }, - - - removeDropIndicators : function(n){ - if(n && n.ddel){ - var el = n.ddel; - Ext.fly(el).removeClass([ - "x-tree-drag-insert-above", - "x-tree-drag-insert-below", - "x-tree-drag-append"]); - this.lastInsertClass = "_noclass"; - } - }, - - - beforeDragDrop : function(target, e, id){ - this.cancelExpand(); - return true; - }, - - - afterRepair : function(data){ - if(data && Ext.enableFx){ - data.node.ui.highlight(); - } - this.hideProxy(); - } -}); - -} -if(Ext.dd.DragZone){ -Ext.tree.TreeDragZone = function(tree, config){ - Ext.tree.TreeDragZone.superclass.constructor.call(this, tree.innerCt, config); - - this.tree = tree; -}; - -Ext.extend(Ext.tree.TreeDragZone, Ext.dd.DragZone, { - - ddGroup : "TreeDD", - - - onBeforeDrag : function(data, e){ - var n = data.node; - return n && n.draggable && !n.disabled; - }, - - - onInitDrag : function(e){ - var data = this.dragData; - this.tree.getSelectionModel().select(data.node); - this.tree.eventModel.disable(); - this.proxy.update(""); - data.node.ui.appendDDGhost(this.proxy.ghost.dom); - this.tree.fireEvent("startdrag", this.tree, data.node, e); - }, - - - getRepairXY : function(e, data){ - return data.node.ui.getDDRepairXY(); - }, - - - onEndDrag : function(data, e){ - this.tree.eventModel.enable.defer(100, this.tree.eventModel); - this.tree.fireEvent("enddrag", this.tree, data.node, e); - }, - - - onValidDrop : function(dd, e, id){ - this.tree.fireEvent("dragdrop", this.tree, this.dragData.node, dd, e); - this.hideProxy(); - }, - - - beforeInvalidDrop : function(e, id){ - - var sm = this.tree.getSelectionModel(); - sm.clearSelections(); - sm.select(this.dragData.node); - }, - - - afterRepair : function(){ - if (Ext.enableFx && this.tree.hlDrop) { - Ext.Element.fly(this.dragData.ddel).highlight(this.hlColor || "c3daf9"); - } - this.dragging = false; - } -}); -} -Ext.tree.TreeEditor = function(tree, fc, config){ - fc = fc || {}; - var field = fc.events ? fc : new Ext.form.TextField(fc); - - Ext.tree.TreeEditor.superclass.constructor.call(this, field, config); - - this.tree = tree; - - if(!tree.rendered){ - tree.on('render', this.initEditor, this); - }else{ - this.initEditor(tree); - } -}; - -Ext.extend(Ext.tree.TreeEditor, Ext.Editor, { - - alignment: "l-l", - - autoSize: false, - - hideEl : false, - - cls: "x-small-editor x-tree-editor", - - shim:false, - - shadow:"frame", - - maxWidth: 250, - - editDelay : 350, - - initEditor : function(tree){ - tree.on({ - scope : this, - beforeclick: this.beforeNodeClick, - dblclick : this.onNodeDblClick - }); - - this.on({ - scope : this, - complete : this.updateNode, - beforestartedit: this.fitToTree, - specialkey : this.onSpecialKey - }); - - this.on('startedit', this.bindScroll, this, {delay:10}); - }, - - - fitToTree : function(ed, el){ - var td = this.tree.getTreeEl().dom, nd = el.dom; - if(td.scrollLeft > nd.offsetLeft){ - td.scrollLeft = nd.offsetLeft; - } - var w = Math.min( - this.maxWidth, - (td.clientWidth > 20 ? td.clientWidth : td.offsetWidth) - Math.max(0, nd.offsetLeft-td.scrollLeft) - 5); - this.setSize(w, ''); - }, - - - triggerEdit : function(node, defer){ - this.completeEdit(); - if(node.attributes.editable !== false){ - - this.editNode = node; - if(this.tree.autoScroll){ - Ext.fly(node.ui.getEl()).scrollIntoView(this.tree.body); - } - var value = node.text || ''; - if (!Ext.isGecko && Ext.isEmpty(node.text)){ - node.setText(' '); - } - this.autoEditTimer = this.startEdit.defer(this.editDelay, this, [node.ui.textNode, value]); - return false; - } - }, - - - bindScroll : function(){ - this.tree.getTreeEl().on('scroll', this.cancelEdit, this); - }, - - - beforeNodeClick : function(node, e){ - clearTimeout(this.autoEditTimer); - if(this.tree.getSelectionModel().isSelected(node)){ - e.stopEvent(); - return this.triggerEdit(node); - } - }, - - onNodeDblClick : function(node, e){ - clearTimeout(this.autoEditTimer); - }, - - - updateNode : function(ed, value){ - this.tree.getTreeEl().un('scroll', this.cancelEdit, this); - this.editNode.setText(value); - }, - - - onHide : function(){ - Ext.tree.TreeEditor.superclass.onHide.call(this); - if(this.editNode){ - this.editNode.ui.focus.defer(50, this.editNode.ui); - } - }, - - - onSpecialKey : function(field, e){ - var k = e.getKey(); - if(k == e.ESC){ - e.stopEvent(); - this.cancelEdit(); - }else if(k == e.ENTER && !e.hasModifier()){ - e.stopEvent(); - this.completeEdit(); - } - }, - - onDestroy : function(){ - clearTimeout(this.autoEditTimer); - Ext.tree.TreeEditor.superclass.onDestroy.call(this); - var tree = this.tree; - tree.un('beforeclick', this.beforeNodeClick, this); - tree.un('dblclick', this.onNodeDblClick, this); - } -}); - -var swfobject = function() { - - var UNDEF = "undefined", - OBJECT = "object", - SHOCKWAVE_FLASH = "Shockwave Flash", - SHOCKWAVE_FLASH_AX = "ShockwaveFlash.ShockwaveFlash", - FLASH_MIME_TYPE = "application/x-shockwave-flash", - EXPRESS_INSTALL_ID = "SWFObjectExprInst", - ON_READY_STATE_CHANGE = "onreadystatechange", - - win = window, - doc = document, - nav = navigator, - - plugin = false, - domLoadFnArr = [main], - regObjArr = [], - objIdArr = [], - listenersArr = [], - storedAltContent, - storedAltContentId, - storedCallbackFn, - storedCallbackObj, - isDomLoaded = false, - isExpressInstallActive = false, - dynamicStylesheet, - dynamicStylesheetMedia, - autoHideShow = true, - - - ua = function() { - var w3cdom = typeof doc.getElementById != UNDEF && typeof doc.getElementsByTagName != UNDEF && typeof doc.createElement != UNDEF, - u = nav.userAgent.toLowerCase(), - p = nav.platform.toLowerCase(), - windows = p ? (/win/).test(p) : /win/.test(u), - mac = p ? (/mac/).test(p) : /mac/.test(u), - webkit = /webkit/.test(u) ? parseFloat(u.replace(/^.*webkit\/(\d+(\.\d+)?).*$/, "$1")) : false, - ie = !+"\v1", - playerVersion = [0,0,0], - d = null; - if (typeof nav.plugins != UNDEF && typeof nav.plugins[SHOCKWAVE_FLASH] == OBJECT) { - d = nav.plugins[SHOCKWAVE_FLASH].description; - if (d && !(typeof nav.mimeTypes != UNDEF && nav.mimeTypes[FLASH_MIME_TYPE] && !nav.mimeTypes[FLASH_MIME_TYPE].enabledPlugin)) { - plugin = true; - ie = false; - d = d.replace(/^.*\s+(\S+\s+\S+$)/, "$1"); - playerVersion[0] = parseInt(d.replace(/^(.*)\..*$/, "$1"), 10); - playerVersion[1] = parseInt(d.replace(/^.*\.(.*)\s.*$/, "$1"), 10); - playerVersion[2] = /[a-zA-Z]/.test(d) ? parseInt(d.replace(/^.*[a-zA-Z]+(.*)$/, "$1"), 10) : 0; - } - } - else if (typeof win.ActiveXObject != UNDEF) { - try { - var a = new ActiveXObject(SHOCKWAVE_FLASH_AX); - if (a) { - d = a.GetVariable("$version"); - if (d) { - ie = true; - d = d.split(" ")[1].split(","); - playerVersion = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)]; - } - } - } - catch(e) {} - } - return { w3:w3cdom, pv:playerVersion, wk:webkit, ie:ie, win:windows, mac:mac }; - }(), - - - onDomLoad = function() { - if (!ua.w3) { return; } - if ((typeof doc.readyState != UNDEF && doc.readyState == "complete") || (typeof doc.readyState == UNDEF && (doc.getElementsByTagName("body")[0] || doc.body))) { - callDomLoadFunctions(); - } - if (!isDomLoaded) { - if (typeof doc.addEventListener != UNDEF) { - doc.addEventListener("DOMContentLoaded", callDomLoadFunctions, false); - } - if (ua.ie && ua.win) { - doc.attachEvent(ON_READY_STATE_CHANGE, function() { - if (doc.readyState == "complete") { - doc.detachEvent(ON_READY_STATE_CHANGE, arguments.callee); - callDomLoadFunctions(); - } - }); - if (win == top) { - (function(){ - if (isDomLoaded) { return; } - try { - doc.documentElement.doScroll("left"); - } - catch(e) { - setTimeout(arguments.callee, 0); - return; - } - callDomLoadFunctions(); - })(); - } - } - if (ua.wk) { - (function(){ - if (isDomLoaded) { return; } - if (!(/loaded|complete/).test(doc.readyState)) { - setTimeout(arguments.callee, 0); - return; - } - callDomLoadFunctions(); - })(); - } - addLoadEvent(callDomLoadFunctions); - } - }(); - - function callDomLoadFunctions() { - if (isDomLoaded) { return; } - try { - var t = doc.getElementsByTagName("body")[0].appendChild(createElement("span")); - t.parentNode.removeChild(t); - } - catch (e) { return; } - isDomLoaded = true; - var dl = domLoadFnArr.length; - for (var i = 0; i < dl; i++) { - domLoadFnArr[i](); - } - } - - function addDomLoadEvent(fn) { - if (isDomLoaded) { - fn(); - } - else { - domLoadFnArr[domLoadFnArr.length] = fn; - } - } - - - function addLoadEvent(fn) { - if (typeof win.addEventListener != UNDEF) { - win.addEventListener("load", fn, false); - } - else if (typeof doc.addEventListener != UNDEF) { - doc.addEventListener("load", fn, false); - } - else if (typeof win.attachEvent != UNDEF) { - addListener(win, "onload", fn); - } - else if (typeof win.onload == "function") { - var fnOld = win.onload; - win.onload = function() { - fnOld(); - fn(); - }; - } - else { - win.onload = fn; - } - } - - - function main() { - if (plugin) { - testPlayerVersion(); - } - else { - matchVersions(); - } - } - - - function testPlayerVersion() { - var b = doc.getElementsByTagName("body")[0]; - var o = createElement(OBJECT); - o.setAttribute("type", FLASH_MIME_TYPE); - var t = b.appendChild(o); - if (t) { - var counter = 0; - (function(){ - if (typeof t.GetVariable != UNDEF) { - var d = t.GetVariable("$version"); - if (d) { - d = d.split(" ")[1].split(","); - ua.pv = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)]; - } - } - else if (counter < 10) { - counter++; - setTimeout(arguments.callee, 10); - return; - } - b.removeChild(o); - t = null; - matchVersions(); - })(); - } - else { - matchVersions(); - } - } - - - function matchVersions() { - var rl = regObjArr.length; - if (rl > 0) { - for (var i = 0; i < rl; i++) { - var id = regObjArr[i].id; - var cb = regObjArr[i].callbackFn; - var cbObj = {success:false, id:id}; - if (ua.pv[0] > 0) { - var obj = getElementById(id); - if (obj) { - if (hasPlayerVersion(regObjArr[i].swfVersion) && !(ua.wk && ua.wk < 312)) { - setVisibility(id, true); - if (cb) { - cbObj.success = true; - cbObj.ref = getObjectById(id); - cb(cbObj); - } - } - else if (regObjArr[i].expressInstall && canExpressInstall()) { - var att = {}; - att.data = regObjArr[i].expressInstall; - att.width = obj.getAttribute("width") || "0"; - att.height = obj.getAttribute("height") || "0"; - if (obj.getAttribute("class")) { att.styleclass = obj.getAttribute("class"); } - if (obj.getAttribute("align")) { att.align = obj.getAttribute("align"); } - - var par = {}; - var p = obj.getElementsByTagName("param"); - var pl = p.length; - for (var j = 0; j < pl; j++) { - if (p[j].getAttribute("name").toLowerCase() != "movie") { - par[p[j].getAttribute("name")] = p[j].getAttribute("value"); - } - } - showExpressInstall(att, par, id, cb); - } - else { - displayAltContent(obj); - if (cb) { cb(cbObj); } - } - } - } - else { - setVisibility(id, true); - if (cb) { - var o = getObjectById(id); - if (o && typeof o.SetVariable != UNDEF) { - cbObj.success = true; - cbObj.ref = o; - } - cb(cbObj); - } - } - } - } - } - - function getObjectById(objectIdStr) { - var r = null; - var o = getElementById(objectIdStr); - if (o && o.nodeName == "OBJECT") { - if (typeof o.SetVariable != UNDEF) { - r = o; - } - else { - var n = o.getElementsByTagName(OBJECT)[0]; - if (n) { - r = n; - } - } - } - return r; - } - - - function canExpressInstall() { - return !isExpressInstallActive && hasPlayerVersion("6.0.65") && (ua.win || ua.mac) && !(ua.wk && ua.wk < 312); - } - - - function showExpressInstall(att, par, replaceElemIdStr, callbackFn) { - isExpressInstallActive = true; - storedCallbackFn = callbackFn || null; - storedCallbackObj = {success:false, id:replaceElemIdStr}; - var obj = getElementById(replaceElemIdStr); - if (obj) { - if (obj.nodeName == "OBJECT") { - storedAltContent = abstractAltContent(obj); - storedAltContentId = null; - } - else { - storedAltContent = obj; - storedAltContentId = replaceElemIdStr; - } - att.id = EXPRESS_INSTALL_ID; - if (typeof att.width == UNDEF || (!(/%$/).test(att.width) && parseInt(att.width, 10) < 310)) { - att.width = "310"; - } - - if (typeof att.height == UNDEF || (!(/%$/).test(att.height) && parseInt(att.height, 10) < 137)) { - att.height = "137"; - } - doc.title = doc.title.slice(0, 47) + " - Flash Player Installation"; - var pt = ua.ie && ua.win ? "ActiveX" : "PlugIn", - fv = "MMredirectURL=" + win.location.toString().replace(/&/g,"%26") + "&MMplayerType=" + pt + "&MMdoctitle=" + doc.title; - if (typeof par.flashvars != UNDEF) { - par.flashvars += "&" + fv; - } - else { - par.flashvars = fv; - } - - - if (ua.ie && ua.win && obj.readyState != 4) { - var newObj = createElement("div"); - replaceElemIdStr += "SWFObjectNew"; - newObj.setAttribute("id", replaceElemIdStr); - obj.parentNode.insertBefore(newObj, obj); - obj.style.display = "none"; - (function(){ - if (obj.readyState == 4) { - obj.parentNode.removeChild(obj); - } - else { - setTimeout(arguments.callee, 10); - } - })(); - } - createSWF(att, par, replaceElemIdStr); - } - } - - - function displayAltContent(obj) { - if (ua.ie && ua.win && obj.readyState != 4) { - - - var el = createElement("div"); - obj.parentNode.insertBefore(el, obj); - el.parentNode.replaceChild(abstractAltContent(obj), el); - obj.style.display = "none"; - (function(){ - if (obj.readyState == 4) { - obj.parentNode.removeChild(obj); - } - else { - setTimeout(arguments.callee, 10); - } - })(); - } - else { - obj.parentNode.replaceChild(abstractAltContent(obj), obj); - } - } - - function abstractAltContent(obj) { - var ac = createElement("div"); - if (ua.win && ua.ie) { - ac.innerHTML = obj.innerHTML; - } - else { - var nestedObj = obj.getElementsByTagName(OBJECT)[0]; - if (nestedObj) { - var c = nestedObj.childNodes; - if (c) { - var cl = c.length; - for (var i = 0; i < cl; i++) { - if (!(c[i].nodeType == 1 && c[i].nodeName == "PARAM") && !(c[i].nodeType == 8)) { - ac.appendChild(c[i].cloneNode(true)); - } - } - } - } - } - return ac; - } - - - function createSWF(attObj, parObj, id) { - var r, el = getElementById(id); - if (ua.wk && ua.wk < 312) { return r; } - if (el) { - if (typeof attObj.id == UNDEF) { - attObj.id = id; - } - if (ua.ie && ua.win) { - var att = ""; - for (var i in attObj) { - if (attObj[i] != Object.prototype[i]) { - if (i.toLowerCase() == "data") { - parObj.movie = attObj[i]; - } - else if (i.toLowerCase() == "styleclass") { - att += ' class="' + attObj[i] + '"'; - } - else if (i.toLowerCase() != "classid") { - att += ' ' + i + '="' + attObj[i] + '"'; - } - } - } - var par = ""; - for (var j in parObj) { - if (parObj[j] != Object.prototype[j]) { - par += ''; - } - } - el.outerHTML = '' + par + ''; - objIdArr[objIdArr.length] = attObj.id; - r = getElementById(attObj.id); - } - else { - var o = createElement(OBJECT); - o.setAttribute("type", FLASH_MIME_TYPE); - for (var m in attObj) { - if (attObj[m] != Object.prototype[m]) { - if (m.toLowerCase() == "styleclass") { - o.setAttribute("class", attObj[m]); - } - else if (m.toLowerCase() != "classid") { - o.setAttribute(m, attObj[m]); - } - } - } - for (var n in parObj) { - if (parObj[n] != Object.prototype[n] && n.toLowerCase() != "movie") { - createObjParam(o, n, parObj[n]); - } - } - el.parentNode.replaceChild(o, el); - r = o; - } - } - return r; - } - - function createObjParam(el, pName, pValue) { - var p = createElement("param"); - p.setAttribute("name", pName); - p.setAttribute("value", pValue); - el.appendChild(p); - } - - - function removeSWF(id) { - var obj = getElementById(id); - if (obj && obj.nodeName == "OBJECT") { - if (ua.ie && ua.win) { - obj.style.display = "none"; - (function(){ - if (obj.readyState == 4) { - removeObjectInIE(id); - } - else { - setTimeout(arguments.callee, 10); - } - })(); - } - else { - obj.parentNode.removeChild(obj); - } - } - } - - function removeObjectInIE(id) { - var obj = getElementById(id); - if (obj) { - for (var i in obj) { - if (typeof obj[i] == "function") { - obj[i] = null; - } - } - obj.parentNode.removeChild(obj); - } - } - - - function getElementById(id) { - var el = null; - try { - el = doc.getElementById(id); - } - catch (e) {} - return el; - } - - function createElement(el) { - return doc.createElement(el); - } - - - function addListener(target, eventType, fn) { - target.attachEvent(eventType, fn); - listenersArr[listenersArr.length] = [target, eventType, fn]; - } - - - function hasPlayerVersion(rv) { - var pv = ua.pv, v = rv.split("."); - v[0] = parseInt(v[0], 10); - v[1] = parseInt(v[1], 10) || 0; - v[2] = parseInt(v[2], 10) || 0; - return (pv[0] > v[0] || (pv[0] == v[0] && pv[1] > v[1]) || (pv[0] == v[0] && pv[1] == v[1] && pv[2] >= v[2])) ? true : false; - } - - - function createCSS(sel, decl, media, newStyle) { - if (ua.ie && ua.mac) { return; } - var h = doc.getElementsByTagName("head")[0]; - if (!h) { return; } - var m = (media && typeof media == "string") ? media : "screen"; - if (newStyle) { - dynamicStylesheet = null; - dynamicStylesheetMedia = null; - } - if (!dynamicStylesheet || dynamicStylesheetMedia != m) { - - var s = createElement("style"); - s.setAttribute("type", "text/css"); - s.setAttribute("media", m); - dynamicStylesheet = h.appendChild(s); - if (ua.ie && ua.win && typeof doc.styleSheets != UNDEF && doc.styleSheets.length > 0) { - dynamicStylesheet = doc.styleSheets[doc.styleSheets.length - 1]; - } - dynamicStylesheetMedia = m; - } - - if (ua.ie && ua.win) { - if (dynamicStylesheet && typeof dynamicStylesheet.addRule == OBJECT) { - dynamicStylesheet.addRule(sel, decl); - } - } - else { - if (dynamicStylesheet && typeof doc.createTextNode != UNDEF) { - dynamicStylesheet.appendChild(doc.createTextNode(sel + " {" + decl + "}")); - } - } - } - - function setVisibility(id, isVisible) { - if (!autoHideShow) { return; } - var v = isVisible ? "visible" : "hidden"; - if (isDomLoaded && getElementById(id)) { - getElementById(id).style.visibility = v; - } - else { - createCSS("#" + id, "visibility:" + v); - } - } - - - function urlEncodeIfNecessary(s) { - var regex = /[\\\"<>\.;]/; - var hasBadChars = regex.exec(s) != null; - return hasBadChars && typeof encodeURIComponent != UNDEF ? encodeURIComponent(s) : s; - } - - - var cleanup = function() { - if (ua.ie && ua.win) { - window.attachEvent("onunload", function() { - - var ll = listenersArr.length; - for (var i = 0; i < ll; i++) { - listenersArr[i][0].detachEvent(listenersArr[i][1], listenersArr[i][2]); - } - - var il = objIdArr.length; - for (var j = 0; j < il; j++) { - removeSWF(objIdArr[j]); - } - - for (var k in ua) { - ua[k] = null; - } - ua = null; - for (var l in swfobject) { - swfobject[l] = null; - } - swfobject = null; - window.detachEvent('onunload', arguments.callee); - }); - } - }(); - - return { - - registerObject: function(objectIdStr, swfVersionStr, xiSwfUrlStr, callbackFn) { - if (ua.w3 && objectIdStr && swfVersionStr) { - var regObj = {}; - regObj.id = objectIdStr; - regObj.swfVersion = swfVersionStr; - regObj.expressInstall = xiSwfUrlStr; - regObj.callbackFn = callbackFn; - regObjArr[regObjArr.length] = regObj; - setVisibility(objectIdStr, false); - } - else if (callbackFn) { - callbackFn({success:false, id:objectIdStr}); - } - }, - - getObjectById: function(objectIdStr) { - if (ua.w3) { - return getObjectById(objectIdStr); - } - }, - - embedSWF: function(swfUrlStr, replaceElemIdStr, widthStr, heightStr, swfVersionStr, xiSwfUrlStr, flashvarsObj, parObj, attObj, callbackFn) { - var callbackObj = {success:false, id:replaceElemIdStr}; - if (ua.w3 && !(ua.wk && ua.wk < 312) && swfUrlStr && replaceElemIdStr && widthStr && heightStr && swfVersionStr) { - setVisibility(replaceElemIdStr, false); - addDomLoadEvent(function() { - widthStr += ""; - heightStr += ""; - var att = {}; - if (attObj && typeof attObj === OBJECT) { - for (var i in attObj) { - att[i] = attObj[i]; - } - } - att.data = swfUrlStr; - att.width = widthStr; - att.height = heightStr; - var par = {}; - if (parObj && typeof parObj === OBJECT) { - for (var j in parObj) { - par[j] = parObj[j]; - } - } - if (flashvarsObj && typeof flashvarsObj === OBJECT) { - for (var k in flashvarsObj) { - if (typeof par.flashvars != UNDEF) { - par.flashvars += "&" + k + "=" + flashvarsObj[k]; - } - else { - par.flashvars = k + "=" + flashvarsObj[k]; - } - } - } - if (hasPlayerVersion(swfVersionStr)) { - var obj = createSWF(att, par, replaceElemIdStr); - if (att.id == replaceElemIdStr) { - setVisibility(replaceElemIdStr, true); - } - callbackObj.success = true; - callbackObj.ref = obj; - } - else if (xiSwfUrlStr && canExpressInstall()) { - att.data = xiSwfUrlStr; - showExpressInstall(att, par, replaceElemIdStr, callbackFn); - return; - } - else { - setVisibility(replaceElemIdStr, true); - } - if (callbackFn) { callbackFn(callbackObj); } - }); - } - else if (callbackFn) { callbackFn(callbackObj); } - }, - - switchOffAutoHideShow: function() { - autoHideShow = false; - }, - - ua: ua, - - getFlashPlayerVersion: function() { - return { major:ua.pv[0], minor:ua.pv[1], release:ua.pv[2] }; - }, - - hasFlashPlayerVersion: hasPlayerVersion, - - createSWF: function(attObj, parObj, replaceElemIdStr) { - if (ua.w3) { - return createSWF(attObj, parObj, replaceElemIdStr); - } - else { - return undefined; - } - }, - - showExpressInstall: function(att, par, replaceElemIdStr, callbackFn) { - if (ua.w3 && canExpressInstall()) { - showExpressInstall(att, par, replaceElemIdStr, callbackFn); - } - }, - - removeSWF: function(objElemIdStr) { - if (ua.w3) { - removeSWF(objElemIdStr); - } - }, - - createCSS: function(selStr, declStr, mediaStr, newStyleBoolean) { - if (ua.w3) { - createCSS(selStr, declStr, mediaStr, newStyleBoolean); - } - }, - - addDomLoadEvent: addDomLoadEvent, - - addLoadEvent: addLoadEvent, - - getQueryParamValue: function(param) { - var q = doc.location.search || doc.location.hash; - if (q) { - if (/\?/.test(q)) { q = q.split("?")[1]; } - if (param == null) { - return urlEncodeIfNecessary(q); - } - var pairs = q.split("&"); - for (var i = 0; i < pairs.length; i++) { - if (pairs[i].substring(0, pairs[i].indexOf("=")) == param) { - return urlEncodeIfNecessary(pairs[i].substring((pairs[i].indexOf("=") + 1))); - } - } - } - return ""; - }, - - - expressInstallCallback: function() { - if (isExpressInstallActive) { - var obj = getElementById(EXPRESS_INSTALL_ID); - if (obj && storedAltContent) { - obj.parentNode.replaceChild(storedAltContent, obj); - if (storedAltContentId) { - setVisibility(storedAltContentId, true); - if (ua.ie && ua.win) { storedAltContent.style.display = "block"; } - } - if (storedCallbackFn) { storedCallbackFn(storedCallbackObj); } - } - isExpressInstallActive = false; - } - } - }; -}(); - -Ext.FlashComponent = Ext.extend(Ext.BoxComponent, { - - flashVersion : '9.0.115', - - - backgroundColor: '#ffffff', - - - wmode: 'opaque', - - - flashVars: undefined, - - - flashParams: undefined, - - - url: undefined, - swfId : undefined, - swfWidth: '100%', - swfHeight: '100%', - - - expressInstall: false, - - initComponent : function(){ - Ext.FlashComponent.superclass.initComponent.call(this); - - this.addEvents( - - 'initialize' - ); - }, - - onRender : function(){ - Ext.FlashComponent.superclass.onRender.apply(this, arguments); - - var params = Ext.apply({ - allowScriptAccess: 'always', - bgcolor: this.backgroundColor, - wmode: this.wmode - }, this.flashParams), vars = Ext.apply({ - allowedDomain: document.location.hostname, - YUISwfId: this.getId(), - YUIBridgeCallback: 'Ext.FlashEventProxy.onEvent' - }, this.flashVars); - - new swfobject.embedSWF(this.url, this.id, this.swfWidth, this.swfHeight, this.flashVersion, - this.expressInstall ? Ext.FlashComponent.EXPRESS_INSTALL_URL : undefined, vars, params); - - this.swf = Ext.getDom(this.id); - this.el = Ext.get(this.swf); - }, - - getSwfId : function(){ - return this.swfId || (this.swfId = "extswf" + (++Ext.Component.AUTO_ID)); - }, - - getId : function(){ - return this.id || (this.id = "extflashcmp" + (++Ext.Component.AUTO_ID)); - }, - - onFlashEvent : function(e){ - switch(e.type){ - case "swfReady": - this.initSwf(); - return; - case "log": - return; - } - e.component = this; - this.fireEvent(e.type.toLowerCase().replace(/event$/, ''), e); - }, - - initSwf : function(){ - this.onSwfReady(!!this.isInitialized); - this.isInitialized = true; - this.fireEvent('initialize', this); - }, - - beforeDestroy: function(){ - if(this.rendered){ - swfobject.removeSWF(this.swf.id); - } - Ext.FlashComponent.superclass.beforeDestroy.call(this); - }, - - onSwfReady : Ext.emptyFn -}); - - -Ext.FlashComponent.EXPRESS_INSTALL_URL = 'http:/' + '/swfobject.googlecode.com/svn/trunk/swfobject/expressInstall.swf'; - -Ext.reg('flash', Ext.FlashComponent); -Ext.FlashEventProxy = { - onEvent : function(id, e){ - var fp = Ext.getCmp(id); - if(fp){ - fp.onFlashEvent(e); - }else{ - arguments.callee.defer(10, this, [id, e]); - } - } -}; - - Ext.chart.Chart = Ext.extend(Ext.FlashComponent, { - refreshBuffer: 100, - - - - - chartStyle: { - padding: 10, - animationEnabled: true, - font: { - name: 'Tahoma', - color: 0x444444, - size: 11 - }, - dataTip: { - padding: 5, - border: { - color: 0x99bbe8, - size:1 - }, - background: { - color: 0xDAE7F6, - alpha: .9 - }, - font: { - name: 'Tahoma', - color: 0x15428B, - size: 10, - bold: true - } - } - }, - - - - - extraStyle: null, - - - seriesStyles: null, - - - disableCaching: Ext.isIE || Ext.isOpera, - disableCacheParam: '_dc', - - initComponent : function(){ - Ext.chart.Chart.superclass.initComponent.call(this); - if(!this.url){ - this.url = Ext.chart.Chart.CHART_URL; - } - if(this.disableCaching){ - this.url = Ext.urlAppend(this.url, String.format('{0}={1}', this.disableCacheParam, new Date().getTime())); - } - this.addEvents( - 'itemmouseover', - 'itemmouseout', - 'itemclick', - 'itemdoubleclick', - 'itemdragstart', - 'itemdrag', - 'itemdragend', - - 'beforerefresh', - - 'refresh' - ); - this.store = Ext.StoreMgr.lookup(this.store); - }, - - - setStyle: function(name, value){ - this.swf.setStyle(name, Ext.encode(value)); - }, - - - setStyles: function(styles){ - this.swf.setStyles(Ext.encode(styles)); - }, - - - setSeriesStyles: function(styles){ - this.seriesStyles = styles; - var s = []; - Ext.each(styles, function(style){ - s.push(Ext.encode(style)); - }); - this.swf.setSeriesStyles(s); - }, - - setCategoryNames : function(names){ - this.swf.setCategoryNames(names); - }, - - setLegendRenderer : function(fn, scope){ - var chart = this; - scope = scope || chart; - chart.removeFnProxy(chart.legendFnName); - chart.legendFnName = chart.createFnProxy(function(name){ - return fn.call(scope, name); - }); - chart.swf.setLegendLabelFunction(chart.legendFnName); - }, - - setTipRenderer : function(fn, scope){ - var chart = this; - scope = scope || chart; - chart.removeFnProxy(chart.tipFnName); - chart.tipFnName = chart.createFnProxy(function(item, index, series){ - var record = chart.store.getAt(index); - return fn.call(scope, chart, record, index, series); - }); - chart.swf.setDataTipFunction(chart.tipFnName); - }, - - setSeries : function(series){ - this.series = series; - this.refresh(); - }, - - - bindStore : function(store, initial){ - if(!initial && this.store){ - if(store !== this.store && this.store.autoDestroy){ - this.store.destroy(); - }else{ - this.store.un("datachanged", this.refresh, this); - this.store.un("add", this.delayRefresh, this); - this.store.un("remove", this.delayRefresh, this); - this.store.un("update", this.delayRefresh, this); - this.store.un("clear", this.refresh, this); - } - } - if(store){ - store = Ext.StoreMgr.lookup(store); - store.on({ - scope: this, - datachanged: this.refresh, - add: this.delayRefresh, - remove: this.delayRefresh, - update: this.delayRefresh, - clear: this.refresh - }); - } - this.store = store; - if(store && !initial){ - this.refresh(); - } - }, - - onSwfReady : function(isReset){ - Ext.chart.Chart.superclass.onSwfReady.call(this, isReset); - var ref; - this.swf.setType(this.type); - - if(this.chartStyle){ - this.setStyles(Ext.apply({}, this.extraStyle, this.chartStyle)); - } - - if(this.categoryNames){ - this.setCategoryNames(this.categoryNames); - } - - if(this.tipRenderer){ - ref = this.getFunctionRef(this.tipRenderer); - this.setTipRenderer(ref.fn, ref.scope); - } - if(this.legendRenderer){ - ref = this.getFunctionRef(this.legendRenderer); - this.setLegendRenderer(ref.fn, ref.scope); - } - if(!isReset){ - this.bindStore(this.store, true); - } - this.refresh.defer(10, this); - }, - - delayRefresh : function(){ - if(!this.refreshTask){ - this.refreshTask = new Ext.util.DelayedTask(this.refresh, this); - } - this.refreshTask.delay(this.refreshBuffer); - }, - - refresh : function(){ - if(this.fireEvent('beforerefresh', this) !== false){ - var styleChanged = false; - - var data = [], rs = this.store.data.items; - for(var j = 0, len = rs.length; j < len; j++){ - data[j] = rs[j].data; - } - - - var dataProvider = []; - var seriesCount = 0; - var currentSeries = null; - var i = 0; - if(this.series){ - seriesCount = this.series.length; - for(i = 0; i < seriesCount; i++){ - currentSeries = this.series[i]; - var clonedSeries = {}; - for(var prop in currentSeries){ - if(prop == "style" && currentSeries.style !== null){ - clonedSeries.style = Ext.encode(currentSeries.style); - styleChanged = true; - - - - - } else{ - clonedSeries[prop] = currentSeries[prop]; - } - } - dataProvider.push(clonedSeries); - } - } - - if(seriesCount > 0){ - for(i = 0; i < seriesCount; i++){ - currentSeries = dataProvider[i]; - if(!currentSeries.type){ - currentSeries.type = this.type; - } - currentSeries.dataProvider = data; - } - } else{ - dataProvider.push({type: this.type, dataProvider: data}); - } - this.swf.setDataProvider(dataProvider); - if(this.seriesStyles){ - this.setSeriesStyles(this.seriesStyles); - } - this.fireEvent('refresh', this); - } - }, - - - createFnProxy : function(fn){ - var fnName = 'extFnProxy' + (++Ext.chart.Chart.PROXY_FN_ID); - Ext.chart.Chart.proxyFunction[fnName] = fn; - return 'Ext.chart.Chart.proxyFunction.' + fnName; - }, - - - removeFnProxy : function(fn){ - if(!Ext.isEmpty(fn)){ - fn = fn.replace('Ext.chart.Chart.proxyFunction.', ''); - delete Ext.chart.Chart.proxyFunction[fn]; - } - }, - - - getFunctionRef : function(val){ - if(Ext.isFunction(val)){ - return { - fn: val, - scope: this - }; - }else{ - return { - fn: val.fn, - scope: val.scope || this - }; - } - }, - - - onDestroy: function(){ - if (this.refreshTask && this.refreshTask.cancel){ - this.refreshTask.cancel(); - } - Ext.chart.Chart.superclass.onDestroy.call(this); - this.bindStore(null); - this.removeFnProxy(this.tipFnName); - this.removeFnProxy(this.legendFnName); - } -}); -Ext.reg('chart', Ext.chart.Chart); -Ext.chart.Chart.PROXY_FN_ID = 0; -Ext.chart.Chart.proxyFunction = {}; - - -Ext.chart.Chart.CHART_URL = 'http:/' + '/yui.yahooapis.com/2.8.2/build/charts/assets/charts.swf'; - - -Ext.chart.PieChart = Ext.extend(Ext.chart.Chart, { - type: 'pie', - - onSwfReady : function(isReset){ - Ext.chart.PieChart.superclass.onSwfReady.call(this, isReset); - - this.setDataField(this.dataField); - this.setCategoryField(this.categoryField); - }, - - setDataField : function(field){ - this.dataField = field; - this.swf.setDataField(field); - }, - - setCategoryField : function(field){ - this.categoryField = field; - this.swf.setCategoryField(field); - } -}); -Ext.reg('piechart', Ext.chart.PieChart); - - -Ext.chart.CartesianChart = Ext.extend(Ext.chart.Chart, { - onSwfReady : function(isReset){ - Ext.chart.CartesianChart.superclass.onSwfReady.call(this, isReset); - this.labelFn = []; - if(this.xField){ - this.setXField(this.xField); - } - if(this.yField){ - this.setYField(this.yField); - } - if(this.xAxis){ - this.setXAxis(this.xAxis); - } - if(this.xAxes){ - this.setXAxes(this.xAxes); - } - if(this.yAxis){ - this.setYAxis(this.yAxis); - } - if(this.yAxes){ - this.setYAxes(this.yAxes); - } - if(Ext.isDefined(this.constrainViewport)){ - this.swf.setConstrainViewport(this.constrainViewport); - } - }, - - setXField : function(value){ - this.xField = value; - this.swf.setHorizontalField(value); - }, - - setYField : function(value){ - this.yField = value; - this.swf.setVerticalField(value); - }, - - setXAxis : function(value){ - this.xAxis = this.createAxis('xAxis', value); - this.swf.setHorizontalAxis(this.xAxis); - }, - - setXAxes : function(value){ - var axis; - for(var i = 0; i < value.length; i++) { - axis = this.createAxis('xAxis' + i, value[i]); - this.swf.setHorizontalAxis(axis); - } - }, - - setYAxis : function(value){ - this.yAxis = this.createAxis('yAxis', value); - this.swf.setVerticalAxis(this.yAxis); - }, - - setYAxes : function(value){ - var axis; - for(var i = 0; i < value.length; i++) { - axis = this.createAxis('yAxis' + i, value[i]); - this.swf.setVerticalAxis(axis); - } - }, - - createAxis : function(axis, value){ - var o = Ext.apply({}, value), - ref, - old; - - if(this[axis]){ - old = this[axis].labelFunction; - this.removeFnProxy(old); - this.labelFn.remove(old); - } - if(o.labelRenderer){ - ref = this.getFunctionRef(o.labelRenderer); - o.labelFunction = this.createFnProxy(function(v){ - return ref.fn.call(ref.scope, v); - }); - delete o.labelRenderer; - this.labelFn.push(o.labelFunction); - } - if(axis.indexOf('xAxis') > -1 && o.position == 'left'){ - o.position = 'bottom'; - } - return o; - }, - - onDestroy : function(){ - Ext.chart.CartesianChart.superclass.onDestroy.call(this); - Ext.each(this.labelFn, function(fn){ - this.removeFnProxy(fn); - }, this); - } -}); -Ext.reg('cartesianchart', Ext.chart.CartesianChart); - - -Ext.chart.LineChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'line' -}); -Ext.reg('linechart', Ext.chart.LineChart); - - -Ext.chart.ColumnChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'column' -}); -Ext.reg('columnchart', Ext.chart.ColumnChart); - - -Ext.chart.StackedColumnChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'stackcolumn' -}); -Ext.reg('stackedcolumnchart', Ext.chart.StackedColumnChart); - - -Ext.chart.BarChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'bar' -}); -Ext.reg('barchart', Ext.chart.BarChart); - - -Ext.chart.StackedBarChart = Ext.extend(Ext.chart.CartesianChart, { - type: 'stackbar' -}); -Ext.reg('stackedbarchart', Ext.chart.StackedBarChart); - - - - -Ext.chart.Axis = function(config){ - Ext.apply(this, config); -}; - -Ext.chart.Axis.prototype = -{ - - type: null, - - - orientation: "horizontal", - - - reverse: false, - - - labelFunction: null, - - - hideOverlappingLabels: true, - - - labelSpacing: 2 -}; - - -Ext.chart.NumericAxis = Ext.extend(Ext.chart.Axis, { - type: "numeric", - - - minimum: NaN, - - - maximum: NaN, - - - majorUnit: NaN, - - - minorUnit: NaN, - - - snapToUnits: true, - - - alwaysShowZero: true, - - - scale: "linear", - - - roundMajorUnit: true, - - - calculateByLabelSize: true, - - - position: 'left', - - - adjustMaximumByMajorUnit: true, - - - adjustMinimumByMajorUnit: true - -}); - - -Ext.chart.TimeAxis = Ext.extend(Ext.chart.Axis, { - type: "time", - - - minimum: null, - - - maximum: null, - - - majorUnit: NaN, - - - majorTimeUnit: null, - - - minorUnit: NaN, - - - minorTimeUnit: null, - - - snapToUnits: true, - - - stackingEnabled: false, - - - calculateByLabelSize: true - -}); - - -Ext.chart.CategoryAxis = Ext.extend(Ext.chart.Axis, { - type: "category", - - - categoryNames: null, - - - calculateCategoryCount: false - -}); - - -Ext.chart.Series = function(config) { Ext.apply(this, config); }; - -Ext.chart.Series.prototype = -{ - - type: null, - - - displayName: null -}; - - -Ext.chart.CartesianSeries = Ext.extend(Ext.chart.Series, { - - xField: null, - - - yField: null, - - - showInLegend: true, - - - axis: 'primary' -}); - - -Ext.chart.ColumnSeries = Ext.extend(Ext.chart.CartesianSeries, { - type: "column" -}); - - -Ext.chart.LineSeries = Ext.extend(Ext.chart.CartesianSeries, { - type: "line" -}); - - -Ext.chart.BarSeries = Ext.extend(Ext.chart.CartesianSeries, { - type: "bar" -}); - - - -Ext.chart.PieSeries = Ext.extend(Ext.chart.Series, { - type: "pie", - dataField: null, - categoryField: null -}); -Ext.menu.Menu = Ext.extend(Ext.Container, { - - - - minWidth : 120, - - shadow : 'sides', - - subMenuAlign : 'tl-tr?', - - defaultAlign : 'tl-bl?', - - allowOtherMenus : false, - - ignoreParentClicks : false, - - enableScrolling : true, - - maxHeight : null, - - scrollIncrement : 24, - - showSeparator : true, - - defaultOffsets : [0, 0], - - - plain : false, - - - floating : true, - - - - zIndex: 15000, - - - hidden : true, - - - layout : 'menu', - hideMode : 'offsets', - scrollerHeight : 8, - autoLayout : true, - defaultType : 'menuitem', - bufferResize : false, - - initComponent : function(){ - if(Ext.isArray(this.initialConfig)){ - Ext.apply(this, {items:this.initialConfig}); - } - this.addEvents( - - 'click', - - 'mouseover', - - 'mouseout', - - 'itemclick' - ); - Ext.menu.MenuMgr.register(this); - if(this.floating){ - Ext.EventManager.onWindowResize(this.hide, this); - }else{ - if(this.initialConfig.hidden !== false){ - this.hidden = false; - } - this.internalDefaults = {hideOnClick: false}; - } - Ext.menu.Menu.superclass.initComponent.call(this); - if(this.autoLayout){ - var fn = this.doLayout.createDelegate(this, []); - this.on({ - add: fn, - remove: fn - }); - } - }, - - - getLayoutTarget : function() { - return this.ul; - }, - - - onRender : function(ct, position){ - if(!ct){ - ct = Ext.getBody(); - } - - var dh = { - id: this.getId(), - cls: 'x-menu ' + ((this.floating) ? 'x-menu-floating x-layer ' : '') + (this.cls || '') + (this.plain ? ' x-menu-plain' : '') + (this.showSeparator ? '' : ' x-menu-nosep'), - style: this.style, - cn: [ - {tag: 'a', cls: 'x-menu-focus', href: '#', onclick: 'return false;', tabIndex: '-1'}, - {tag: 'ul', cls: 'x-menu-list'} - ] - }; - if(this.floating){ - this.el = new Ext.Layer({ - shadow: this.shadow, - dh: dh, - constrain: false, - parentEl: ct, - zindex: this.zIndex - }); - }else{ - this.el = ct.createChild(dh); - } - Ext.menu.Menu.superclass.onRender.call(this, ct, position); - - if(!this.keyNav){ - this.keyNav = new Ext.menu.MenuNav(this); - } - - this.focusEl = this.el.child('a.x-menu-focus'); - this.ul = this.el.child('ul.x-menu-list'); - this.mon(this.ul, { - scope: this, - click: this.onClick, - mouseover: this.onMouseOver, - mouseout: this.onMouseOut - }); - if(this.enableScrolling){ - this.mon(this.el, { - scope: this, - delegate: '.x-menu-scroller', - click: this.onScroll, - mouseover: this.deactivateActive - }); - } - }, - - - findTargetItem : function(e){ - var t = e.getTarget('.x-menu-list-item', this.ul, true); - if(t && t.menuItemId){ - return this.items.get(t.menuItemId); - } - }, - - - onClick : function(e){ - var t = this.findTargetItem(e); - if(t){ - if(t.isFormField){ - this.setActiveItem(t); - }else if(t instanceof Ext.menu.BaseItem){ - if(t.menu && this.ignoreParentClicks){ - t.expandMenu(); - e.preventDefault(); - }else if(t.onClick){ - t.onClick(e); - this.fireEvent('click', this, t, e); - } - } - } - }, - - - setActiveItem : function(item, autoExpand){ - if(item != this.activeItem){ - this.deactivateActive(); - if((this.activeItem = item).isFormField){ - item.focus(); - }else{ - item.activate(autoExpand); - } - }else if(autoExpand){ - item.expandMenu(); - } - }, - - deactivateActive : function(){ - var a = this.activeItem; - if(a){ - if(a.isFormField){ - - if(a.collapse){ - a.collapse(); - } - }else{ - a.deactivate(); - } - delete this.activeItem; - } - }, - - - tryActivate : function(start, step){ - var items = this.items; - for(var i = start, len = items.length; i >= 0 && i < len; i+= step){ - var item = items.get(i); - if(item.isVisible() && !item.disabled && (item.canActivate || item.isFormField)){ - this.setActiveItem(item, false); - return item; - } - } - return false; - }, - - - onMouseOver : function(e){ - var t = this.findTargetItem(e); - if(t){ - if(t.canActivate && !t.disabled){ - this.setActiveItem(t, true); - } - } - this.over = true; - this.fireEvent('mouseover', this, e, t); - }, - - - onMouseOut : function(e){ - var t = this.findTargetItem(e); - if(t){ - if(t == this.activeItem && t.shouldDeactivate && t.shouldDeactivate(e)){ - this.activeItem.deactivate(); - delete this.activeItem; - } - } - this.over = false; - this.fireEvent('mouseout', this, e, t); - }, - - - onScroll : function(e, t){ - if(e){ - e.stopEvent(); - } - var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top'); - ul.scrollTop += this.scrollIncrement * (top ? -1 : 1); - if(top ? ul.scrollTop <= 0 : ul.scrollTop + this.activeMax >= ul.scrollHeight){ - this.onScrollerOut(null, t); - } - }, - - - onScrollerIn : function(e, t){ - var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top'); - if(top ? ul.scrollTop > 0 : ul.scrollTop + this.activeMax < ul.scrollHeight){ - Ext.fly(t).addClass(['x-menu-item-active', 'x-menu-scroller-active']); - } - }, - - - onScrollerOut : function(e, t){ - Ext.fly(t).removeClass(['x-menu-item-active', 'x-menu-scroller-active']); - }, - - - show : function(el, pos, parentMenu){ - if(this.floating){ - this.parentMenu = parentMenu; - if(!this.el){ - this.render(); - this.doLayout(false, true); - } - this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign, this.defaultOffsets), parentMenu); - }else{ - Ext.menu.Menu.superclass.show.call(this); - } - }, - - - showAt : function(xy, parentMenu){ - if(this.fireEvent('beforeshow', this) !== false){ - this.parentMenu = parentMenu; - if(!this.el){ - this.render(); - } - if(this.enableScrolling){ - - this.el.setXY(xy); - - xy[1] = this.constrainScroll(xy[1]); - xy = [this.el.adjustForConstraints(xy)[0], xy[1]]; - }else{ - - xy = this.el.adjustForConstraints(xy); - } - this.el.setXY(xy); - this.el.show(); - Ext.menu.Menu.superclass.onShow.call(this); - if(Ext.isIE){ - - this.fireEvent('autosize', this); - if(!Ext.isIE8){ - this.el.repaint(); - } - } - this.hidden = false; - this.focus(); - this.fireEvent('show', this); - } - }, - - constrainScroll : function(y){ - var max, full = this.ul.setHeight('auto').getHeight(), - returnY = y, normalY, parentEl, scrollTop, viewHeight; - if(this.floating){ - parentEl = Ext.fly(this.el.dom.parentNode); - scrollTop = parentEl.getScroll().top; - viewHeight = parentEl.getViewSize().height; - - - normalY = y - scrollTop; - max = this.maxHeight ? this.maxHeight : viewHeight - normalY; - if(full > viewHeight) { - max = viewHeight; - - returnY = y - normalY; - } else if(max < full) { - returnY = y - (full - max); - max = full; - } - }else{ - max = this.getHeight(); - } - - if (this.maxHeight){ - max = Math.min(this.maxHeight, max); - } - if(full > max && max > 0){ - this.activeMax = max - this.scrollerHeight * 2 - this.el.getFrameWidth('tb') - Ext.num(this.el.shadowOffset, 0); - this.ul.setHeight(this.activeMax); - this.createScrollers(); - this.el.select('.x-menu-scroller').setDisplayed(''); - }else{ - this.ul.setHeight(full); - this.el.select('.x-menu-scroller').setDisplayed('none'); - } - this.ul.dom.scrollTop = 0; - return returnY; - }, - - createScrollers : function(){ - if(!this.scroller){ - this.scroller = { - pos: 0, - top: this.el.insertFirst({ - tag: 'div', - cls: 'x-menu-scroller x-menu-scroller-top', - html: ' ' - }), - bottom: this.el.createChild({ - tag: 'div', - cls: 'x-menu-scroller x-menu-scroller-bottom', - html: ' ' - }) - }; - this.scroller.top.hover(this.onScrollerIn, this.onScrollerOut, this); - this.scroller.topRepeater = new Ext.util.ClickRepeater(this.scroller.top, { - listeners: { - click: this.onScroll.createDelegate(this, [null, this.scroller.top], false) - } - }); - this.scroller.bottom.hover(this.onScrollerIn, this.onScrollerOut, this); - this.scroller.bottomRepeater = new Ext.util.ClickRepeater(this.scroller.bottom, { - listeners: { - click: this.onScroll.createDelegate(this, [null, this.scroller.bottom], false) - } - }); - } - }, - - onLayout : function(){ - if(this.isVisible()){ - if(this.enableScrolling){ - this.constrainScroll(this.el.getTop()); - } - if(this.floating){ - this.el.sync(); - } - } - }, - - focus : function(){ - if(!this.hidden){ - this.doFocus.defer(50, this); - } - }, - - doFocus : function(){ - if(!this.hidden){ - this.focusEl.focus(); - } - }, - - - hide : function(deep){ - if (!this.isDestroyed) { - this.deepHide = deep; - Ext.menu.Menu.superclass.hide.call(this); - delete this.deepHide; - } - }, - - - onHide : function(){ - Ext.menu.Menu.superclass.onHide.call(this); - this.deactivateActive(); - if(this.el && this.floating){ - this.el.hide(); - } - var pm = this.parentMenu; - if(this.deepHide === true && pm){ - if(pm.floating){ - pm.hide(true); - }else{ - pm.deactivateActive(); - } - } - }, - - - lookupComponent : function(c){ - if(Ext.isString(c)){ - c = (c == 'separator' || c == '-') ? new Ext.menu.Separator() : new Ext.menu.TextItem(c); - this.applyDefaults(c); - }else{ - if(Ext.isObject(c)){ - c = this.getMenuItem(c); - }else if(c.tagName || c.el){ - c = new Ext.BoxComponent({ - el: c - }); - } - } - return c; - }, - - applyDefaults : function(c) { - if (!Ext.isString(c)) { - c = Ext.menu.Menu.superclass.applyDefaults.call(this, c); - var d = this.internalDefaults; - if(d){ - if(c.events){ - Ext.applyIf(c.initialConfig, d); - Ext.apply(c, d); - }else{ - Ext.applyIf(c, d); - } - } - } - return c; - }, - - - getMenuItem : function(config) { - config.ownerCt = this; - - if (!config.isXType) { - if (!config.xtype && Ext.isBoolean(config.checked)) { - return new Ext.menu.CheckItem(config); - } - return Ext.create(config, this.defaultType); - } - return config; - }, - - - addSeparator : function() { - return this.add(new Ext.menu.Separator()); - }, - - - addElement : function(el) { - return this.add(new Ext.menu.BaseItem({ - el: el - })); - }, - - - addItem : function(item) { - return this.add(item); - }, - - - addMenuItem : function(config) { - return this.add(this.getMenuItem(config)); - }, - - - addText : function(text){ - return this.add(new Ext.menu.TextItem(text)); - }, - - - onDestroy : function(){ - Ext.EventManager.removeResizeListener(this.hide, this); - var pm = this.parentMenu; - if(pm && pm.activeChild == this){ - delete pm.activeChild; - } - delete this.parentMenu; - Ext.menu.Menu.superclass.onDestroy.call(this); - Ext.menu.MenuMgr.unregister(this); - if(this.keyNav) { - this.keyNav.disable(); - } - var s = this.scroller; - if(s){ - Ext.destroy(s.topRepeater, s.bottomRepeater, s.top, s.bottom); - } - Ext.destroy( - this.el, - this.focusEl, - this.ul - ); - } -}); - -Ext.reg('menu', Ext.menu.Menu); - - -Ext.menu.MenuNav = Ext.extend(Ext.KeyNav, function(){ - function up(e, m){ - if(!m.tryActivate(m.items.indexOf(m.activeItem)-1, -1)){ - m.tryActivate(m.items.length-1, -1); - } - } - function down(e, m){ - if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){ - m.tryActivate(0, 1); - } - } - return { - constructor : function(menu){ - Ext.menu.MenuNav.superclass.constructor.call(this, menu.el); - this.scope = this.menu = menu; - }, - - doRelay : function(e, h){ - var k = e.getKey(); - - if (this.menu.activeItem && this.menu.activeItem.isFormField && k != e.TAB) { - return false; - } - if(!this.menu.activeItem && e.isNavKeyPress() && k != e.SPACE && k != e.RETURN){ - this.menu.tryActivate(0, 1); - return false; - } - return h.call(this.scope || this, e, this.menu); - }, - - tab: function(e, m) { - e.stopEvent(); - if (e.shiftKey) { - up(e, m); - } else { - down(e, m); - } - }, - - up : up, - - down : down, - - right : function(e, m){ - if(m.activeItem){ - m.activeItem.expandMenu(true); - } - }, - - left : function(e, m){ - m.hide(); - if(m.parentMenu && m.parentMenu.activeItem){ - m.parentMenu.activeItem.activate(); - } - }, - - enter : function(e, m){ - if(m.activeItem){ - e.stopPropagation(); - m.activeItem.onClick(e); - m.fireEvent('click', this, m.activeItem); - return true; - } - } - }; -}()); - -Ext.menu.MenuMgr = function(){ - var menus, - active, - map, - groups = {}, - attached = false, - lastShow = new Date(); - - - - function init(){ - menus = {}; - active = new Ext.util.MixedCollection(); - map = Ext.getDoc().addKeyListener(27, hideAll); - map.disable(); - } - - - function hideAll(){ - if(active && active.length > 0){ - var c = active.clone(); - c.each(function(m){ - m.hide(); - }); - return true; - } - return false; - } - - - function onHide(m){ - active.remove(m); - if(active.length < 1){ - map.disable(); - Ext.getDoc().un("mousedown", onMouseDown); - attached = false; - } - } - - - function onShow(m){ - var last = active.last(); - lastShow = new Date(); - active.add(m); - if(!attached){ - map.enable(); - Ext.getDoc().on("mousedown", onMouseDown); - attached = true; - } - if(m.parentMenu){ - m.getEl().setZIndex(parseInt(m.parentMenu.getEl().getStyle("z-index"), 10) + 3); - m.parentMenu.activeChild = m; - }else if(last && !last.isDestroyed && last.isVisible()){ - m.getEl().setZIndex(parseInt(last.getEl().getStyle("z-index"), 10) + 3); - } - } - - - function onBeforeHide(m){ - if(m.activeChild){ - m.activeChild.hide(); - } - if(m.autoHideTimer){ - clearTimeout(m.autoHideTimer); - delete m.autoHideTimer; - } - } - - - function onBeforeShow(m){ - var pm = m.parentMenu; - if(!pm && !m.allowOtherMenus){ - hideAll(); - }else if(pm && pm.activeChild){ - pm.activeChild.hide(); - } - } - - - function onMouseDown(e){ - if(lastShow.getElapsed() > 50 && active.length > 0 && !e.getTarget(".x-menu")){ - hideAll(); - } - } - - return { - - - hideAll : function(){ - return hideAll(); - }, - - - register : function(menu){ - if(!menus){ - init(); - } - menus[menu.id] = menu; - menu.on({ - beforehide: onBeforeHide, - hide: onHide, - beforeshow: onBeforeShow, - show: onShow - }); - }, - - - get : function(menu){ - if(typeof menu == "string"){ - if(!menus){ - return null; - } - return menus[menu]; - }else if(menu.events){ - return menu; - }else if(typeof menu.length == 'number'){ - return new Ext.menu.Menu({items:menu}); - }else{ - return Ext.create(menu, 'menu'); - } - }, - - - unregister : function(menu){ - delete menus[menu.id]; - menu.un("beforehide", onBeforeHide); - menu.un("hide", onHide); - menu.un("beforeshow", onBeforeShow); - menu.un("show", onShow); - }, - - - registerCheckable : function(menuItem){ - var g = menuItem.group; - if(g){ - if(!groups[g]){ - groups[g] = []; - } - groups[g].push(menuItem); - } - }, - - - unregisterCheckable : function(menuItem){ - var g = menuItem.group; - if(g){ - groups[g].remove(menuItem); - } - }, - - - onCheckChange: function(item, state){ - if(item.group && state){ - var group = groups[item.group], - i = 0, - len = group.length, - current; - - for(; i < len; i++){ - current = group[i]; - if(current != item){ - current.setChecked(false); - } - } - } - }, - - getCheckedItem : function(groupId){ - var g = groups[groupId]; - if(g){ - for(var i = 0, l = g.length; i < l; i++){ - if(g[i].checked){ - return g[i]; - } - } - } - return null; - }, - - setCheckedItem : function(groupId, itemId){ - var g = groups[groupId]; - if(g){ - for(var i = 0, l = g.length; i < l; i++){ - if(g[i].id == itemId){ - g[i].setChecked(true); - } - } - } - return null; - } - }; -}(); - -Ext.menu.BaseItem = Ext.extend(Ext.Component, { - - - - - canActivate : false, - - activeClass : "x-menu-item-active", - - hideOnClick : true, - - clickHideDelay : 1, - - - ctype : "Ext.menu.BaseItem", - - - actionMode : "container", - - initComponent : function(){ - Ext.menu.BaseItem.superclass.initComponent.call(this); - this.addEvents( - - 'click', - - 'activate', - - 'deactivate' - ); - if(this.handler){ - this.on("click", this.handler, this.scope); - } - }, - - - onRender : function(container, position){ - Ext.menu.BaseItem.superclass.onRender.apply(this, arguments); - if(this.ownerCt && this.ownerCt instanceof Ext.menu.Menu){ - this.parentMenu = this.ownerCt; - }else{ - this.container.addClass('x-menu-list-item'); - this.mon(this.el, { - scope: this, - click: this.onClick, - mouseenter: this.activate, - mouseleave: this.deactivate - }); - } - }, - - - setHandler : function(handler, scope){ - if(this.handler){ - this.un("click", this.handler, this.scope); - } - this.on("click", this.handler = handler, this.scope = scope); - }, - - - onClick : function(e){ - if(!this.disabled && this.fireEvent("click", this, e) !== false - && (this.parentMenu && this.parentMenu.fireEvent("itemclick", this, e) !== false)){ - this.handleClick(e); - }else{ - e.stopEvent(); - } - }, - - - activate : function(){ - if(this.disabled){ - return false; - } - var li = this.container; - li.addClass(this.activeClass); - this.region = li.getRegion().adjust(2, 2, -2, -2); - this.fireEvent("activate", this); - return true; - }, - - - deactivate : function(){ - this.container.removeClass(this.activeClass); - this.fireEvent("deactivate", this); - }, - - - shouldDeactivate : function(e){ - return !this.region || !this.region.contains(e.getPoint()); - }, - - - handleClick : function(e){ - var pm = this.parentMenu; - if(this.hideOnClick){ - if(pm.floating){ - this.clickHideDelayTimer = pm.hide.defer(this.clickHideDelay, pm, [true]); - }else{ - pm.deactivateActive(); - } - } - }, - - beforeDestroy: function(){ - clearTimeout(this.clickHideDelayTimer); - Ext.menu.BaseItem.superclass.beforeDestroy.call(this); - }, - - - expandMenu : Ext.emptyFn, - - - hideMenu : Ext.emptyFn -}); -Ext.reg('menubaseitem', Ext.menu.BaseItem); -Ext.menu.TextItem = Ext.extend(Ext.menu.BaseItem, { - - - hideOnClick : false, - - itemCls : "x-menu-text", - - constructor : function(config) { - if (typeof config == 'string') { - config = { - text: config - }; - } - Ext.menu.TextItem.superclass.constructor.call(this, config); - }, - - - onRender : function() { - var s = document.createElement("span"); - s.className = this.itemCls; - s.innerHTML = this.text; - this.el = s; - Ext.menu.TextItem.superclass.onRender.apply(this, arguments); - } -}); -Ext.reg('menutextitem', Ext.menu.TextItem); -Ext.menu.Separator = Ext.extend(Ext.menu.BaseItem, { - - itemCls : "x-menu-sep", - - hideOnClick : false, - - - activeClass: '', - - - onRender : function(li){ - var s = document.createElement("span"); - s.className = this.itemCls; - s.innerHTML = " "; - this.el = s; - li.addClass("x-menu-sep-li"); - Ext.menu.Separator.superclass.onRender.apply(this, arguments); - } -}); -Ext.reg('menuseparator', Ext.menu.Separator); -Ext.menu.Item = Ext.extend(Ext.menu.BaseItem, { - - - - - - - - - itemCls : 'x-menu-item', - - canActivate : true, - - showDelay: 200, - - - altText: '', - - - hideDelay: 200, - - - ctype: 'Ext.menu.Item', - - initComponent : function(){ - Ext.menu.Item.superclass.initComponent.call(this); - if(this.menu){ - - - if (Ext.isArray(this.menu)){ - this.menu = { items: this.menu }; - } - - - - if (Ext.isObject(this.menu)){ - this.menu.ownerCt = this; - } - - this.menu = Ext.menu.MenuMgr.get(this.menu); - this.menu.ownerCt = undefined; - } - }, - - - onRender : function(container, position){ - if (!this.itemTpl) { - this.itemTpl = Ext.menu.Item.prototype.itemTpl = new Ext.XTemplate( - '', - ' target="{hrefTarget}"', - '', - '>', - '{altText}', - '{text}', - '' - ); - } - var a = this.getTemplateArgs(); - this.el = position ? this.itemTpl.insertBefore(position, a, true) : this.itemTpl.append(container, a, true); - this.iconEl = this.el.child('img.x-menu-item-icon'); - this.textEl = this.el.child('.x-menu-item-text'); - if(!this.href) { - this.mon(this.el, 'click', Ext.emptyFn, null, { preventDefault: true }); - } - Ext.menu.Item.superclass.onRender.call(this, container, position); - }, - - getTemplateArgs: function() { - return { - id: this.id, - cls: this.itemCls + (this.menu ? ' x-menu-item-arrow' : '') + (this.cls ? ' ' + this.cls : ''), - href: this.href || '#', - hrefTarget: this.hrefTarget, - icon: this.icon || Ext.BLANK_IMAGE_URL, - iconCls: this.iconCls || '', - text: this.itemText||this.text||' ', - altText: this.altText || '' - }; - }, - - - setText : function(text){ - this.text = text||' '; - if(this.rendered){ - this.textEl.update(this.text); - this.parentMenu.layout.doAutoSize(); - } - }, - - - setIconClass : function(cls){ - var oldCls = this.iconCls; - this.iconCls = cls; - if(this.rendered){ - this.iconEl.replaceClass(oldCls, this.iconCls); - } - }, - - - beforeDestroy: function(){ - clearTimeout(this.showTimer); - clearTimeout(this.hideTimer); - if (this.menu){ - delete this.menu.ownerCt; - this.menu.destroy(); - } - Ext.menu.Item.superclass.beforeDestroy.call(this); - }, - - - handleClick : function(e){ - if(!this.href){ - e.stopEvent(); - } - Ext.menu.Item.superclass.handleClick.apply(this, arguments); - }, - - - activate : function(autoExpand){ - if(Ext.menu.Item.superclass.activate.apply(this, arguments)){ - this.focus(); - if(autoExpand){ - this.expandMenu(); - } - } - return true; - }, - - - shouldDeactivate : function(e){ - if(Ext.menu.Item.superclass.shouldDeactivate.call(this, e)){ - if(this.menu && this.menu.isVisible()){ - return !this.menu.getEl().getRegion().contains(e.getPoint()); - } - return true; - } - return false; - }, - - - deactivate : function(){ - Ext.menu.Item.superclass.deactivate.apply(this, arguments); - this.hideMenu(); - }, - - - expandMenu : function(autoActivate){ - if(!this.disabled && this.menu){ - clearTimeout(this.hideTimer); - delete this.hideTimer; - if(!this.menu.isVisible() && !this.showTimer){ - this.showTimer = this.deferExpand.defer(this.showDelay, this, [autoActivate]); - }else if (this.menu.isVisible() && autoActivate){ - this.menu.tryActivate(0, 1); - } - } - }, - - - deferExpand : function(autoActivate){ - delete this.showTimer; - this.menu.show(this.container, this.parentMenu.subMenuAlign || 'tl-tr?', this.parentMenu); - if(autoActivate){ - this.menu.tryActivate(0, 1); - } - }, - - - hideMenu : function(){ - clearTimeout(this.showTimer); - delete this.showTimer; - if(!this.hideTimer && this.menu && this.menu.isVisible()){ - this.hideTimer = this.deferHide.defer(this.hideDelay, this); - } - }, - - - deferHide : function(){ - delete this.hideTimer; - if(this.menu.over){ - this.parentMenu.setActiveItem(this, false); - }else{ - this.menu.hide(); - } - } -}); -Ext.reg('menuitem', Ext.menu.Item); -Ext.menu.CheckItem = Ext.extend(Ext.menu.Item, { - - - itemCls : "x-menu-item x-menu-check-item", - - groupClass : "x-menu-group-item", - - - checked: false, - - - ctype: "Ext.menu.CheckItem", - - initComponent : function(){ - Ext.menu.CheckItem.superclass.initComponent.call(this); - this.addEvents( - - "beforecheckchange" , - - "checkchange" - ); - - if(this.checkHandler){ - this.on('checkchange', this.checkHandler, this.scope); - } - Ext.menu.MenuMgr.registerCheckable(this); - }, - - - onRender : function(c){ - Ext.menu.CheckItem.superclass.onRender.apply(this, arguments); - if(this.group){ - this.el.addClass(this.groupClass); - } - if(this.checked){ - this.checked = false; - this.setChecked(true, true); - } - }, - - - destroy : function(){ - Ext.menu.MenuMgr.unregisterCheckable(this); - Ext.menu.CheckItem.superclass.destroy.apply(this, arguments); - }, - - - setChecked : function(state, suppressEvent){ - var suppress = suppressEvent === true; - if(this.checked != state && (suppress || this.fireEvent("beforecheckchange", this, state) !== false)){ - Ext.menu.MenuMgr.onCheckChange(this, state); - if(this.container){ - this.container[state ? "addClass" : "removeClass"]("x-menu-item-checked"); - } - this.checked = state; - if(!suppress){ - this.fireEvent("checkchange", this, state); - } - } - }, - - - handleClick : function(e){ - if(!this.disabled && !(this.checked && this.group)){ - this.setChecked(!this.checked); - } - Ext.menu.CheckItem.superclass.handleClick.apply(this, arguments); - } -}); -Ext.reg('menucheckitem', Ext.menu.CheckItem); - Ext.menu.DateMenu = Ext.extend(Ext.menu.Menu, { - - enableScrolling : false, - - - - hideOnClick : true, - - - pickerId : null, - - - - - cls : 'x-date-menu', - - - - - - initComponent : function(){ - this.on('beforeshow', this.onBeforeShow, this); - if(this.strict = (Ext.isIE7 && Ext.isStrict)){ - this.on('show', this.onShow, this, {single: true, delay: 20}); - } - Ext.apply(this, { - plain: true, - showSeparator: false, - items: this.picker = new Ext.DatePicker(Ext.applyIf({ - internalRender: this.strict || !Ext.isIE, - ctCls: 'x-menu-date-item', - id: this.pickerId - }, this.initialConfig)) - }); - this.picker.purgeListeners(); - Ext.menu.DateMenu.superclass.initComponent.call(this); - - this.relayEvents(this.picker, ['select']); - this.on('show', this.picker.focus, this.picker); - this.on('select', this.menuHide, this); - if(this.handler){ - this.on('select', this.handler, this.scope || this); - } - }, - - menuHide : function() { - if(this.hideOnClick){ - this.hide(true); - } - }, - - onBeforeShow : function(){ - if(this.picker){ - this.picker.hideMonthPicker(true); - } - }, - - onShow : function(){ - var el = this.picker.getEl(); - el.setWidth(el.getWidth()); - } - }); - Ext.reg('datemenu', Ext.menu.DateMenu); - - Ext.menu.ColorMenu = Ext.extend(Ext.menu.Menu, { - - enableScrolling : false, - - - - - hideOnClick : true, - - cls : 'x-color-menu', - - - paletteId : null, - - - - - - - - - - - initComponent : function(){ - Ext.apply(this, { - plain: true, - showSeparator: false, - items: this.palette = new Ext.ColorPalette(Ext.applyIf({ - id: this.paletteId - }, this.initialConfig)) - }); - this.palette.purgeListeners(); - Ext.menu.ColorMenu.superclass.initComponent.call(this); - - this.relayEvents(this.palette, ['select']); - this.on('select', this.menuHide, this); - if(this.handler){ - this.on('select', this.handler, this.scope || this); - } - }, - - menuHide : function(){ - if(this.hideOnClick){ - this.hide(true); - } - } -}); -Ext.reg('colormenu', Ext.menu.ColorMenu); - -Ext.form.Field = Ext.extend(Ext.BoxComponent, { - - - - - - - - - invalidClass : 'x-form-invalid', - - invalidText : 'The value in this field is invalid', - - focusClass : 'x-form-focus', - - - validationEvent : 'keyup', - - validateOnBlur : true, - - validationDelay : 250, - - defaultAutoCreate : {tag: 'input', type: 'text', size: '20', autocomplete: 'off'}, - - fieldClass : 'x-form-field', - - msgTarget : 'qtip', - - msgFx : 'normal', - - readOnly : false, - - disabled : false, - - submitValue: true, - - - isFormField : true, - - - msgDisplay: '', - - - hasFocus : false, - - - initComponent : function(){ - Ext.form.Field.superclass.initComponent.call(this); - this.addEvents( - - 'focus', - - 'blur', - - 'specialkey', - - 'change', - - 'invalid', - - 'valid' - ); - }, - - - getName : function(){ - return this.rendered && this.el.dom.name ? this.el.dom.name : this.name || this.id || ''; - }, - - - onRender : function(ct, position){ - if(!this.el){ - var cfg = this.getAutoCreate(); - - if(!cfg.name){ - cfg.name = this.name || this.id; - } - if(this.inputType){ - cfg.type = this.inputType; - } - this.autoEl = cfg; - } - Ext.form.Field.superclass.onRender.call(this, ct, position); - if(this.submitValue === false){ - this.el.dom.removeAttribute('name'); - } - var type = this.el.dom.type; - if(type){ - if(type == 'password'){ - type = 'text'; - } - this.el.addClass('x-form-'+type); - } - if(this.readOnly){ - this.setReadOnly(true); - } - if(this.tabIndex !== undefined){ - this.el.dom.setAttribute('tabIndex', this.tabIndex); - } - - this.el.addClass([this.fieldClass, this.cls]); - }, - - - getItemCt : function(){ - return this.itemCt; - }, - - - initValue : function(){ - if(this.value !== undefined){ - this.setValue(this.value); - }else if(!Ext.isEmpty(this.el.dom.value) && this.el.dom.value != this.emptyText){ - this.setValue(this.el.dom.value); - } - - this.originalValue = this.getValue(); - }, - - - isDirty : function() { - if(this.disabled || !this.rendered) { - return false; - } - return String(this.getValue()) !== String(this.originalValue); - }, - - - setReadOnly : function(readOnly){ - if(this.rendered){ - this.el.dom.readOnly = readOnly; - } - this.readOnly = readOnly; - }, - - - afterRender : function(){ - Ext.form.Field.superclass.afterRender.call(this); - this.initEvents(); - this.initValue(); - }, - - - fireKey : function(e){ - if(e.isSpecialKey()){ - this.fireEvent('specialkey', this, e); - } - }, - - - reset : function(){ - this.setValue(this.originalValue); - this.clearInvalid(); - }, - - - initEvents : function(){ - this.mon(this.el, Ext.EventManager.getKeyEvent(), this.fireKey, this); - this.mon(this.el, 'focus', this.onFocus, this); - - - - this.mon(this.el, 'blur', this.onBlur, this, this.inEditor ? {buffer:10} : null); - }, - - - preFocus: Ext.emptyFn, - - - onFocus : function(){ - this.preFocus(); - if(this.focusClass){ - this.el.addClass(this.focusClass); - } - if(!this.hasFocus){ - this.hasFocus = true; - - this.startValue = this.getValue(); - this.fireEvent('focus', this); - } - }, - - - beforeBlur : Ext.emptyFn, - - - onBlur : function(){ - this.beforeBlur(); - if(this.focusClass){ - this.el.removeClass(this.focusClass); - } - this.hasFocus = false; - if(this.validationEvent !== false && (this.validateOnBlur || this.validationEvent == 'blur')){ - this.validate(); - } - var v = this.getValue(); - if(String(v) !== String(this.startValue)){ - this.fireEvent('change', this, v, this.startValue); - } - this.fireEvent('blur', this); - this.postBlur(); - }, - - - postBlur : Ext.emptyFn, - - - isValid : function(preventMark){ - if(this.disabled){ - return true; - } - var restore = this.preventMark; - this.preventMark = preventMark === true; - var v = this.validateValue(this.processValue(this.getRawValue()), preventMark); - this.preventMark = restore; - return v; - }, - - - validate : function(){ - if(this.disabled || this.validateValue(this.processValue(this.getRawValue()))){ - this.clearInvalid(); - return true; - } - return false; - }, - - - processValue : function(value){ - return value; - }, - - - validateValue : function(value) { - - var error = this.getErrors(value)[0]; - - if (error == undefined) { - return true; - } else { - this.markInvalid(error); - return false; - } - }, - - - getErrors: function() { - return []; - }, - - - getActiveError : function(){ - return this.activeError || ''; - }, - - - markInvalid : function(msg){ - - if (this.rendered && !this.preventMark) { - msg = msg || this.invalidText; - - var mt = this.getMessageHandler(); - if(mt){ - mt.mark(this, msg); - }else if(this.msgTarget){ - this.el.addClass(this.invalidClass); - var t = Ext.getDom(this.msgTarget); - if(t){ - t.innerHTML = msg; - t.style.display = this.msgDisplay; - } - } - } - - this.setActiveError(msg); - }, - - - clearInvalid : function(){ - - if (this.rendered && !this.preventMark) { - this.el.removeClass(this.invalidClass); - var mt = this.getMessageHandler(); - if(mt){ - mt.clear(this); - }else if(this.msgTarget){ - this.el.removeClass(this.invalidClass); - var t = Ext.getDom(this.msgTarget); - if(t){ - t.innerHTML = ''; - t.style.display = 'none'; - } - } - } - - this.unsetActiveError(); - }, - - - setActiveError: function(msg, suppressEvent) { - this.activeError = msg; - if (suppressEvent !== true) this.fireEvent('invalid', this, msg); - }, - - - unsetActiveError: function(suppressEvent) { - delete this.activeError; - if (suppressEvent !== true) this.fireEvent('valid', this); - }, - - - getMessageHandler : function(){ - return Ext.form.MessageTargets[this.msgTarget]; - }, - - - getErrorCt : function(){ - return this.el.findParent('.x-form-element', 5, true) || - this.el.findParent('.x-form-field-wrap', 5, true); - }, - - - alignErrorEl : function(){ - this.errorEl.setWidth(this.getErrorCt().getWidth(true) - 20); - }, - - - alignErrorIcon : function(){ - this.errorIcon.alignTo(this.el, 'tl-tr', [2, 0]); - }, - - - getRawValue : function(){ - var v = this.rendered ? this.el.getValue() : Ext.value(this.value, ''); - if(v === this.emptyText){ - v = ''; - } - return v; - }, - - - getValue : function(){ - if(!this.rendered) { - return this.value; - } - var v = this.el.getValue(); - if(v === this.emptyText || v === undefined){ - v = ''; - } - return v; - }, - - - setRawValue : function(v){ - return this.rendered ? (this.el.dom.value = (Ext.isEmpty(v) ? '' : v)) : ''; - }, - - - setValue : function(v){ - this.value = v; - if(this.rendered){ - this.el.dom.value = (Ext.isEmpty(v) ? '' : v); - this.validate(); - } - return this; - }, - - - append : function(v){ - this.setValue([this.getValue(), v].join('')); - } - - - - - -}); - - -Ext.form.MessageTargets = { - 'qtip' : { - mark: function(field, msg){ - field.el.addClass(field.invalidClass); - field.el.dom.qtip = msg; - field.el.dom.qclass = 'x-form-invalid-tip'; - if(Ext.QuickTips){ - Ext.QuickTips.enable(); - } - }, - clear: function(field){ - field.el.removeClass(field.invalidClass); - field.el.dom.qtip = ''; - } - }, - 'title' : { - mark: function(field, msg){ - field.el.addClass(field.invalidClass); - field.el.dom.title = msg; - }, - clear: function(field){ - field.el.dom.title = ''; - } - }, - 'under' : { - mark: function(field, msg){ - field.el.addClass(field.invalidClass); - if(!field.errorEl){ - var elp = field.getErrorCt(); - if(!elp){ - field.el.dom.title = msg; - return; - } - field.errorEl = elp.createChild({cls:'x-form-invalid-msg'}); - field.on('resize', field.alignErrorEl, field); - field.on('destroy', function(){ - Ext.destroy(this.errorEl); - }, field); - } - field.alignErrorEl(); - field.errorEl.update(msg); - Ext.form.Field.msgFx[field.msgFx].show(field.errorEl, field); - }, - clear: function(field){ - field.el.removeClass(field.invalidClass); - if(field.errorEl){ - Ext.form.Field.msgFx[field.msgFx].hide(field.errorEl, field); - }else{ - field.el.dom.title = ''; - } - } - }, - 'side' : { - mark: function(field, msg){ - field.el.addClass(field.invalidClass); - if(!field.errorIcon){ - var elp = field.getErrorCt(); - - if(!elp){ - field.el.dom.title = msg; - return; - } - field.errorIcon = elp.createChild({cls:'x-form-invalid-icon'}); - if (field.ownerCt) { - field.ownerCt.on('afterlayout', field.alignErrorIcon, field); - field.ownerCt.on('expand', field.alignErrorIcon, field); - } - field.on('resize', field.alignErrorIcon, field); - field.on('destroy', function(){ - Ext.destroy(this.errorIcon); - }, field); - } - field.alignErrorIcon(); - field.errorIcon.dom.qtip = msg; - field.errorIcon.dom.qclass = 'x-form-invalid-tip'; - field.errorIcon.show(); - }, - clear: function(field){ - field.el.removeClass(field.invalidClass); - if(field.errorIcon){ - field.errorIcon.dom.qtip = ''; - field.errorIcon.hide(); - }else{ - field.el.dom.title = ''; - } - } - } -}; - - -Ext.form.Field.msgFx = { - normal : { - show: function(msgEl, f){ - msgEl.setDisplayed('block'); - }, - - hide : function(msgEl, f){ - msgEl.setDisplayed(false).update(''); - } - }, - - slide : { - show: function(msgEl, f){ - msgEl.slideIn('t', {stopFx:true}); - }, - - hide : function(msgEl, f){ - msgEl.slideOut('t', {stopFx:true,useDisplay:true}); - } - }, - - slideRight : { - show: function(msgEl, f){ - msgEl.fixDisplay(); - msgEl.alignTo(f.el, 'tl-tr'); - msgEl.slideIn('l', {stopFx:true}); - }, - - hide : function(msgEl, f){ - msgEl.slideOut('l', {stopFx:true,useDisplay:true}); - } - } -}; -Ext.reg('field', Ext.form.Field); - -Ext.form.TextField = Ext.extend(Ext.form.Field, { - - - - grow : false, - - growMin : 30, - - growMax : 800, - - vtype : null, - - maskRe : null, - - disableKeyFilter : false, - - allowBlank : true, - - minLength : 0, - - maxLength : Number.MAX_VALUE, - - minLengthText : 'The minimum length for this field is {0}', - - maxLengthText : 'The maximum length for this field is {0}', - - selectOnFocus : false, - - blankText : 'This field is required', - - validator : null, - - regex : null, - - regexText : '', - - emptyText : null, - - emptyClass : 'x-form-empty-field', - - - - initComponent : function(){ - Ext.form.TextField.superclass.initComponent.call(this); - this.addEvents( - - 'autosize', - - - 'keydown', - - 'keyup', - - 'keypress' - ); - }, - - - initEvents : function(){ - Ext.form.TextField.superclass.initEvents.call(this); - if(this.validationEvent == 'keyup'){ - this.validationTask = new Ext.util.DelayedTask(this.validate, this); - this.mon(this.el, 'keyup', this.filterValidation, this); - } - else if(this.validationEvent !== false && this.validationEvent != 'blur'){ - this.mon(this.el, this.validationEvent, this.validate, this, {buffer: this.validationDelay}); - } - if(this.selectOnFocus || this.emptyText){ - this.mon(this.el, 'mousedown', this.onMouseDown, this); - - if(this.emptyText){ - this.applyEmptyText(); - } - } - if(this.maskRe || (this.vtype && this.disableKeyFilter !== true && (this.maskRe = Ext.form.VTypes[this.vtype+'Mask']))){ - this.mon(this.el, 'keypress', this.filterKeys, this); - } - if(this.grow){ - this.mon(this.el, 'keyup', this.onKeyUpBuffered, this, {buffer: 50}); - this.mon(this.el, 'click', this.autoSize, this); - } - if(this.enableKeyEvents){ - this.mon(this.el, { - scope: this, - keyup: this.onKeyUp, - keydown: this.onKeyDown, - keypress: this.onKeyPress - }); - } - }, - - onMouseDown: function(e){ - if(!this.hasFocus){ - this.mon(this.el, 'mouseup', Ext.emptyFn, this, { single: true, preventDefault: true }); - } - }, - - processValue : function(value){ - if(this.stripCharsRe){ - var newValue = value.replace(this.stripCharsRe, ''); - if(newValue !== value){ - this.setRawValue(newValue); - return newValue; - } - } - return value; - }, - - filterValidation : function(e){ - if(!e.isNavKeyPress()){ - this.validationTask.delay(this.validationDelay); - } - }, - - - onDisable: function(){ - Ext.form.TextField.superclass.onDisable.call(this); - if(Ext.isIE){ - this.el.dom.unselectable = 'on'; - } - }, - - - onEnable: function(){ - Ext.form.TextField.superclass.onEnable.call(this); - if(Ext.isIE){ - this.el.dom.unselectable = ''; - } - }, - - - onKeyUpBuffered : function(e){ - if(this.doAutoSize(e)){ - this.autoSize(); - } - }, - - - doAutoSize : function(e){ - return !e.isNavKeyPress(); - }, - - - onKeyUp : function(e){ - this.fireEvent('keyup', this, e); - }, - - - onKeyDown : function(e){ - this.fireEvent('keydown', this, e); - }, - - - onKeyPress : function(e){ - this.fireEvent('keypress', this, e); - }, - - - reset : function(){ - Ext.form.TextField.superclass.reset.call(this); - this.applyEmptyText(); - }, - - applyEmptyText : function(){ - if(this.rendered && this.emptyText && this.getRawValue().length < 1 && !this.hasFocus){ - this.setRawValue(this.emptyText); - this.el.addClass(this.emptyClass); - } - }, - - - preFocus : function(){ - var el = this.el, - isEmpty; - if(this.emptyText){ - if(el.dom.value == this.emptyText){ - this.setRawValue(''); - isEmpty = true; - } - el.removeClass(this.emptyClass); - } - if(this.selectOnFocus || isEmpty){ - el.dom.select(); - } - }, - - - postBlur : function(){ - this.applyEmptyText(); - }, - - - filterKeys : function(e){ - if(e.ctrlKey){ - return; - } - var k = e.getKey(); - if(Ext.isGecko && (e.isNavKeyPress() || k == e.BACKSPACE || (k == e.DELETE && e.button == -1))){ - return; - } - var cc = String.fromCharCode(e.getCharCode()); - if(!Ext.isGecko && e.isSpecialKey() && !cc){ - return; - } - if(!this.maskRe.test(cc)){ - e.stopEvent(); - } - }, - - setValue : function(v){ - if(this.emptyText && this.el && !Ext.isEmpty(v)){ - this.el.removeClass(this.emptyClass); - } - Ext.form.TextField.superclass.setValue.apply(this, arguments); - this.applyEmptyText(); - this.autoSize(); - return this; - }, - - - getErrors: function(value) { - var errors = Ext.form.TextField.superclass.getErrors.apply(this, arguments); - - value = Ext.isDefined(value) ? value : this.processValue(this.getRawValue()); - - if (Ext.isFunction(this.validator)) { - var msg = this.validator(value); - if (msg !== true) { - errors.push(msg); - } - } - - if (value.length < 1 || value === this.emptyText) { - if (this.allowBlank) { - - return errors; - } else { - errors.push(this.blankText); - } - } - - if (!this.allowBlank && (value.length < 1 || value === this.emptyText)) { - errors.push(this.blankText); - } - - if (value.length < this.minLength) { - errors.push(String.format(this.minLengthText, this.minLength)); - } - - if (value.length > this.maxLength) { - errors.push(String.format(this.maxLengthText, this.maxLength)); - } - - if (this.vtype) { - var vt = Ext.form.VTypes; - if(!vt[this.vtype](value, this)){ - errors.push(this.vtypeText || vt[this.vtype +'Text']); - } - } - - if (this.regex && !this.regex.test(value)) { - errors.push(this.regexText); - } - - return errors; - }, - - - selectText : function(start, end){ - var v = this.getRawValue(); - var doFocus = false; - if(v.length > 0){ - start = start === undefined ? 0 : start; - end = end === undefined ? v.length : end; - var d = this.el.dom; - if(d.setSelectionRange){ - d.setSelectionRange(start, end); - }else if(d.createTextRange){ - var range = d.createTextRange(); - range.moveStart('character', start); - range.moveEnd('character', end-v.length); - range.select(); - } - doFocus = Ext.isGecko || Ext.isOpera; - }else{ - doFocus = true; - } - if(doFocus){ - this.focus(); - } - }, - - - autoSize : function(){ - if(!this.grow || !this.rendered){ - return; - } - if(!this.metrics){ - this.metrics = Ext.util.TextMetrics.createInstance(this.el); - } - var el = this.el; - var v = el.dom.value; - var d = document.createElement('div'); - d.appendChild(document.createTextNode(v)); - v = d.innerHTML; - Ext.removeNode(d); - d = null; - v += ' '; - var w = Math.min(this.growMax, Math.max(this.metrics.getWidth(v) + 10, this.growMin)); - this.el.setWidth(w); - this.fireEvent('autosize', this, w); - }, - - onDestroy: function(){ - if(this.validationTask){ - this.validationTask.cancel(); - this.validationTask = null; - } - Ext.form.TextField.superclass.onDestroy.call(this); - } -}); -Ext.reg('textfield', Ext.form.TextField); - -Ext.form.TriggerField = Ext.extend(Ext.form.TextField, { - - - - defaultAutoCreate : {tag: "input", type: "text", size: "16", autocomplete: "off"}, - - hideTrigger:false, - - editable: true, - - readOnly: false, - - wrapFocusClass: 'x-trigger-wrap-focus', - - autoSize: Ext.emptyFn, - - monitorTab : true, - - deferHeight : true, - - mimicing : false, - - actionMode: 'wrap', - - defaultTriggerWidth: 17, - - - onResize : function(w, h){ - Ext.form.TriggerField.superclass.onResize.call(this, w, h); - var tw = this.getTriggerWidth(); - if(Ext.isNumber(w)){ - this.el.setWidth(w - tw); - } - this.wrap.setWidth(this.el.getWidth() + tw); - }, - - getTriggerWidth: function(){ - var tw = this.trigger.getWidth(); - if(!this.hideTrigger && !this.readOnly && tw === 0){ - tw = this.defaultTriggerWidth; - } - return tw; - }, - - - alignErrorIcon : function(){ - if(this.wrap){ - this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]); - } - }, - - - onRender : function(ct, position){ - this.doc = Ext.isIE ? Ext.getBody() : Ext.getDoc(); - Ext.form.TriggerField.superclass.onRender.call(this, ct, position); - - this.wrap = this.el.wrap({cls: 'x-form-field-wrap x-form-field-trigger-wrap'}); - this.trigger = this.wrap.createChild(this.triggerConfig || - {tag: "img", src: Ext.BLANK_IMAGE_URL, alt: "", cls: "x-form-trigger " + this.triggerClass}); - this.initTrigger(); - if(!this.width){ - this.wrap.setWidth(this.el.getWidth()+this.trigger.getWidth()); - } - this.resizeEl = this.positionEl = this.wrap; - }, - - getWidth: function() { - return(this.el.getWidth() + this.trigger.getWidth()); - }, - - updateEditState: function(){ - if(this.rendered){ - if (this.readOnly) { - this.el.dom.readOnly = true; - this.el.addClass('x-trigger-noedit'); - this.mun(this.el, 'click', this.onTriggerClick, this); - this.trigger.setDisplayed(false); - } else { - if (!this.editable) { - this.el.dom.readOnly = true; - this.el.addClass('x-trigger-noedit'); - this.mon(this.el, 'click', this.onTriggerClick, this); - } else { - this.el.dom.readOnly = false; - this.el.removeClass('x-trigger-noedit'); - this.mun(this.el, 'click', this.onTriggerClick, this); - } - this.trigger.setDisplayed(!this.hideTrigger); - } - this.onResize(this.width || this.wrap.getWidth()); - } - }, - - - setHideTrigger: function(hideTrigger){ - if(hideTrigger != this.hideTrigger){ - this.hideTrigger = hideTrigger; - this.updateEditState(); - } - }, - - - setEditable: function(editable){ - if(editable != this.editable){ - this.editable = editable; - this.updateEditState(); - } - }, - - - setReadOnly: function(readOnly){ - if(readOnly != this.readOnly){ - this.readOnly = readOnly; - this.updateEditState(); - } - }, - - afterRender : function(){ - Ext.form.TriggerField.superclass.afterRender.call(this); - this.updateEditState(); - }, - - - initTrigger : function(){ - this.mon(this.trigger, 'click', this.onTriggerClick, this, {preventDefault:true}); - this.trigger.addClassOnOver('x-form-trigger-over'); - this.trigger.addClassOnClick('x-form-trigger-click'); - }, - - - onDestroy : function(){ - Ext.destroy(this.trigger, this.wrap); - if (this.mimicing){ - this.doc.un('mousedown', this.mimicBlur, this); - } - delete this.doc; - Ext.form.TriggerField.superclass.onDestroy.call(this); - }, - - - onFocus : function(){ - Ext.form.TriggerField.superclass.onFocus.call(this); - if(!this.mimicing){ - this.wrap.addClass(this.wrapFocusClass); - this.mimicing = true; - this.doc.on('mousedown', this.mimicBlur, this, {delay: 10}); - if(this.monitorTab){ - this.on('specialkey', this.checkTab, this); - } - } - }, - - - checkTab : function(me, e){ - if(e.getKey() == e.TAB){ - this.triggerBlur(); - } - }, - - - onBlur : Ext.emptyFn, - - - mimicBlur : function(e){ - if(!this.isDestroyed && !this.wrap.contains(e.target) && this.validateBlur(e)){ - this.triggerBlur(); - } - }, - - - triggerBlur : function(){ - this.mimicing = false; - this.doc.un('mousedown', this.mimicBlur, this); - if(this.monitorTab && this.el){ - this.un('specialkey', this.checkTab, this); - } - Ext.form.TriggerField.superclass.onBlur.call(this); - if(this.wrap){ - this.wrap.removeClass(this.wrapFocusClass); - } - }, - - beforeBlur : Ext.emptyFn, - - - - validateBlur : function(e){ - return true; - }, - - - onTriggerClick : Ext.emptyFn - - - - -}); - - -Ext.form.TwinTriggerField = Ext.extend(Ext.form.TriggerField, { - - - - - initComponent : function(){ - Ext.form.TwinTriggerField.superclass.initComponent.call(this); - - this.triggerConfig = { - tag:'span', cls:'x-form-twin-triggers', cn:[ - {tag: "img", src: Ext.BLANK_IMAGE_URL, alt: "", cls: "x-form-trigger " + this.trigger1Class}, - {tag: "img", src: Ext.BLANK_IMAGE_URL, alt: "", cls: "x-form-trigger " + this.trigger2Class} - ]}; - }, - - getTrigger : function(index){ - return this.triggers[index]; - }, - - afterRender: function(){ - Ext.form.TwinTriggerField.superclass.afterRender.call(this); - var triggers = this.triggers, - i = 0, - len = triggers.length; - - for(; i < len; ++i){ - if(this['hideTrigger' + (i + 1)]){ - triggers[i].hide(); - } - - } - }, - - initTrigger : function(){ - var ts = this.trigger.select('.x-form-trigger', true), - triggerField = this; - - ts.each(function(t, all, index){ - var triggerIndex = 'Trigger'+(index+1); - t.hide = function(){ - var w = triggerField.wrap.getWidth(); - this.dom.style.display = 'none'; - triggerField.el.setWidth(w-triggerField.trigger.getWidth()); - triggerField['hidden' + triggerIndex] = true; - }; - t.show = function(){ - var w = triggerField.wrap.getWidth(); - this.dom.style.display = ''; - triggerField.el.setWidth(w-triggerField.trigger.getWidth()); - triggerField['hidden' + triggerIndex] = false; - }; - this.mon(t, 'click', this['on'+triggerIndex+'Click'], this, {preventDefault:true}); - t.addClassOnOver('x-form-trigger-over'); - t.addClassOnClick('x-form-trigger-click'); - }, this); - this.triggers = ts.elements; - }, - - getTriggerWidth: function(){ - var tw = 0; - Ext.each(this.triggers, function(t, index){ - var triggerIndex = 'Trigger' + (index + 1), - w = t.getWidth(); - if(w === 0 && !this['hidden' + triggerIndex]){ - tw += this.defaultTriggerWidth; - }else{ - tw += w; - } - }, this); - return tw; - }, - - - onDestroy : function() { - Ext.destroy(this.triggers); - Ext.form.TwinTriggerField.superclass.onDestroy.call(this); - }, - - - onTrigger1Click : Ext.emptyFn, - - onTrigger2Click : Ext.emptyFn -}); -Ext.reg('trigger', Ext.form.TriggerField); - -Ext.form.TextArea = Ext.extend(Ext.form.TextField, { - - growMin : 60, - - growMax: 1000, - growAppend : ' \n ', - - enterIsSpecial : false, - - - preventScrollbars: false, - - - - onRender : function(ct, position){ - if(!this.el){ - this.defaultAutoCreate = { - tag: "textarea", - style:"width:100px;height:60px;", - autocomplete: "off" - }; - } - Ext.form.TextArea.superclass.onRender.call(this, ct, position); - if(this.grow){ - this.textSizeEl = Ext.DomHelper.append(document.body, { - tag: "pre", cls: "x-form-grow-sizer" - }); - if(this.preventScrollbars){ - this.el.setStyle("overflow", "hidden"); - } - this.el.setHeight(this.growMin); - } - }, - - onDestroy : function(){ - Ext.removeNode(this.textSizeEl); - Ext.form.TextArea.superclass.onDestroy.call(this); - }, - - fireKey : function(e){ - if(e.isSpecialKey() && (this.enterIsSpecial || (e.getKey() != e.ENTER || e.hasModifier()))){ - this.fireEvent("specialkey", this, e); - } - }, - - - doAutoSize : function(e){ - return !e.isNavKeyPress() || e.getKey() == e.ENTER; - }, - - - filterValidation: function(e) { - if(!e.isNavKeyPress() || (!this.enterIsSpecial && e.keyCode == e.ENTER)){ - this.validationTask.delay(this.validationDelay); - } - }, - - - autoSize: function(){ - if(!this.grow || !this.textSizeEl){ - return; - } - var el = this.el, - v = Ext.util.Format.htmlEncode(el.dom.value), - ts = this.textSizeEl, - h; - - Ext.fly(ts).setWidth(this.el.getWidth()); - if(v.length < 1){ - v = "  "; - }else{ - v += this.growAppend; - if(Ext.isIE){ - v = v.replace(/\n/g, ' 
        '); - } - } - ts.innerHTML = v; - h = Math.min(this.growMax, Math.max(ts.offsetHeight, this.growMin)); - if(h != this.lastHeight){ - this.lastHeight = h; - this.el.setHeight(h); - this.fireEvent("autosize", this, h); - } - } -}); -Ext.reg('textarea', Ext.form.TextArea); -Ext.form.NumberField = Ext.extend(Ext.form.TextField, { - - - - fieldClass: "x-form-field x-form-num-field", - - - allowDecimals : true, - - - decimalSeparator : ".", - - - decimalPrecision : 2, - - - allowNegative : true, - - - minValue : Number.NEGATIVE_INFINITY, - - - maxValue : Number.MAX_VALUE, - - - minText : "The minimum value for this field is {0}", - - - maxText : "The maximum value for this field is {0}", - - - nanText : "{0} is not a valid number", - - - baseChars : "0123456789", - - - autoStripChars: false, - - - initEvents : function() { - var allowed = this.baseChars + ''; - if (this.allowDecimals) { - allowed += this.decimalSeparator; - } - if (this.allowNegative) { - allowed += '-'; - } - allowed = Ext.escapeRe(allowed); - this.maskRe = new RegExp('[' + allowed + ']'); - if (this.autoStripChars) { - this.stripCharsRe = new RegExp('[^' + allowed + ']', 'gi'); - } - - Ext.form.NumberField.superclass.initEvents.call(this); - }, - - - getErrors: function(value) { - var errors = Ext.form.NumberField.superclass.getErrors.apply(this, arguments); - - value = Ext.isDefined(value) ? value : this.processValue(this.getRawValue()); - - if (value.length < 1) { - return errors; - } - - value = String(value).replace(this.decimalSeparator, "."); - - if(isNaN(value)){ - errors.push(String.format(this.nanText, value)); - } - - var num = this.parseValue(value); - - if (num < this.minValue) { - errors.push(String.format(this.minText, this.minValue)); - } - - if (num > this.maxValue) { - errors.push(String.format(this.maxText, this.maxValue)); - } - - return errors; - }, - - getValue : function() { - return this.fixPrecision(this.parseValue(Ext.form.NumberField.superclass.getValue.call(this))); - }, - - setValue : function(v) { - v = Ext.isNumber(v) ? v : parseFloat(String(v).replace(this.decimalSeparator, ".")); - v = this.fixPrecision(v); - v = isNaN(v) ? '' : String(v).replace(".", this.decimalSeparator); - return Ext.form.NumberField.superclass.setValue.call(this, v); - }, - - - setMinValue : function(value) { - this.minValue = Ext.num(value, Number.NEGATIVE_INFINITY); - }, - - - setMaxValue : function(value) { - this.maxValue = Ext.num(value, Number.MAX_VALUE); - }, - - - parseValue : function(value) { - value = parseFloat(String(value).replace(this.decimalSeparator, ".")); - return isNaN(value) ? '' : value; - }, - - - fixPrecision : function(value) { - var nan = isNaN(value); - - if (!this.allowDecimals || this.decimalPrecision == -1 || nan || !value) { - return nan ? '' : value; - } - - return parseFloat(parseFloat(value).toFixed(this.decimalPrecision)); - }, - - beforeBlur : function() { - var v = this.parseValue(this.getRawValue()); - - if (!Ext.isEmpty(v)) { - this.setValue(v); - } - } -}); - -Ext.reg('numberfield', Ext.form.NumberField); - -Ext.form.DateField = Ext.extend(Ext.form.TriggerField, { - - format : "m/d/Y", - - altFormats : "m/d/Y|n/j/Y|n/j/y|m/j/y|n/d/y|m/j/Y|n/d/Y|m-d-y|m-d-Y|m/d|m-d|md|mdy|mdY|d|Y-m-d|n-j|n/j", - - disabledDaysText : "Disabled", - - disabledDatesText : "Disabled", - - minText : "The date in this field must be equal to or after {0}", - - maxText : "The date in this field must be equal to or before {0}", - - invalidText : "{0} is not a valid date - it must be in the format {1}", - - triggerClass : 'x-form-date-trigger', - - showToday : true, - - - startDay : 0, - - - - - - - - - defaultAutoCreate : {tag: "input", type: "text", size: "10", autocomplete: "off"}, - - - - initTime: '12', - - initTimeFormat: 'H', - - - safeParse : function(value, format) { - if (Date.formatContainsHourInfo(format)) { - - return Date.parseDate(value, format); - } else { - - var parsedDate = Date.parseDate(value + ' ' + this.initTime, format + ' ' + this.initTimeFormat); - - if (parsedDate) { - return parsedDate.clearTime(); - } - } - }, - - initComponent : function(){ - Ext.form.DateField.superclass.initComponent.call(this); - - this.addEvents( - - 'select' - ); - - if(Ext.isString(this.minValue)){ - this.minValue = this.parseDate(this.minValue); - } - if(Ext.isString(this.maxValue)){ - this.maxValue = this.parseDate(this.maxValue); - } - this.disabledDatesRE = null; - this.initDisabledDays(); - }, - - initEvents: function() { - Ext.form.DateField.superclass.initEvents.call(this); - this.keyNav = new Ext.KeyNav(this.el, { - "down": function(e) { - this.onTriggerClick(); - }, - scope: this, - forceKeyDown: true - }); - }, - - - - initDisabledDays : function(){ - if(this.disabledDates){ - var dd = this.disabledDates, - len = dd.length - 1, - re = "(?:"; - - Ext.each(dd, function(d, i){ - re += Ext.isDate(d) ? '^' + Ext.escapeRe(d.dateFormat(this.format)) + '$' : dd[i]; - if(i != len){ - re += '|'; - } - }, this); - this.disabledDatesRE = new RegExp(re + ')'); - } - }, - - - setDisabledDates : function(dd){ - this.disabledDates = dd; - this.initDisabledDays(); - if(this.menu){ - this.menu.picker.setDisabledDates(this.disabledDatesRE); - } - }, - - - setDisabledDays : function(dd){ - this.disabledDays = dd; - if(this.menu){ - this.menu.picker.setDisabledDays(dd); - } - }, - - - setMinValue : function(dt){ - this.minValue = (Ext.isString(dt) ? this.parseDate(dt) : dt); - if(this.menu){ - this.menu.picker.setMinDate(this.minValue); - } - }, - - - setMaxValue : function(dt){ - this.maxValue = (Ext.isString(dt) ? this.parseDate(dt) : dt); - if(this.menu){ - this.menu.picker.setMaxDate(this.maxValue); - } - }, - - - getErrors: function(value) { - var errors = Ext.form.DateField.superclass.getErrors.apply(this, arguments); - - value = this.formatDate(value || this.processValue(this.getRawValue())); - - if (value.length < 1) { - return errors; - } - - var svalue = value; - value = this.parseDate(value); - if (!value) { - errors.push(String.format(this.invalidText, svalue, this.format)); - return errors; - } - - var time = value.getTime(); - if (this.minValue && time < this.minValue.clearTime().getTime()) { - errors.push(String.format(this.minText, this.formatDate(this.minValue))); - } - - if (this.maxValue && time > this.maxValue.clearTime().getTime()) { - errors.push(String.format(this.maxText, this.formatDate(this.maxValue))); - } - - if (this.disabledDays) { - var day = value.getDay(); - - for(var i = 0; i < this.disabledDays.length; i++) { - if (day === this.disabledDays[i]) { - errors.push(this.disabledDaysText); - break; - } - } - } - - var fvalue = this.formatDate(value); - if (this.disabledDatesRE && this.disabledDatesRE.test(fvalue)) { - errors.push(String.format(this.disabledDatesText, fvalue)); - } - - return errors; - }, - - - - validateBlur : function(){ - return !this.menu || !this.menu.isVisible(); - }, - - - getValue : function(){ - return this.parseDate(Ext.form.DateField.superclass.getValue.call(this)) || ""; - }, - - - setValue : function(date){ - return Ext.form.DateField.superclass.setValue.call(this, this.formatDate(this.parseDate(date))); - }, - - - parseDate : function(value) { - if(!value || Ext.isDate(value)){ - return value; - } - - var v = this.safeParse(value, this.format), - af = this.altFormats, - afa = this.altFormatsArray; - - if (!v && af) { - afa = afa || af.split("|"); - - for (var i = 0, len = afa.length; i < len && !v; i++) { - v = this.safeParse(value, afa[i]); - } - } - return v; - }, - - - onDestroy : function(){ - Ext.destroy(this.menu, this.keyNav); - Ext.form.DateField.superclass.onDestroy.call(this); - }, - - - formatDate : function(date){ - return Ext.isDate(date) ? date.dateFormat(this.format) : date; - }, - - - - - onTriggerClick : function(){ - if(this.disabled){ - return; - } - if(this.menu == null){ - this.menu = new Ext.menu.DateMenu({ - hideOnClick: false, - focusOnSelect: false - }); - } - this.onFocus(); - Ext.apply(this.menu.picker, { - minDate : this.minValue, - maxDate : this.maxValue, - disabledDatesRE : this.disabledDatesRE, - disabledDatesText : this.disabledDatesText, - disabledDays : this.disabledDays, - disabledDaysText : this.disabledDaysText, - format : this.format, - showToday : this.showToday, - startDay: this.startDay, - minText : String.format(this.minText, this.formatDate(this.minValue)), - maxText : String.format(this.maxText, this.formatDate(this.maxValue)) - }); - this.menu.picker.setValue(this.getValue() || new Date()); - this.menu.show(this.el, "tl-bl?"); - this.menuEvents('on'); - }, - - - menuEvents: function(method){ - this.menu[method]('select', this.onSelect, this); - this.menu[method]('hide', this.onMenuHide, this); - this.menu[method]('show', this.onFocus, this); - }, - - onSelect: function(m, d){ - this.setValue(d); - this.fireEvent('select', this, d); - this.menu.hide(); - }, - - onMenuHide: function(){ - this.focus(false, 60); - this.menuEvents('un'); - }, - - - beforeBlur : function(){ - var v = this.parseDate(this.getRawValue()); - if(v){ - this.setValue(v); - } - } - - - - - -}); -Ext.reg('datefield', Ext.form.DateField); - -Ext.form.DisplayField = Ext.extend(Ext.form.Field, { - validationEvent : false, - validateOnBlur : false, - defaultAutoCreate : {tag: "div"}, - - fieldClass : "x-form-display-field", - - htmlEncode: false, - - - initEvents : Ext.emptyFn, - - isValid : function(){ - return true; - }, - - validate : function(){ - return true; - }, - - getRawValue : function(){ - var v = this.rendered ? this.el.dom.innerHTML : Ext.value(this.value, ''); - if(v === this.emptyText){ - v = ''; - } - if(this.htmlEncode){ - v = Ext.util.Format.htmlDecode(v); - } - return v; - }, - - getValue : function(){ - return this.getRawValue(); - }, - - getName: function() { - return this.name; - }, - - setRawValue : function(v){ - if(this.htmlEncode){ - v = Ext.util.Format.htmlEncode(v); - } - return this.rendered ? (this.el.dom.innerHTML = (Ext.isEmpty(v) ? '' : v)) : (this.value = v); - }, - - setValue : function(v){ - this.setRawValue(v); - return this; - } - - - - - - -}); - -Ext.reg('displayfield', Ext.form.DisplayField); - -Ext.form.ComboBox = Ext.extend(Ext.form.TriggerField, { - - - - - - - - defaultAutoCreate : {tag: "input", type: "text", size: "24", autocomplete: "off"}, - - - - - - - - listClass : '', - - selectedClass : 'x-combo-selected', - - listEmptyText: '', - - triggerClass : 'x-form-arrow-trigger', - - shadow : 'sides', - - listAlign : 'tl-bl?', - - maxHeight : 300, - - minHeight : 90, - - triggerAction : 'query', - - minChars : 4, - - autoSelect : true, - - typeAhead : false, - - queryDelay : 500, - - pageSize : 0, - - selectOnFocus : false, - - queryParam : 'query', - - loadingText : 'Loading...', - - resizable : false, - - handleHeight : 8, - - allQuery: '', - - mode: 'remote', - - minListWidth : 70, - - forceSelection : false, - - typeAheadDelay : 250, - - - - lazyInit : true, - - - clearFilterOnReset : true, - - - submitValue: undefined, - - - - - initComponent : function(){ - Ext.form.ComboBox.superclass.initComponent.call(this); - this.addEvents( - - 'expand', - - 'collapse', - - - 'beforeselect', - - 'select', - - 'beforequery' - ); - if(this.transform){ - var s = Ext.getDom(this.transform); - if(!this.hiddenName){ - this.hiddenName = s.name; - } - if(!this.store){ - this.mode = 'local'; - var d = [], opts = s.options; - for(var i = 0, len = opts.length;i < len; i++){ - var o = opts[i], - value = (o.hasAttribute ? o.hasAttribute('value') : o.getAttributeNode('value').specified) ? o.value : o.text; - if(o.selected && Ext.isEmpty(this.value, true)) { - this.value = value; - } - d.push([value, o.text]); - } - this.store = new Ext.data.ArrayStore({ - idIndex: 0, - fields: ['value', 'text'], - data : d, - autoDestroy: true - }); - this.valueField = 'value'; - this.displayField = 'text'; - } - s.name = Ext.id(); - if(!this.lazyRender){ - this.target = true; - this.el = Ext.DomHelper.insertBefore(s, this.autoCreate || this.defaultAutoCreate); - this.render(this.el.parentNode, s); - } - Ext.removeNode(s); - } - - else if(this.store){ - this.store = Ext.StoreMgr.lookup(this.store); - if(this.store.autoCreated){ - this.displayField = this.valueField = 'field1'; - if(!this.store.expandData){ - this.displayField = 'field2'; - } - this.mode = 'local'; - } - } - - this.selectedIndex = -1; - if(this.mode == 'local'){ - if(!Ext.isDefined(this.initialConfig.queryDelay)){ - this.queryDelay = 10; - } - if(!Ext.isDefined(this.initialConfig.minChars)){ - this.minChars = 0; - } - } - }, - - - onRender : function(ct, position){ - if(this.hiddenName && !Ext.isDefined(this.submitValue)){ - this.submitValue = false; - } - Ext.form.ComboBox.superclass.onRender.call(this, ct, position); - if(this.hiddenName){ - this.hiddenField = this.el.insertSibling({tag:'input', type:'hidden', name: this.hiddenName, - id: (this.hiddenId || Ext.id())}, 'before', true); - - } - if(Ext.isGecko){ - this.el.dom.setAttribute('autocomplete', 'off'); - } - - if(!this.lazyInit){ - this.initList(); - }else{ - this.on('focus', this.initList, this, {single: true}); - } - }, - - - initValue : function(){ - Ext.form.ComboBox.superclass.initValue.call(this); - if(this.hiddenField){ - this.hiddenField.value = - Ext.value(Ext.isDefined(this.hiddenValue) ? this.hiddenValue : this.value, ''); - } - }, - - getParentZIndex : function(){ - var zindex; - if (this.ownerCt){ - this.findParentBy(function(ct){ - zindex = parseInt(ct.getPositionEl().getStyle('z-index'), 10); - return !!zindex; - }); - } - return zindex; - }, - - getZIndex : function(listParent){ - listParent = listParent || Ext.getDom(this.getListParent() || Ext.getBody()); - var zindex = parseInt(Ext.fly(listParent).getStyle('z-index'), 10); - if(!zindex){ - zindex = this.getParentZIndex(); - } - return (zindex || 12000) + 5; - }, - - - initList : function(){ - if(!this.list){ - var cls = 'x-combo-list', - listParent = Ext.getDom(this.getListParent() || Ext.getBody()); - - this.list = new Ext.Layer({ - parentEl: listParent, - shadow: this.shadow, - cls: [cls, this.listClass].join(' '), - constrain:false, - zindex: this.getZIndex(listParent) - }); - - var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth); - this.list.setSize(lw, 0); - this.list.swallowEvent('mousewheel'); - this.assetHeight = 0; - if(this.syncFont !== false){ - this.list.setStyle('font-size', this.el.getStyle('font-size')); - } - if(this.title){ - this.header = this.list.createChild({cls:cls+'-hd', html: this.title}); - this.assetHeight += this.header.getHeight(); - } - - this.innerList = this.list.createChild({cls:cls+'-inner'}); - this.mon(this.innerList, 'mouseover', this.onViewOver, this); - this.mon(this.innerList, 'mousemove', this.onViewMove, this); - this.innerList.setWidth(lw - this.list.getFrameWidth('lr')); - - if(this.pageSize){ - this.footer = this.list.createChild({cls:cls+'-ft'}); - this.pageTb = new Ext.PagingToolbar({ - store: this.store, - pageSize: this.pageSize, - renderTo:this.footer - }); - this.assetHeight += this.footer.getHeight(); - } - - if(!this.tpl){ - - this.tpl = '
        {' + this.displayField + '}
        '; - - } - - - this.view = new Ext.DataView({ - applyTo: this.innerList, - tpl: this.tpl, - singleSelect: true, - selectedClass: this.selectedClass, - itemSelector: this.itemSelector || '.' + cls + '-item', - emptyText: this.listEmptyText, - deferEmptyText: false - }); - - this.mon(this.view, { - containerclick : this.onViewClick, - click : this.onViewClick, - scope :this - }); - - this.bindStore(this.store, true); - - if(this.resizable){ - this.resizer = new Ext.Resizable(this.list, { - pinned:true, handles:'se' - }); - this.mon(this.resizer, 'resize', function(r, w, h){ - this.maxHeight = h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight; - this.listWidth = w; - this.innerList.setWidth(w - this.list.getFrameWidth('lr')); - this.restrictHeight(); - }, this); - - this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom', this.handleHeight+'px'); - } - } - }, - - - getListParent : function() { - return document.body; - }, - - - getStore : function(){ - return this.store; - }, - - - bindStore : function(store, initial){ - if(this.store && !initial){ - if(this.store !== store && this.store.autoDestroy){ - this.store.destroy(); - }else{ - this.store.un('beforeload', this.onBeforeLoad, this); - this.store.un('load', this.onLoad, this); - this.store.un('exception', this.collapse, this); - } - if(!store){ - this.store = null; - if(this.view){ - this.view.bindStore(null); - } - if(this.pageTb){ - this.pageTb.bindStore(null); - } - } - } - if(store){ - if(!initial) { - this.lastQuery = null; - if(this.pageTb) { - this.pageTb.bindStore(store); - } - } - - this.store = Ext.StoreMgr.lookup(store); - this.store.on({ - scope: this, - beforeload: this.onBeforeLoad, - load: this.onLoad, - exception: this.collapse - }); - - if(this.view){ - this.view.bindStore(store); - } - } - }, - - reset : function(){ - if(this.clearFilterOnReset && this.mode == 'local'){ - this.store.clearFilter(); - } - Ext.form.ComboBox.superclass.reset.call(this); - }, - - - initEvents : function(){ - Ext.form.ComboBox.superclass.initEvents.call(this); - - - this.keyNav = new Ext.KeyNav(this.el, { - "up" : function(e){ - this.inKeyMode = true; - this.selectPrev(); - }, - - "down" : function(e){ - if(!this.isExpanded()){ - this.onTriggerClick(); - }else{ - this.inKeyMode = true; - this.selectNext(); - } - }, - - "enter" : function(e){ - this.onViewClick(); - }, - - "esc" : function(e){ - this.collapse(); - }, - - "tab" : function(e){ - if (this.forceSelection === true) { - this.collapse(); - } else { - this.onViewClick(false); - } - return true; - }, - - scope : this, - - doRelay : function(e, h, hname){ - if(hname == 'down' || this.scope.isExpanded()){ - - var relay = Ext.KeyNav.prototype.doRelay.apply(this, arguments); - if(!Ext.isIE && Ext.EventManager.useKeydown){ - - this.scope.fireKey(e); - } - return relay; - } - return true; - }, - - forceKeyDown : true, - defaultEventAction: 'stopEvent' - }); - this.queryDelay = Math.max(this.queryDelay || 10, - this.mode == 'local' ? 10 : 250); - this.dqTask = new Ext.util.DelayedTask(this.initQuery, this); - if(this.typeAhead){ - this.taTask = new Ext.util.DelayedTask(this.onTypeAhead, this); - } - if(!this.enableKeyEvents){ - this.mon(this.el, 'keyup', this.onKeyUp, this); - } - }, - - - - onDestroy : function(){ - if (this.dqTask){ - this.dqTask.cancel(); - this.dqTask = null; - } - this.bindStore(null); - Ext.destroy( - this.resizer, - this.view, - this.pageTb, - this.list - ); - Ext.destroyMembers(this, 'hiddenField'); - Ext.form.ComboBox.superclass.onDestroy.call(this); - }, - - - fireKey : function(e){ - if (!this.isExpanded()) { - Ext.form.ComboBox.superclass.fireKey.call(this, e); - } - }, - - - onResize : function(w, h){ - Ext.form.ComboBox.superclass.onResize.apply(this, arguments); - if(!isNaN(w) && this.isVisible() && this.list){ - this.doResize(w); - }else{ - this.bufferSize = w; - } - }, - - doResize: function(w){ - if(!Ext.isDefined(this.listWidth)){ - var lw = Math.max(w, this.minListWidth); - this.list.setWidth(lw); - this.innerList.setWidth(lw - this.list.getFrameWidth('lr')); - } - }, - - - onEnable : function(){ - Ext.form.ComboBox.superclass.onEnable.apply(this, arguments); - if(this.hiddenField){ - this.hiddenField.disabled = false; - } - }, - - - onDisable : function(){ - Ext.form.ComboBox.superclass.onDisable.apply(this, arguments); - if(this.hiddenField){ - this.hiddenField.disabled = true; - } - }, - - - onBeforeLoad : function(){ - if(!this.hasFocus){ - return; - } - this.innerList.update(this.loadingText ? - '
        '+this.loadingText+'
        ' : ''); - this.restrictHeight(); - this.selectedIndex = -1; - }, - - - onLoad : function(){ - if(!this.hasFocus){ - return; - } - if(this.store.getCount() > 0 || this.listEmptyText){ - this.expand(); - this.restrictHeight(); - if(this.lastQuery == this.allQuery){ - if(this.editable){ - this.el.dom.select(); - } - - if(this.autoSelect !== false && !this.selectByValue(this.value, true)){ - this.select(0, true); - } - }else{ - if(this.autoSelect !== false){ - this.selectNext(); - } - if(this.typeAhead && this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE){ - this.taTask.delay(this.typeAheadDelay); - } - } - }else{ - this.collapse(); - } - - }, - - - onTypeAhead : function(){ - if(this.store.getCount() > 0){ - var r = this.store.getAt(0); - var newValue = r.data[this.displayField]; - var len = newValue.length; - var selStart = this.getRawValue().length; - if(selStart != len){ - this.setRawValue(newValue); - this.selectText(selStart, newValue.length); - } - } - }, - - - assertValue : function(){ - var val = this.getRawValue(), - rec; - - if(this.valueField && Ext.isDefined(this.value)){ - rec = this.findRecord(this.valueField, this.value); - } - if(!rec || rec.get(this.displayField) != val){ - rec = this.findRecord(this.displayField, val); - } - if(!rec && this.forceSelection){ - if(val.length > 0 && val != this.emptyText){ - this.el.dom.value = Ext.value(this.lastSelectionText, ''); - this.applyEmptyText(); - }else{ - this.clearValue(); - } - }else{ - if(rec && this.valueField){ - - - - if (this.value == val){ - return; - } - val = rec.get(this.valueField || this.displayField); - } - this.setValue(val); - } - }, - - - onSelect : function(record, index){ - if(this.fireEvent('beforeselect', this, record, index) !== false){ - this.setValue(record.data[this.valueField || this.displayField]); - this.collapse(); - this.fireEvent('select', this, record, index); - } - }, - - - getName: function(){ - var hf = this.hiddenField; - return hf && hf.name ? hf.name : this.hiddenName || Ext.form.ComboBox.superclass.getName.call(this); - }, - - - getValue : function(){ - if(this.valueField){ - return Ext.isDefined(this.value) ? this.value : ''; - }else{ - return Ext.form.ComboBox.superclass.getValue.call(this); - } - }, - - - clearValue : function(){ - if(this.hiddenField){ - this.hiddenField.value = ''; - } - this.setRawValue(''); - this.lastSelectionText = ''; - this.applyEmptyText(); - this.value = ''; - }, - - - setValue : function(v){ - var text = v; - if(this.valueField){ - var r = this.findRecord(this.valueField, v); - if(r){ - text = r.data[this.displayField]; - }else if(Ext.isDefined(this.valueNotFoundText)){ - text = this.valueNotFoundText; - } - } - this.lastSelectionText = text; - if(this.hiddenField){ - this.hiddenField.value = Ext.value(v, ''); - } - Ext.form.ComboBox.superclass.setValue.call(this, text); - this.value = v; - return this; - }, - - - findRecord : function(prop, value){ - var record; - if(this.store.getCount() > 0){ - this.store.each(function(r){ - if(r.data[prop] == value){ - record = r; - return false; - } - }); - } - return record; - }, - - - onViewMove : function(e, t){ - this.inKeyMode = false; - }, - - - onViewOver : function(e, t){ - if(this.inKeyMode){ - return; - } - var item = this.view.findItemFromChild(t); - if(item){ - var index = this.view.indexOf(item); - this.select(index, false); - } - }, - - - onViewClick : function(doFocus){ - var index = this.view.getSelectedIndexes()[0], - s = this.store, - r = s.getAt(index); - if(r){ - this.onSelect(r, index); - }else { - this.collapse(); - } - if(doFocus !== false){ - this.el.focus(); - } - }, - - - - restrictHeight : function(){ - this.innerList.dom.style.height = ''; - var inner = this.innerList.dom, - pad = this.list.getFrameWidth('tb') + (this.resizable ? this.handleHeight : 0) + this.assetHeight, - h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight), - ha = this.getPosition()[1]-Ext.getBody().getScroll().top, - hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height, - space = Math.max(ha, hb, this.minHeight || 0)-this.list.shadowOffset-pad-5; - - h = Math.min(h, space, this.maxHeight); - - this.innerList.setHeight(h); - this.list.beginUpdate(); - this.list.setHeight(h+pad); - this.list.alignTo.apply(this.list, [this.el].concat(this.listAlign)); - this.list.endUpdate(); - }, - - - isExpanded : function(){ - return this.list && this.list.isVisible(); - }, - - - selectByValue : function(v, scrollIntoView){ - if(!Ext.isEmpty(v, true)){ - var r = this.findRecord(this.valueField || this.displayField, v); - if(r){ - this.select(this.store.indexOf(r), scrollIntoView); - return true; - } - } - return false; - }, - - - select : function(index, scrollIntoView){ - this.selectedIndex = index; - this.view.select(index); - if(scrollIntoView !== false){ - var el = this.view.getNode(index); - if(el){ - this.innerList.scrollChildIntoView(el, false); - } - } - - }, - - - selectNext : function(){ - var ct = this.store.getCount(); - if(ct > 0){ - if(this.selectedIndex == -1){ - this.select(0); - }else if(this.selectedIndex < ct-1){ - this.select(this.selectedIndex+1); - } - } - }, - - - selectPrev : function(){ - var ct = this.store.getCount(); - if(ct > 0){ - if(this.selectedIndex == -1){ - this.select(0); - }else if(this.selectedIndex !== 0){ - this.select(this.selectedIndex-1); - } - } - }, - - - onKeyUp : function(e){ - var k = e.getKey(); - if(this.editable !== false && this.readOnly !== true && (k == e.BACKSPACE || !e.isSpecialKey())){ - - this.lastKey = k; - this.dqTask.delay(this.queryDelay); - } - Ext.form.ComboBox.superclass.onKeyUp.call(this, e); - }, - - - validateBlur : function(){ - return !this.list || !this.list.isVisible(); - }, - - - initQuery : function(){ - this.doQuery(this.getRawValue()); - }, - - - beforeBlur : function(){ - this.assertValue(); - }, - - - postBlur : function(){ - Ext.form.ComboBox.superclass.postBlur.call(this); - this.collapse(); - this.inKeyMode = false; - }, - - - doQuery : function(q, forceAll){ - q = Ext.isEmpty(q) ? '' : q; - var qe = { - query: q, - forceAll: forceAll, - combo: this, - cancel:false - }; - if(this.fireEvent('beforequery', qe)===false || qe.cancel){ - return false; - } - q = qe.query; - forceAll = qe.forceAll; - if(forceAll === true || (q.length >= this.minChars)){ - if(this.lastQuery !== q){ - this.lastQuery = q; - if(this.mode == 'local'){ - this.selectedIndex = -1; - if(forceAll){ - this.store.clearFilter(); - }else{ - this.store.filter(this.displayField, q); - } - this.onLoad(); - }else{ - this.store.baseParams[this.queryParam] = q; - this.store.load({ - params: this.getParams(q) - }); - this.expand(); - } - }else{ - this.selectedIndex = -1; - this.onLoad(); - } - } - }, - - - getParams : function(q){ - var params = {}, - paramNames = this.store.paramNames; - if(this.pageSize){ - params[paramNames.start] = 0; - params[paramNames.limit] = this.pageSize; - } - return params; - }, - - - collapse : function(){ - if(!this.isExpanded()){ - return; - } - this.list.hide(); - Ext.getDoc().un('mousewheel', this.collapseIf, this); - Ext.getDoc().un('mousedown', this.collapseIf, this); - this.fireEvent('collapse', this); - }, - - - collapseIf : function(e){ - if(!this.isDestroyed && !e.within(this.wrap) && !e.within(this.list)){ - this.collapse(); - } - }, - - - expand : function(){ - if(this.isExpanded() || !this.hasFocus){ - return; - } - - if(this.title || this.pageSize){ - this.assetHeight = 0; - if(this.title){ - this.assetHeight += this.header.getHeight(); - } - if(this.pageSize){ - this.assetHeight += this.footer.getHeight(); - } - } - - if(this.bufferSize){ - this.doResize(this.bufferSize); - delete this.bufferSize; - } - this.list.alignTo.apply(this.list, [this.el].concat(this.listAlign)); - - - this.list.setZIndex(this.getZIndex()); - this.list.show(); - if(Ext.isGecko2){ - this.innerList.setOverflow('auto'); - } - this.mon(Ext.getDoc(), { - scope: this, - mousewheel: this.collapseIf, - mousedown: this.collapseIf - }); - this.fireEvent('expand', this); - }, - - - - - onTriggerClick : function(){ - if(this.readOnly || this.disabled){ - return; - } - if(this.isExpanded()){ - this.collapse(); - this.el.focus(); - }else { - this.onFocus({}); - if(this.triggerAction == 'all') { - this.doQuery(this.allQuery, true); - } else { - this.doQuery(this.getRawValue()); - } - this.el.focus(); - } - } - - - - - - -}); -Ext.reg('combo', Ext.form.ComboBox); - -Ext.form.Checkbox = Ext.extend(Ext.form.Field, { - - focusClass : undefined, - - fieldClass : 'x-form-field', - - checked : false, - - boxLabel: ' ', - - defaultAutoCreate : { tag: 'input', type: 'checkbox', autocomplete: 'off'}, - - - - - - actionMode : 'wrap', - - - initComponent : function(){ - Ext.form.Checkbox.superclass.initComponent.call(this); - this.addEvents( - - 'check' - ); - }, - - - onResize : function(){ - Ext.form.Checkbox.superclass.onResize.apply(this, arguments); - if(!this.boxLabel && !this.fieldLabel){ - this.el.alignTo(this.wrap, 'c-c'); - } - }, - - - initEvents : function(){ - Ext.form.Checkbox.superclass.initEvents.call(this); - this.mon(this.el, { - scope: this, - click: this.onClick, - change: this.onClick - }); - }, - - - markInvalid : Ext.emptyFn, - - clearInvalid : Ext.emptyFn, - - - onRender : function(ct, position){ - Ext.form.Checkbox.superclass.onRender.call(this, ct, position); - if(this.inputValue !== undefined){ - this.el.dom.value = this.inputValue; - } - this.wrap = this.el.wrap({cls: 'x-form-check-wrap'}); - if(this.boxLabel){ - this.wrap.createChild({tag: 'label', htmlFor: this.el.id, cls: 'x-form-cb-label', html: this.boxLabel}); - } - if(this.checked){ - this.setValue(true); - }else{ - this.checked = this.el.dom.checked; - } - - if (Ext.isIE && !Ext.isStrict) { - this.wrap.repaint(); - } - this.resizeEl = this.positionEl = this.wrap; - }, - - - onDestroy : function(){ - Ext.destroy(this.wrap); - Ext.form.Checkbox.superclass.onDestroy.call(this); - }, - - - initValue : function() { - this.originalValue = this.getValue(); - }, - - - getValue : function(){ - if(this.rendered){ - return this.el.dom.checked; - } - return this.checked; - }, - - - onClick : function(){ - if(this.el.dom.checked != this.checked){ - this.setValue(this.el.dom.checked); - } - }, - - - setValue : function(v){ - var checked = this.checked, - inputVal = this.inputValue; - - if (v === false) { - this.checked = false; - } else { - this.checked = (v === true || v === 'true' || v == '1' || (inputVal ? v == inputVal : String(v).toLowerCase() == 'on')); - } - - if(this.rendered){ - this.el.dom.checked = this.checked; - this.el.dom.defaultChecked = this.checked; - } - if(checked != this.checked){ - this.fireEvent('check', this, this.checked); - if(this.handler){ - this.handler.call(this.scope || this, this, this.checked); - } - } - return this; - } -}); -Ext.reg('checkbox', Ext.form.Checkbox); - -Ext.form.CheckboxGroup = Ext.extend(Ext.form.Field, { - - - columns : 'auto', - - vertical : false, - - allowBlank : true, - - blankText : "You must select at least one item in this group", - - - defaultType : 'checkbox', - - - groupCls : 'x-form-check-group', - - - initComponent: function(){ - this.addEvents( - - 'change' - ); - this.on('change', this.validate, this); - Ext.form.CheckboxGroup.superclass.initComponent.call(this); - }, - - - onRender : function(ct, position){ - if(!this.el){ - var panelCfg = { - autoEl: { - id: this.id - }, - cls: this.groupCls, - layout: 'column', - renderTo: ct, - bufferResize: false - }; - var colCfg = { - xtype: 'container', - defaultType: this.defaultType, - layout: 'form', - defaults: { - hideLabel: true, - anchor: '100%' - } - }; - - if(this.items[0].items){ - - - - Ext.apply(panelCfg, { - layoutConfig: {columns: this.items.length}, - defaults: this.defaults, - items: this.items - }); - for(var i=0, len=this.items.length; i0 && i%rows==0){ - ri++; - } - if(this.items[i].fieldLabel){ - this.items[i].hideLabel = false; - } - cols[ri].items.push(this.items[i]); - }; - }else{ - for(var i=0, len=this.items.length; i -1){ - item.setValue(true); - } - }); - }, - - - getBox : function(id){ - var box = null; - this.eachItem(function(f){ - if(id == f || f.dataIndex == id || f.id == id || f.getName() == id){ - box = f; - return false; - } - }); - return box; - }, - - - getValue : function(){ - var out = []; - this.eachItem(function(item){ - if(item.checked){ - out.push(item); - } - }); - return out; - }, - - - eachItem: function(fn, scope) { - if(this.items && this.items.each){ - this.items.each(fn, scope || this); - } - }, - - - - - getRawValue : Ext.emptyFn, - - - setRawValue : Ext.emptyFn - -}); - -Ext.reg('checkboxgroup', Ext.form.CheckboxGroup); - -Ext.form.CompositeField = Ext.extend(Ext.form.Field, { - - - defaultMargins: '0 5 0 0', - - - skipLastItemMargin: true, - - - isComposite: true, - - - combineErrors: true, - - - labelConnector: ', ', - - - - - - initComponent: function() { - var labels = [], - items = this.items, - item; - - for (var i=0, j = items.length; i < j; i++) { - item = items[i]; - - if (!Ext.isEmpty(item.ref)){ - item.ref = '../' + item.ref; - } - - labels.push(item.fieldLabel); - - - Ext.applyIf(item, this.defaults); - - - if (!(i == j - 1 && this.skipLastItemMargin)) { - Ext.applyIf(item, {margins: this.defaultMargins}); - } - } - - this.fieldLabel = this.fieldLabel || this.buildLabel(labels); - - - this.fieldErrors = new Ext.util.MixedCollection(true, function(item) { - return item.field; - }); - - this.fieldErrors.on({ - scope : this, - add : this.updateInvalidMark, - remove : this.updateInvalidMark, - replace: this.updateInvalidMark - }); - - Ext.form.CompositeField.superclass.initComponent.apply(this, arguments); - - this.innerCt = new Ext.Container({ - layout : 'hbox', - items : this.items, - cls : 'x-form-composite', - defaultMargins: '0 3 0 0', - ownerCt: this - }); - this.innerCt.ownerCt = undefined; - - var fields = this.innerCt.findBy(function(c) { - return c.isFormField; - }, this); - - - this.items = new Ext.util.MixedCollection(); - this.items.addAll(fields); - - }, - - - onRender: function(ct, position) { - if (!this.el) { - - var innerCt = this.innerCt; - innerCt.render(ct); - - this.el = innerCt.getEl(); - - - - if (this.combineErrors) { - this.eachItem(function(field) { - Ext.apply(field, { - markInvalid : this.onFieldMarkInvalid.createDelegate(this, [field], 0), - clearInvalid: this.onFieldClearInvalid.createDelegate(this, [field], 0) - }); - }); - } - - - var l = this.el.parent().parent().child('label', true); - if (l) { - l.setAttribute('for', this.items.items[0].id); - } - } - - Ext.form.CompositeField.superclass.onRender.apply(this, arguments); - }, - - - onFieldMarkInvalid: function(field, message) { - var name = field.getName(), - error = { - field: name, - errorName: field.fieldLabel || name, - error: message - }; - - this.fieldErrors.replace(name, error); - - if (!field.preventMark) { - field.el.addClass(field.invalidClass); - } - }, - - - onFieldClearInvalid: function(field) { - this.fieldErrors.removeKey(field.getName()); - - field.el.removeClass(field.invalidClass); - }, - - - updateInvalidMark: function() { - var ieStrict = Ext.isIE6 && Ext.isStrict; - - if (this.fieldErrors.length == 0) { - this.clearInvalid(); - - - if (ieStrict) { - this.clearInvalid.defer(50, this); - } - } else { - var message = this.buildCombinedErrorMessage(this.fieldErrors.items); - - this.sortErrors(); - this.markInvalid(message); - - - if (ieStrict) { - this.markInvalid(message); - } - } - }, - - - validateValue: function(value, preventMark) { - var valid = true; - - this.eachItem(function(field) { - if (!field.isValid(preventMark)) { - valid = false; - } - }); - - return valid; - }, - - - buildCombinedErrorMessage: function(errors) { - var combined = [], - error; - - for (var i = 0, j = errors.length; i < j; i++) { - error = errors[i]; - - combined.push(String.format("{0}: {1}", error.errorName, error.error)); - } - - return combined.join("
        "); - }, - - - sortErrors: function() { - var fields = this.items; - - this.fieldErrors.sort("ASC", function(a, b) { - var findByName = function(key) { - return function(field) { - return field.getName() == key; - }; - }; - - var aIndex = fields.findIndexBy(findByName(a.field)), - bIndex = fields.findIndexBy(findByName(b.field)); - - return aIndex < bIndex ? -1 : 1; - }); - }, - - - reset: function() { - this.eachItem(function(item) { - item.reset(); - }); - - - - (function() { - this.clearInvalid(); - }).defer(50, this); - }, - - - clearInvalidChildren: function() { - this.eachItem(function(item) { - item.clearInvalid(); - }); - }, - - - buildLabel: function(segments) { - return Ext.clean(segments).join(this.labelConnector); - }, - - - isDirty: function(){ - - if (this.disabled || !this.rendered) { - return false; - } - - var dirty = false; - this.eachItem(function(item){ - if(item.isDirty()){ - dirty = true; - return false; - } - }); - return dirty; - }, - - - eachItem: function(fn, scope) { - if(this.items && this.items.each){ - this.items.each(fn, scope || this); - } - }, - - - onResize: function(adjWidth, adjHeight, rawWidth, rawHeight) { - var innerCt = this.innerCt; - - if (this.rendered && innerCt.rendered) { - innerCt.setSize(adjWidth, adjHeight); - } - - Ext.form.CompositeField.superclass.onResize.apply(this, arguments); - }, - - - doLayout: function(shallow, force) { - if (this.rendered) { - var innerCt = this.innerCt; - - innerCt.forceLayout = this.ownerCt.forceLayout; - innerCt.doLayout(shallow, force); - } - }, - - - beforeDestroy: function(){ - Ext.destroy(this.innerCt); - - Ext.form.CompositeField.superclass.beforeDestroy.call(this); - }, - - - setReadOnly : function(readOnly) { - if (readOnly == undefined) { - readOnly = true; - } - readOnly = !!readOnly; - - if(this.rendered){ - this.eachItem(function(item){ - item.setReadOnly(readOnly); - }); - } - this.readOnly = readOnly; - }, - - onShow : function() { - Ext.form.CompositeField.superclass.onShow.call(this); - this.doLayout(); - }, - - - onDisable : function(){ - this.eachItem(function(item){ - item.disable(); - }); - }, - - - onEnable : function(){ - this.eachItem(function(item){ - item.enable(); - }); - } -}); - -Ext.reg('compositefield', Ext.form.CompositeField); -Ext.form.Radio = Ext.extend(Ext.form.Checkbox, { - inputType: 'radio', - - - markInvalid : Ext.emptyFn, - - clearInvalid : Ext.emptyFn, - - - getGroupValue : function(){ - var p = this.el.up('form') || Ext.getBody(); - var c = p.child('input[name="'+this.el.dom.name+'"]:checked', true); - return c ? c.value : null; - }, - - - setValue : function(v){ - var checkEl, - els, - radio; - if (typeof v == 'boolean') { - Ext.form.Radio.superclass.setValue.call(this, v); - } else if (this.rendered) { - checkEl = this.getCheckEl(); - radio = checkEl.child('input[name="' + this.el.dom.name + '"][value="' + v + '"]', true); - if(radio){ - Ext.getCmp(radio.id).setValue(true); - } - } - if(this.rendered && this.checked){ - checkEl = checkEl || this.getCheckEl(); - els = this.getCheckEl().select('input[name="' + this.el.dom.name + '"]'); - els.each(function(el){ - if(el.dom.id != this.id){ - Ext.getCmp(el.dom.id).setValue(false); - } - }, this); - } - return this; - }, - - - getCheckEl: function(){ - if(this.inGroup){ - return this.el.up('.x-form-radio-group'); - } - return this.el.up('form') || Ext.getBody(); - } -}); -Ext.reg('radio', Ext.form.Radio); - -Ext.form.RadioGroup = Ext.extend(Ext.form.CheckboxGroup, { - - - allowBlank : true, - - blankText : 'You must select one item in this group', - - - defaultType : 'radio', - - - groupCls : 'x-form-radio-group', - - - - - getValue : function(){ - var out = null; - this.eachItem(function(item){ - if(item.checked){ - out = item; - return false; - } - }); - return out; - }, - - - onSetValue : function(id, value){ - if(arguments.length > 1){ - var f = this.getBox(id); - if(f){ - f.setValue(value); - if(f.checked){ - this.eachItem(function(item){ - if (item !== f){ - item.setValue(false); - } - }); - } - } - }else{ - this.setValueForItem(id); - } - }, - - setValueForItem : function(val){ - val = String(val).split(',')[0]; - this.eachItem(function(item){ - item.setValue(val == item.inputValue); - }); - }, - - - fireChecked : function(){ - if(!this.checkTask){ - this.checkTask = new Ext.util.DelayedTask(this.bufferChecked, this); - } - this.checkTask.delay(10); - }, - - - bufferChecked : function(){ - var out = null; - this.eachItem(function(item){ - if(item.checked){ - out = item; - return false; - } - }); - this.fireEvent('change', this, out); - }, - - onDestroy : function(){ - if(this.checkTask){ - this.checkTask.cancel(); - this.checkTask = null; - } - Ext.form.RadioGroup.superclass.onDestroy.call(this); - } - -}); - -Ext.reg('radiogroup', Ext.form.RadioGroup); - -Ext.form.Hidden = Ext.extend(Ext.form.Field, { - - inputType : 'hidden', - - shouldLayout: false, - - - onRender : function(){ - Ext.form.Hidden.superclass.onRender.apply(this, arguments); - }, - - - initEvents : function(){ - this.originalValue = this.getValue(); - }, - - - setSize : Ext.emptyFn, - setWidth : Ext.emptyFn, - setHeight : Ext.emptyFn, - setPosition : Ext.emptyFn, - setPagePosition : Ext.emptyFn, - markInvalid : Ext.emptyFn, - clearInvalid : Ext.emptyFn -}); -Ext.reg('hidden', Ext.form.Hidden); -Ext.form.BasicForm = Ext.extend(Ext.util.Observable, { - - constructor: function(el, config){ - Ext.apply(this, config); - if(Ext.isString(this.paramOrder)){ - this.paramOrder = this.paramOrder.split(/[\s,|]/); - } - - this.items = new Ext.util.MixedCollection(false, function(o){ - return o.getItemId(); - }); - this.addEvents( - - 'beforeaction', - - 'actionfailed', - - 'actioncomplete' - ); - - if(el){ - this.initEl(el); - } - Ext.form.BasicForm.superclass.constructor.call(this); - }, - - - - - - - - - timeout: 30, - - - - - paramOrder: undefined, - - - paramsAsHash: false, - - - waitTitle: 'Please Wait...', - - - activeAction : null, - - - trackResetOnLoad : false, - - - - - - initEl : function(el){ - this.el = Ext.get(el); - this.id = this.el.id || Ext.id(); - if(!this.standardSubmit){ - this.el.on('submit', this.onSubmit, this); - } - this.el.addClass('x-form'); - }, - - - getEl: function(){ - return this.el; - }, - - - onSubmit : function(e){ - e.stopEvent(); - }, - - - destroy: function(bound){ - if(bound !== true){ - this.items.each(function(f){ - Ext.destroy(f); - }); - Ext.destroy(this.el); - } - this.items.clear(); - this.purgeListeners(); - }, - - - isValid : function(){ - var valid = true; - this.items.each(function(f){ - if(!f.validate()){ - valid = false; - } - }); - return valid; - }, - - - isDirty : function(){ - var dirty = false; - this.items.each(function(f){ - if(f.isDirty()){ - dirty = true; - return false; - } - }); - return dirty; - }, - - - doAction : function(action, options){ - if(Ext.isString(action)){ - action = new Ext.form.Action.ACTION_TYPES[action](this, options); - } - if(this.fireEvent('beforeaction', this, action) !== false){ - this.beforeAction(action); - action.run.defer(100, action); - } - return this; - }, - - - submit : function(options){ - options = options || {}; - if(this.standardSubmit){ - var v = options.clientValidation === false || this.isValid(); - if(v){ - var el = this.el.dom; - if(this.url && Ext.isEmpty(el.action)){ - el.action = this.url; - } - el.submit(); - } - return v; - } - var submitAction = String.format('{0}submit', this.api ? 'direct' : ''); - this.doAction(submitAction, options); - return this; - }, - - - load : function(options){ - var loadAction = String.format('{0}load', this.api ? 'direct' : ''); - this.doAction(loadAction, options); - return this; - }, - - - updateRecord : function(record){ - record.beginEdit(); - var fs = record.fields, - field, - value; - fs.each(function(f){ - field = this.findField(f.name); - if(field){ - value = field.getValue(); - if (Ext.type(value) !== false && value.getGroupValue) { - value = value.getGroupValue(); - } else if ( field.eachItem ) { - value = []; - field.eachItem(function(item){ - value.push(item.getValue()); - }); - } - record.set(f.name, value); - } - }, this); - record.endEdit(); - return this; - }, - - - loadRecord : function(record){ - this.setValues(record.data); - return this; - }, - - - beforeAction : function(action){ - - this.items.each(function(f){ - if(f.isFormField && f.syncValue){ - f.syncValue(); - } - }); - var o = action.options; - if(o.waitMsg){ - if(this.waitMsgTarget === true){ - this.el.mask(o.waitMsg, 'x-mask-loading'); - }else if(this.waitMsgTarget){ - this.waitMsgTarget = Ext.get(this.waitMsgTarget); - this.waitMsgTarget.mask(o.waitMsg, 'x-mask-loading'); - }else{ - Ext.MessageBox.wait(o.waitMsg, o.waitTitle || this.waitTitle); - } - } - }, - - - afterAction : function(action, success){ - this.activeAction = null; - var o = action.options; - if(o.waitMsg){ - if(this.waitMsgTarget === true){ - this.el.unmask(); - }else if(this.waitMsgTarget){ - this.waitMsgTarget.unmask(); - }else{ - Ext.MessageBox.updateProgress(1); - Ext.MessageBox.hide(); - } - } - if(success){ - if(o.reset){ - this.reset(); - } - Ext.callback(o.success, o.scope, [this, action]); - this.fireEvent('actioncomplete', this, action); - }else{ - Ext.callback(o.failure, o.scope, [this, action]); - this.fireEvent('actionfailed', this, action); - } - }, - - - findField : function(id) { - var field = this.items.get(id); - - if (!Ext.isObject(field)) { - - var findMatchingField = function(f) { - if (f.isFormField) { - if (f.dataIndex == id || f.id == id || f.getName() == id) { - field = f; - return false; - } else if (f.isComposite) { - return f.items.each(findMatchingField); - } else if (f instanceof Ext.form.CheckboxGroup && f.rendered) { - return f.eachItem(findMatchingField); - } - } - }; - - this.items.each(findMatchingField); - } - return field || null; - }, - - - - markInvalid : function(errors){ - if (Ext.isArray(errors)) { - for(var i = 0, len = errors.length; i < len; i++){ - var fieldError = errors[i]; - var f = this.findField(fieldError.id); - if(f){ - f.markInvalid(fieldError.msg); - } - } - } else { - var field, id; - for(id in errors){ - if(!Ext.isFunction(errors[id]) && (field = this.findField(id))){ - field.markInvalid(errors[id]); - } - } - } - - return this; - }, - - - setValues : function(values){ - if(Ext.isArray(values)){ - for(var i = 0, len = values.length; i < len; i++){ - var v = values[i]; - var f = this.findField(v.id); - if(f){ - f.setValue(v.value); - if(this.trackResetOnLoad){ - f.originalValue = f.getValue(); - } - } - } - }else{ - var field, id; - for(id in values){ - if(!Ext.isFunction(values[id]) && (field = this.findField(id))){ - field.setValue(values[id]); - if(this.trackResetOnLoad){ - field.originalValue = field.getValue(); - } - } - } - } - return this; - }, - - - getValues : function(asString){ - var fs = Ext.lib.Ajax.serializeForm(this.el.dom); - if(asString === true){ - return fs; - } - return Ext.urlDecode(fs); - }, - - - getFieldValues : function(dirtyOnly){ - var o = {}, - n, - key, - val; - this.items.each(function(f) { - if (!f.disabled && (dirtyOnly !== true || f.isDirty())) { - n = f.getName(); - key = o[n]; - val = f.getValue(); - - if(Ext.isDefined(key)){ - if(Ext.isArray(key)){ - o[n].push(val); - }else{ - o[n] = [key, val]; - } - }else{ - o[n] = val; - } - } - }); - return o; - }, - - - clearInvalid : function(){ - this.items.each(function(f){ - f.clearInvalid(); - }); - return this; - }, - - - reset : function(){ - this.items.each(function(f){ - f.reset(); - }); - return this; - }, - - - add : function(){ - this.items.addAll(Array.prototype.slice.call(arguments, 0)); - return this; - }, - - - remove : function(field){ - this.items.remove(field); - return this; - }, - - - cleanDestroyed : function() { - this.items.filterBy(function(o) { return !!o.isDestroyed; }).each(this.remove, this); - }, - - - render : function(){ - this.items.each(function(f){ - if(f.isFormField && !f.rendered && document.getElementById(f.id)){ - f.applyToMarkup(f.id); - } - }); - return this; - }, - - - applyToFields : function(o){ - this.items.each(function(f){ - Ext.apply(f, o); - }); - return this; - }, - - - applyIfToFields : function(o){ - this.items.each(function(f){ - Ext.applyIf(f, o); - }); - return this; - }, - - callFieldMethod : function(fnName, args){ - args = args || []; - this.items.each(function(f){ - if(Ext.isFunction(f[fnName])){ - f[fnName].apply(f, args); - } - }); - return this; - } -}); - - -Ext.BasicForm = Ext.form.BasicForm; - -Ext.FormPanel = Ext.extend(Ext.Panel, { - - - - - - - - - - - minButtonWidth : 75, - - - labelAlign : 'left', - - - monitorValid : false, - - - monitorPoll : 200, - - - layout : 'form', - - - initComponent : function(){ - this.form = this.createForm(); - Ext.FormPanel.superclass.initComponent.call(this); - - this.bodyCfg = { - tag: 'form', - cls: this.baseCls + '-body', - method : this.method || 'POST', - id : this.formId || Ext.id() - }; - if(this.fileUpload) { - this.bodyCfg.enctype = 'multipart/form-data'; - } - this.initItems(); - - this.addEvents( - - 'clientvalidation' - ); - - this.relayEvents(this.form, ['beforeaction', 'actionfailed', 'actioncomplete']); - }, - - - createForm : function(){ - var config = Ext.applyIf({listeners: {}}, this.initialConfig); - return new Ext.form.BasicForm(null, config); - }, - - - initFields : function(){ - var f = this.form; - var formPanel = this; - var fn = function(c){ - if(formPanel.isField(c)){ - f.add(c); - }else if(c.findBy && c != formPanel){ - formPanel.applySettings(c); - - if(c.items && c.items.each){ - c.items.each(fn, this); - } - } - }; - this.items.each(fn, this); - }, - - - applySettings: function(c){ - var ct = c.ownerCt; - Ext.applyIf(c, { - labelAlign: ct.labelAlign, - labelWidth: ct.labelWidth, - itemCls: ct.itemCls - }); - }, - - - getLayoutTarget : function(){ - return this.form.el; - }, - - - getForm : function(){ - return this.form; - }, - - - onRender : function(ct, position){ - this.initFields(); - Ext.FormPanel.superclass.onRender.call(this, ct, position); - this.form.initEl(this.body); - }, - - - beforeDestroy : function(){ - this.stopMonitoring(); - this.form.destroy(true); - Ext.FormPanel.superclass.beforeDestroy.call(this); - }, - - - isField : function(c) { - return !!c.setValue && !!c.getValue && !!c.markInvalid && !!c.clearInvalid; - }, - - - initEvents : function(){ - Ext.FormPanel.superclass.initEvents.call(this); - - this.on({ - scope: this, - add: this.onAddEvent, - remove: this.onRemoveEvent - }); - if(this.monitorValid){ - this.startMonitoring(); - } - }, - - - onAdd: function(c){ - Ext.FormPanel.superclass.onAdd.call(this, c); - this.processAdd(c); - }, - - - onAddEvent: function(ct, c){ - if(ct !== this){ - this.processAdd(c); - } - }, - - - processAdd : function(c){ - - if(this.isField(c)){ - this.form.add(c); - - }else if(c.findBy){ - this.applySettings(c); - this.form.add.apply(this.form, c.findBy(this.isField)); - } - }, - - - onRemove: function(c){ - Ext.FormPanel.superclass.onRemove.call(this, c); - this.processRemove(c); - }, - - onRemoveEvent: function(ct, c){ - if(ct !== this){ - this.processRemove(c); - } - }, - - - processRemove: function(c){ - if(!this.destroying){ - - if(this.isField(c)){ - this.form.remove(c); - - }else if (c.findBy){ - Ext.each(c.findBy(this.isField), this.form.remove, this.form); - - this.form.cleanDestroyed(); - } - } - }, - - - startMonitoring : function(){ - if(!this.validTask){ - this.validTask = new Ext.util.TaskRunner(); - this.validTask.start({ - run : this.bindHandler, - interval : this.monitorPoll || 200, - scope: this - }); - } - }, - - - stopMonitoring : function(){ - if(this.validTask){ - this.validTask.stopAll(); - this.validTask = null; - } - }, - - - load : function(){ - this.form.load.apply(this.form, arguments); - }, - - - onDisable : function(){ - Ext.FormPanel.superclass.onDisable.call(this); - if(this.form){ - this.form.items.each(function(){ - this.disable(); - }); - } - }, - - - onEnable : function(){ - Ext.FormPanel.superclass.onEnable.call(this); - if(this.form){ - this.form.items.each(function(){ - this.enable(); - }); - } - }, - - - bindHandler : function(){ - var valid = true; - this.form.items.each(function(f){ - if(!f.isValid(true)){ - valid = false; - return false; - } - }); - if(this.fbar){ - var fitems = this.fbar.items.items; - for(var i = 0, len = fitems.length; i < len; i++){ - var btn = fitems[i]; - if(btn.formBind === true && btn.disabled === valid){ - btn.setDisabled(!valid); - } - } - } - this.fireEvent('clientvalidation', this, valid); - } -}); -Ext.reg('form', Ext.FormPanel); - -Ext.form.FormPanel = Ext.FormPanel; - -Ext.form.FieldSet = Ext.extend(Ext.Panel, { - - - - - - - baseCls : 'x-fieldset', - - layout : 'form', - - animCollapse : false, - - - onRender : function(ct, position){ - if(!this.el){ - this.el = document.createElement('fieldset'); - this.el.id = this.id; - if (this.title || this.header || this.checkboxToggle) { - this.el.appendChild(document.createElement('legend')).className = this.baseCls + '-header'; - } - } - - Ext.form.FieldSet.superclass.onRender.call(this, ct, position); - - if(this.checkboxToggle){ - var o = typeof this.checkboxToggle == 'object' ? - this.checkboxToggle : - {tag: 'input', type: 'checkbox', name: this.checkboxName || this.id+'-checkbox'}; - this.checkbox = this.header.insertFirst(o); - this.checkbox.dom.checked = !this.collapsed; - this.mon(this.checkbox, 'click', this.onCheckClick, this); - } - }, - - - onCollapse : function(doAnim, animArg){ - if(this.checkbox){ - this.checkbox.dom.checked = false; - } - Ext.form.FieldSet.superclass.onCollapse.call(this, doAnim, animArg); - - }, - - - onExpand : function(doAnim, animArg){ - if(this.checkbox){ - this.checkbox.dom.checked = true; - } - Ext.form.FieldSet.superclass.onExpand.call(this, doAnim, animArg); - }, - - - onCheckClick : function(){ - this[this.checkbox.dom.checked ? 'expand' : 'collapse'](); - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -}); -Ext.reg('fieldset', Ext.form.FieldSet); - -Ext.form.HtmlEditor = Ext.extend(Ext.form.Field, { - - enableFormat : true, - - enableFontSize : true, - - enableColors : true, - - enableAlignments : true, - - enableLists : true, - - enableSourceEdit : true, - - enableLinks : true, - - enableFont : true, - - createLinkText : 'Please enter the URL for the link:', - - defaultLinkValue : 'http:/'+'/', - - fontFamilies : [ - 'Arial', - 'Courier New', - 'Tahoma', - 'Times New Roman', - 'Verdana' - ], - defaultFont: 'tahoma', - - defaultValue: (Ext.isOpera || Ext.isIE6) ? ' ' : '​', - - - actionMode: 'wrap', - validationEvent : false, - deferHeight: true, - initialized : false, - activated : false, - sourceEditMode : false, - onFocus : Ext.emptyFn, - iframePad:3, - hideMode:'offsets', - defaultAutoCreate : { - tag: "textarea", - style:"width:500px;height:300px;", - autocomplete: "off" - }, - - - initComponent : function(){ - this.addEvents( - - 'initialize', - - 'activate', - - 'beforesync', - - 'beforepush', - - 'sync', - - 'push', - - 'editmodechange' - ); - Ext.form.HtmlEditor.superclass.initComponent.call(this); - }, - - - createFontOptions : function(){ - var buf = [], fs = this.fontFamilies, ff, lc; - for(var i = 0, len = fs.length; i< len; i++){ - ff = fs[i]; - lc = ff.toLowerCase(); - buf.push( - '' - ); - } - return buf.join(''); - }, - - - createToolbar : function(editor){ - var items = []; - var tipsEnabled = Ext.QuickTips && Ext.QuickTips.isEnabled(); - - - function btn(id, toggle, handler){ - return { - itemId : id, - cls : 'x-btn-icon', - iconCls: 'x-edit-'+id, - enableToggle:toggle !== false, - scope: editor, - handler:handler||editor.relayBtnCmd, - clickEvent:'mousedown', - tooltip: tipsEnabled ? editor.buttonTips[id] || undefined : undefined, - overflowText: editor.buttonTips[id].title || undefined, - tabIndex:-1 - }; - } - - - if(this.enableFont && !Ext.isSafari2){ - var fontSelectItem = new Ext.Toolbar.Item({ - autoEl: { - tag:'select', - cls:'x-font-select', - html: this.createFontOptions() - } - }); - - items.push( - fontSelectItem, - '-' - ); - } - - if(this.enableFormat){ - items.push( - btn('bold'), - btn('italic'), - btn('underline') - ); - } - - if(this.enableFontSize){ - items.push( - '-', - btn('increasefontsize', false, this.adjustFont), - btn('decreasefontsize', false, this.adjustFont) - ); - } - - if(this.enableColors){ - items.push( - '-', { - itemId:'forecolor', - cls:'x-btn-icon', - iconCls: 'x-edit-forecolor', - clickEvent:'mousedown', - tooltip: tipsEnabled ? editor.buttonTips.forecolor || undefined : undefined, - tabIndex:-1, - menu : new Ext.menu.ColorMenu({ - allowReselect: true, - focus: Ext.emptyFn, - value:'000000', - plain:true, - listeners: { - scope: this, - select: function(cp, color){ - this.execCmd('forecolor', Ext.isWebKit || Ext.isIE ? '#'+color : color); - this.deferFocus(); - } - }, - clickEvent:'mousedown' - }) - }, { - itemId:'backcolor', - cls:'x-btn-icon', - iconCls: 'x-edit-backcolor', - clickEvent:'mousedown', - tooltip: tipsEnabled ? editor.buttonTips.backcolor || undefined : undefined, - tabIndex:-1, - menu : new Ext.menu.ColorMenu({ - focus: Ext.emptyFn, - value:'FFFFFF', - plain:true, - allowReselect: true, - listeners: { - scope: this, - select: function(cp, color){ - if(Ext.isGecko){ - this.execCmd('useCSS', false); - this.execCmd('hilitecolor', color); - this.execCmd('useCSS', true); - this.deferFocus(); - }else{ - this.execCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE ? '#'+color : color); - this.deferFocus(); - } - } - }, - clickEvent:'mousedown' - }) - } - ); - } - - if(this.enableAlignments){ - items.push( - '-', - btn('justifyleft'), - btn('justifycenter'), - btn('justifyright') - ); - } - - if(!Ext.isSafari2){ - if(this.enableLinks){ - items.push( - '-', - btn('createlink', false, this.createLink) - ); - } - - if(this.enableLists){ - items.push( - '-', - btn('insertorderedlist'), - btn('insertunorderedlist') - ); - } - if(this.enableSourceEdit){ - items.push( - '-', - btn('sourceedit', true, function(btn){ - this.toggleSourceEdit(!this.sourceEditMode); - }) - ); - } - } - - - var tb = new Ext.Toolbar({ - renderTo: this.wrap.dom.firstChild, - items: items - }); - - if (fontSelectItem) { - this.fontSelect = fontSelectItem.el; - - this.mon(this.fontSelect, 'change', function(){ - var font = this.fontSelect.dom.value; - this.relayCmd('fontname', font); - this.deferFocus(); - }, this); - } - - - this.mon(tb.el, 'click', function(e){ - e.preventDefault(); - }); - - this.tb = tb; - this.tb.doLayout(); - }, - - onDisable: function(){ - this.wrap.mask(); - Ext.form.HtmlEditor.superclass.onDisable.call(this); - }, - - onEnable: function(){ - this.wrap.unmask(); - Ext.form.HtmlEditor.superclass.onEnable.call(this); - }, - - setReadOnly: function(readOnly){ - - Ext.form.HtmlEditor.superclass.setReadOnly.call(this, readOnly); - if(this.initialized){ - if(Ext.isIE){ - this.getEditorBody().contentEditable = !readOnly; - }else{ - this.setDesignMode(!readOnly); - } - var bd = this.getEditorBody(); - if(bd){ - bd.style.cursor = this.readOnly ? 'default' : 'text'; - } - this.disableItems(readOnly); - } - }, - - - getDocMarkup : function(){ - var h = Ext.fly(this.iframe).getHeight() - this.iframePad * 2; - return String.format('', this.iframePad, h); - }, - - - getEditorBody : function(){ - var doc = this.getDoc(); - return doc.body || doc.documentElement; - }, - - - getDoc : function(){ - return Ext.isIE ? this.getWin().document : (this.iframe.contentDocument || this.getWin().document); - }, - - - getWin : function(){ - return Ext.isIE ? this.iframe.contentWindow : window.frames[this.iframe.name]; - }, - - - onRender : function(ct, position){ - Ext.form.HtmlEditor.superclass.onRender.call(this, ct, position); - this.el.dom.style.border = '0 none'; - this.el.dom.setAttribute('tabIndex', -1); - this.el.addClass('x-hidden'); - if(Ext.isIE){ - this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;'); - } - this.wrap = this.el.wrap({ - cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'} - }); - - this.createToolbar(this); - - this.disableItems(true); - - this.tb.doLayout(); - - this.createIFrame(); - - if(!this.width){ - var sz = this.el.getSize(); - this.setSize(sz.width, this.height || sz.height); - } - this.resizeEl = this.positionEl = this.wrap; - }, - - createIFrame: function(){ - var iframe = document.createElement('iframe'); - iframe.name = Ext.id(); - iframe.frameBorder = '0'; - iframe.style.overflow = 'auto'; - iframe.src = Ext.SSL_SECURE_URL; - - this.wrap.dom.appendChild(iframe); - this.iframe = iframe; - - this.monitorTask = Ext.TaskMgr.start({ - run: this.checkDesignMode, - scope: this, - interval:100 - }); - }, - - initFrame : function(){ - Ext.TaskMgr.stop(this.monitorTask); - var doc = this.getDoc(); - this.win = this.getWin(); - - doc.open(); - doc.write(this.getDocMarkup()); - doc.close(); - - var task = { - run : function(){ - var doc = this.getDoc(); - if(doc.body || doc.readyState == 'complete'){ - Ext.TaskMgr.stop(task); - this.setDesignMode(true); - this.initEditor.defer(10, this); - } - }, - interval : 10, - duration:10000, - scope: this - }; - Ext.TaskMgr.start(task); - }, - - - checkDesignMode : function(){ - if(this.wrap && this.wrap.dom.offsetWidth){ - var doc = this.getDoc(); - if(!doc){ - return; - } - if(!doc.editorInitialized || this.getDesignMode() != 'on'){ - this.initFrame(); - } - } - }, - - - setDesignMode : function(mode){ - var doc = this.getDoc(); - if (doc) { - if(this.readOnly){ - mode = false; - } - doc.designMode = (/on|true/i).test(String(mode).toLowerCase()) ?'on':'off'; - } - - }, - - - getDesignMode : function(){ - var doc = this.getDoc(); - if(!doc){ return ''; } - return String(doc.designMode).toLowerCase(); - - }, - - disableItems: function(disabled){ - if(this.fontSelect){ - this.fontSelect.dom.disabled = disabled; - } - this.tb.items.each(function(item){ - if(item.getItemId() != 'sourceedit'){ - item.setDisabled(disabled); - } - }); - }, - - - onResize : function(w, h){ - Ext.form.HtmlEditor.superclass.onResize.apply(this, arguments); - if(this.el && this.iframe){ - if(Ext.isNumber(w)){ - var aw = w - this.wrap.getFrameWidth('lr'); - this.el.setWidth(aw); - this.tb.setWidth(aw); - this.iframe.style.width = Math.max(aw, 0) + 'px'; - } - if(Ext.isNumber(h)){ - var ah = h - this.wrap.getFrameWidth('tb') - this.tb.el.getHeight(); - this.el.setHeight(ah); - this.iframe.style.height = Math.max(ah, 0) + 'px'; - var bd = this.getEditorBody(); - if(bd){ - bd.style.height = Math.max((ah - (this.iframePad*2)), 0) + 'px'; - } - } - } - }, - - - toggleSourceEdit : function(sourceEditMode){ - var iframeHeight, - elHeight; - - if (sourceEditMode === undefined) { - sourceEditMode = !this.sourceEditMode; - } - this.sourceEditMode = sourceEditMode === true; - var btn = this.tb.getComponent('sourceedit'); - - if (btn.pressed !== this.sourceEditMode) { - btn.toggle(this.sourceEditMode); - if (!btn.xtbHidden) { - return; - } - } - if (this.sourceEditMode) { - - this.previousSize = this.getSize(); - - iframeHeight = Ext.get(this.iframe).getHeight(); - - this.disableItems(true); - this.syncValue(); - this.iframe.className = 'x-hidden'; - this.el.removeClass('x-hidden'); - this.el.dom.removeAttribute('tabIndex'); - this.el.focus(); - this.el.dom.style.height = iframeHeight + 'px'; - } - else { - elHeight = parseInt(this.el.dom.style.height, 10); - if (this.initialized) { - this.disableItems(this.readOnly); - } - this.pushValue(); - this.iframe.className = ''; - this.el.addClass('x-hidden'); - this.el.dom.setAttribute('tabIndex', -1); - this.deferFocus(); - - this.setSize(this.previousSize); - delete this.previousSize; - this.iframe.style.height = elHeight + 'px'; - } - this.fireEvent('editmodechange', this, this.sourceEditMode); - }, - - - createLink : function() { - var url = prompt(this.createLinkText, this.defaultLinkValue); - if(url && url != 'http:/'+'/'){ - this.relayCmd('createlink', url); - } - }, - - - initEvents : function(){ - this.originalValue = this.getValue(); - }, - - - markInvalid : Ext.emptyFn, - - - clearInvalid : Ext.emptyFn, - - - setValue : function(v){ - Ext.form.HtmlEditor.superclass.setValue.call(this, v); - this.pushValue(); - return this; - }, - - - cleanHtml: function(html) { - html = String(html); - if(Ext.isWebKit){ - html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, ''); - } - - - if(html.charCodeAt(0) == this.defaultValue.replace(/\D/g, '')){ - html = html.substring(1); - } - return html; - }, - - - syncValue : function(){ - if(this.initialized){ - var bd = this.getEditorBody(); - var html = bd.innerHTML; - if(Ext.isWebKit){ - var bs = bd.getAttribute('style'); - var m = bs.match(/text-align:(.*?);/i); - if(m && m[1]){ - html = '
        ' + html + '
        '; - } - } - html = this.cleanHtml(html); - if(this.fireEvent('beforesync', this, html) !== false){ - this.el.dom.value = html; - this.fireEvent('sync', this, html); - } - } - }, - - - getValue : function() { - this[this.sourceEditMode ? 'pushValue' : 'syncValue'](); - return Ext.form.HtmlEditor.superclass.getValue.call(this); - }, - - - pushValue : function(){ - if(this.initialized){ - var v = this.el.dom.value; - if(!this.activated && v.length < 1){ - v = this.defaultValue; - } - if(this.fireEvent('beforepush', this, v) !== false){ - this.getEditorBody().innerHTML = v; - if(Ext.isGecko){ - - this.setDesignMode(false); - this.setDesignMode(true); - } - this.fireEvent('push', this, v); - } - - } - }, - - - deferFocus : function(){ - this.focus.defer(10, this); - }, - - - focus : function(){ - if(this.win && !this.sourceEditMode){ - this.win.focus(); - }else{ - this.el.focus(); - } - }, - - - initEditor : function(){ - - try{ - var dbody = this.getEditorBody(), - ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat', 'background-color', 'color'), - doc, - fn; - - ss['background-attachment'] = 'fixed'; - dbody.bgProperties = 'fixed'; - - Ext.DomHelper.applyStyles(dbody, ss); - - doc = this.getDoc(); - - if(doc){ - try{ - Ext.EventManager.removeAll(doc); - }catch(e){} - } - - - fn = this.onEditorEvent.createDelegate(this); - Ext.EventManager.on(doc, { - mousedown: fn, - dblclick: fn, - click: fn, - keyup: fn, - buffer:100 - }); - - if(Ext.isGecko){ - Ext.EventManager.on(doc, 'keypress', this.applyCommand, this); - } - if(Ext.isIE || Ext.isWebKit || Ext.isOpera){ - Ext.EventManager.on(doc, 'keydown', this.fixKeys, this); - } - doc.editorInitialized = true; - this.initialized = true; - this.pushValue(); - this.setReadOnly(this.readOnly); - this.fireEvent('initialize', this); - }catch(e){} - }, - - - beforeDestroy : function(){ - if(this.monitorTask){ - Ext.TaskMgr.stop(this.monitorTask); - } - if(this.rendered){ - Ext.destroy(this.tb); - var doc = this.getDoc(); - if(doc){ - try{ - Ext.EventManager.removeAll(doc); - for (var prop in doc){ - delete doc[prop]; - } - }catch(e){} - } - if(this.wrap){ - this.wrap.dom.innerHTML = ''; - this.wrap.remove(); - } - } - Ext.form.HtmlEditor.superclass.beforeDestroy.call(this); - }, - - - onFirstFocus : function(){ - this.activated = true; - this.disableItems(this.readOnly); - if(Ext.isGecko){ - this.win.focus(); - var s = this.win.getSelection(); - if(!s.focusNode || s.focusNode.nodeType != 3){ - var r = s.getRangeAt(0); - r.selectNodeContents(this.getEditorBody()); - r.collapse(true); - this.deferFocus(); - } - try{ - this.execCmd('useCSS', true); - this.execCmd('styleWithCSS', false); - }catch(e){} - } - this.fireEvent('activate', this); - }, - - - adjustFont: function(btn){ - var adjust = btn.getItemId() == 'increasefontsize' ? 1 : -1, - doc = this.getDoc(), - v = parseInt(doc.queryCommandValue('FontSize') || 2, 10); - if((Ext.isSafari && !Ext.isSafari2) || Ext.isChrome || Ext.isAir){ - - - if(v <= 10){ - v = 1 + adjust; - }else if(v <= 13){ - v = 2 + adjust; - }else if(v <= 16){ - v = 3 + adjust; - }else if(v <= 18){ - v = 4 + adjust; - }else if(v <= 24){ - v = 5 + adjust; - }else { - v = 6 + adjust; - } - v = v.constrain(1, 6); - }else{ - if(Ext.isSafari){ - adjust *= 2; - } - v = Math.max(1, v+adjust) + (Ext.isSafari ? 'px' : 0); - } - this.execCmd('FontSize', v); - }, - - - onEditorEvent : function(e){ - this.updateToolbar(); - }, - - - - updateToolbar: function(){ - - if(this.readOnly){ - return; - } - - if(!this.activated){ - this.onFirstFocus(); - return; - } - - var btns = this.tb.items.map, - doc = this.getDoc(); - - if(this.enableFont && !Ext.isSafari2){ - var name = (doc.queryCommandValue('FontName')||this.defaultFont).toLowerCase(); - if(name != this.fontSelect.dom.value){ - this.fontSelect.dom.value = name; - } - } - if(this.enableFormat){ - btns.bold.toggle(doc.queryCommandState('bold')); - btns.italic.toggle(doc.queryCommandState('italic')); - btns.underline.toggle(doc.queryCommandState('underline')); - } - if(this.enableAlignments){ - btns.justifyleft.toggle(doc.queryCommandState('justifyleft')); - btns.justifycenter.toggle(doc.queryCommandState('justifycenter')); - btns.justifyright.toggle(doc.queryCommandState('justifyright')); - } - if(!Ext.isSafari2 && this.enableLists){ - btns.insertorderedlist.toggle(doc.queryCommandState('insertorderedlist')); - btns.insertunorderedlist.toggle(doc.queryCommandState('insertunorderedlist')); - } - - Ext.menu.MenuMgr.hideAll(); - - this.syncValue(); - }, - - - relayBtnCmd : function(btn){ - this.relayCmd(btn.getItemId()); - }, - - - relayCmd : function(cmd, value){ - (function(){ - this.focus(); - this.execCmd(cmd, value); - this.updateToolbar(); - }).defer(10, this); - }, - - - execCmd : function(cmd, value){ - var doc = this.getDoc(); - doc.execCommand(cmd, false, value === undefined ? null : value); - this.syncValue(); - }, - - - applyCommand : function(e){ - if(e.ctrlKey){ - var c = e.getCharCode(), cmd; - if(c > 0){ - c = String.fromCharCode(c); - switch(c){ - case 'b': - cmd = 'bold'; - break; - case 'i': - cmd = 'italic'; - break; - case 'u': - cmd = 'underline'; - break; - } - if(cmd){ - this.win.focus(); - this.execCmd(cmd); - this.deferFocus(); - e.preventDefault(); - } - } - } - }, - - - insertAtCursor : function(text){ - if(!this.activated){ - return; - } - if(Ext.isIE){ - this.win.focus(); - var doc = this.getDoc(), - r = doc.selection.createRange(); - if(r){ - r.pasteHTML(text); - this.syncValue(); - this.deferFocus(); - } - }else{ - this.win.focus(); - this.execCmd('InsertHTML', text); - this.deferFocus(); - } - }, - - - fixKeys : function(){ - if(Ext.isIE){ - return function(e){ - var k = e.getKey(), - doc = this.getDoc(), - r; - if(k == e.TAB){ - e.stopEvent(); - r = doc.selection.createRange(); - if(r){ - r.collapse(true); - r.pasteHTML('    '); - this.deferFocus(); - } - }else if(k == e.ENTER){ - r = doc.selection.createRange(); - if(r){ - var target = r.parentElement(); - if(!target || target.tagName.toLowerCase() != 'li'){ - e.stopEvent(); - r.pasteHTML('
        '); - r.collapse(false); - r.select(); - } - } - } - }; - }else if(Ext.isOpera){ - return function(e){ - var k = e.getKey(); - if(k == e.TAB){ - e.stopEvent(); - this.win.focus(); - this.execCmd('InsertHTML','    '); - this.deferFocus(); - } - }; - }else if(Ext.isWebKit){ - return function(e){ - var k = e.getKey(); - if(k == e.TAB){ - e.stopEvent(); - this.execCmd('InsertText','\t'); - this.deferFocus(); - }else if(k == e.ENTER){ - e.stopEvent(); - this.execCmd('InsertHtml','

        '); - this.deferFocus(); - } - }; - } - }(), - - - getToolbar : function(){ - return this.tb; - }, - - - buttonTips : { - bold : { - title: 'Bold (Ctrl+B)', - text: 'Make the selected text bold.', - cls: 'x-html-editor-tip' - }, - italic : { - title: 'Italic (Ctrl+I)', - text: 'Make the selected text italic.', - cls: 'x-html-editor-tip' - }, - underline : { - title: 'Underline (Ctrl+U)', - text: 'Underline the selected text.', - cls: 'x-html-editor-tip' - }, - increasefontsize : { - title: 'Grow Text', - text: 'Increase the font size.', - cls: 'x-html-editor-tip' - }, - decreasefontsize : { - title: 'Shrink Text', - text: 'Decrease the font size.', - cls: 'x-html-editor-tip' - }, - backcolor : { - title: 'Text Highlight Color', - text: 'Change the background color of the selected text.', - cls: 'x-html-editor-tip' - }, - forecolor : { - title: 'Font Color', - text: 'Change the color of the selected text.', - cls: 'x-html-editor-tip' - }, - justifyleft : { - title: 'Align Text Left', - text: 'Align text to the left.', - cls: 'x-html-editor-tip' - }, - justifycenter : { - title: 'Center Text', - text: 'Center text in the editor.', - cls: 'x-html-editor-tip' - }, - justifyright : { - title: 'Align Text Right', - text: 'Align text to the right.', - cls: 'x-html-editor-tip' - }, - insertunorderedlist : { - title: 'Bullet List', - text: 'Start a bulleted list.', - cls: 'x-html-editor-tip' - }, - insertorderedlist : { - title: 'Numbered List', - text: 'Start a numbered list.', - cls: 'x-html-editor-tip' - }, - createlink : { - title: 'Hyperlink', - text: 'Make the selected text a hyperlink.', - cls: 'x-html-editor-tip' - }, - sourceedit : { - title: 'Source Edit', - text: 'Switch to source editing mode.', - cls: 'x-html-editor-tip' - } - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -}); -Ext.reg('htmleditor', Ext.form.HtmlEditor); - -Ext.form.TimeField = Ext.extend(Ext.form.ComboBox, { - - minValue : undefined, - - maxValue : undefined, - - minText : "The time in this field must be equal to or after {0}", - - maxText : "The time in this field must be equal to or before {0}", - - invalidText : "{0} is not a valid time", - - format : "g:i A", - - altFormats : "g:ia|g:iA|g:i a|g:i A|h:i|g:i|H:i|ga|ha|gA|h a|g a|g A|gi|hi|gia|hia|g|H|gi a|hi a|giA|hiA|gi A|hi A", - - increment: 15, - - - mode: 'local', - - triggerAction: 'all', - - typeAhead: false, - - - - - initDate: '1/1/2008', - - initDateFormat: 'j/n/Y', - - - initComponent : function(){ - if(Ext.isDefined(this.minValue)){ - this.setMinValue(this.minValue, true); - } - if(Ext.isDefined(this.maxValue)){ - this.setMaxValue(this.maxValue, true); - } - if(!this.store){ - this.generateStore(true); - } - Ext.form.TimeField.superclass.initComponent.call(this); - }, - - - setMinValue: function(value, initial){ - this.setLimit(value, true, initial); - return this; - }, - - - setMaxValue: function(value, initial){ - this.setLimit(value, false, initial); - return this; - }, - - - generateStore: function(initial){ - var min = this.minValue || new Date(this.initDate).clearTime(), - max = this.maxValue || new Date(this.initDate).clearTime().add('mi', (24 * 60) - 1), - times = []; - - while(min <= max){ - times.push(min.dateFormat(this.format)); - min = min.add('mi', this.increment); - } - this.bindStore(times, initial); - }, - - - setLimit: function(value, isMin, initial){ - var d; - if(Ext.isString(value)){ - d = this.parseDate(value); - }else if(Ext.isDate(value)){ - d = value; - } - if(d){ - var val = new Date(this.initDate).clearTime(); - val.setHours(d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()); - this[isMin ? 'minValue' : 'maxValue'] = val; - if(!initial){ - this.generateStore(); - } - } - }, - - - getValue : function(){ - var v = Ext.form.TimeField.superclass.getValue.call(this); - return this.formatDate(this.parseDate(v)) || ''; - }, - - - setValue : function(value){ - return Ext.form.TimeField.superclass.setValue.call(this, this.formatDate(this.parseDate(value))); - }, - - - validateValue : Ext.form.DateField.prototype.validateValue, - - formatDate : Ext.form.DateField.prototype.formatDate, - - parseDate: function(value) { - if (!value || Ext.isDate(value)) { - return value; - } - - var id = this.initDate + ' ', - idf = this.initDateFormat + ' ', - v = Date.parseDate(id + value, idf + this.format), - af = this.altFormats; - - if (!v && af) { - if (!this.altFormatsArray) { - this.altFormatsArray = af.split("|"); - } - for (var i = 0, afa = this.altFormatsArray, len = afa.length; i < len && !v; i++) { - v = Date.parseDate(id + value, idf + afa[i]); - } - } - - return v; - } -}); -Ext.reg('timefield', Ext.form.TimeField); -Ext.form.SliderField = Ext.extend(Ext.form.Field, { - - - useTips : true, - - - tipText : null, - - - actionMode: 'wrap', - - - initComponent : function() { - var cfg = Ext.copyTo({ - id: this.id + '-slider' - }, this.initialConfig, ['vertical', 'minValue', 'maxValue', 'decimalPrecision', 'keyIncrement', 'increment', 'clickToChange', 'animate']); - - - if (this.useTips) { - var plug = this.tipText ? {getText: this.tipText} : {}; - cfg.plugins = [new Ext.slider.Tip(plug)]; - } - this.slider = new Ext.Slider(cfg); - Ext.form.SliderField.superclass.initComponent.call(this); - }, - - - onRender : function(ct, position){ - this.autoCreate = { - id: this.id, - name: this.name, - type: 'hidden', - tag: 'input' - }; - Ext.form.SliderField.superclass.onRender.call(this, ct, position); - this.wrap = this.el.wrap({cls: 'x-form-field-wrap'}); - this.resizeEl = this.positionEl = this.wrap; - this.slider.render(this.wrap); - }, - - - onResize : function(w, h, aw, ah){ - Ext.form.SliderField.superclass.onResize.call(this, w, h, aw, ah); - this.slider.setSize(w, h); - }, - - - initEvents : function(){ - Ext.form.SliderField.superclass.initEvents.call(this); - this.slider.on('change', this.onChange, this); - }, - - - onChange : function(slider, v){ - this.setValue(v, undefined, true); - }, - - - onEnable : function(){ - Ext.form.SliderField.superclass.onEnable.call(this); - this.slider.enable(); - }, - - - onDisable : function(){ - Ext.form.SliderField.superclass.onDisable.call(this); - this.slider.disable(); - }, - - - beforeDestroy : function(){ - Ext.destroy(this.slider); - Ext.form.SliderField.superclass.beforeDestroy.call(this); - }, - - - alignErrorIcon : function(){ - this.errorIcon.alignTo(this.slider.el, 'tl-tr', [2, 0]); - }, - - - setMinValue : function(v){ - this.slider.setMinValue(v); - return this; - }, - - - setMaxValue : function(v){ - this.slider.setMaxValue(v); - return this; - }, - - - setValue : function(v, animate, silent){ - - - if(!silent){ - this.slider.setValue(v, animate); - } - return Ext.form.SliderField.superclass.setValue.call(this, this.slider.getValue()); - }, - - - getValue : function(){ - return this.slider.getValue(); - } -}); - -Ext.reg('sliderfield', Ext.form.SliderField); -Ext.form.Label = Ext.extend(Ext.BoxComponent, { - - - - - - onRender : function(ct, position){ - if(!this.el){ - this.el = document.createElement('label'); - this.el.id = this.getId(); - this.el.innerHTML = this.text ? Ext.util.Format.htmlEncode(this.text) : (this.html || ''); - if(this.forId){ - this.el.setAttribute('for', this.forId); - } - } - Ext.form.Label.superclass.onRender.call(this, ct, position); - }, - - - setText : function(t, encode){ - var e = encode === false; - this[!e ? 'text' : 'html'] = t; - delete this[e ? 'text' : 'html']; - if(this.rendered){ - this.el.dom.innerHTML = encode !== false ? Ext.util.Format.htmlEncode(t) : t; - } - return this; - } -}); - -Ext.reg('label', Ext.form.Label); -Ext.form.Action = function(form, options){ - this.form = form; - this.options = options || {}; -}; - - -Ext.form.Action.CLIENT_INVALID = 'client'; - -Ext.form.Action.SERVER_INVALID = 'server'; - -Ext.form.Action.CONNECT_FAILURE = 'connect'; - -Ext.form.Action.LOAD_FAILURE = 'load'; - -Ext.form.Action.prototype = { - - - - - - - - - - - - - - - type : 'default', - - - - - - run : function(options){ - - }, - - - success : function(response){ - - }, - - - handleResponse : function(response){ - - }, - - - failure : function(response){ - this.response = response; - this.failureType = Ext.form.Action.CONNECT_FAILURE; - this.form.afterAction(this, false); - }, - - - - - processResponse : function(response){ - this.response = response; - if(!response.responseText && !response.responseXML){ - return true; - } - this.result = this.handleResponse(response); - return this.result; - }, - - decodeResponse: function(response) { - try { - return Ext.decode(response.responseText); - } catch(e) { - return false; - } - }, - - - getUrl : function(appendParams){ - var url = this.options.url || this.form.url || this.form.el.dom.action; - if(appendParams){ - var p = this.getParams(); - if(p){ - url = Ext.urlAppend(url, p); - } - } - return url; - }, - - - getMethod : function(){ - return (this.options.method || this.form.method || this.form.el.dom.method || 'POST').toUpperCase(); - }, - - - getParams : function(){ - var bp = this.form.baseParams; - var p = this.options.params; - if(p){ - if(typeof p == "object"){ - p = Ext.urlEncode(Ext.applyIf(p, bp)); - }else if(typeof p == 'string' && bp){ - p += '&' + Ext.urlEncode(bp); - } - }else if(bp){ - p = Ext.urlEncode(bp); - } - return p; - }, - - - createCallback : function(opts){ - var opts = opts || {}; - return { - success: this.success, - failure: this.failure, - scope: this, - timeout: (opts.timeout*1000) || (this.form.timeout*1000), - upload: this.form.fileUpload ? this.success : undefined - }; - } -}; - - -Ext.form.Action.Submit = function(form, options){ - Ext.form.Action.Submit.superclass.constructor.call(this, form, options); -}; - -Ext.extend(Ext.form.Action.Submit, Ext.form.Action, { - - - type : 'submit', - - - run : function(){ - var o = this.options, - method = this.getMethod(), - isGet = method == 'GET'; - if(o.clientValidation === false || this.form.isValid()){ - if (o.submitEmptyText === false) { - var fields = this.form.items, - emptyFields = [], - setupEmptyFields = function(f){ - if (f.el.getValue() == f.emptyText) { - emptyFields.push(f); - f.el.dom.value = ""; - } - if(f.isComposite && f.rendered){ - f.items.each(setupEmptyFields); - } - }; - - fields.each(setupEmptyFields); - } - Ext.Ajax.request(Ext.apply(this.createCallback(o), { - form:this.form.el.dom, - url:this.getUrl(isGet), - method: method, - headers: o.headers, - params:!isGet ? this.getParams() : null, - isUpload: this.form.fileUpload - })); - if (o.submitEmptyText === false) { - Ext.each(emptyFields, function(f) { - if (f.applyEmptyText) { - f.applyEmptyText(); - } - }); - } - }else if (o.clientValidation !== false){ - this.failureType = Ext.form.Action.CLIENT_INVALID; - this.form.afterAction(this, false); - } - }, - - - success : function(response){ - var result = this.processResponse(response); - if(result === true || result.success){ - this.form.afterAction(this, true); - return; - } - if(result.errors){ - this.form.markInvalid(result.errors); - } - this.failureType = Ext.form.Action.SERVER_INVALID; - this.form.afterAction(this, false); - }, - - - handleResponse : function(response){ - if(this.form.errorReader){ - var rs = this.form.errorReader.read(response); - var errors = []; - if(rs.records){ - for(var i = 0, len = rs.records.length; i < len; i++) { - var r = rs.records[i]; - errors[i] = r.data; - } - } - if(errors.length < 1){ - errors = null; - } - return { - success : rs.success, - errors : errors - }; - } - return this.decodeResponse(response); - } -}); - - - -Ext.form.Action.Load = function(form, options){ - Ext.form.Action.Load.superclass.constructor.call(this, form, options); - this.reader = this.form.reader; -}; - -Ext.extend(Ext.form.Action.Load, Ext.form.Action, { - - type : 'load', - - - run : function(){ - Ext.Ajax.request(Ext.apply( - this.createCallback(this.options), { - method:this.getMethod(), - url:this.getUrl(false), - headers: this.options.headers, - params:this.getParams() - })); - }, - - - success : function(response){ - var result = this.processResponse(response); - if(result === true || !result.success || !result.data){ - this.failureType = Ext.form.Action.LOAD_FAILURE; - this.form.afterAction(this, false); - return; - } - this.form.clearInvalid(); - this.form.setValues(result.data); - this.form.afterAction(this, true); - }, - - - handleResponse : function(response){ - if(this.form.reader){ - var rs = this.form.reader.read(response); - var data = rs.records && rs.records[0] ? rs.records[0].data : null; - return { - success : rs.success, - data : data - }; - } - return this.decodeResponse(response); - } -}); - - - - -Ext.form.Action.DirectLoad = Ext.extend(Ext.form.Action.Load, { - constructor: function(form, opts) { - Ext.form.Action.DirectLoad.superclass.constructor.call(this, form, opts); - }, - type : 'directload', - - run : function(){ - var args = this.getParams(); - args.push(this.success, this); - this.form.api.load.apply(window, args); - }, - - getParams : function() { - var buf = [], o = {}; - var bp = this.form.baseParams; - var p = this.options.params; - Ext.apply(o, p, bp); - var paramOrder = this.form.paramOrder; - if(paramOrder){ - for(var i = 0, len = paramOrder.length; i < len; i++){ - buf.push(o[paramOrder[i]]); - } - }else if(this.form.paramsAsHash){ - buf.push(o); - } - return buf; - }, - - - - processResponse : function(result) { - this.result = result; - return result; - }, - - success : function(response, trans){ - if(trans.type == Ext.Direct.exceptions.SERVER){ - response = {}; - } - Ext.form.Action.DirectLoad.superclass.success.call(this, response); - } -}); - - -Ext.form.Action.DirectSubmit = Ext.extend(Ext.form.Action.Submit, { - constructor : function(form, opts) { - Ext.form.Action.DirectSubmit.superclass.constructor.call(this, form, opts); - }, - type : 'directsubmit', - - run : function(){ - var o = this.options; - if(o.clientValidation === false || this.form.isValid()){ - - - this.success.params = this.getParams(); - this.form.api.submit(this.form.el.dom, this.success, this); - }else if (o.clientValidation !== false){ - this.failureType = Ext.form.Action.CLIENT_INVALID; - this.form.afterAction(this, false); - } - }, - - getParams : function() { - var o = {}; - var bp = this.form.baseParams; - var p = this.options.params; - Ext.apply(o, p, bp); - return o; - }, - - - - processResponse : function(result) { - this.result = result; - return result; - }, - - success : function(response, trans){ - if(trans.type == Ext.Direct.exceptions.SERVER){ - response = {}; - } - Ext.form.Action.DirectSubmit.superclass.success.call(this, response); - } -}); - -Ext.form.Action.ACTION_TYPES = { - 'load' : Ext.form.Action.Load, - 'submit' : Ext.form.Action.Submit, - 'directload' : Ext.form.Action.DirectLoad, - 'directsubmit' : Ext.form.Action.DirectSubmit -}; - -Ext.form.VTypes = function(){ - - var alpha = /^[a-zA-Z_]+$/, - alphanum = /^[a-zA-Z0-9_]+$/, - email = /^(\w+)([\-+.\'][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/, - url = /(((^https?)|(^ftp)):\/\/([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i; - - - return { - - 'email' : function(v){ - return email.test(v); - }, - - 'emailText' : 'This field should be an e-mail address in the format "user@example.com"', - - 'emailMask' : /[a-z0-9_\.\-\+\'@]/i, - - /** - * The function used to validate URLs - * @param {String} value The URL - * @return {Boolean} true if the RegExp test passed, and false if not. - */ - 'url' : function(v){ - return url.test(v); - }, - /** - * The error text to display when the url validation function returns false. Defaults to: - * 'This field should be a URL in the format "http:/'+'/www.example.com"' - * @type String - */ - 'urlText' : 'This field should be a URL in the format "http:/'+'/www.example.com"', - - /** - * The function used to validate alpha values - * @param {String} value The value - * @return {Boolean} true if the RegExp test passed, and false if not. - */ - 'alpha' : function(v){ - return alpha.test(v); - }, - /** - * The error text to display when the alpha validation function returns false. Defaults to: - * 'This field should only contain letters and _' - * @type String - */ - 'alphaText' : 'This field should only contain letters and _', - /** - * The keystroke filter mask to be applied on alpha input. Defaults to: - * /[a-z_]/i - * @type RegExp - */ - 'alphaMask' : /[a-z_]/i, - - /** - * The function used to validate alphanumeric values - * @param {String} value The value - * @return {Boolean} true if the RegExp test passed, and false if not. - */ - 'alphanum' : function(v){ - return alphanum.test(v); - }, - /** - * The error text to display when the alphanumeric validation function returns false. Defaults to: - * 'This field should only contain letters, numbers and _' - * @type String - */ - 'alphanumText' : 'This field should only contain letters, numbers and _', - /** - * The keystroke filter mask to be applied on alphanumeric input. Defaults to: - * /[a-z0-9_]/i - * @type RegExp - */ - 'alphanumMask' : /[a-z0-9_]/i - }; -}(); -/** - * @class Ext.grid.GridPanel - * @extends Ext.Panel - *

        This class represents the primary interface of a component based grid control to represent data - * in a tabular format of rows and columns. The GridPanel is composed of the following:

        - *
          - *
        • {@link Ext.data.Store Store} : The Model holding the data records (rows) - *
        • - *
        • {@link Ext.grid.ColumnModel Column model} : Column makeup - *
        • - *
        • {@link Ext.grid.GridView View} : Encapsulates the user interface - *
        • - *
        • {@link Ext.grid.AbstractSelectionModel selection model} : Selection behavior - *
        • - *
        - *

        Example usage:

        - *
        
        -var grid = new Ext.grid.GridPanel({
        -    {@link #store}: new {@link Ext.data.Store}({
        -        {@link Ext.data.Store#autoDestroy autoDestroy}: true,
        -        {@link Ext.data.Store#reader reader}: reader,
        -        {@link Ext.data.Store#data data}: xg.dummyData
        -    }),
        -    {@link #colModel}: new {@link Ext.grid.ColumnModel}({
        -        {@link Ext.grid.ColumnModel#defaults defaults}: {
        -            width: 120,
        -            sortable: true
        -        },
        -        {@link Ext.grid.ColumnModel#columns columns}: [
        -            {id: 'company', header: 'Company', width: 200, sortable: true, dataIndex: 'company'},
        -            {header: 'Price', renderer: Ext.util.Format.usMoney, dataIndex: 'price'},
        -            {header: 'Change', dataIndex: 'change'},
        -            {header: '% Change', dataIndex: 'pctChange'},
        -            // instead of specifying renderer: Ext.util.Format.dateRenderer('m/d/Y') use xtype
        -            {
        -                header: 'Last Updated', width: 135, dataIndex: 'lastChange',
        -                xtype: 'datecolumn', format: 'M d, Y'
        -            }
        -        ]
        -    }),
        -    {@link #viewConfig}: {
        -        {@link Ext.grid.GridView#forceFit forceFit}: true,
        -
        -//      Return CSS class to apply to rows depending upon data values
        -        {@link Ext.grid.GridView#getRowClass getRowClass}: function(record, index) {
        -            var c = record.{@link Ext.data.Record#get get}('change');
        -            if (c < 0) {
        -                return 'price-fall';
        -            } else if (c > 0) {
        -                return 'price-rise';
        -            }
        -        }
        -    },
        -    {@link #sm}: new Ext.grid.RowSelectionModel({singleSelect:true}),
        -    width: 600,
        -    height: 300,
        -    frame: true,
        -    title: 'Framed with Row Selection and Horizontal Scrolling',
        -    iconCls: 'icon-grid'
        -});
        - * 
        - *

        Notes:

        - *
          - *
        • Although this class inherits many configuration options from base classes, some of them - * (such as autoScroll, autoWidth, layout, items, etc) are not used by this class, and will - * have no effect.
        • - *
        • A grid requires a width in which to scroll its columns, and a height in which to - * scroll its rows. These dimensions can either be set explicitly through the - * {@link Ext.BoxComponent#height height} and {@link Ext.BoxComponent#width width} - * configuration options or implicitly set by using the grid as a child item of a - * {@link Ext.Container Container} which will have a {@link Ext.Container#layout layout manager} - * provide the sizing of its child items (for example the Container of the Grid may specify - * {@link Ext.Container#layout layout}:'fit').
        • - *
        • To access the data in a Grid, it is necessary to use the data model encapsulated - * by the {@link #store Store}. See the {@link #cellclick} event for more details.
        • - *
        - * @constructor - * @param {Object} config The config object - * @xtype grid - */ -Ext.grid.GridPanel = Ext.extend(Ext.Panel, { - /** - * @cfg {String} autoExpandColumn - *

        The {@link Ext.grid.Column#id id} of a {@link Ext.grid.Column column} in - * this grid that should expand to fill unused space. This value specified here can not - * be 0.

        - *

        Note: If the Grid's {@link Ext.grid.GridView view} is configured with - * {@link Ext.grid.GridView#forceFit forceFit}=true the autoExpandColumn - * is ignored. See {@link Ext.grid.Column}.{@link Ext.grid.Column#width width} - * for additional details.

        - *

        See {@link #autoExpandMax} and {@link #autoExpandMin} also.

        - */ - autoExpandColumn : false, - - - autoExpandMax : 1000, - - - autoExpandMin : 50, - - - columnLines : false, - - - - - - - ddText : '{0} selected row{1}', - - - deferRowRender : true, - - - - - enableColumnHide : true, - - - enableColumnMove : true, - - - enableDragDrop : false, - - - enableHdMenu : true, - - - - loadMask : false, - - - - minColumnWidth : 25, - - - - - - stripeRows : false, - - - trackMouseOver : true, - - - stateEvents : ['columnmove', 'columnresize', 'sortchange', 'groupchange'], - - - view : null, - - - bubbleEvents: [], - - - - - rendered : false, - - - viewReady : false, - - - initComponent : function() { - Ext.grid.GridPanel.superclass.initComponent.call(this); - - if (this.columnLines) { - this.cls = (this.cls || '') + ' x-grid-with-col-lines'; - } - - - this.autoScroll = false; - this.autoWidth = false; - - if(Ext.isArray(this.columns)){ - this.colModel = new Ext.grid.ColumnModel(this.columns); - delete this.columns; - } - - - if(this.ds){ - this.store = this.ds; - delete this.ds; - } - if(this.cm){ - this.colModel = this.cm; - delete this.cm; - } - if(this.sm){ - this.selModel = this.sm; - delete this.sm; - } - this.store = Ext.StoreMgr.lookup(this.store); - - this.addEvents( - - - 'click', - - 'dblclick', - - 'contextmenu', - - 'mousedown', - - 'mouseup', - - 'mouseover', - - 'mouseout', - - 'keypress', - - 'keydown', - - - - 'cellmousedown', - - 'rowmousedown', - - 'headermousedown', - - - 'groupmousedown', - - - 'rowbodymousedown', - - - 'containermousedown', - - - 'cellclick', - - 'celldblclick', - - 'rowclick', - - 'rowdblclick', - - 'headerclick', - - 'headerdblclick', - - 'groupclick', - - 'groupdblclick', - - 'containerclick', - - 'containerdblclick', - - - 'rowbodyclick', - - 'rowbodydblclick', - - - 'rowcontextmenu', - - 'cellcontextmenu', - - 'headercontextmenu', - - 'groupcontextmenu', - - 'containercontextmenu', - - 'rowbodycontextmenu', - - 'bodyscroll', - - 'columnresize', - - 'columnmove', - - 'sortchange', - - 'groupchange', - - 'reconfigure', - - 'viewready' - ); - }, - - - onRender : function(ct, position){ - Ext.grid.GridPanel.superclass.onRender.apply(this, arguments); - - var c = this.getGridEl(); - - this.el.addClass('x-grid-panel'); - - this.mon(c, { - scope: this, - mousedown: this.onMouseDown, - click: this.onClick, - dblclick: this.onDblClick, - contextmenu: this.onContextMenu - }); - - this.relayEvents(c, ['mousedown','mouseup','mouseover','mouseout','keypress', 'keydown']); - - var view = this.getView(); - view.init(this); - view.render(); - this.getSelectionModel().init(this); - }, - - - initEvents : function(){ - Ext.grid.GridPanel.superclass.initEvents.call(this); - - if(this.loadMask){ - this.loadMask = new Ext.LoadMask(this.bwrap, - Ext.apply({store:this.store}, this.loadMask)); - } - }, - - initStateEvents : function(){ - Ext.grid.GridPanel.superclass.initStateEvents.call(this); - this.mon(this.colModel, 'hiddenchange', this.saveState, this, {delay: 100}); - }, - - applyState : function(state){ - var cm = this.colModel, - cs = state.columns, - store = this.store, - s, - c, - colIndex; - - if(cs){ - for(var i = 0, len = cs.length; i < len; i++){ - s = cs[i]; - c = cm.getColumnById(s.id); - if(c){ - colIndex = cm.getIndexById(s.id); - cm.setState(colIndex, { - hidden: s.hidden, - width: s.width, - sortable: s.sortable - }); - if(colIndex != i){ - cm.moveColumn(colIndex, i); - } - } - } - } - if(store){ - s = state.sort; - if(s){ - store[store.remoteSort ? 'setDefaultSort' : 'sort'](s.field, s.direction); - } - s = state.group; - if(store.groupBy){ - if(s){ - store.groupBy(s); - }else{ - store.clearGrouping(); - } - } - - } - var o = Ext.apply({}, state); - delete o.columns; - delete o.sort; - Ext.grid.GridPanel.superclass.applyState.call(this, o); - }, - - getState : function(){ - var o = {columns: []}, - store = this.store, - ss, - gs; - - for(var i = 0, c; (c = this.colModel.config[i]); i++){ - o.columns[i] = { - id: c.id, - width: c.width - }; - if(c.hidden){ - o.columns[i].hidden = true; - } - if(c.sortable){ - o.columns[i].sortable = true; - } - } - if(store){ - ss = store.getSortState(); - if(ss){ - o.sort = ss; - } - if(store.getGroupState){ - gs = store.getGroupState(); - if(gs){ - o.group = gs; - } - } - } - return o; - }, - - - afterRender : function(){ - Ext.grid.GridPanel.superclass.afterRender.call(this); - var v = this.view; - this.on('bodyresize', v.layout, v); - v.layout(true); - if(this.deferRowRender){ - if (!this.deferRowRenderTask){ - this.deferRowRenderTask = new Ext.util.DelayedTask(v.afterRender, this.view); - } - this.deferRowRenderTask.delay(10); - }else{ - v.afterRender(); - } - this.viewReady = true; - }, - - - reconfigure : function(store, colModel){ - var rendered = this.rendered; - if(rendered){ - if(this.loadMask){ - this.loadMask.destroy(); - this.loadMask = new Ext.LoadMask(this.bwrap, - Ext.apply({}, {store:store}, this.initialConfig.loadMask)); - } - } - if(this.view){ - this.view.initData(store, colModel); - } - this.store = store; - this.colModel = colModel; - if(rendered){ - this.view.refresh(true); - } - this.fireEvent('reconfigure', this, store, colModel); - }, - - - onDestroy : function(){ - if (this.deferRowRenderTask && this.deferRowRenderTask.cancel){ - this.deferRowRenderTask.cancel(); - } - if(this.rendered){ - Ext.destroy(this.view, this.loadMask); - }else if(this.store && this.store.autoDestroy){ - this.store.destroy(); - } - Ext.destroy(this.colModel, this.selModel); - this.store = this.selModel = this.colModel = this.view = this.loadMask = null; - Ext.grid.GridPanel.superclass.onDestroy.call(this); - }, - - - processEvent : function(name, e){ - this.view.processEvent(name, e); - }, - - - onClick : function(e){ - this.processEvent('click', e); - }, - - - onMouseDown : function(e){ - this.processEvent('mousedown', e); - }, - - - onContextMenu : function(e, t){ - this.processEvent('contextmenu', e); - }, - - - onDblClick : function(e){ - this.processEvent('dblclick', e); - }, - - - walkCells : function(row, col, step, fn, scope){ - var cm = this.colModel, - clen = cm.getColumnCount(), - ds = this.store, - rlen = ds.getCount(), - first = true; - - if(step < 0){ - if(col < 0){ - row--; - first = false; - } - while(row >= 0){ - if(!first){ - col = clen-1; - } - first = false; - while(col >= 0){ - if(fn.call(scope || this, row, col, cm) === true){ - return [row, col]; - } - col--; - } - row--; - } - } else { - if(col >= clen){ - row++; - first = false; - } - while(row < rlen){ - if(!first){ - col = 0; - } - first = false; - while(col < clen){ - if(fn.call(scope || this, row, col, cm) === true){ - return [row, col]; - } - col++; - } - row++; - } - } - return null; - }, - - - getGridEl : function(){ - return this.body; - }, - - - stopEditing : Ext.emptyFn, - - - getSelectionModel : function(){ - if(!this.selModel){ - this.selModel = new Ext.grid.RowSelectionModel( - this.disableSelection ? {selectRow: Ext.emptyFn} : null); - } - return this.selModel; - }, - - - getStore : function(){ - return this.store; - }, - - - getColumnModel : function(){ - return this.colModel; - }, - - - getView : function() { - if (!this.view) { - this.view = new Ext.grid.GridView(this.viewConfig); - } - - return this.view; - }, - - getDragDropText : function(){ - var count = this.selModel.getCount(); - return String.format(this.ddText, count, count == 1 ? '' : 's'); - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -}); -Ext.reg('grid', Ext.grid.GridPanel); -Ext.grid.PivotGrid = Ext.extend(Ext.grid.GridPanel, { - - - aggregator: 'sum', - - - renderer: undefined, - - - - - - - - - initComponent: function() { - Ext.grid.PivotGrid.superclass.initComponent.apply(this, arguments); - - this.initAxes(); - - - this.enableColumnResize = false; - - this.viewConfig = Ext.apply(this.viewConfig || {}, { - forceFit: true - }); - - - - this.colModel = new Ext.grid.ColumnModel({}); - }, - - - getAggregator: function() { - if (typeof this.aggregator == 'string') { - return Ext.grid.PivotAggregatorMgr.types[this.aggregator]; - } else { - return this.aggregator; - } - }, - - - setAggregator: function(aggregator) { - this.aggregator = aggregator; - }, - - - setMeasure: function(measure) { - this.measure = measure; - }, - - - setLeftAxis: function(axis, refresh) { - - this.leftAxis = axis; - - if (refresh) { - this.view.refresh(); - } - }, - - - setTopAxis: function(axis, refresh) { - - this.topAxis = axis; - - if (refresh) { - this.view.refresh(); - } - }, - - - initAxes: function() { - var PivotAxis = Ext.grid.PivotAxis; - - if (!(this.leftAxis instanceof PivotAxis)) { - this.setLeftAxis(new PivotAxis({ - orientation: 'vertical', - dimensions : this.leftAxis || [], - store : this.store - })); - }; - - if (!(this.topAxis instanceof PivotAxis)) { - this.setTopAxis(new PivotAxis({ - orientation: 'horizontal', - dimensions : this.topAxis || [], - store : this.store - })); - }; - }, - - - extractData: function() { - var records = this.store.data.items, - recCount = records.length, - cells = [], - record, i, j, k; - - if (recCount == 0) { - return []; - } - - var leftTuples = this.leftAxis.getTuples(), - leftCount = leftTuples.length, - topTuples = this.topAxis.getTuples(), - topCount = topTuples.length, - aggregator = this.getAggregator(); - - for (i = 0; i < recCount; i++) { - record = records[i]; - - for (j = 0; j < leftCount; j++) { - cells[j] = cells[j] || []; - - if (leftTuples[j].matcher(record) === true) { - for (k = 0; k < topCount; k++) { - cells[j][k] = cells[j][k] || []; - - if (topTuples[k].matcher(record)) { - cells[j][k].push(record); - } - } - } - } - } - - var rowCount = cells.length, - colCount, row; - - for (i = 0; i < rowCount; i++) { - row = cells[i]; - colCount = row.length; - - for (j = 0; j < colCount; j++) { - cells[i][j] = aggregator(cells[i][j], this.measure); - } - } - - return cells; - }, - - - getView: function() { - if (!this.view) { - this.view = new Ext.grid.PivotGridView(this.viewConfig); - } - - return this.view; - } -}); - -Ext.reg('pivotgrid', Ext.grid.PivotGrid); - - -Ext.grid.PivotAggregatorMgr = new Ext.AbstractManager(); - -Ext.grid.PivotAggregatorMgr.registerType('sum', function(records, measure) { - var length = records.length, - total = 0, - i; - - for (i = 0; i < length; i++) { - total += records[i].get(measure); - } - - return total; -}); - -Ext.grid.PivotAggregatorMgr.registerType('avg', function(records, measure) { - var length = records.length, - total = 0, - i; - - for (i = 0; i < length; i++) { - total += records[i].get(measure); - } - - return (total / length) || 'n/a'; -}); - -Ext.grid.PivotAggregatorMgr.registerType('min', function(records, measure) { - var data = [], - length = records.length, - i; - - for (i = 0; i < length; i++) { - data.push(records[i].get(measure)); - } - - return Math.min.apply(this, data) || 'n/a'; -}); - -Ext.grid.PivotAggregatorMgr.registerType('max', function(records, measure) { - var data = [], - length = records.length, - i; - - for (i = 0; i < length; i++) { - data.push(records[i].get(measure)); - } - - return Math.max.apply(this, data) || 'n/a'; -}); - -Ext.grid.PivotAggregatorMgr.registerType('count', function(records, measure) { - return records.length; -}); -Ext.grid.GridView = Ext.extend(Ext.util.Observable, { - - - - - - - - - - - - deferEmptyText : true, - - - scrollOffset : undefined, - - - autoFill : false, - - - forceFit : false, - - - sortClasses : ['sort-asc', 'sort-desc'], - - - sortAscText : 'Sort Ascending', - - - sortDescText : 'Sort Descending', - - - columnsText : 'Columns', - - - selectedRowClass : 'x-grid3-row-selected', - - - borderWidth : 2, - tdClass : 'x-grid3-cell', - hdCls : 'x-grid3-hd', - - - - markDirty : true, - - - cellSelectorDepth : 4, - - - rowSelectorDepth : 10, - - - rowBodySelectorDepth : 10, - - - cellSelector : 'td.x-grid3-cell', - - - rowSelector : 'div.x-grid3-row', - - - rowBodySelector : 'div.x-grid3-row-body', - - - firstRowCls: 'x-grid3-row-first', - lastRowCls: 'x-grid3-row-last', - rowClsRe: /(?:^|\s+)x-grid3-row-(first|last|alt)(?:\s+|$)/g, - - - headerMenuOpenCls: 'x-grid3-hd-menu-open', - - - rowOverCls: 'x-grid3-row-over', - - constructor : function(config) { - Ext.apply(this, config); - - - this.addEvents( - - 'beforerowremoved', - - - 'beforerowsinserted', - - - 'beforerefresh', - - - 'rowremoved', - - - 'rowsinserted', - - - 'rowupdated', - - - 'refresh' - ); - - Ext.grid.GridView.superclass.constructor.call(this); - }, - - - - - masterTpl: new Ext.Template( - '
        ', - '
        ', - '
        ', - '
        ', - '
        {header}
        ', - '
        ', - '
        ', - '
        ', - '
        ', - '
        {body}
        ', - '', - '
        ', - '
        ', - '
         
        ', - '
         
        ', - '
        ' - ), - - - headerTpl: new Ext.Template( - '', - '', - '{cells}', - '', - '
        ' - ), - - - bodyTpl: new Ext.Template('{rows}'), - - - cellTpl: new Ext.Template( - '', - '
        {value}
        ', - '' - ), - - - initTemplates : function() { - var templates = this.templates || {}, - template, name, - - headerCellTpl = new Ext.Template( - '', - '
        ', - this.grid.enableHdMenu ? '' : '', - '{value}', - '', - '
        ', - '' - ), - - rowBodyText = [ - '', - '', - '
        {body}
        ', - '', - '' - ].join(""), - - innerText = [ - '', - '', - '{cells}', - this.enableRowBody ? rowBodyText : '', - '', - '
        ' - ].join(""); - - Ext.applyIf(templates, { - hcell : headerCellTpl, - cell : this.cellTpl, - body : this.bodyTpl, - header : this.headerTpl, - master : this.masterTpl, - row : new Ext.Template('
        ' + innerText + '
        '), - rowInner: new Ext.Template(innerText) - }); - - for (name in templates) { - template = templates[name]; - - if (template && Ext.isFunction(template.compile) && !template.compiled) { - template.disableFormats = true; - template.compile(); - } - } - - this.templates = templates; - this.colRe = new RegExp('x-grid3-td-([^\\s]+)', ''); - }, - - - fly : function(el) { - if (!this._flyweight) { - this._flyweight = new Ext.Element.Flyweight(document.body); - } - this._flyweight.dom = el; - return this._flyweight; - }, - - - getEditorParent : function() { - return this.scroller.dom; - }, - - - initElements : function() { - var Element = Ext.Element, - el = Ext.get(this.grid.getGridEl().dom.firstChild), - mainWrap = new Element(el.child('div.x-grid3-viewport')), - mainHd = new Element(mainWrap.child('div.x-grid3-header')), - scroller = new Element(mainWrap.child('div.x-grid3-scroller')); - - if (this.grid.hideHeaders) { - mainHd.setDisplayed(false); - } - - if (this.forceFit) { - scroller.setStyle('overflow-x', 'hidden'); - } - - - - Ext.apply(this, { - el : el, - mainWrap: mainWrap, - scroller: scroller, - mainHd : mainHd, - innerHd : mainHd.child('div.x-grid3-header-inner').dom, - mainBody: new Element(Element.fly(scroller).child('div.x-grid3-body')), - focusEl : new Element(Element.fly(scroller).child('a')), - - resizeMarker: new Element(el.child('div.x-grid3-resize-marker')), - resizeProxy : new Element(el.child('div.x-grid3-resize-proxy')) - }); - - this.focusEl.swallowEvent('click', true); - }, - - - getRows : function() { - return this.hasRows() ? this.mainBody.dom.childNodes : []; - }, - - - - - findCell : function(el) { - if (!el) { - return false; - } - return this.fly(el).findParent(this.cellSelector, this.cellSelectorDepth); - }, - - - findCellIndex : function(el, requiredCls) { - var cell = this.findCell(el), - hasCls; - - if (cell) { - hasCls = this.fly(cell).hasClass(requiredCls); - if (!requiredCls || hasCls) { - return this.getCellIndex(cell); - } - } - return false; - }, - - - getCellIndex : function(el) { - if (el) { - var match = el.className.match(this.colRe); - - if (match && match[1]) { - return this.cm.getIndexById(match[1]); - } - } - return false; - }, - - - findHeaderCell : function(el) { - var cell = this.findCell(el); - return cell && this.fly(cell).hasClass(this.hdCls) ? cell : null; - }, - - - findHeaderIndex : function(el){ - return this.findCellIndex(el, this.hdCls); - }, - - - findRow : function(el) { - if (!el) { - return false; - } - return this.fly(el).findParent(this.rowSelector, this.rowSelectorDepth); - }, - - - findRowIndex : function(el) { - var row = this.findRow(el); - return row ? row.rowIndex : false; - }, - - - findRowBody : function(el) { - if (!el) { - return false; - } - - return this.fly(el).findParent(this.rowBodySelector, this.rowBodySelectorDepth); - }, - - - - - getRow : function(row) { - return this.getRows()[row]; - }, - - - getCell : function(row, col) { - return Ext.fly(this.getRow(row)).query(this.cellSelector)[col]; - }, - - - getHeaderCell : function(index) { - return this.mainHd.dom.getElementsByTagName('td')[index]; - }, - - - - - addRowClass : function(rowId, cls) { - var row = this.getRow(rowId); - if (row) { - this.fly(row).addClass(cls); - } - }, - - - removeRowClass : function(row, cls) { - var r = this.getRow(row); - if(r){ - this.fly(r).removeClass(cls); - } - }, - - - removeRow : function(row) { - Ext.removeNode(this.getRow(row)); - this.syncFocusEl(row); - }, - - - removeRows : function(firstRow, lastRow) { - var bd = this.mainBody.dom, - rowIndex; - - for (rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){ - Ext.removeNode(bd.childNodes[firstRow]); - } - - this.syncFocusEl(firstRow); - }, - - - - - getScrollState : function() { - var sb = this.scroller.dom; - - return { - left: sb.scrollLeft, - top : sb.scrollTop - }; - }, - - - restoreScroll : function(state) { - var sb = this.scroller.dom; - sb.scrollLeft = state.left; - sb.scrollTop = state.top; - }, - - - scrollToTop : function() { - var dom = this.scroller.dom; - - dom.scrollTop = 0; - dom.scrollLeft = 0; - }, - - - syncScroll : function() { - this.syncHeaderScroll(); - var mb = this.scroller.dom; - this.grid.fireEvent('bodyscroll', mb.scrollLeft, mb.scrollTop); - }, - - - syncHeaderScroll : function() { - var innerHd = this.innerHd, - scrollLeft = this.scroller.dom.scrollLeft; - - innerHd.scrollLeft = scrollLeft; - innerHd.scrollLeft = scrollLeft; - }, - - - updateSortIcon : function(col, dir) { - var sortClasses = this.sortClasses, - sortClass = sortClasses[dir == "DESC" ? 1 : 0], - headers = this.mainHd.select('td').removeClass(sortClasses); - - headers.item(col).addClass(sortClass); - }, - - - updateAllColumnWidths : function() { - var totalWidth = this.getTotalWidth(), - colCount = this.cm.getColumnCount(), - rows = this.getRows(), - rowCount = rows.length, - widths = [], - row, rowFirstChild, trow, i, j; - - for (i = 0; i < colCount; i++) { - widths[i] = this.getColumnWidth(i); - this.getHeaderCell(i).style.width = widths[i]; - } - - this.updateHeaderWidth(); - - for (i = 0; i < rowCount; i++) { - row = rows[i]; - row.style.width = totalWidth; - rowFirstChild = row.firstChild; - - if (rowFirstChild) { - rowFirstChild.style.width = totalWidth; - trow = rowFirstChild.rows[0]; - - for (j = 0; j < colCount; j++) { - trow.childNodes[j].style.width = widths[j]; - } - } - } - - this.onAllColumnWidthsUpdated(widths, totalWidth); - }, - - - updateColumnWidth : function(column, width) { - var columnWidth = this.getColumnWidth(column), - totalWidth = this.getTotalWidth(), - headerCell = this.getHeaderCell(column), - nodes = this.getRows(), - nodeCount = nodes.length, - row, i, firstChild; - - this.updateHeaderWidth(); - headerCell.style.width = columnWidth; - - for (i = 0; i < nodeCount; i++) { - row = nodes[i]; - firstChild = row.firstChild; - - row.style.width = totalWidth; - if (firstChild) { - firstChild.style.width = totalWidth; - firstChild.rows[0].childNodes[column].style.width = columnWidth; - } - } - - this.onColumnWidthUpdated(column, columnWidth, totalWidth); - }, - - - updateColumnHidden : function(col, hidden) { - var totalWidth = this.getTotalWidth(), - display = hidden ? 'none' : '', - headerCell = this.getHeaderCell(col), - nodes = this.getRows(), - nodeCount = nodes.length, - row, rowFirstChild, i; - - this.updateHeaderWidth(); - headerCell.style.display = display; - - for (i = 0; i < nodeCount; i++) { - row = nodes[i]; - row.style.width = totalWidth; - rowFirstChild = row.firstChild; - - if (rowFirstChild) { - rowFirstChild.style.width = totalWidth; - rowFirstChild.rows[0].childNodes[col].style.display = display; - } - } - - this.onColumnHiddenUpdated(col, hidden, totalWidth); - delete this.lastViewWidth; - this.layout(); - }, - - - doRender : function(columns, records, store, startRow, colCount, stripe) { - var templates = this.templates, - cellTemplate = templates.cell, - rowTemplate = templates.row, - last = colCount - 1, - tstyle = 'width:' + this.getTotalWidth() + ';', - - rowBuffer = [], - colBuffer = [], - rowParams = {tstyle: tstyle}, - meta = {}, - len = records.length, - alt, - column, - record, i, j, rowIndex; - - - for (j = 0; j < len; j++) { - record = records[j]; - colBuffer = []; - - rowIndex = j + startRow; - - - for (i = 0; i < colCount; i++) { - column = columns[i]; - - meta.id = column.id; - meta.css = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : ''); - meta.attr = meta.cellAttr = ''; - meta.style = column.style; - meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store); - - if (Ext.isEmpty(meta.value)) { - meta.value = ' '; - } - - if (this.markDirty && record.dirty && typeof record.modified[column.name] != 'undefined') { - meta.css += ' x-grid3-dirty-cell'; - } - - colBuffer[colBuffer.length] = cellTemplate.apply(meta); - } - - alt = []; - - if (stripe && ((rowIndex + 1) % 2 === 0)) { - alt[0] = 'x-grid3-row-alt'; - } - - if (record.dirty) { - alt[1] = ' x-grid3-dirty-row'; - } - - rowParams.cols = colCount; - - if (this.getRowClass) { - alt[2] = this.getRowClass(record, rowIndex, rowParams, store); - } - - rowParams.alt = alt.join(' '); - rowParams.cells = colBuffer.join(''); - - rowBuffer[rowBuffer.length] = rowTemplate.apply(rowParams); - } - - return rowBuffer.join(''); - }, - - - processRows : function(startRow, skipStripe) { - if (!this.ds || this.ds.getCount() < 1) { - return; - } - - var rows = this.getRows(), - length = rows.length, - row, i; - - skipStripe = skipStripe || !this.grid.stripeRows; - startRow = startRow || 0; - - for (i = 0; i < length; i++) { - row = rows[i]; - if (row) { - row.rowIndex = i; - if (!skipStripe) { - row.className = row.className.replace(this.rowClsRe, ' '); - if ((i + 1) % 2 === 0){ - row.className += ' x-grid3-row-alt'; - } - } - } - } - - - if (startRow === 0) { - Ext.fly(rows[0]).addClass(this.firstRowCls); - } - - Ext.fly(rows[length - 1]).addClass(this.lastRowCls); - }, - - - afterRender : function() { - if (!this.ds || !this.cm) { - return; - } - - this.mainBody.dom.innerHTML = this.renderBody() || ' '; - this.processRows(0, true); - - if (this.deferEmptyText !== true) { - this.applyEmptyText(); - } - - this.grid.fireEvent('viewready', this.grid); - }, - - - afterRenderUI: function() { - var grid = this.grid; - - this.initElements(); - - - Ext.fly(this.innerHd).on('click', this.handleHdDown, this); - - this.mainHd.on({ - scope : this, - mouseover: this.handleHdOver, - mouseout : this.handleHdOut, - mousemove: this.handleHdMove - }); - - this.scroller.on('scroll', this.syncScroll, this); - - if (grid.enableColumnResize !== false) { - this.splitZone = new Ext.grid.GridView.SplitDragZone(grid, this.mainHd.dom); - } - - if (grid.enableColumnMove) { - this.columnDrag = new Ext.grid.GridView.ColumnDragZone(grid, this.innerHd); - this.columnDrop = new Ext.grid.HeaderDropZone(grid, this.mainHd.dom); - } - - if (grid.enableHdMenu !== false) { - this.hmenu = new Ext.menu.Menu({id: grid.id + '-hctx'}); - this.hmenu.add( - {itemId:'asc', text: this.sortAscText, cls: 'xg-hmenu-sort-asc'}, - {itemId:'desc', text: this.sortDescText, cls: 'xg-hmenu-sort-desc'} - ); - - if (grid.enableColumnHide !== false) { - this.colMenu = new Ext.menu.Menu({id:grid.id + '-hcols-menu'}); - this.colMenu.on({ - scope : this, - beforeshow: this.beforeColMenuShow, - itemclick : this.handleHdMenuClick - }); - this.hmenu.add('-', { - itemId:'columns', - hideOnClick: false, - text: this.columnsText, - menu: this.colMenu, - iconCls: 'x-cols-icon' - }); - } - - this.hmenu.on('itemclick', this.handleHdMenuClick, this); - } - - if (grid.trackMouseOver) { - this.mainBody.on({ - scope : this, - mouseover: this.onRowOver, - mouseout : this.onRowOut - }); - } - - if (grid.enableDragDrop || grid.enableDrag) { - this.dragZone = new Ext.grid.GridDragZone(grid, { - ddGroup : grid.ddGroup || 'GridDD' - }); - } - - this.updateHeaderSortState(); - }, - - - renderUI : function() { - var templates = this.templates; - - return templates.master.apply({ - body : templates.body.apply({rows:' '}), - header: this.renderHeaders(), - ostyle: 'width:' + this.getOffsetWidth() + ';', - bstyle: 'width:' + this.getTotalWidth() + ';' - }); - }, - - - processEvent : function(name, e) { - var target = e.getTarget(), - grid = this.grid, - header = this.findHeaderIndex(target), - row, cell, col, body; - - grid.fireEvent(name, e); - - if (header !== false) { - grid.fireEvent('header' + name, grid, header, e); - } else { - row = this.findRowIndex(target); - - - - - if (row !== false) { - cell = this.findCellIndex(target); - if (cell !== false) { - col = grid.colModel.getColumnAt(cell); - if (grid.fireEvent('cell' + name, grid, row, cell, e) !== false) { - if (!col || (col.processEvent && (col.processEvent(name, e, grid, row, cell) !== false))) { - grid.fireEvent('row' + name, grid, row, e); - } - } - } else { - if (grid.fireEvent('row' + name, grid, row, e) !== false) { - (body = this.findRowBody(target)) && grid.fireEvent('rowbody' + name, grid, row, e); - } - } - } else { - grid.fireEvent('container' + name, grid, e); - } - } - }, - - - layout : function(initial) { - if (!this.mainBody) { - return; - } - - var grid = this.grid, - gridEl = grid.getGridEl(), - gridSize = gridEl.getSize(true), - gridWidth = gridSize.width, - gridHeight = gridSize.height, - scroller = this.scroller, - scrollStyle, headerHeight, scrollHeight; - - if (gridWidth < 20 || gridHeight < 20) { - return; - } - - if (grid.autoHeight) { - scrollStyle = scroller.dom.style; - scrollStyle.overflow = 'visible'; - - if (Ext.isWebKit) { - scrollStyle.position = 'static'; - } - } else { - this.el.setSize(gridWidth, gridHeight); - - headerHeight = this.mainHd.getHeight(); - scrollHeight = gridHeight - headerHeight; - - scroller.setSize(gridWidth, scrollHeight); - - if (this.innerHd) { - this.innerHd.style.width = (gridWidth) + "px"; - } - } - - if (this.forceFit || (initial === true && this.autoFill)) { - if (this.lastViewWidth != gridWidth) { - this.fitColumns(false, false); - this.lastViewWidth = gridWidth; - } - } else { - this.autoExpand(); - this.syncHeaderScroll(); - } - - this.onLayout(gridWidth, scrollHeight); - }, - - - - onLayout : function(vw, vh) { - - }, - - onColumnWidthUpdated : function(col, w, tw) { - - }, - - onAllColumnWidthsUpdated : function(ws, tw) { - - }, - - onColumnHiddenUpdated : function(col, hidden, tw) { - - }, - - updateColumnText : function(col, text) { - - }, - - afterMove : function(colIndex) { - - }, - - - - init : function(grid) { - this.grid = grid; - - this.initTemplates(); - this.initData(grid.store, grid.colModel); - this.initUI(grid); - }, - - - getColumnId : function(index){ - return this.cm.getColumnId(index); - }, - - - getOffsetWidth : function() { - return (this.cm.getTotalWidth() + this.getScrollOffset()) + 'px'; - }, - - - getScrollOffset: function() { - return Ext.num(this.scrollOffset, Ext.getScrollBarWidth()); - }, - - - renderHeaders : function() { - var colModel = this.cm, - templates = this.templates, - headerTpl = templates.hcell, - properties = {}, - colCount = colModel.getColumnCount(), - last = colCount - 1, - cells = [], - i, cssCls; - - for (i = 0; i < colCount; i++) { - if (i == 0) { - cssCls = 'x-grid3-cell-first '; - } else { - cssCls = i == last ? 'x-grid3-cell-last ' : ''; - } - - properties = { - id : colModel.getColumnId(i), - value : colModel.getColumnHeader(i) || '', - style : this.getColumnStyle(i, true), - css : cssCls, - tooltip: this.getColumnTooltip(i) - }; - - if (colModel.config[i].align == 'right') { - properties.istyle = 'padding-right: 16px;'; - } else { - delete properties.istyle; - } - - cells[i] = headerTpl.apply(properties); - } - - return templates.header.apply({ - cells : cells.join(""), - tstyle: String.format("width: {0};", this.getTotalWidth()) - }); - }, - - - getColumnTooltip : function(i) { - var tooltip = this.cm.getColumnTooltip(i); - if (tooltip) { - if (Ext.QuickTips.isEnabled()) { - return 'ext:qtip="' + tooltip + '"'; - } else { - return 'title="' + tooltip + '"'; - } - } - - return ''; - }, - - - beforeUpdate : function() { - this.grid.stopEditing(true); - }, - - - updateHeaders : function() { - this.innerHd.firstChild.innerHTML = this.renderHeaders(); - - this.updateHeaderWidth(false); - }, - - - updateHeaderWidth: function(updateMain) { - var innerHdChild = this.innerHd.firstChild, - totalWidth = this.getTotalWidth(); - - innerHdChild.style.width = this.getOffsetWidth(); - innerHdChild.firstChild.style.width = totalWidth; - - if (updateMain !== false) { - this.mainBody.dom.style.width = totalWidth; - } - }, - - - focusRow : function(row) { - this.focusCell(row, 0, false); - }, - - - focusCell : function(row, col, hscroll) { - this.syncFocusEl(this.ensureVisible(row, col, hscroll)); - - var focusEl = this.focusEl; - - if (Ext.isGecko) { - focusEl.focus(); - } else { - focusEl.focus.defer(1, focusEl); - } - }, - - - resolveCell : function(row, col, hscroll) { - if (!Ext.isNumber(row)) { - row = row.rowIndex; - } - - if (!this.ds) { - return null; - } - - if (row < 0 || row >= this.ds.getCount()) { - return null; - } - col = (col !== undefined ? col : 0); - - var rowEl = this.getRow(row), - colModel = this.cm, - colCount = colModel.getColumnCount(), - cellEl; - - if (!(hscroll === false && col === 0)) { - while (col < colCount && colModel.isHidden(col)) { - col++; - } - - cellEl = this.getCell(row, col); - } - - return {row: rowEl, cell: cellEl}; - }, - - - getResolvedXY : function(resolved) { - if (!resolved) { - return null; - } - - var cell = resolved.cell, - row = resolved.row; - - if (cell) { - return Ext.fly(cell).getXY(); - } else { - return [this.el.getX(), Ext.fly(row).getY()]; - } - }, - - - syncFocusEl : function(row, col, hscroll) { - var xy = row; - - if (!Ext.isArray(xy)) { - row = Math.min(row, Math.max(0, this.getRows().length-1)); - - if (isNaN(row)) { - return; - } - - xy = this.getResolvedXY(this.resolveCell(row, col, hscroll)); - } - - this.focusEl.setXY(xy || this.scroller.getXY()); - }, - - - ensureVisible : function(row, col, hscroll) { - var resolved = this.resolveCell(row, col, hscroll); - - if (!resolved || !resolved.row) { - return null; - } - - var rowEl = resolved.row, - cellEl = resolved.cell, - c = this.scroller.dom, - p = rowEl, - ctop = 0, - stop = this.el.dom; - - while (p && p != stop) { - ctop += p.offsetTop; - p = p.offsetParent; - } - - ctop -= this.mainHd.dom.offsetHeight; - stop = parseInt(c.scrollTop, 10); - - var cbot = ctop + rowEl.offsetHeight, - ch = c.clientHeight, - sbot = stop + ch; - - - if (ctop < stop) { - c.scrollTop = ctop; - } else if(cbot > sbot) { - c.scrollTop = cbot-ch; - } - - if (hscroll !== false) { - var cleft = parseInt(cellEl.offsetLeft, 10), - cright = cleft + cellEl.offsetWidth, - sleft = parseInt(c.scrollLeft, 10), - sright = sleft + c.clientWidth; - - if (cleft < sleft) { - c.scrollLeft = cleft; - } else if(cright > sright) { - c.scrollLeft = cright-c.clientWidth; - } - } - - return this.getResolvedXY(resolved); - }, - - - insertRows : function(dm, firstRow, lastRow, isUpdate) { - var last = dm.getCount() - 1; - if( !isUpdate && firstRow === 0 && lastRow >= last) { - this.fireEvent('beforerowsinserted', this, firstRow, lastRow); - this.refresh(); - this.fireEvent('rowsinserted', this, firstRow, lastRow); - } else { - if (!isUpdate) { - this.fireEvent('beforerowsinserted', this, firstRow, lastRow); - } - var html = this.renderRows(firstRow, lastRow), - before = this.getRow(firstRow); - if (before) { - if(firstRow === 0){ - Ext.fly(this.getRow(0)).removeClass(this.firstRowCls); - } - Ext.DomHelper.insertHtml('beforeBegin', before, html); - } else { - var r = this.getRow(last - 1); - if(r){ - Ext.fly(r).removeClass(this.lastRowCls); - } - Ext.DomHelper.insertHtml('beforeEnd', this.mainBody.dom, html); - } - if (!isUpdate) { - this.processRows(firstRow); - this.fireEvent('rowsinserted', this, firstRow, lastRow); - } else if (firstRow === 0 || firstRow >= last) { - - Ext.fly(this.getRow(firstRow)).addClass(firstRow === 0 ? this.firstRowCls : this.lastRowCls); - } - } - this.syncFocusEl(firstRow); - }, - - - deleteRows : function(dm, firstRow, lastRow) { - if (dm.getRowCount() < 1) { - this.refresh(); - } else { - this.fireEvent('beforerowsdeleted', this, firstRow, lastRow); - - this.removeRows(firstRow, lastRow); - - this.processRows(firstRow); - this.fireEvent('rowsdeleted', this, firstRow, lastRow); - } - }, - - - getColumnStyle : function(colIndex, isHeader) { - var colModel = this.cm, - colConfig = colModel.config, - style = isHeader ? '' : colConfig[colIndex].css || '', - align = colConfig[colIndex].align; - - style += String.format("width: {0};", this.getColumnWidth(colIndex)); - - if (colModel.isHidden(colIndex)) { - style += 'display: none; '; - } - - if (align) { - style += String.format("text-align: {0};", align); - } - - return style; - }, - - - getColumnWidth : function(column) { - var columnWidth = this.cm.getColumnWidth(column), - borderWidth = this.borderWidth; - - if (Ext.isNumber(columnWidth)) { - if (Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2)) { - return columnWidth + "px"; - } else { - return Math.max(columnWidth - borderWidth, 0) + "px"; - } - } else { - return columnWidth; - } - }, - - - getTotalWidth : function() { - return this.cm.getTotalWidth() + 'px'; - }, - - - fitColumns : function(preventRefresh, onlyExpand, omitColumn) { - var grid = this.grid, - colModel = this.cm, - totalColWidth = colModel.getTotalWidth(false), - gridWidth = this.getGridInnerWidth(), - extraWidth = gridWidth - totalColWidth, - columns = [], - extraCol = 0, - width = 0, - colWidth, fraction, i; - - - if (gridWidth < 20 || extraWidth === 0) { - return false; - } - - var visibleColCount = colModel.getColumnCount(true), - totalColCount = colModel.getColumnCount(false), - adjCount = visibleColCount - (Ext.isNumber(omitColumn) ? 1 : 0); - - if (adjCount === 0) { - adjCount = 1; - omitColumn = undefined; - } - - - for (i = 0; i < totalColCount; i++) { - if (!colModel.isFixed(i) && i !== omitColumn) { - colWidth = colModel.getColumnWidth(i); - columns.push(i, colWidth); - - if (!colModel.isHidden(i)) { - extraCol = i; - width += colWidth; - } - } - } - - fraction = (gridWidth - colModel.getTotalWidth()) / width; - - while (columns.length) { - colWidth = columns.pop(); - i = columns.pop(); - - colModel.setColumnWidth(i, Math.max(grid.minColumnWidth, Math.floor(colWidth + colWidth * fraction)), true); - } - - - totalColWidth = colModel.getTotalWidth(false); - - if (totalColWidth > gridWidth) { - var adjustCol = (adjCount == visibleColCount) ? extraCol : omitColumn, - newWidth = Math.max(1, colModel.getColumnWidth(adjustCol) - (totalColWidth - gridWidth)); - - colModel.setColumnWidth(adjustCol, newWidth, true); - } - - if (preventRefresh !== true) { - this.updateAllColumnWidths(); - } - - return true; - }, - - - autoExpand : function(preventUpdate) { - var grid = this.grid, - colModel = this.cm, - gridWidth = this.getGridInnerWidth(), - totalColumnWidth = colModel.getTotalWidth(false), - autoExpandColumn = grid.autoExpandColumn; - - if (!this.userResized && autoExpandColumn) { - if (gridWidth != totalColumnWidth) { - - var colIndex = colModel.getIndexById(autoExpandColumn), - currentWidth = colModel.getColumnWidth(colIndex), - desiredWidth = gridWidth - totalColumnWidth + currentWidth, - newWidth = Math.min(Math.max(desiredWidth, grid.autoExpandMin), grid.autoExpandMax); - - if (currentWidth != newWidth) { - colModel.setColumnWidth(colIndex, newWidth, true); - - if (preventUpdate !== true) { - this.updateColumnWidth(colIndex, newWidth); - } - } - } - } - }, - - - getGridInnerWidth: function() { - return this.grid.getGridEl().getWidth(true) - this.getScrollOffset(); - }, - - - getColumnData : function() { - var columns = [], - colModel = this.cm, - colCount = colModel.getColumnCount(), - fields = this.ds.fields, - i, name; - - for (i = 0; i < colCount; i++) { - name = colModel.getDataIndex(i); - - columns[i] = { - name : Ext.isDefined(name) ? name : (fields.get(i) ? fields.get(i).name : undefined), - renderer: colModel.getRenderer(i), - scope : colModel.getRendererScope(i), - id : colModel.getColumnId(i), - style : this.getColumnStyle(i) - }; - } - - return columns; - }, - - - renderRows : function(startRow, endRow) { - var grid = this.grid, - store = grid.store, - stripe = grid.stripeRows, - colModel = grid.colModel, - colCount = colModel.getColumnCount(), - rowCount = store.getCount(), - records; - - if (rowCount < 1) { - return ''; - } - - startRow = startRow || 0; - endRow = Ext.isDefined(endRow) ? endRow : rowCount - 1; - records = store.getRange(startRow, endRow); - - return this.doRender(this.getColumnData(), records, store, startRow, colCount, stripe); - }, - - - renderBody : function(){ - var markup = this.renderRows() || ' '; - return this.templates.body.apply({rows: markup}); - }, - - - refreshRow: function(record) { - var store = this.ds, - colCount = this.cm.getColumnCount(), - columns = this.getColumnData(), - last = colCount - 1, - cls = ['x-grid3-row'], - rowParams = { - tstyle: String.format("width: {0};", this.getTotalWidth()) - }, - colBuffer = [], - cellTpl = this.templates.cell, - rowIndex, row, column, meta, css, i; - - if (Ext.isNumber(record)) { - rowIndex = record; - record = store.getAt(rowIndex); - } else { - rowIndex = store.indexOf(record); - } - - - if (!record || rowIndex < 0) { - return; - } - - - for (i = 0; i < colCount; i++) { - column = columns[i]; - - if (i == 0) { - css = 'x-grid3-cell-first'; - } else { - css = (i == last) ? 'x-grid3-cell-last ' : ''; - } - - meta = { - id : column.id, - style : column.style, - css : css, - attr : "", - cellAttr: "" - }; - - meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store); - - if (Ext.isEmpty(meta.value)) { - meta.value = ' '; - } - - if (this.markDirty && record.dirty && typeof record.modified[column.name] != 'undefined') { - meta.css += ' x-grid3-dirty-cell'; - } - - colBuffer[i] = cellTpl.apply(meta); - } - - row = this.getRow(rowIndex); - row.className = ''; - - if (this.grid.stripeRows && ((rowIndex + 1) % 2 === 0)) { - cls.push('x-grid3-row-alt'); - } - - if (this.getRowClass) { - rowParams.cols = colCount; - cls.push(this.getRowClass(record, rowIndex, rowParams, store)); - } - - this.fly(row).addClass(cls).setStyle(rowParams.tstyle); - rowParams.cells = colBuffer.join(""); - row.innerHTML = this.templates.rowInner.apply(rowParams); - - this.fireEvent('rowupdated', this, rowIndex, record); - }, - - - refresh : function(headersToo) { - this.fireEvent('beforerefresh', this); - this.grid.stopEditing(true); - - var result = this.renderBody(); - this.mainBody.update(result).setWidth(this.getTotalWidth()); - if (headersToo === true) { - this.updateHeaders(); - this.updateHeaderSortState(); - } - this.processRows(0, true); - this.layout(); - this.applyEmptyText(); - this.fireEvent('refresh', this); - }, - - - applyEmptyText : function() { - if (this.emptyText && !this.hasRows()) { - this.mainBody.update('
        ' + this.emptyText + '
        '); - } - }, - - - updateHeaderSortState : function() { - var state = this.ds.getSortState(); - if (!state) { - return; - } - - if (!this.sortState || (this.sortState.field != state.field || this.sortState.direction != state.direction)) { - this.grid.fireEvent('sortchange', this.grid, state); - } - - this.sortState = state; - - var sortColumn = this.cm.findColumnIndex(state.field); - if (sortColumn != -1) { - var sortDir = state.direction; - this.updateSortIcon(sortColumn, sortDir); - } - }, - - - clearHeaderSortState : function() { - if (!this.sortState) { - return; - } - this.grid.fireEvent('sortchange', this.grid, null); - this.mainHd.select('td').removeClass(this.sortClasses); - delete this.sortState; - }, - - - destroy : function() { - var me = this, - grid = me.grid, - gridEl = grid.getGridEl(), - dragZone = me.dragZone, - splitZone = me.splitZone, - columnDrag = me.columnDrag, - columnDrop = me.columnDrop, - scrollToTopTask = me.scrollToTopTask, - columnDragData, - columnDragProxy; - - if (scrollToTopTask && scrollToTopTask.cancel) { - scrollToTopTask.cancel(); - } - - Ext.destroyMembers(me, 'colMenu', 'hmenu'); - - me.initData(null, null); - me.purgeListeners(); - - Ext.fly(me.innerHd).un("click", me.handleHdDown, me); - - if (grid.enableColumnMove) { - columnDragData = columnDrag.dragData; - columnDragProxy = columnDrag.proxy; - Ext.destroy( - columnDrag.el, - columnDragProxy.ghost, - columnDragProxy.el, - columnDrop.el, - columnDrop.proxyTop, - columnDrop.proxyBottom, - columnDragData.ddel, - columnDragData.header - ); - - if (columnDragProxy.anim) { - Ext.destroy(columnDragProxy.anim); - } - - delete columnDragProxy.ghost; - delete columnDragData.ddel; - delete columnDragData.header; - columnDrag.destroy(); - - delete Ext.dd.DDM.locationCache[columnDrag.id]; - delete columnDrag._domRef; - - delete columnDrop.proxyTop; - delete columnDrop.proxyBottom; - columnDrop.destroy(); - delete Ext.dd.DDM.locationCache["gridHeader" + gridEl.id]; - delete columnDrop._domRef; - delete Ext.dd.DDM.ids[columnDrop.ddGroup]; - } - - if (splitZone) { - splitZone.destroy(); - delete splitZone._domRef; - delete Ext.dd.DDM.ids["gridSplitters" + gridEl.id]; - } - - Ext.fly(me.innerHd).removeAllListeners(); - Ext.removeNode(me.innerHd); - delete me.innerHd; - - Ext.destroy( - me.el, - me.mainWrap, - me.mainHd, - me.scroller, - me.mainBody, - me.focusEl, - me.resizeMarker, - me.resizeProxy, - me.activeHdBtn, - me._flyweight, - dragZone, - splitZone - ); - - delete grid.container; - - if (dragZone) { - dragZone.destroy(); - } - - Ext.dd.DDM.currentTarget = null; - delete Ext.dd.DDM.locationCache[gridEl.id]; - - Ext.EventManager.removeResizeListener(me.onWindowResize, me); - }, - - - onDenyColumnHide : function() { - - }, - - - render : function() { - if (this.autoFill) { - var ct = this.grid.ownerCt; - - if (ct && ct.getLayout()) { - ct.on('afterlayout', function() { - this.fitColumns(true, true); - this.updateHeaders(); - this.updateHeaderSortState(); - }, this, {single: true}); - } - } else if (this.forceFit) { - this.fitColumns(true, false); - } else if (this.grid.autoExpandColumn) { - this.autoExpand(true); - } - - this.grid.getGridEl().dom.innerHTML = this.renderUI(); - - this.afterRenderUI(); - }, - - - - - initData : function(newStore, newColModel) { - var me = this; - - if (me.ds) { - var oldStore = me.ds; - - oldStore.un('add', me.onAdd, me); - oldStore.un('load', me.onLoad, me); - oldStore.un('clear', me.onClear, me); - oldStore.un('remove', me.onRemove, me); - oldStore.un('update', me.onUpdate, me); - oldStore.un('datachanged', me.onDataChange, me); - - if (oldStore !== newStore && oldStore.autoDestroy) { - oldStore.destroy(); - } - } - - if (newStore) { - newStore.on({ - scope : me, - load : me.onLoad, - add : me.onAdd, - remove : me.onRemove, - update : me.onUpdate, - clear : me.onClear, - datachanged: me.onDataChange - }); - } - - if (me.cm) { - var oldColModel = me.cm; - - oldColModel.un('configchange', me.onColConfigChange, me); - oldColModel.un('widthchange', me.onColWidthChange, me); - oldColModel.un('headerchange', me.onHeaderChange, me); - oldColModel.un('hiddenchange', me.onHiddenChange, me); - oldColModel.un('columnmoved', me.onColumnMove, me); - } - - if (newColModel) { - delete me.lastViewWidth; - - newColModel.on({ - scope : me, - configchange: me.onColConfigChange, - widthchange : me.onColWidthChange, - headerchange: me.onHeaderChange, - hiddenchange: me.onHiddenChange, - columnmoved : me.onColumnMove - }); - } - - me.ds = newStore; - me.cm = newColModel; - }, - - - onDataChange : function(){ - this.refresh(true); - this.updateHeaderSortState(); - this.syncFocusEl(0); - }, - - - onClear : function() { - this.refresh(); - this.syncFocusEl(0); - }, - - - onUpdate : function(store, record) { - this.refreshRow(record); - }, - - - onAdd : function(store, records, index) { - this.insertRows(store, index, index + (records.length-1)); - }, - - - onRemove : function(store, record, index, isUpdate) { - if (isUpdate !== true) { - this.fireEvent('beforerowremoved', this, index, record); - } - - this.removeRow(index); - - if (isUpdate !== true) { - this.processRows(index); - this.applyEmptyText(); - this.fireEvent('rowremoved', this, index, record); - } - }, - - - onLoad : function() { - if (Ext.isGecko) { - if (!this.scrollToTopTask) { - this.scrollToTopTask = new Ext.util.DelayedTask(this.scrollToTop, this); - } - this.scrollToTopTask.delay(1); - } else { - this.scrollToTop(); - } - }, - - - onColWidthChange : function(cm, col, width) { - this.updateColumnWidth(col, width); - }, - - - onHeaderChange : function(cm, col, text) { - this.updateHeaders(); - }, - - - onHiddenChange : function(cm, col, hidden) { - this.updateColumnHidden(col, hidden); - }, - - - onColumnMove : function(cm, oldIndex, newIndex) { - this.indexMap = null; - this.refresh(true); - this.restoreScroll(this.getScrollState()); - - this.afterMove(newIndex); - this.grid.fireEvent('columnmove', oldIndex, newIndex); - }, - - - onColConfigChange : function() { - delete this.lastViewWidth; - this.indexMap = null; - this.refresh(true); - }, - - - - initUI : function(grid) { - grid.on('headerclick', this.onHeaderClick, this); - }, - - - initEvents : Ext.emptyFn, - - - onHeaderClick : function(g, index) { - if (this.headersDisabled || !this.cm.isSortable(index)) { - return; - } - g.stopEditing(true); - g.store.sort(this.cm.getDataIndex(index)); - }, - - - onRowOver : function(e, target) { - var row = this.findRowIndex(target); - - if (row !== false) { - this.addRowClass(row, this.rowOverCls); - } - }, - - - onRowOut : function(e, target) { - var row = this.findRowIndex(target); - - if (row !== false && !e.within(this.getRow(row), true)) { - this.removeRowClass(row, this.rowOverCls); - } - }, - - - onRowSelect : function(row) { - this.addRowClass(row, this.selectedRowClass); - }, - - - onRowDeselect : function(row) { - this.removeRowClass(row, this.selectedRowClass); - }, - - - onCellSelect : function(row, col) { - var cell = this.getCell(row, col); - if (cell) { - this.fly(cell).addClass('x-grid3-cell-selected'); - } - }, - - - onCellDeselect : function(row, col) { - var cell = this.getCell(row, col); - if (cell) { - this.fly(cell).removeClass('x-grid3-cell-selected'); - } - }, - - - handleWheel : function(e) { - e.stopPropagation(); - }, - - - onColumnSplitterMoved : function(cellIndex, width) { - this.userResized = true; - this.grid.colModel.setColumnWidth(cellIndex, width, true); - - if (this.forceFit) { - this.fitColumns(true, false, cellIndex); - this.updateAllColumnWidths(); - } else { - this.updateColumnWidth(cellIndex, width); - this.syncHeaderScroll(); - } - - this.grid.fireEvent('columnresize', cellIndex, width); - }, - - - beforeColMenuShow : function() { - var colModel = this.cm, - colCount = colModel.getColumnCount(), - colMenu = this.colMenu, - i; - - colMenu.removeAll(); - - for (i = 0; i < colCount; i++) { - if (colModel.config[i].hideable !== false) { - colMenu.add(new Ext.menu.CheckItem({ - text : colModel.getColumnHeader(i), - itemId : 'col-' + colModel.getColumnId(i), - checked : !colModel.isHidden(i), - disabled : colModel.config[i].hideable === false, - hideOnClick: false - })); - } - } - }, - - - handleHdMenuClick : function(item) { - var store = this.ds, - dataIndex = this.cm.getDataIndex(this.hdCtxIndex); - - switch (item.getItemId()) { - case 'asc': - store.sort(dataIndex, 'ASC'); - break; - case 'desc': - store.sort(dataIndex, 'DESC'); - break; - default: - this.handleHdMenuClickDefault(item); - } - return true; - }, - - - handleHdMenuClickDefault: function(item) { - var colModel = this.cm, - itemId = item.getItemId(), - index = colModel.getIndexById(itemId.substr(4)); - - if (index != -1) { - if (item.checked && colModel.getColumnsBy(this.isHideableColumn, this).length <= 1) { - this.onDenyColumnHide(); - return; - } - colModel.setHidden(index, item.checked); - } - }, - - - handleHdDown : function(e, target) { - if (Ext.fly(target).hasClass('x-grid3-hd-btn')) { - e.stopEvent(); - - var colModel = this.cm, - header = this.findHeaderCell(target), - index = this.getCellIndex(header), - sortable = colModel.isSortable(index), - menu = this.hmenu, - menuItems = menu.items, - menuCls = this.headerMenuOpenCls; - - this.hdCtxIndex = index; - - Ext.fly(header).addClass(menuCls); - menuItems.get('asc').setDisabled(!sortable); - menuItems.get('desc').setDisabled(!sortable); - - menu.on('hide', function() { - Ext.fly(header).removeClass(menuCls); - }, this, {single:true}); - - menu.show(target, 'tl-bl?'); - } - }, - - - handleHdMove : function(e) { - var header = this.findHeaderCell(this.activeHdRef); - - if (header && !this.headersDisabled) { - var handleWidth = this.splitHandleWidth || 5, - activeRegion = this.activeHdRegion, - headerStyle = header.style, - colModel = this.cm, - cursor = '', - pageX = e.getPageX(); - - if (this.grid.enableColumnResize !== false) { - var activeHeaderIndex = this.activeHdIndex, - previousVisible = this.getPreviousVisible(activeHeaderIndex), - currentResizable = colModel.isResizable(activeHeaderIndex), - previousResizable = previousVisible && colModel.isResizable(previousVisible), - inLeftResizer = pageX - activeRegion.left <= handleWidth, - inRightResizer = activeRegion.right - pageX <= (!this.activeHdBtn ? handleWidth : 2); - - if (inLeftResizer && previousResizable) { - cursor = Ext.isAir ? 'move' : Ext.isWebKit ? 'e-resize' : 'col-resize'; - } else if (inRightResizer && currentResizable) { - cursor = Ext.isAir ? 'move' : Ext.isWebKit ? 'w-resize' : 'col-resize'; - } - } - - headerStyle.cursor = cursor; - } - }, - - - getPreviousVisible: function(index) { - while (index > 0) { - if (!this.cm.isHidden(index - 1)) { - return index; - } - index--; - } - return undefined; - }, - - - handleHdOver : function(e, target) { - var header = this.findHeaderCell(target); - - if (header && !this.headersDisabled) { - var fly = this.fly(header); - - this.activeHdRef = target; - this.activeHdIndex = this.getCellIndex(header); - this.activeHdRegion = fly.getRegion(); - - if (!this.isMenuDisabled(this.activeHdIndex, fly)) { - fly.addClass('x-grid3-hd-over'); - this.activeHdBtn = fly.child('.x-grid3-hd-btn'); - - if (this.activeHdBtn) { - this.activeHdBtn.dom.style.height = (header.firstChild.offsetHeight - 1) + 'px'; - } - } - } - }, - - - handleHdOut : function(e, target) { - var header = this.findHeaderCell(target); - - if (header && (!Ext.isIE || !e.within(header, true))) { - this.activeHdRef = null; - this.fly(header).removeClass('x-grid3-hd-over'); - header.style.cursor = ''; - } - }, - - - isMenuDisabled: function(cellIndex, el) { - return this.cm.isMenuDisabled(cellIndex); - }, - - - hasRows : function() { - var fc = this.mainBody.dom.firstChild; - return fc && fc.nodeType == 1 && fc.className != 'x-grid-empty'; - }, - - - isHideableColumn : function(c) { - return !c.hidden; - }, - - - bind : function(d, c) { - this.initData(d, c); - } -}); - - - - -Ext.grid.GridView.SplitDragZone = Ext.extend(Ext.dd.DDProxy, { - - constructor: function(grid, hd){ - this.grid = grid; - this.view = grid.getView(); - this.marker = this.view.resizeMarker; - this.proxy = this.view.resizeProxy; - Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this, hd, - 'gridSplitters' + this.grid.getGridEl().id, { - dragElId : Ext.id(this.proxy.dom), resizeFrame:false - }); - this.scroll = false; - this.hw = this.view.splitHandleWidth || 5; - }, - - b4StartDrag : function(x, y){ - this.dragHeadersDisabled = this.view.headersDisabled; - this.view.headersDisabled = true; - var h = this.view.mainWrap.getHeight(); - this.marker.setHeight(h); - this.marker.show(); - this.marker.alignTo(this.view.getHeaderCell(this.cellIndex), 'tl-tl', [-2, 0]); - this.proxy.setHeight(h); - var w = this.cm.getColumnWidth(this.cellIndex), - minw = Math.max(w-this.grid.minColumnWidth, 0); - this.resetConstraints(); - this.setXConstraint(minw, 1000); - this.setYConstraint(0, 0); - this.minX = x - minw; - this.maxX = x + 1000; - this.startPos = x; - Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y); - }, - - allowHeaderDrag : function(e){ - return true; - }, - - handleMouseDown : function(e){ - var t = this.view.findHeaderCell(e.getTarget()); - if(t && this.allowHeaderDrag(e)){ - var xy = this.view.fly(t).getXY(), - x = xy[0], - exy = e.getXY(), - ex = exy[0], - w = t.offsetWidth, - adjust = false; - - if((ex - x) <= this.hw){ - adjust = -1; - }else if((x+w) - ex <= this.hw){ - adjust = 0; - } - if(adjust !== false){ - this.cm = this.grid.colModel; - var ci = this.view.getCellIndex(t); - if(adjust == -1){ - if (ci + adjust < 0) { - return; - } - while(this.cm.isHidden(ci+adjust)){ - --adjust; - if(ci+adjust < 0){ - return; - } - } - } - this.cellIndex = ci+adjust; - this.split = t.dom; - if(this.cm.isResizable(this.cellIndex) && !this.cm.isFixed(this.cellIndex)){ - Ext.grid.GridView.SplitDragZone.superclass.handleMouseDown.apply(this, arguments); - } - }else if(this.view.columnDrag){ - this.view.columnDrag.callHandleMouseDown(e); - } - } - }, - - endDrag : function(e){ - this.marker.hide(); - var v = this.view, - endX = Math.max(this.minX, e.getPageX()), - diff = endX - this.startPos, - disabled = this.dragHeadersDisabled; - - v.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex)+diff); - setTimeout(function(){ - v.headersDisabled = disabled; - }, 50); - }, - - autoOffset : function(){ - this.setDelta(0,0); - } -}); - -Ext.grid.PivotGridView = Ext.extend(Ext.grid.GridView, { - - - colHeaderCellCls: 'grid-hd-group-cell', - - - title: '', - - - - - getColumnHeaders: function() { - return this.grid.topAxis.buildHeaders();; - }, - - - getRowHeaders: function() { - return this.grid.leftAxis.buildHeaders(); - }, - - - renderRows : function(startRow, endRow) { - var grid = this.grid, - rows = grid.extractData(), - rowCount = rows.length, - templates = this.templates, - renderer = grid.renderer, - hasRenderer = typeof renderer == 'function', - getCellCls = this.getCellCls, - hasGetCellCls = typeof getCellCls == 'function', - cellTemplate = templates.cell, - rowTemplate = templates.row, - rowBuffer = [], - meta = {}, - tstyle = 'width:' + this.getGridInnerWidth() + 'px;', - colBuffer, colCount, column, i, row; - - startRow = startRow || 0; - endRow = Ext.isDefined(endRow) ? endRow : rowCount - 1; - - for (i = 0; i < rowCount; i++) { - row = rows[i]; - colCount = row.length; - colBuffer = []; - - - for (var j = 0; j < colCount; j++) { - - meta.id = i + '-' + j; - meta.css = j === 0 ? 'x-grid3-cell-first ' : (j == (colCount - 1) ? 'x-grid3-cell-last ' : ''); - meta.attr = meta.cellAttr = ''; - meta.value = row[j]; - - if (Ext.isEmpty(meta.value)) { - meta.value = ' '; - } - - if (hasRenderer) { - meta.value = renderer(meta.value); - } - - if (hasGetCellCls) { - meta.css += getCellCls(meta.value) + ' '; - } - - colBuffer[colBuffer.length] = cellTemplate.apply(meta); - } - - rowBuffer[rowBuffer.length] = rowTemplate.apply({ - tstyle: tstyle, - cols : colCount, - cells : colBuffer.join(""), - alt : '' - }); - } - - return rowBuffer.join(""); - }, - - - masterTpl: new Ext.Template( - '
        ', - '
        ', - '
        ', - '
        {title}
        ', - '
        ', - '
        ', - '
        ', - '
        ', - '
        ', - '
        ', - '
        ', - '
        {body}
        ', - '', - '
        ', - '
        ', - '
         
        ', - '
         
        ', - '
        ' - ), - - - initTemplates: function() { - Ext.grid.PivotGridView.superclass.initTemplates.apply(this, arguments); - - var templates = this.templates || {}; - if (!templates.gcell) { - templates.gcell = new Ext.XTemplate( - '', - '
        ', - this.grid.enableHdMenu ? '' : '', '{value}', - '
        ', - '' - ); - } - - this.templates = templates; - this.hrowRe = new RegExp("ux-grid-hd-group-row-(\\d+)", ""); - }, - - - initElements: function() { - Ext.grid.PivotGridView.superclass.initElements.apply(this, arguments); - - - this.rowHeadersEl = new Ext.Element(this.scroller.child('div.x-grid3-row-headers')); - - - this.headerTitleEl = new Ext.Element(this.mainHd.child('div.x-grid3-header-title')); - }, - - - getGridInnerWidth: function() { - var previousWidth = Ext.grid.PivotGridView.superclass.getGridInnerWidth.apply(this, arguments); - - return previousWidth - this.getTotalRowHeaderWidth(); - }, - - - getTotalRowHeaderWidth: function() { - var headers = this.getRowHeaders(), - length = headers.length, - total = 0, - i; - - for (i = 0; i< length; i++) { - total += headers[i].width; - } - - return total; - }, - - - getTotalColumnHeaderHeight: function() { - return this.getColumnHeaders().length * 21; - }, - - - getCellIndex : function(el) { - if (el) { - var match = el.className.match(this.colRe), - data; - - if (match && (data = match[1])) { - return parseInt(data.split('-')[1], 10); - } - } - return false; - }, - - - - renderUI : function() { - var templates = this.templates, - innerWidth = this.getGridInnerWidth(); - - return templates.master.apply({ - body : templates.body.apply({rows:' '}), - ostyle: 'width:' + innerWidth + 'px', - bstyle: 'width:' + innerWidth + 'px' - }); - }, - - - onLayout: function(width, height) { - Ext.grid.PivotGridView.superclass.onLayout.apply(this, arguments); - - var width = this.getGridInnerWidth(); - - this.resizeColumnHeaders(width); - this.resizeAllRows(width); - }, - - - refresh : function(headersToo) { - this.fireEvent('beforerefresh', this); - this.grid.stopEditing(true); - - var result = this.renderBody(); - this.mainBody.update(result).setWidth(this.getGridInnerWidth()); - if (headersToo === true) { - this.updateHeaders(); - this.updateHeaderSortState(); - } - this.processRows(0, true); - this.layout(); - this.applyEmptyText(); - this.fireEvent('refresh', this); - }, - - - renderHeaders: Ext.emptyFn, - - - fitColumns: Ext.emptyFn, - - - resizeColumnHeaders: function(width) { - var topAxis = this.grid.topAxis; - - if (topAxis.rendered) { - topAxis.el.setWidth(width); - } - }, - - - resizeRowHeaders: function() { - var rowHeaderWidth = this.getTotalRowHeaderWidth(), - marginStyle = String.format("margin-left: {0}px;", rowHeaderWidth); - - this.rowHeadersEl.setWidth(rowHeaderWidth); - this.mainBody.applyStyles(marginStyle); - Ext.fly(this.innerHd).applyStyles(marginStyle); - - this.headerTitleEl.setWidth(rowHeaderWidth); - this.headerTitleEl.setHeight(this.getTotalColumnHeaderHeight()); - }, - - - resizeAllRows: function(width) { - var rows = this.getRows(), - length = rows.length, - i; - - for (i = 0; i < length; i++) { - Ext.fly(rows[i]).setWidth(width); - Ext.fly(rows[i]).child('table').setWidth(width); - } - }, - - - updateHeaders: function() { - this.renderGroupRowHeaders(); - this.renderGroupColumnHeaders(); - }, - - - renderGroupRowHeaders: function() { - var leftAxis = this.grid.leftAxis; - - this.resizeRowHeaders(); - leftAxis.rendered = false; - leftAxis.render(this.rowHeadersEl); - - this.setTitle(this.title); - }, - - - setTitle: function(title) { - this.headerTitleEl.child('span').dom.innerHTML = title; - }, - - - renderGroupColumnHeaders: function() { - var topAxis = this.grid.topAxis; - - topAxis.rendered = false; - topAxis.render(this.innerHd.firstChild); - }, - - - isMenuDisabled: function(cellIndex, el) { - return true; - } -}); -Ext.grid.PivotAxis = Ext.extend(Ext.Component, { - - orientation: 'horizontal', - - - defaultHeaderWidth: 80, - - - paddingWidth: 7, - - - setDimensions: function(dimensions) { - this.dimensions = dimensions; - }, - - - onRender: function(ct, position) { - var rows = this.orientation == 'horizontal' - ? this.renderHorizontalRows() - : this.renderVerticalRows(); - - this.el = Ext.DomHelper.overwrite(ct.dom, {tag: 'table', cn: rows}, true); - }, - - - renderHorizontalRows: function() { - var headers = this.buildHeaders(), - rowCount = headers.length, - rows = [], - cells, cols, colCount, i, j; - - for (i = 0; i < rowCount; i++) { - cells = []; - cols = headers[i].items; - colCount = cols.length; - - for (j = 0; j < colCount; j++) { - cells.push({ - tag: 'td', - html: cols[j].header, - colspan: cols[j].span - }); - } - - rows[i] = { - tag: 'tr', - cn: cells - }; - } - - return rows; - }, - - - renderVerticalRows: function() { - var headers = this.buildHeaders(), - colCount = headers.length, - rowCells = [], - rows = [], - rowCount, col, row, colWidth, i, j; - - for (i = 0; i < colCount; i++) { - col = headers[i]; - colWidth = col.width || 80; - rowCount = col.items.length; - - for (j = 0; j < rowCount; j++) { - row = col.items[j]; - - rowCells[row.start] = rowCells[row.start] || []; - rowCells[row.start].push({ - tag : 'td', - html : row.header, - rowspan: row.span, - width : Ext.isBorderBox ? colWidth : colWidth - this.paddingWidth - }); - } - } - - rowCount = rowCells.length; - for (i = 0; i < rowCount; i++) { - rows[i] = { - tag: 'tr', - cn : rowCells[i] - }; - } - - return rows; - }, - - - getTuples: function() { - var newStore = new Ext.data.Store({}); - - newStore.data = this.store.data.clone(); - newStore.fields = this.store.fields; - - var sorters = [], - dimensions = this.dimensions, - length = dimensions.length, - i; - - for (i = 0; i < length; i++) { - sorters.push({ - field : dimensions[i].dataIndex, - direction: dimensions[i].direction || 'ASC' - }); - } - - newStore.sort(sorters); - - var records = newStore.data.items, - hashes = [], - tuples = [], - recData, hash, info, data, key; - - length = records.length; - - for (i = 0; i < length; i++) { - info = this.getRecordInfo(records[i]); - data = info.data; - hash = ""; - - for (key in data) { - hash += data[key] + '---'; - } - - if (hashes.indexOf(hash) == -1) { - hashes.push(hash); - tuples.push(info); - } - } - - newStore.destroy(); - - return tuples; - }, - - - getRecordInfo: function(record) { - var dimensions = this.dimensions, - length = dimensions.length, - data = {}, - dimension, dataIndex, i; - - - for (i = 0; i < length; i++) { - dimension = dimensions[i]; - dataIndex = dimension.dataIndex; - - data[dataIndex] = record.get(dataIndex); - } - - - - var createMatcherFunction = function(data) { - return function(record) { - for (var dataIndex in data) { - if (record.get(dataIndex) != data[dataIndex]) { - return false; - } - } - - return true; - }; - }; - - return { - data: data, - matcher: createMatcherFunction(data) - }; - }, - - - buildHeaders: function() { - var tuples = this.getTuples(), - rowCount = tuples.length, - dimensions = this.dimensions, - dimension, - colCount = dimensions.length, - headers = [], - tuple, rows, currentHeader, previousHeader, span, start, isLast, changed, i, j; - - for (i = 0; i < colCount; i++) { - dimension = dimensions[i]; - rows = []; - span = 0; - start = 0; - - for (j = 0; j < rowCount; j++) { - tuple = tuples[j]; - isLast = j == (rowCount - 1); - currentHeader = tuple.data[dimension.dataIndex]; - - - changed = previousHeader != undefined && previousHeader != currentHeader; - if (i > 0 && j > 0) { - changed = changed || tuple.data[dimensions[i-1].dataIndex] != tuples[j-1].data[dimensions[i-1].dataIndex]; - } - - if (changed) { - rows.push({ - header: previousHeader, - span : span, - start : start - }); - - start += span; - span = 0; - } - - if (isLast) { - rows.push({ - header: currentHeader, - span : span + 1, - start : start - }); - - start += span; - span = 0; - } - - previousHeader = currentHeader; - span++; - } - - headers.push({ - items: rows, - width: dimension.width || this.defaultHeaderWidth - }); - - previousHeader = undefined; - } - - return headers; - } -}); - - -Ext.grid.HeaderDragZone = Ext.extend(Ext.dd.DragZone, { - maxDragWidth: 120, - - constructor : function(grid, hd, hd2){ - this.grid = grid; - this.view = grid.getView(); - this.ddGroup = "gridHeader" + this.grid.getGridEl().id; - Ext.grid.HeaderDragZone.superclass.constructor.call(this, hd); - if(hd2){ - this.setHandleElId(Ext.id(hd)); - this.setOuterHandleElId(Ext.id(hd2)); - } - this.scroll = false; - }, - - getDragData : function(e){ - var t = Ext.lib.Event.getTarget(e), - h = this.view.findHeaderCell(t); - if(h){ - return {ddel: h.firstChild, header:h}; - } - return false; - }, - - onInitDrag : function(e){ - - this.dragHeadersDisabled = this.view.headersDisabled; - this.view.headersDisabled = true; - var clone = this.dragData.ddel.cloneNode(true); - clone.id = Ext.id(); - clone.style.width = Math.min(this.dragData.header.offsetWidth,this.maxDragWidth) + "px"; - this.proxy.update(clone); - return true; - }, - - afterValidDrop : function(){ - this.completeDrop(); - }, - - afterInvalidDrop : function(){ - this.completeDrop(); - }, - - completeDrop: function(){ - var v = this.view, - disabled = this.dragHeadersDisabled; - setTimeout(function(){ - v.headersDisabled = disabled; - }, 50); - } -}); - - - -Ext.grid.HeaderDropZone = Ext.extend(Ext.dd.DropZone, { - proxyOffsets : [-4, -9], - fly: Ext.Element.fly, - - constructor : function(grid, hd, hd2){ - this.grid = grid; - this.view = grid.getView(); - - this.proxyTop = Ext.DomHelper.append(document.body, { - cls:"col-move-top", html:" " - }, true); - this.proxyBottom = Ext.DomHelper.append(document.body, { - cls:"col-move-bottom", html:" " - }, true); - this.proxyTop.hide = this.proxyBottom.hide = function(){ - this.setLeftTop(-100,-100); - this.setStyle("visibility", "hidden"); - }; - this.ddGroup = "gridHeader" + this.grid.getGridEl().id; - - - Ext.grid.HeaderDropZone.superclass.constructor.call(this, grid.getGridEl().dom); - }, - - getTargetFromEvent : function(e){ - var t = Ext.lib.Event.getTarget(e), - cindex = this.view.findCellIndex(t); - if(cindex !== false){ - return this.view.getHeaderCell(cindex); - } - }, - - nextVisible : function(h){ - var v = this.view, cm = this.grid.colModel; - h = h.nextSibling; - while(h){ - if(!cm.isHidden(v.getCellIndex(h))){ - return h; - } - h = h.nextSibling; - } - return null; - }, - - prevVisible : function(h){ - var v = this.view, cm = this.grid.colModel; - h = h.prevSibling; - while(h){ - if(!cm.isHidden(v.getCellIndex(h))){ - return h; - } - h = h.prevSibling; - } - return null; - }, - - positionIndicator : function(h, n, e){ - var x = Ext.lib.Event.getPageX(e), - r = Ext.lib.Dom.getRegion(n.firstChild), - px, - pt, - py = r.top + this.proxyOffsets[1]; - if((r.right - x) <= (r.right-r.left)/2){ - px = r.right+this.view.borderWidth; - pt = "after"; - }else{ - px = r.left; - pt = "before"; - } - - if(this.grid.colModel.isFixed(this.view.getCellIndex(n))){ - return false; - } - - px += this.proxyOffsets[0]; - this.proxyTop.setLeftTop(px, py); - this.proxyTop.show(); - if(!this.bottomOffset){ - this.bottomOffset = this.view.mainHd.getHeight(); - } - this.proxyBottom.setLeftTop(px, py+this.proxyTop.dom.offsetHeight+this.bottomOffset); - this.proxyBottom.show(); - return pt; - }, - - onNodeEnter : function(n, dd, e, data){ - if(data.header != n){ - this.positionIndicator(data.header, n, e); - } - }, - - onNodeOver : function(n, dd, e, data){ - var result = false; - if(data.header != n){ - result = this.positionIndicator(data.header, n, e); - } - if(!result){ - this.proxyTop.hide(); - this.proxyBottom.hide(); - } - return result ? this.dropAllowed : this.dropNotAllowed; - }, - - onNodeOut : function(n, dd, e, data){ - this.proxyTop.hide(); - this.proxyBottom.hide(); - }, - - onNodeDrop : function(n, dd, e, data){ - var h = data.header; - if(h != n){ - var cm = this.grid.colModel, - x = Ext.lib.Event.getPageX(e), - r = Ext.lib.Dom.getRegion(n.firstChild), - pt = (r.right - x) <= ((r.right-r.left)/2) ? "after" : "before", - oldIndex = this.view.getCellIndex(h), - newIndex = this.view.getCellIndex(n); - if(pt == "after"){ - newIndex++; - } - if(oldIndex < newIndex){ - newIndex--; - } - cm.moveColumn(oldIndex, newIndex); - return true; - } - return false; - } -}); - -Ext.grid.GridView.ColumnDragZone = Ext.extend(Ext.grid.HeaderDragZone, { - - constructor : function(grid, hd){ - Ext.grid.GridView.ColumnDragZone.superclass.constructor.call(this, grid, hd, null); - this.proxy.el.addClass('x-grid3-col-dd'); - }, - - handleMouseDown : function(e){ - }, - - callHandleMouseDown : function(e){ - Ext.grid.GridView.ColumnDragZone.superclass.handleMouseDown.call(this, e); - } -}); - -Ext.grid.SplitDragZone = Ext.extend(Ext.dd.DDProxy, { - fly: Ext.Element.fly, - - constructor : function(grid, hd, hd2){ - this.grid = grid; - this.view = grid.getView(); - this.proxy = this.view.resizeProxy; - Ext.grid.SplitDragZone.superclass.constructor.call(this, hd, - "gridSplitters" + this.grid.getGridEl().id, { - dragElId : Ext.id(this.proxy.dom), resizeFrame:false - }); - this.setHandleElId(Ext.id(hd)); - this.setOuterHandleElId(Ext.id(hd2)); - this.scroll = false; - }, - - b4StartDrag : function(x, y){ - this.view.headersDisabled = true; - this.proxy.setHeight(this.view.mainWrap.getHeight()); - var w = this.cm.getColumnWidth(this.cellIndex); - var minw = Math.max(w-this.grid.minColumnWidth, 0); - this.resetConstraints(); - this.setXConstraint(minw, 1000); - this.setYConstraint(0, 0); - this.minX = x - minw; - this.maxX = x + 1000; - this.startPos = x; - Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y); - }, - - - handleMouseDown : function(e){ - var ev = Ext.EventObject.setEvent(e); - var t = this.fly(ev.getTarget()); - if(t.hasClass("x-grid-split")){ - this.cellIndex = this.view.getCellIndex(t.dom); - this.split = t.dom; - this.cm = this.grid.colModel; - if(this.cm.isResizable(this.cellIndex) && !this.cm.isFixed(this.cellIndex)){ - Ext.grid.SplitDragZone.superclass.handleMouseDown.apply(this, arguments); - } - } - }, - - endDrag : function(e){ - this.view.headersDisabled = false; - var endX = Math.max(this.minX, Ext.lib.Event.getPageX(e)); - var diff = endX - this.startPos; - this.view.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex)+diff); - }, - - autoOffset : function(){ - this.setDelta(0,0); - } -}); -Ext.grid.GridDragZone = function(grid, config){ - this.view = grid.getView(); - Ext.grid.GridDragZone.superclass.constructor.call(this, this.view.mainBody.dom, config); - this.scroll = false; - this.grid = grid; - this.ddel = document.createElement('div'); - this.ddel.className = 'x-grid-dd-wrap'; -}; - -Ext.extend(Ext.grid.GridDragZone, Ext.dd.DragZone, { - ddGroup : "GridDD", - - - getDragData : function(e){ - var t = Ext.lib.Event.getTarget(e); - var rowIndex = this.view.findRowIndex(t); - if(rowIndex !== false){ - var sm = this.grid.selModel; - if(!sm.isSelected(rowIndex) || e.hasModifier()){ - sm.handleMouseDown(this.grid, rowIndex, e); - } - return {grid: this.grid, ddel: this.ddel, rowIndex: rowIndex, selections:sm.getSelections()}; - } - return false; - }, - - - onInitDrag : function(e){ - var data = this.dragData; - this.ddel.innerHTML = this.grid.getDragDropText(); - this.proxy.update(this.ddel); - - }, - - - afterRepair : function(){ - this.dragging = false; - }, - - - getRepairXY : function(e, data){ - return false; - }, - - onEndDrag : function(data, e){ - - }, - - onValidDrop : function(dd, e, id){ - - this.hideProxy(); - }, - - beforeInvalidDrop : function(e, id){ - - } -}); - -Ext.grid.ColumnModel = Ext.extend(Ext.util.Observable, { - - defaultWidth: 100, - - - defaultSortable: false, - - - - - - constructor : function(config) { - - if (config.columns) { - Ext.apply(this, config); - this.setConfig(config.columns, true); - } else { - this.setConfig(config, true); - } - - this.addEvents( - - "widthchange", - - - "headerchange", - - - "hiddenchange", - - - "columnmoved", - - - "configchange" - ); - - Ext.grid.ColumnModel.superclass.constructor.call(this); - }, - - - getColumnId : function(index) { - return this.config[index].id; - }, - - getColumnAt : function(index) { - return this.config[index]; - }, - - - setConfig : function(config, initial) { - var i, c, len; - - if (!initial) { - delete this.totalWidth; - - for (i = 0, len = this.config.length; i < len; i++) { - c = this.config[i]; - - if (c.setEditor) { - - c.setEditor(null); - } - } - } - - - this.defaults = Ext.apply({ - width: this.defaultWidth, - sortable: this.defaultSortable - }, this.defaults); - - this.config = config; - this.lookup = {}; - - for (i = 0, len = config.length; i < len; i++) { - c = Ext.applyIf(config[i], this.defaults); - - - if (Ext.isEmpty(c.id)) { - c.id = i; - } - - if (!c.isColumn) { - var Cls = Ext.grid.Column.types[c.xtype || 'gridcolumn']; - c = new Cls(c); - config[i] = c; - } - - this.lookup[c.id] = c; - } - - if (!initial) { - this.fireEvent('configchange', this); - } - }, - - - getColumnById : function(id) { - return this.lookup[id]; - }, - - - getIndexById : function(id) { - for (var i = 0, len = this.config.length; i < len; i++) { - if (this.config[i].id == id) { - return i; - } - } - return -1; - }, - - - moveColumn : function(oldIndex, newIndex) { - var config = this.config, - c = config[oldIndex]; - - config.splice(oldIndex, 1); - config.splice(newIndex, 0, c); - this.dataMap = null; - this.fireEvent("columnmoved", this, oldIndex, newIndex); - }, - - - getColumnCount : function(visibleOnly) { - var length = this.config.length, - c = 0, - i; - - if (visibleOnly === true) { - for (i = 0; i < length; i++) { - if (!this.isHidden(i)) { - c++; - } - } - - return c; - } - - return length; - }, - - - getColumnsBy : function(fn, scope) { - var config = this.config, - length = config.length, - result = [], - i, c; - - for (i = 0; i < length; i++){ - c = config[i]; - - if (fn.call(scope || this, c, i) === true) { - result[result.length] = c; - } - } - - return result; - }, - - - isSortable : function(col) { - return !!this.config[col].sortable; - }, - - - isMenuDisabled : function(col) { - return !!this.config[col].menuDisabled; - }, - - - getRenderer : function(col) { - return this.config[col].renderer || Ext.grid.ColumnModel.defaultRenderer; - }, - - getRendererScope : function(col) { - return this.config[col].scope; - }, - - - setRenderer : function(col, fn) { - this.config[col].renderer = fn; - }, - - - getColumnWidth : function(col) { - var width = this.config[col].width; - if(typeof width != 'number'){ - width = this.defaultWidth; - } - return width; - }, - - - setColumnWidth : function(col, width, suppressEvent) { - this.config[col].width = width; - this.totalWidth = null; - - if (!suppressEvent) { - this.fireEvent("widthchange", this, col, width); - } - }, - - - getTotalWidth : function(includeHidden) { - if (!this.totalWidth) { - this.totalWidth = 0; - for (var i = 0, len = this.config.length; i < len; i++) { - if (includeHidden || !this.isHidden(i)) { - this.totalWidth += this.getColumnWidth(i); - } - } - } - return this.totalWidth; - }, - - - getColumnHeader : function(col) { - return this.config[col].header; - }, - - - setColumnHeader : function(col, header) { - this.config[col].header = header; - this.fireEvent("headerchange", this, col, header); - }, - - - getColumnTooltip : function(col) { - return this.config[col].tooltip; - }, - - setColumnTooltip : function(col, tooltip) { - this.config[col].tooltip = tooltip; - }, - - - getDataIndex : function(col) { - return this.config[col].dataIndex; - }, - - - setDataIndex : function(col, dataIndex) { - this.config[col].dataIndex = dataIndex; - }, - - - findColumnIndex : function(dataIndex) { - var c = this.config; - for(var i = 0, len = c.length; i < len; i++){ - if(c[i].dataIndex == dataIndex){ - return i; - } - } - return -1; - }, - - - isCellEditable : function(colIndex, rowIndex) { - var c = this.config[colIndex], - ed = c.editable; - - - return !!(ed || (!Ext.isDefined(ed) && c.editor)); - }, - - - getCellEditor : function(colIndex, rowIndex) { - return this.config[colIndex].getCellEditor(rowIndex); - }, - - - setEditable : function(col, editable) { - this.config[col].editable = editable; - }, - - - isHidden : function(colIndex) { - return !!this.config[colIndex].hidden; - }, - - - isFixed : function(colIndex) { - return !!this.config[colIndex].fixed; - }, - - - isResizable : function(colIndex) { - return colIndex >= 0 && this.config[colIndex].resizable !== false && this.config[colIndex].fixed !== true; - }, - - - setHidden : function(colIndex, hidden) { - var c = this.config[colIndex]; - if(c.hidden !== hidden){ - c.hidden = hidden; - this.totalWidth = null; - this.fireEvent("hiddenchange", this, colIndex, hidden); - } - }, - - - setEditor : function(col, editor) { - this.config[col].setEditor(editor); - }, - - - destroy : function() { - var length = this.config.length, - i = 0; - - for (; i < length; i++){ - this.config[i].destroy(); - } - delete this.config; - delete this.lookup; - this.purgeListeners(); - }, - - - setState : function(col, state) { - state = Ext.applyIf(state, this.defaults); - Ext.apply(this.config[col], state); - } -}); - - -Ext.grid.ColumnModel.defaultRenderer = function(value) { - if (typeof value == "string" && value.length < 1) { - return " "; - } - return value; -}; -Ext.grid.AbstractSelectionModel = Ext.extend(Ext.util.Observable, { - - - constructor : function(){ - this.locked = false; - Ext.grid.AbstractSelectionModel.superclass.constructor.call(this); - }, - - - init : function(grid){ - this.grid = grid; - if(this.lockOnInit){ - delete this.lockOnInit; - this.locked = false; - this.lock(); - } - this.initEvents(); - }, - - - lock : function(){ - if(!this.locked){ - this.locked = true; - - var g = this.grid; - if(g){ - g.getView().on({ - scope: this, - beforerefresh: this.sortUnLock, - refresh: this.sortLock - }); - }else{ - this.lockOnInit = true; - } - } - }, - - - sortLock : function() { - this.locked = true; - }, - - - sortUnLock : function() { - this.locked = false; - }, - - - unlock : function(){ - if(this.locked){ - this.locked = false; - var g = this.grid, - gv; - - - if(g){ - gv = g.getView(); - gv.un('beforerefresh', this.sortUnLock, this); - gv.un('refresh', this.sortLock, this); - }else{ - delete this.lockOnInit; - } - } - }, - - - isLocked : function(){ - return this.locked; - }, - - destroy: function(){ - this.unlock(); - this.purgeListeners(); - } -}); -Ext.grid.RowSelectionModel = Ext.extend(Ext.grid.AbstractSelectionModel, { - - singleSelect : false, - - constructor : function(config){ - Ext.apply(this, config); - this.selections = new Ext.util.MixedCollection(false, function(o){ - return o.id; - }); - - this.last = false; - this.lastActive = false; - - this.addEvents( - - 'selectionchange', - - 'beforerowselect', - - 'rowselect', - - 'rowdeselect' - ); - Ext.grid.RowSelectionModel.superclass.constructor.call(this); - }, - - - - initEvents : function(){ - - if(!this.grid.enableDragDrop && !this.grid.enableDrag){ - this.grid.on('rowmousedown', this.handleMouseDown, this); - } - - this.rowNav = new Ext.KeyNav(this.grid.getGridEl(), { - up: this.onKeyPress, - down: this.onKeyPress, - scope: this - }); - - this.grid.getView().on({ - scope: this, - refresh: this.onRefresh, - rowupdated: this.onRowUpdated, - rowremoved: this.onRemove - }); - }, - - onKeyPress : function(e, name){ - var up = name == 'up', - method = up ? 'selectPrevious' : 'selectNext', - add = up ? -1 : 1, - last; - if(!e.shiftKey || this.singleSelect){ - this[method](false); - }else if(this.last !== false && this.lastActive !== false){ - last = this.last; - this.selectRange(this.last, this.lastActive + add); - this.grid.getView().focusRow(this.lastActive); - if(last !== false){ - this.last = last; - } - }else{ - this.selectFirstRow(); - } - }, - - - onRefresh : function(){ - var ds = this.grid.store, - s = this.getSelections(), - i = 0, - len = s.length, - index, r; - - this.silent = true; - this.clearSelections(true); - for(; i < len; i++){ - r = s[i]; - if((index = ds.indexOfId(r.id)) != -1){ - this.selectRow(index, true); - } - } - if(s.length != this.selections.getCount()){ - this.fireEvent('selectionchange', this); - } - this.silent = false; - }, - - - onRemove : function(v, index, r){ - if(this.selections.remove(r) !== false){ - this.fireEvent('selectionchange', this); - } - }, - - - onRowUpdated : function(v, index, r){ - if(this.isSelected(r)){ - v.onRowSelect(index); - } - }, - - - selectRecords : function(records, keepExisting){ - if(!keepExisting){ - this.clearSelections(); - } - var ds = this.grid.store, - i = 0, - len = records.length; - for(; i < len; i++){ - this.selectRow(ds.indexOf(records[i]), true); - } - }, - - - getCount : function(){ - return this.selections.length; - }, - - - selectFirstRow : function(){ - this.selectRow(0); - }, - - - selectLastRow : function(keepExisting){ - this.selectRow(this.grid.store.getCount() - 1, keepExisting); - }, - - - selectNext : function(keepExisting){ - if(this.hasNext()){ - this.selectRow(this.last+1, keepExisting); - this.grid.getView().focusRow(this.last); - return true; - } - return false; - }, - - - selectPrevious : function(keepExisting){ - if(this.hasPrevious()){ - this.selectRow(this.last-1, keepExisting); - this.grid.getView().focusRow(this.last); - return true; - } - return false; - }, - - - hasNext : function(){ - return this.last !== false && (this.last+1) < this.grid.store.getCount(); - }, - - - hasPrevious : function(){ - return !!this.last; - }, - - - - getSelections : function(){ - return [].concat(this.selections.items); - }, - - - getSelected : function(){ - return this.selections.itemAt(0); - }, - - - each : function(fn, scope){ - var s = this.getSelections(), - i = 0, - len = s.length; - - for(; i < len; i++){ - if(fn.call(scope || this, s[i], i) === false){ - return false; - } - } - return true; - }, - - - clearSelections : function(fast){ - if(this.isLocked()){ - return; - } - if(fast !== true){ - var ds = this.grid.store, - s = this.selections; - s.each(function(r){ - this.deselectRow(ds.indexOfId(r.id)); - }, this); - s.clear(); - }else{ - this.selections.clear(); - } - this.last = false; - }, - - - - selectAll : function(){ - if(this.isLocked()){ - return; - } - this.selections.clear(); - for(var i = 0, len = this.grid.store.getCount(); i < len; i++){ - this.selectRow(i, true); - } - }, - - - hasSelection : function(){ - return this.selections.length > 0; - }, - - - isSelected : function(index){ - var r = Ext.isNumber(index) ? this.grid.store.getAt(index) : index; - return (r && this.selections.key(r.id) ? true : false); - }, - - - isIdSelected : function(id){ - return (this.selections.key(id) ? true : false); - }, - - - handleMouseDown : function(g, rowIndex, e){ - if(e.button !== 0 || this.isLocked()){ - return; - } - var view = this.grid.getView(); - if(e.shiftKey && !this.singleSelect && this.last !== false){ - var last = this.last; - this.selectRange(last, rowIndex, e.ctrlKey); - this.last = last; - view.focusRow(rowIndex); - }else{ - var isSelected = this.isSelected(rowIndex); - if(e.ctrlKey && isSelected){ - this.deselectRow(rowIndex); - }else if(!isSelected || this.getCount() > 1){ - this.selectRow(rowIndex, e.ctrlKey || e.shiftKey); - view.focusRow(rowIndex); - } - } - }, - - - selectRows : function(rows, keepExisting){ - if(!keepExisting){ - this.clearSelections(); - } - for(var i = 0, len = rows.length; i < len; i++){ - this.selectRow(rows[i], true); - } - }, - - - selectRange : function(startRow, endRow, keepExisting){ - var i; - if(this.isLocked()){ - return; - } - if(!keepExisting){ - this.clearSelections(); - } - if(startRow <= endRow){ - for(i = startRow; i <= endRow; i++){ - this.selectRow(i, true); - } - }else{ - for(i = startRow; i >= endRow; i--){ - this.selectRow(i, true); - } - } - }, - - - deselectRange : function(startRow, endRow, preventViewNotify){ - if(this.isLocked()){ - return; - } - for(var i = startRow; i <= endRow; i++){ - this.deselectRow(i, preventViewNotify); - } - }, - - - selectRow : function(index, keepExisting, preventViewNotify){ - if(this.isLocked() || (index < 0 || index >= this.grid.store.getCount()) || (keepExisting && this.isSelected(index))){ - return; - } - var r = this.grid.store.getAt(index); - if(r && this.fireEvent('beforerowselect', this, index, keepExisting, r) !== false){ - if(!keepExisting || this.singleSelect){ - this.clearSelections(); - } - this.selections.add(r); - this.last = this.lastActive = index; - if(!preventViewNotify){ - this.grid.getView().onRowSelect(index); - } - if(!this.silent){ - this.fireEvent('rowselect', this, index, r); - this.fireEvent('selectionchange', this); - } - } - }, - - - deselectRow : function(index, preventViewNotify){ - if(this.isLocked()){ - return; - } - if(this.last == index){ - this.last = false; - } - if(this.lastActive == index){ - this.lastActive = false; - } - var r = this.grid.store.getAt(index); - if(r){ - this.selections.remove(r); - if(!preventViewNotify){ - this.grid.getView().onRowDeselect(index); - } - this.fireEvent('rowdeselect', this, index, r); - this.fireEvent('selectionchange', this); - } - }, - - - acceptsNav : function(row, col, cm){ - return !cm.isHidden(col) && cm.isCellEditable(col, row); - }, - - - onEditorKey : function(field, e){ - var k = e.getKey(), - newCell, - g = this.grid, - last = g.lastEdit, - ed = g.activeEditor, - shift = e.shiftKey, - ae, last, r, c; - - if(k == e.TAB){ - e.stopEvent(); - ed.completeEdit(); - if(shift){ - newCell = g.walkCells(ed.row, ed.col-1, -1, this.acceptsNav, this); - }else{ - newCell = g.walkCells(ed.row, ed.col+1, 1, this.acceptsNav, this); - } - }else if(k == e.ENTER){ - if(this.moveEditorOnEnter !== false){ - if(shift){ - newCell = g.walkCells(last.row - 1, last.col, -1, this.acceptsNav, this); - }else{ - newCell = g.walkCells(last.row + 1, last.col, 1, this.acceptsNav, this); - } - } - } - if(newCell){ - r = newCell[0]; - c = newCell[1]; - - this.onEditorSelect(r, last.row); - - if(g.isEditor && g.editing){ - ae = g.activeEditor; - if(ae && ae.field.triggerBlur){ - - ae.field.triggerBlur(); - } - } - g.startEditing(r, c); - } - }, - - onEditorSelect: function(row, lastRow){ - if(lastRow != row){ - this.selectRow(row); - } - }, - - destroy : function(){ - Ext.destroy(this.rowNav); - this.rowNav = null; - Ext.grid.RowSelectionModel.superclass.destroy.call(this); - } -}); - -Ext.grid.Column = Ext.extend(Ext.util.Observable, { - - - - - - - - - - - - - - - - - - - - - - - - - isColumn : true, - - constructor : function(config){ - Ext.apply(this, config); - - if(Ext.isString(this.renderer)){ - this.renderer = Ext.util.Format[this.renderer]; - }else if(Ext.isObject(this.renderer)){ - this.scope = this.renderer.scope; - this.renderer = this.renderer.fn; - } - if(!this.scope){ - this.scope = this; - } - - var ed = this.editor; - delete this.editor; - this.setEditor(ed); - this.addEvents( - - 'click', - - 'contextmenu', - - 'dblclick', - - 'mousedown' - ); - Ext.grid.Column.superclass.constructor.call(this); - }, - - - processEvent : function(name, e, grid, rowIndex, colIndex){ - return this.fireEvent(name, this, grid, rowIndex, e); - }, - - - destroy: function() { - if(this.setEditor){ - this.setEditor(null); - } - this.purgeListeners(); - }, - - - renderer : function(value){ - return value; - }, - - - getEditor: function(rowIndex){ - return this.editable !== false ? this.editor : null; - }, - - - setEditor : function(editor){ - var ed = this.editor; - if(ed){ - if(ed.gridEditor){ - ed.gridEditor.destroy(); - delete ed.gridEditor; - }else{ - ed.destroy(); - } - } - this.editor = null; - if(editor){ - - if(!editor.isXType){ - editor = Ext.create(editor, 'textfield'); - } - this.editor = editor; - } - }, - - - getCellEditor: function(rowIndex){ - var ed = this.getEditor(rowIndex); - if(ed){ - if(!ed.startEdit){ - if(!ed.gridEditor){ - ed.gridEditor = new Ext.grid.GridEditor(ed); - } - ed = ed.gridEditor; - } - } - return ed; - } -}); - - -Ext.grid.BooleanColumn = Ext.extend(Ext.grid.Column, { - - trueText: 'true', - - falseText: 'false', - - undefinedText: ' ', - - constructor: function(cfg){ - Ext.grid.BooleanColumn.superclass.constructor.call(this, cfg); - var t = this.trueText, f = this.falseText, u = this.undefinedText; - this.renderer = function(v){ - if(v === undefined){ - return u; - } - if(!v || v === 'false'){ - return f; - } - return t; - }; - } -}); - - -Ext.grid.NumberColumn = Ext.extend(Ext.grid.Column, { - - format : '0,000.00', - constructor: function(cfg){ - Ext.grid.NumberColumn.superclass.constructor.call(this, cfg); - this.renderer = Ext.util.Format.numberRenderer(this.format); - } -}); - - -Ext.grid.DateColumn = Ext.extend(Ext.grid.Column, { - - format : 'm/d/Y', - constructor: function(cfg){ - Ext.grid.DateColumn.superclass.constructor.call(this, cfg); - this.renderer = Ext.util.Format.dateRenderer(this.format); - } -}); - - -Ext.grid.TemplateColumn = Ext.extend(Ext.grid.Column, { - - constructor: function(cfg){ - Ext.grid.TemplateColumn.superclass.constructor.call(this, cfg); - var tpl = (!Ext.isPrimitive(this.tpl) && this.tpl.compile) ? this.tpl : new Ext.XTemplate(this.tpl); - this.renderer = function(value, p, r){ - return tpl.apply(r.data); - }; - this.tpl = tpl; - } -}); - - -Ext.grid.ActionColumn = Ext.extend(Ext.grid.Column, { - - - - - - - - - header: ' ', - - actionIdRe: /x-action-col-(\d+)/, - - - altText: '', - - constructor: function(cfg) { - var me = this, - items = cfg.items || (me.items = [me]), - l = items.length, - i, - item; - - Ext.grid.ActionColumn.superclass.constructor.call(me, cfg); - - - - me.renderer = function(v, meta) { - - v = Ext.isFunction(cfg.renderer) ? cfg.renderer.apply(this, arguments)||'' : ''; - - meta.css += ' x-action-col-cell'; - for (i = 0; i < l; i++) { - item = items[i]; - v += '' + (item.altText || me.altText) + ''; - } - return v; - }; - }, - - destroy: function() { - delete this.items; - delete this.renderer; - return Ext.grid.ActionColumn.superclass.destroy.apply(this, arguments); - }, - - - processEvent : function(name, e, grid, rowIndex, colIndex){ - var m = e.getTarget().className.match(this.actionIdRe), - item, fn; - if (m && (item = this.items[parseInt(m[1], 10)])) { - if (name == 'click') { - (fn = item.handler || this.handler) && fn.call(item.scope||this.scope||this, grid, rowIndex, colIndex, item, e); - } else if ((name == 'mousedown') && (item.stopSelection !== false)) { - return false; - } - } - return Ext.grid.ActionColumn.superclass.processEvent.apply(this, arguments); - } -}); - - -Ext.grid.Column.types = { - gridcolumn : Ext.grid.Column, - booleancolumn: Ext.grid.BooleanColumn, - numbercolumn: Ext.grid.NumberColumn, - datecolumn: Ext.grid.DateColumn, - templatecolumn: Ext.grid.TemplateColumn, - actioncolumn: Ext.grid.ActionColumn -}; -Ext.grid.RowNumberer = Ext.extend(Object, { - - header: "", - - width: 23, - - sortable: false, - - constructor : function(config){ - Ext.apply(this, config); - if(this.rowspan){ - this.renderer = this.renderer.createDelegate(this); - } - }, - - - fixed:true, - hideable: false, - menuDisabled:true, - dataIndex: '', - id: 'numberer', - rowspan: undefined, - - - renderer : function(v, p, record, rowIndex){ - if(this.rowspan){ - p.cellAttr = 'rowspan="'+this.rowspan+'"'; - } - return rowIndex+1; - } -}); -Ext.grid.CheckboxSelectionModel = Ext.extend(Ext.grid.RowSelectionModel, { - - - - header : '
         
        ', - - width : 20, - - sortable : false, - - - menuDisabled : true, - fixed : true, - hideable: false, - dataIndex : '', - id : 'checker', - isColumn: true, - - constructor : function(){ - Ext.grid.CheckboxSelectionModel.superclass.constructor.apply(this, arguments); - if(this.checkOnly){ - this.handleMouseDown = Ext.emptyFn; - } - }, - - - initEvents : function(){ - Ext.grid.CheckboxSelectionModel.superclass.initEvents.call(this); - this.grid.on('render', function(){ - Ext.fly(this.grid.getView().innerHd).on('mousedown', this.onHdMouseDown, this); - }, this); - }, - - - processEvent : function(name, e, grid, rowIndex, colIndex){ - if (name == 'mousedown') { - this.onMouseDown(e, e.getTarget()); - return false; - } else { - return Ext.grid.Column.prototype.processEvent.apply(this, arguments); - } - }, - - - onMouseDown : function(e, t){ - if(e.button === 0 && t.className == 'x-grid3-row-checker'){ - e.stopEvent(); - var row = e.getTarget('.x-grid3-row'); - if(row){ - var index = row.rowIndex; - if(this.isSelected(index)){ - this.deselectRow(index); - }else{ - this.selectRow(index, true); - this.grid.getView().focusRow(index); - } - } - } - }, - - - onHdMouseDown : function(e, t) { - if(t.className == 'x-grid3-hd-checker'){ - e.stopEvent(); - var hd = Ext.fly(t.parentNode); - var isChecked = hd.hasClass('x-grid3-hd-checker-on'); - if(isChecked){ - hd.removeClass('x-grid3-hd-checker-on'); - this.clearSelections(); - }else{ - hd.addClass('x-grid3-hd-checker-on'); - this.selectAll(); - } - } - }, - - - renderer : function(v, p, record){ - return '
         
        '; - }, - - onEditorSelect: function(row, lastRow){ - if(lastRow != row && !this.checkOnly){ - this.selectRow(row); - } - } -}); -Ext.grid.CellSelectionModel = Ext.extend(Ext.grid.AbstractSelectionModel, { - - constructor : function(config){ - Ext.apply(this, config); - - this.selection = null; - - this.addEvents( - - "beforecellselect", - - "cellselect", - - "selectionchange" - ); - - Ext.grid.CellSelectionModel.superclass.constructor.call(this); - }, - - - initEvents : function(){ - this.grid.on('cellmousedown', this.handleMouseDown, this); - this.grid.on(Ext.EventManager.getKeyEvent(), this.handleKeyDown, this); - this.grid.getView().on({ - scope: this, - refresh: this.onViewChange, - rowupdated: this.onRowUpdated, - beforerowremoved: this.clearSelections, - beforerowsinserted: this.clearSelections - }); - if(this.grid.isEditor){ - this.grid.on('beforeedit', this.beforeEdit, this); - } - }, - - - beforeEdit : function(e){ - this.select(e.row, e.column, false, true, e.record); - }, - - - onRowUpdated : function(v, index, r){ - if(this.selection && this.selection.record == r){ - v.onCellSelect(index, this.selection.cell[1]); - } - }, - - - onViewChange : function(){ - this.clearSelections(true); - }, - - - getSelectedCell : function(){ - return this.selection ? this.selection.cell : null; - }, - - - clearSelections : function(preventNotify){ - var s = this.selection; - if(s){ - if(preventNotify !== true){ - this.grid.view.onCellDeselect(s.cell[0], s.cell[1]); - } - this.selection = null; - this.fireEvent("selectionchange", this, null); - } - }, - - - hasSelection : function(){ - return this.selection ? true : false; - }, - - - handleMouseDown : function(g, row, cell, e){ - if(e.button !== 0 || this.isLocked()){ - return; - } - this.select(row, cell); - }, - - - select : function(rowIndex, colIndex, preventViewNotify, preventFocus, r){ - if(this.fireEvent("beforecellselect", this, rowIndex, colIndex) !== false){ - this.clearSelections(); - r = r || this.grid.store.getAt(rowIndex); - this.selection = { - record : r, - cell : [rowIndex, colIndex] - }; - if(!preventViewNotify){ - var v = this.grid.getView(); - v.onCellSelect(rowIndex, colIndex); - if(preventFocus !== true){ - v.focusCell(rowIndex, colIndex); - } - } - this.fireEvent("cellselect", this, rowIndex, colIndex); - this.fireEvent("selectionchange", this, this.selection); - } - }, - - - isSelectable : function(rowIndex, colIndex, cm){ - return !cm.isHidden(colIndex); - }, - - - onEditorKey: function(field, e){ - if(e.getKey() == e.TAB){ - this.handleKeyDown(e); - } - }, - - - handleKeyDown : function(e){ - if(!e.isNavKeyPress()){ - return; - } - - var k = e.getKey(), - g = this.grid, - s = this.selection, - sm = this, - walk = function(row, col, step){ - return g.walkCells( - row, - col, - step, - g.isEditor && g.editing ? sm.acceptsNav : sm.isSelectable, - sm - ); - }, - cell, newCell, r, c, ae; - - switch(k){ - case e.ESC: - case e.PAGE_UP: - case e.PAGE_DOWN: - - break; - default: - - e.stopEvent(); - break; - } - - if(!s){ - cell = walk(0, 0, 1); - if(cell){ - this.select(cell[0], cell[1]); - } - return; - } - - cell = s.cell; - r = cell[0]; - c = cell[1]; - - switch(k){ - case e.TAB: - if(e.shiftKey){ - newCell = walk(r, c - 1, -1); - }else{ - newCell = walk(r, c + 1, 1); - } - break; - case e.DOWN: - newCell = walk(r + 1, c, 1); - break; - case e.UP: - newCell = walk(r - 1, c, -1); - break; - case e.RIGHT: - newCell = walk(r, c + 1, 1); - break; - case e.LEFT: - newCell = walk(r, c - 1, -1); - break; - case e.ENTER: - if (g.isEditor && !g.editing) { - g.startEditing(r, c); - return; - } - break; - } - - if(newCell){ - - r = newCell[0]; - c = newCell[1]; - - this.select(r, c); - - if(g.isEditor && g.editing){ - ae = g.activeEditor; - if(ae && ae.field.triggerBlur){ - - ae.field.triggerBlur(); - } - g.startEditing(r, c); - } - } - }, - - acceptsNav : function(row, col, cm){ - return !cm.isHidden(col) && cm.isCellEditable(col, row); - } -}); -Ext.grid.EditorGridPanel = Ext.extend(Ext.grid.GridPanel, { - - clicksToEdit: 2, - - - forceValidation: false, - - - isEditor : true, - - detectEdit: false, - - - autoEncode : false, - - - - trackMouseOver: false, - - - initComponent : function(){ - Ext.grid.EditorGridPanel.superclass.initComponent.call(this); - - if(!this.selModel){ - - this.selModel = new Ext.grid.CellSelectionModel(); - } - - this.activeEditor = null; - - this.addEvents( - - "beforeedit", - - "afteredit", - - "validateedit" - ); - }, - - - initEvents : function(){ - Ext.grid.EditorGridPanel.superclass.initEvents.call(this); - - this.getGridEl().on('mousewheel', this.stopEditing.createDelegate(this, [true]), this); - this.on('columnresize', this.stopEditing, this, [true]); - - if(this.clicksToEdit == 1){ - this.on("cellclick", this.onCellDblClick, this); - }else { - var view = this.getView(); - if(this.clicksToEdit == 'auto' && view.mainBody){ - view.mainBody.on('mousedown', this.onAutoEditClick, this); - } - this.on('celldblclick', this.onCellDblClick, this); - } - }, - - onResize : function(){ - Ext.grid.EditorGridPanel.superclass.onResize.apply(this, arguments); - var ae = this.activeEditor; - if(this.editing && ae){ - ae.realign(true); - } - }, - - - onCellDblClick : function(g, row, col){ - this.startEditing(row, col); - }, - - - onAutoEditClick : function(e, t){ - if(e.button !== 0){ - return; - } - var row = this.view.findRowIndex(t), - col = this.view.findCellIndex(t); - if(row !== false && col !== false){ - this.stopEditing(); - if(this.selModel.getSelectedCell){ - var sc = this.selModel.getSelectedCell(); - if(sc && sc[0] === row && sc[1] === col){ - this.startEditing(row, col); - } - }else{ - if(this.selModel.isSelected(row)){ - this.startEditing(row, col); - } - } - } - }, - - - onEditComplete : function(ed, value, startValue){ - this.editing = false; - this.lastActiveEditor = this.activeEditor; - this.activeEditor = null; - - var r = ed.record, - field = this.colModel.getDataIndex(ed.col); - value = this.postEditValue(value, startValue, r, field); - if(this.forceValidation === true || String(value) !== String(startValue)){ - var e = { - grid: this, - record: r, - field: field, - originalValue: startValue, - value: value, - row: ed.row, - column: ed.col, - cancel:false - }; - if(this.fireEvent("validateedit", e) !== false && !e.cancel && String(value) !== String(startValue)){ - r.set(field, e.value); - delete e.cancel; - this.fireEvent("afteredit", e); - } - } - this.view.focusCell(ed.row, ed.col); - }, - - - startEditing : function(row, col){ - this.stopEditing(); - if(this.colModel.isCellEditable(col, row)){ - this.view.ensureVisible(row, col, true); - var r = this.store.getAt(row), - field = this.colModel.getDataIndex(col), - e = { - grid: this, - record: r, - field: field, - value: r.data[field], - row: row, - column: col, - cancel:false - }; - if(this.fireEvent("beforeedit", e) !== false && !e.cancel){ - this.editing = true; - var ed = this.colModel.getCellEditor(col, row); - if(!ed){ - return; - } - if(!ed.rendered){ - ed.parentEl = this.view.getEditorParent(ed); - ed.on({ - scope: this, - render: { - fn: function(c){ - c.field.focus(false, true); - }, - single: true, - scope: this - }, - specialkey: function(field, e){ - this.getSelectionModel().onEditorKey(field, e); - }, - complete: this.onEditComplete, - canceledit: this.stopEditing.createDelegate(this, [true]) - }); - } - Ext.apply(ed, { - row : row, - col : col, - record : r - }); - this.lastEdit = { - row: row, - col: col - }; - this.activeEditor = ed; - - - ed.selectSameEditor = (this.activeEditor == this.lastActiveEditor); - var v = this.preEditValue(r, field); - ed.startEdit(this.view.getCell(row, col).firstChild, Ext.isDefined(v) ? v : ''); - - - (function(){ - delete ed.selectSameEditor; - }).defer(50); - } - } - }, - - - preEditValue : function(r, field){ - var value = r.data[field]; - return this.autoEncode && Ext.isString(value) ? Ext.util.Format.htmlDecode(value) : value; - }, - - - postEditValue : function(value, originalValue, r, field){ - return this.autoEncode && Ext.isString(value) ? Ext.util.Format.htmlEncode(value) : value; - }, - - - stopEditing : function(cancel){ - if(this.editing){ - - var ae = this.lastActiveEditor = this.activeEditor; - if(ae){ - ae[cancel === true ? 'cancelEdit' : 'completeEdit'](); - this.view.focusCell(ae.row, ae.col); - } - this.activeEditor = null; - } - this.editing = false; - } -}); -Ext.reg('editorgrid', Ext.grid.EditorGridPanel); - -Ext.grid.GridEditor = function(field, config){ - Ext.grid.GridEditor.superclass.constructor.call(this, field, config); - field.monitorTab = false; -}; - -Ext.extend(Ext.grid.GridEditor, Ext.Editor, { - alignment: "tl-tl", - autoSize: "width", - hideEl : false, - cls: "x-small-editor x-grid-editor", - shim:false, - shadow:false -}); -Ext.grid.PropertyRecord = Ext.data.Record.create([ - {name:'name',type:'string'}, 'value' -]); - - -Ext.grid.PropertyStore = Ext.extend(Ext.util.Observable, { - - constructor : function(grid, source){ - this.grid = grid; - this.store = new Ext.data.Store({ - recordType : Ext.grid.PropertyRecord - }); - this.store.on('update', this.onUpdate, this); - if(source){ - this.setSource(source); - } - Ext.grid.PropertyStore.superclass.constructor.call(this); - }, - - - setSource : function(o){ - this.source = o; - this.store.removeAll(); - var data = []; - for(var k in o){ - if(this.isEditableValue(o[k])){ - data.push(new Ext.grid.PropertyRecord({name: k, value: o[k]}, k)); - } - } - this.store.loadRecords({records: data}, {}, true); - }, - - - onUpdate : function(ds, record, type){ - if(type == Ext.data.Record.EDIT){ - var v = record.data.value; - var oldValue = record.modified.value; - if(this.grid.fireEvent('beforepropertychange', this.source, record.id, v, oldValue) !== false){ - this.source[record.id] = v; - record.commit(); - this.grid.fireEvent('propertychange', this.source, record.id, v, oldValue); - }else{ - record.reject(); - } - } - }, - - - getProperty : function(row){ - return this.store.getAt(row); - }, - - - isEditableValue: function(val){ - return Ext.isPrimitive(val) || Ext.isDate(val); - }, - - - setValue : function(prop, value, create){ - var r = this.getRec(prop); - if(r){ - r.set('value', value); - this.source[prop] = value; - }else if(create){ - - this.source[prop] = value; - r = new Ext.grid.PropertyRecord({name: prop, value: value}, prop); - this.store.add(r); - - } - }, - - - remove : function(prop){ - var r = this.getRec(prop); - if(r){ - this.store.remove(r); - delete this.source[prop]; - } - }, - - - getRec : function(prop){ - return this.store.getById(prop); - }, - - - getSource : function(){ - return this.source; - } -}); - - -Ext.grid.PropertyColumnModel = Ext.extend(Ext.grid.ColumnModel, { - - nameText : 'Name', - valueText : 'Value', - dateFormat : 'm/j/Y', - trueText: 'true', - falseText: 'false', - - constructor : function(grid, store){ - var g = Ext.grid, - f = Ext.form; - - this.grid = grid; - g.PropertyColumnModel.superclass.constructor.call(this, [ - {header: this.nameText, width:50, sortable: true, dataIndex:'name', id: 'name', menuDisabled:true}, - {header: this.valueText, width:50, resizable:false, dataIndex: 'value', id: 'value', menuDisabled:true} - ]); - this.store = store; - - var bfield = new f.Field({ - autoCreate: {tag: 'select', children: [ - {tag: 'option', value: 'true', html: this.trueText}, - {tag: 'option', value: 'false', html: this.falseText} - ]}, - getValue : function(){ - return this.el.dom.value == 'true'; - } - }); - this.editors = { - 'date' : new g.GridEditor(new f.DateField({selectOnFocus:true})), - 'string' : new g.GridEditor(new f.TextField({selectOnFocus:true})), - 'number' : new g.GridEditor(new f.NumberField({selectOnFocus:true, style:'text-align:left;'})), - 'boolean' : new g.GridEditor(bfield, { - autoSize: 'both' - }) - }; - this.renderCellDelegate = this.renderCell.createDelegate(this); - this.renderPropDelegate = this.renderProp.createDelegate(this); - }, - - - renderDate : function(dateVal){ - return dateVal.dateFormat(this.dateFormat); - }, - - - renderBool : function(bVal){ - return this[bVal ? 'trueText' : 'falseText']; - }, - - - isCellEditable : function(colIndex, rowIndex){ - return colIndex == 1; - }, - - - getRenderer : function(col){ - return col == 1 ? - this.renderCellDelegate : this.renderPropDelegate; - }, - - - renderProp : function(v){ - return this.getPropertyName(v); - }, - - - renderCell : function(val, meta, rec){ - var renderer = this.grid.customRenderers[rec.get('name')]; - if(renderer){ - return renderer.apply(this, arguments); - } - var rv = val; - if(Ext.isDate(val)){ - rv = this.renderDate(val); - }else if(typeof val == 'boolean'){ - rv = this.renderBool(val); - } - return Ext.util.Format.htmlEncode(rv); - }, - - - getPropertyName : function(name){ - var pn = this.grid.propertyNames; - return pn && pn[name] ? pn[name] : name; - }, - - - getCellEditor : function(colIndex, rowIndex){ - var p = this.store.getProperty(rowIndex), - n = p.data.name, - val = p.data.value; - if(this.grid.customEditors[n]){ - return this.grid.customEditors[n]; - } - if(Ext.isDate(val)){ - return this.editors.date; - }else if(typeof val == 'number'){ - return this.editors.number; - }else if(typeof val == 'boolean'){ - return this.editors['boolean']; - }else{ - return this.editors.string; - } - }, - - - destroy : function(){ - Ext.grid.PropertyColumnModel.superclass.destroy.call(this); - this.destroyEditors(this.editors); - this.destroyEditors(this.grid.customEditors); - }, - - destroyEditors: function(editors){ - for(var ed in editors){ - Ext.destroy(editors[ed]); - } - } -}); - - -Ext.grid.PropertyGrid = Ext.extend(Ext.grid.EditorGridPanel, { - - - - - - - - enableColumnMove:false, - stripeRows:false, - trackMouseOver: false, - clicksToEdit:1, - enableHdMenu : false, - viewConfig : { - forceFit:true - }, - - - initComponent : function(){ - this.customRenderers = this.customRenderers || {}; - this.customEditors = this.customEditors || {}; - this.lastEditRow = null; - var store = new Ext.grid.PropertyStore(this); - this.propStore = store; - var cm = new Ext.grid.PropertyColumnModel(this, store); - store.store.sort('name', 'ASC'); - this.addEvents( - - 'beforepropertychange', - - 'propertychange' - ); - this.cm = cm; - this.ds = store.store; - Ext.grid.PropertyGrid.superclass.initComponent.call(this); - - this.mon(this.selModel, 'beforecellselect', function(sm, rowIndex, colIndex){ - if(colIndex === 0){ - this.startEditing.defer(200, this, [rowIndex, 1]); - return false; - } - }, this); - }, - - - onRender : function(){ - Ext.grid.PropertyGrid.superclass.onRender.apply(this, arguments); - - this.getGridEl().addClass('x-props-grid'); - }, - - - afterRender: function(){ - Ext.grid.PropertyGrid.superclass.afterRender.apply(this, arguments); - if(this.source){ - this.setSource(this.source); - } - }, - - - setSource : function(source){ - this.propStore.setSource(source); - }, - - - getSource : function(){ - return this.propStore.getSource(); - }, - - - setProperty : function(prop, value, create){ - this.propStore.setValue(prop, value, create); - }, - - - removeProperty : function(prop){ - this.propStore.remove(prop); - } - - - - - -}); -Ext.reg("propertygrid", Ext.grid.PropertyGrid); - -Ext.grid.GroupingView = Ext.extend(Ext.grid.GridView, { - - - groupByText : 'Group By This Field', - - showGroupsText : 'Show in Groups', - - hideGroupedColumn : false, - - showGroupName : true, - - startCollapsed : false, - - enableGrouping : true, - - enableGroupingMenu : true, - - enableNoGroups : true, - - emptyGroupText : '(None)', - - ignoreAdd : false, - - groupTextTpl : '{text}', - - - groupMode: 'value', - - - - - cancelEditOnToggle: true, - - - initTemplates : function(){ - Ext.grid.GroupingView.superclass.initTemplates.call(this); - this.state = {}; - - var sm = this.grid.getSelectionModel(); - sm.on(sm.selectRow ? 'beforerowselect' : 'beforecellselect', - this.onBeforeRowSelect, this); - - if(!this.startGroup){ - this.startGroup = new Ext.XTemplate( - '
        ', - '
        ', this.groupTextTpl ,'
        ', - '
        ' - ); - } - this.startGroup.compile(); - - if (!this.endGroup) { - this.endGroup = '
        '; - } - }, - - - findGroup : function(el){ - return Ext.fly(el).up('.x-grid-group', this.mainBody.dom); - }, - - - getGroups : function(){ - return this.hasRows() ? this.mainBody.dom.childNodes : []; - }, - - - onAdd : function(ds, records, index) { - if (this.canGroup() && !this.ignoreAdd) { - var ss = this.getScrollState(); - this.fireEvent('beforerowsinserted', ds, index, index + (records.length-1)); - this.refresh(); - this.restoreScroll(ss); - this.fireEvent('rowsinserted', ds, index, index + (records.length-1)); - } else if (!this.canGroup()) { - Ext.grid.GroupingView.superclass.onAdd.apply(this, arguments); - } - }, - - - onRemove : function(ds, record, index, isUpdate){ - Ext.grid.GroupingView.superclass.onRemove.apply(this, arguments); - var g = document.getElementById(record._groupId); - if(g && g.childNodes[1].childNodes.length < 1){ - Ext.removeNode(g); - } - this.applyEmptyText(); - }, - - - refreshRow : function(record){ - if(this.ds.getCount()==1){ - this.refresh(); - }else{ - this.isUpdating = true; - Ext.grid.GroupingView.superclass.refreshRow.apply(this, arguments); - this.isUpdating = false; - } - }, - - - beforeMenuShow : function(){ - var item, items = this.hmenu.items, disabled = this.cm.config[this.hdCtxIndex].groupable === false; - if((item = items.get('groupBy'))){ - item.setDisabled(disabled); - } - if((item = items.get('showGroups'))){ - item.setDisabled(disabled); - item.setChecked(this.canGroup(), true); - } - }, - - - renderUI : function(){ - var markup = Ext.grid.GroupingView.superclass.renderUI.call(this); - - if(this.enableGroupingMenu && this.hmenu){ - this.hmenu.add('-',{ - itemId:'groupBy', - text: this.groupByText, - handler: this.onGroupByClick, - scope: this, - iconCls:'x-group-by-icon' - }); - if(this.enableNoGroups){ - this.hmenu.add({ - itemId:'showGroups', - text: this.showGroupsText, - checked: true, - checkHandler: this.onShowGroupsClick, - scope: this - }); - } - this.hmenu.on('beforeshow', this.beforeMenuShow, this); - } - return markup; - }, - - processEvent: function(name, e){ - Ext.grid.GroupingView.superclass.processEvent.call(this, name, e); - var hd = e.getTarget('.x-grid-group-hd', this.mainBody); - if(hd){ - - var field = this.getGroupField(), - prefix = this.getPrefix(field), - groupValue = hd.id.substring(prefix.length), - emptyRe = new RegExp('gp-' + Ext.escapeRe(field) + '--hd'); - - - groupValue = groupValue.substr(0, groupValue.length - 3); - - - if(groupValue || emptyRe.test(hd.id)){ - this.grid.fireEvent('group' + name, this.grid, field, groupValue, e); - } - if(name == 'mousedown' && e.button == 0){ - this.toggleGroup(hd.parentNode); - } - } - - }, - - - onGroupByClick : function(){ - var grid = this.grid; - this.enableGrouping = true; - grid.store.groupBy(this.cm.getDataIndex(this.hdCtxIndex)); - grid.fireEvent('groupchange', grid, grid.store.getGroupState()); - this.beforeMenuShow(); - this.refresh(); - }, - - - onShowGroupsClick : function(mi, checked){ - this.enableGrouping = checked; - if(checked){ - this.onGroupByClick(); - }else{ - this.grid.store.clearGrouping(); - this.grid.fireEvent('groupchange', this, null); - } - }, - - - toggleRowIndex : function(rowIndex, expanded){ - if(!this.canGroup()){ - return; - } - var row = this.getRow(rowIndex); - if(row){ - this.toggleGroup(this.findGroup(row), expanded); - } - }, - - - toggleGroup : function(group, expanded){ - var gel = Ext.get(group), - id = Ext.util.Format.htmlEncode(gel.id); - - expanded = Ext.isDefined(expanded) ? expanded : gel.hasClass('x-grid-group-collapsed'); - if(this.state[id] !== expanded){ - if (this.cancelEditOnToggle !== false) { - this.grid.stopEditing(true); - } - this.state[id] = expanded; - gel[expanded ? 'removeClass' : 'addClass']('x-grid-group-collapsed'); - } - }, - - - toggleAllGroups : function(expanded){ - var groups = this.getGroups(); - for(var i = 0, len = groups.length; i < len; i++){ - this.toggleGroup(groups[i], expanded); - } - }, - - - expandAllGroups : function(){ - this.toggleAllGroups(true); - }, - - - collapseAllGroups : function(){ - this.toggleAllGroups(false); - }, - - - getGroup : function(v, r, groupRenderer, rowIndex, colIndex, ds){ - var column = this.cm.config[colIndex], - g = groupRenderer ? groupRenderer.call(column.scope, v, {}, r, rowIndex, colIndex, ds) : String(v); - if(g === '' || g === ' '){ - g = column.emptyGroupText || this.emptyGroupText; - } - return g; - }, - - - getGroupField : function(){ - return this.grid.store.getGroupState(); - }, - - - afterRender : function(){ - if(!this.ds || !this.cm){ - return; - } - Ext.grid.GroupingView.superclass.afterRender.call(this); - if(this.grid.deferRowRender){ - this.updateGroupWidths(); - } - }, - - afterRenderUI: function () { - Ext.grid.GroupingView.superclass.afterRenderUI.call(this); - - if (this.enableGroupingMenu && this.hmenu) { - this.hmenu.add('-',{ - itemId:'groupBy', - text: this.groupByText, - handler: this.onGroupByClick, - scope: this, - iconCls:'x-group-by-icon' - }); - - if (this.enableNoGroups) { - this.hmenu.add({ - itemId:'showGroups', - text: this.showGroupsText, - checked: true, - checkHandler: this.onShowGroupsClick, - scope: this - }); - } - - this.hmenu.on('beforeshow', this.beforeMenuShow, this); - } - }, - - - renderRows : function(){ - var groupField = this.getGroupField(); - var eg = !!groupField; - - if(this.hideGroupedColumn) { - var colIndex = this.cm.findColumnIndex(groupField), - hasLastGroupField = Ext.isDefined(this.lastGroupField); - if(!eg && hasLastGroupField){ - this.mainBody.update(''); - this.cm.setHidden(this.cm.findColumnIndex(this.lastGroupField), false); - delete this.lastGroupField; - }else if (eg && !hasLastGroupField){ - this.lastGroupField = groupField; - this.cm.setHidden(colIndex, true); - }else if (eg && hasLastGroupField && groupField !== this.lastGroupField) { - this.mainBody.update(''); - var oldIndex = this.cm.findColumnIndex(this.lastGroupField); - this.cm.setHidden(oldIndex, false); - this.lastGroupField = groupField; - this.cm.setHidden(colIndex, true); - } - } - return Ext.grid.GroupingView.superclass.renderRows.apply( - this, arguments); - }, - - - doRender : function(cs, rs, ds, startRow, colCount, stripe){ - if(rs.length < 1){ - return ''; - } - - if(!this.canGroup() || this.isUpdating){ - return Ext.grid.GroupingView.superclass.doRender.apply(this, arguments); - } - - var groupField = this.getGroupField(), - colIndex = this.cm.findColumnIndex(groupField), - g, - gstyle = 'width:' + this.getTotalWidth() + ';', - cfg = this.cm.config[colIndex], - groupRenderer = cfg.groupRenderer || cfg.renderer, - prefix = this.showGroupName ? (cfg.groupName || cfg.header)+': ' : '', - groups = [], - curGroup, i, len, gid; - - for(i = 0, len = rs.length; i < len; i++){ - var rowIndex = startRow + i, - r = rs[i], - gvalue = r.data[groupField]; - - g = this.getGroup(gvalue, r, groupRenderer, rowIndex, colIndex, ds); - if(!curGroup || curGroup.group != g){ - gid = this.constructId(gvalue, groupField, colIndex); - - - this.state[gid] = !(Ext.isDefined(this.state[gid]) ? !this.state[gid] : this.startCollapsed); - curGroup = { - group: g, - gvalue: gvalue, - text: prefix + g, - groupId: gid, - startRow: rowIndex, - rs: [r], - cls: this.state[gid] ? '' : 'x-grid-group-collapsed', - style: gstyle - }; - groups.push(curGroup); - }else{ - curGroup.rs.push(r); - } - r._groupId = gid; - } - - var buf = []; - for(i = 0, len = groups.length; i < len; i++){ - g = groups[i]; - this.doGroupStart(buf, g, cs, ds, colCount); - buf[buf.length] = Ext.grid.GroupingView.superclass.doRender.call( - this, cs, g.rs, ds, g.startRow, colCount, stripe); - - this.doGroupEnd(buf, g, cs, ds, colCount); - } - return buf.join(''); - }, - - - getGroupId : function(value){ - var field = this.getGroupField(); - return this.constructId(value, field, this.cm.findColumnIndex(field)); - }, - - - constructId : function(value, field, idx){ - var cfg = this.cm.config[idx], - groupRenderer = cfg.groupRenderer || cfg.renderer, - val = (this.groupMode == 'value') ? value : this.getGroup(value, {data:{}}, groupRenderer, 0, idx, this.ds); - - return this.getPrefix(field) + Ext.util.Format.htmlEncode(val); - }, - - - canGroup : function(){ - return this.enableGrouping && !!this.getGroupField(); - }, - - - getPrefix: function(field){ - return this.grid.getGridEl().id + '-gp-' + field + '-'; - }, - - - doGroupStart : function(buf, g, cs, ds, colCount){ - buf[buf.length] = this.startGroup.apply(g); - }, - - - doGroupEnd : function(buf, g, cs, ds, colCount){ - buf[buf.length] = this.endGroup; - }, - - - getRows : function(){ - if(!this.canGroup()){ - return Ext.grid.GroupingView.superclass.getRows.call(this); - } - var r = [], - gs = this.getGroups(), - g, - i = 0, - len = gs.length, - j, - jlen; - for(; i < len; ++i){ - g = gs[i].childNodes[1]; - if(g){ - g = g.childNodes; - for(j = 0, jlen = g.length; j < jlen; ++j){ - r[r.length] = g[j]; - } - } - } - return r; - }, - - - updateGroupWidths : function(){ - if(!this.canGroup() || !this.hasRows()){ - return; - } - var tw = Math.max(this.cm.getTotalWidth(), this.el.dom.offsetWidth-this.getScrollOffset()) +'px'; - var gs = this.getGroups(); - for(var i = 0, len = gs.length; i < len; i++){ - gs[i].firstChild.style.width = tw; - } - }, - - - onColumnWidthUpdated : function(col, w, tw){ - Ext.grid.GroupingView.superclass.onColumnWidthUpdated.call(this, col, w, tw); - this.updateGroupWidths(); - }, - - - onAllColumnWidthsUpdated : function(ws, tw){ - Ext.grid.GroupingView.superclass.onAllColumnWidthsUpdated.call(this, ws, tw); - this.updateGroupWidths(); - }, - - - onColumnHiddenUpdated : function(col, hidden, tw){ - Ext.grid.GroupingView.superclass.onColumnHiddenUpdated.call(this, col, hidden, tw); - this.updateGroupWidths(); - }, - - - onLayout : function(){ - this.updateGroupWidths(); - }, - - - onBeforeRowSelect : function(sm, rowIndex){ - this.toggleRowIndex(rowIndex, true); - } -}); - -Ext.grid.GroupingView.GROUP_ID = 1000; diff --git a/scm-webapp/src/main/webapp/resources/extjs/ext-all.js b/scm-webapp/src/main/webapp/resources/extjs/ext-all.js deleted file mode 100644 index 5b0034a595..0000000000 --- a/scm-webapp/src/main/webapp/resources/extjs/ext-all.js +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Ext JS Library 3.4.0 - * Copyright(c) 2006-2011 Sencha Inc. - * licensing@sencha.com - * http://www.sencha.com/license - */ -(function(){var h=Ext.util,j=Ext.each,g=true,i=false;h.Observable=function(){var k=this,l=k.events;if(k.listeners){k.on(k.listeners);delete k.listeners}k.events=l||{}};h.Observable.prototype={filterOptRe:/^(?:scope|delay|buffer|single)$/,fireEvent:function(){var k=Array.prototype.slice.call(arguments,0),m=k[0].toLowerCase(),n=this,l=g,p=n.events[m],s,o,r;if(n.eventsSuspended===g){if(o=n.eventQueue){o.push(k)}}else{if(typeof p=="object"){if(p.bubble){if(p.fire.apply(p,k.slice(1))===i){return i}r=n.getBubbleTarget&&n.getBubbleTarget();if(r&&r.enableBubble){s=r.events[m];if(!s||typeof s!="object"||!s.bubble){r.enableBubble(m)}return r.fireEvent.apply(r,k)}}else{k.shift();l=p.fire.apply(p,k)}}}return l},addListener:function(k,m,l,r){var n=this,q,s,p;if(typeof k=="object"){r=k;for(q in r){s=r[q];if(!n.filterOptRe.test(q)){n.addListener(q,s.fn||s,s.scope||r.scope,s.fn?s:r)}}}else{k=k.toLowerCase();p=n.events[k]||g;if(typeof p=="boolean"){n.events[k]=p=new h.Event(n,k)}p.addListener(m,l,typeof r=="object"?r:{})}},removeListener:function(k,m,l){var n=this.events[k.toLowerCase()];if(typeof n=="object"){n.removeListener(m,l)}},purgeListeners:function(){var m=this.events,k,l;for(l in m){k=m[l];if(typeof k=="object"){k.clearListeners()}}},addEvents:function(n){var m=this;m.events=m.events||{};if(typeof n=="string"){var k=arguments,l=k.length;while(l--){m.events[k[l]]=m.events[k[l]]||g}}else{Ext.applyIf(m.events,n)}},hasListener:function(k){var l=this.events[k.toLowerCase()];return typeof l=="object"&&l.listeners.length>0},suspendEvents:function(k){this.eventsSuspended=g;if(k&&!this.eventQueue){this.eventQueue=[]}},resumeEvents:function(){var k=this,l=k.eventQueue||[];k.eventsSuspended=i;delete k.eventQueue;j(l,function(m){k.fireEvent.apply(k,m)})}};var d=h.Observable.prototype;d.on=d.addListener;d.un=d.removeListener;h.Observable.releaseCapture=function(k){k.fireEvent=d.fireEvent};function e(l,m,k){return function(){if(m.target==arguments[0]){l.apply(k,Array.prototype.slice.call(arguments,0))}}}function b(n,p,k,m){k.task=new h.DelayedTask();return function(){k.task.delay(p.buffer,n,m,Array.prototype.slice.call(arguments,0))}}function c(m,n,l,k){return function(){n.removeListener(l,k);return m.apply(k,arguments)}}function a(n,p,k,m){return function(){var l=new h.DelayedTask(),o=Array.prototype.slice.call(arguments,0);if(!k.tasks){k.tasks=[]}k.tasks.push(l);l.delay(p.delay||10,function(){k.tasks.remove(l);n.apply(m,o)},m)}}h.Event=function(l,k){this.name=k;this.obj=l;this.listeners=[]};h.Event.prototype={addListener:function(o,n,m){var p=this,k;n=n||p.obj;if(!p.isListening(o,n)){k=p.createListener(o,n,m);if(p.firing){p.listeners=p.listeners.slice(0)}p.listeners.push(k)}},createListener:function(p,n,q){q=q||{};n=n||this.obj;var k={fn:p,scope:n,options:q},m=p;if(q.target){m=e(m,q,n)}if(q.delay){m=a(m,q,k,n)}if(q.single){m=c(m,this,p,n)}if(q.buffer){m=b(m,q,k,n)}k.fireFn=m;return k},findListener:function(o,n){var p=this.listeners,m=p.length,k;n=n||this.obj;while(m--){k=p[m];if(k){if(k.fn==o&&k.scope==n){return m}}}return -1},isListening:function(l,k){return this.findListener(l,k)!=-1},removeListener:function(r,q){var p,m,n,s=this,o=i;if((p=s.findListener(r,q))!=-1){if(s.firing){s.listeners=s.listeners.slice(0)}m=s.listeners[p];if(m.task){m.task.cancel();delete m.task}n=m.tasks&&m.tasks.length;if(n){while(n--){m.tasks[n].cancel()}delete m.tasks}s.listeners.splice(p,1);o=g}return o},clearListeners:function(){var n=this,k=n.listeners,m=k.length;while(m--){n.removeListener(k[m].fn,k[m].scope)}},fire:function(){var q=this,p=q.listeners,k=p.length,o=0,m;if(k>0){q.firing=g;var n=Array.prototype.slice.call(arguments,0);for(;o",i="",b=a+"",j=""+i,l=b+"",w=""+j;function h(B,D,C,E,A,y){var z=r.insertHtml(E,Ext.getDom(B),u(D));return C?Ext.get(z,true):z}function u(D){var z="",y,C,B,E;if(typeof D=="string"){z=D}else{if(Ext.isArray(D)){for(var A=0;A"}}}return z}function g(F,C,B,D){x.innerHTML=[C,B,D].join("");var y=-1,A=x,z;while(++y "'+D+'"'},insertBefore:function(y,A,z){return h(y,A,z,c)},insertAfter:function(y,A,z){return h(y,A,z,p,"nextSibling")},insertFirst:function(y,A,z){return h(y,A,z,n,"firstChild")},append:function(y,A,z){return h(y,A,z,q,"",true)},overwrite:function(y,A,z){y=Ext.getDom(y);y.innerHTML=u(A);return z?Ext.get(y.firstChild):y.firstChild},createHtml:u};return r}();Ext.Template=function(h){var j=this,c=arguments,e=[],d;if(Ext.isArray(h)){h=h.join("")}else{if(c.length>1){for(var g=0,b=c.length;g+~]\s?|\s|$)/,tagTokenRe=/^(#)?([\w\-\*]+)/,nthRe=/(\d*)n\+?(\d*)/,nthRe2=/\D/,isIE=window.ActiveXObject?true:false,key=30803;eval("var batch = 30803;");function child(parent,index){var i=0,n=parent.firstChild;while(n){if(n.nodeType==1){if(++i==index){return n}}n=n.nextSibling}return null}function next(n){while((n=n.nextSibling)&&n.nodeType!=1){}return n}function prev(n){while((n=n.previousSibling)&&n.nodeType!=1){}return n}function children(parent){var n=parent.firstChild,nodeIndex=-1,nextNode;while(n){nextNode=n.nextSibling;if(n.nodeType==3&&!nonSpace.test(n.nodeValue)){parent.removeChild(n)}else{n.nodeIndex=++nodeIndex}n=nextNode}return this}function byClassName(nodeSet,cls){if(!cls){return nodeSet}var result=[],ri=-1;for(var i=0,ci;ci=nodeSet[i];i++){if((" "+ci.className+" ").indexOf(cls)!=-1){result[++ri]=ci}}return result}function attrValue(n,attr){if(!n.tagName&&typeof n.length!="undefined"){n=n[0]}if(!n){return null}if(attr=="for"){return n.htmlFor}if(attr=="class"||attr=="className"){return n.className}return n.getAttribute(attr)||n[attr]}function getNodes(ns,mode,tagName){var result=[],ri=-1,cs;if(!ns){return result}tagName=tagName||"*";if(typeof ns.getElementsByTagName!="undefined"){ns=[ns]}if(!mode){for(var i=0,ni;ni=ns[i];i++){cs=ni.getElementsByTagName(tagName);for(var j=0,ci;ci=cs[j];j++){result[++ri]=ci}}}else{if(mode=="/"||mode==">"){var utag=tagName.toUpperCase();for(var i=0,ni,cn;ni=ns[i];i++){cn=ni.childNodes;for(var j=0,cj;cj=cn[j];j++){if(cj.nodeName==utag||cj.nodeName==tagName||tagName=="*"){result[++ri]=cj}}}}else{if(mode=="+"){var utag=tagName.toUpperCase();for(var i=0,n;n=ns[i];i++){while((n=n.nextSibling)&&n.nodeType!=1){}if(n&&(n.nodeName==utag||n.nodeName==tagName||tagName=="*")){result[++ri]=n}}}else{if(mode=="~"){var utag=tagName.toUpperCase();for(var i=0,n;n=ns[i];i++){while((n=n.nextSibling)){if(n.nodeName==utag||n.nodeName==tagName||tagName=="*"){result[++ri]=n}}}}}}}return result}function concat(a,b){if(b.slice){return a.concat(b)}for(var i=0,l=b.length;i1){return nodup(results)}return results},isXml:function(el){var docEl=(el?el.ownerDocument||el:0).documentElement;return docEl?docEl.nodeName!=="HTML":false},select:document.querySelectorAll?function(path,root,type){root=root||document;if(!Ext.DomQuery.isXml(root)){try{var cs=root.querySelectorAll(path);return Ext.toArray(cs)}catch(ex){}}return Ext.DomQuery.jsSelect.call(this,path,root,type)}:function(path,root,type){return Ext.DomQuery.jsSelect.call(this,path,root,type)},selectNode:function(path,root){return Ext.DomQuery.select(path,root)[0]},selectValue:function(path,root,defaultValue){path=path.replace(trimRe,"");if(!valueCache[path]){valueCache[path]=Ext.DomQuery.compile(path,"select")}var n=valueCache[path](root),v;n=n[0]?n[0]:n;if(typeof n.normalize=="function"){n.normalize()}v=(n&&n.firstChild?n.firstChild.nodeValue:null);return((v===null||v===undefined||v==="")?defaultValue:v)},selectNumber:function(path,root,defaultValue){var v=Ext.DomQuery.selectValue(path,root,defaultValue||0);return parseFloat(v)},is:function(el,ss){if(typeof el=="string"){el=document.getElementById(el)}var isArray=Ext.isArray(el),result=Ext.DomQuery.filter(isArray?el:[el],ss);return isArray?(result.length==el.length):(result.length>0)},filter:function(els,ss,nonMatches){ss=ss.replace(trimRe,"");if(!simpleCache[ss]){simpleCache[ss]=Ext.DomQuery.compile(ss,"simple")}var result=simpleCache[ss](els);return nonMatches?quickDiff(result,els):result},matchers:[{re:/^\.([\w\-]+)/,select:'n = byClassName(n, " {1} ");'},{re:/^\:([\w\-]+)(?:\(((?:[^\s>\/]*|.*?))\))?/,select:'n = byPseudo(n, "{1}", "{2}");'},{re:/^(?:([\[\{])(?:@)?([\w\-]+)\s?(?:(=|.=)\s?(["']?)(.*?)\4)?[\]\}])/,select:'n = byAttribute(n, "{2}", "{5}", "{3}", "{1}");'},{re:/^#([\w\-]+)/,select:'n = byId(n, "{1}");'},{re:/^@([\w\-]+)/,select:'return {firstChild:{nodeValue:attrValue(n, "{1}")}};'}],operators:{"=":function(a,v){return a==v},"!=":function(a,v){return a!=v},"^=":function(a,v){return a&&a.substr(0,v.length)==v},"$=":function(a,v){return a&&a.substr(a.length-v.length)==v},"*=":function(a,v){return a&&a.indexOf(v)!==-1},"%=":function(a,v){return(a%v)==0},"|=":function(a,v){return a&&(a==v||a.substr(0,v.length+1)==v+"-")},"~=":function(a,v){return a&&(" "+a+" ").indexOf(" "+v+" ")!=-1}},pseudos:{"first-child":function(c){var r=[],ri=-1,n;for(var i=0,ci;ci=n=c[i];i++){while((n=n.previousSibling)&&n.nodeType!=1){}if(!n){r[++ri]=ci}}return r},"last-child":function(c){var r=[],ri=-1,n;for(var i=0,ci;ci=n=c[i];i++){while((n=n.nextSibling)&&n.nodeType!=1){}if(!n){r[++ri]=ci}}return r},"nth-child":function(c,a){var r=[],ri=-1,m=nthRe.exec(a=="even"&&"2n"||a=="odd"&&"2n+1"||!nthRe2.test(a)&&"n+"+a||a),f=(m[1]||1)-0,l=m[2]-0;for(var i=0,n;n=c[i];i++){var pn=n.parentNode;if(batch!=pn._batch){var j=0;for(var cn=pn.firstChild;cn;cn=cn.nextSibling){if(cn.nodeType==1){cn.nodeIndex=++j}}pn._batch=batch}if(f==1){if(l==0||n.nodeIndex==l){r[++ri]=n}}else{if((n.nodeIndex+l)%f==0){r[++ri]=n}}}return r},"only-child":function(c){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(!prev(ci)&&!next(ci)){r[++ri]=ci}}return r},empty:function(c){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){var cns=ci.childNodes,j=0,cn,empty=true;while(cn=cns[j]){++j;if(cn.nodeType==1||cn.nodeType==3){empty=false;break}}if(empty){r[++ri]=ci}}return r},contains:function(c,v){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if((ci.textContent||ci.innerText||"").indexOf(v)!=-1){r[++ri]=ci}}return r},nodeValue:function(c,v){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(ci.firstChild&&ci.firstChild.nodeValue==v){r[++ri]=ci}}return r},checked:function(c){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(ci.checked==true){r[++ri]=ci}}return r},not:function(c,ss){return Ext.DomQuery.filter(c,ss,true)},any:function(c,selectors){var ss=selectors.split("|"),r=[],ri=-1,s;for(var i=0,ci;ci=c[i];i++){for(var j=0;s=ss[j];j++){if(Ext.DomQuery.is(ci,s)){r[++ri]=ci;break}}}return r},odd:function(c){return this["nth-child"](c,"odd")},even:function(c){return this["nth-child"](c,"even")},nth:function(c,a){return c[a-1]||[]},first:function(c){return c[0]||[]},last:function(c){return c[c.length-1]||[]},has:function(c,ss){var s=Ext.DomQuery.select,r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(s(ss,ci).length>0){r[++ri]=ci}}return r},next:function(c,ss){var is=Ext.DomQuery.is,r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){var n=next(ci);if(n&&is(n,ss)){r[++ri]=ci}}return r},prev:function(c,ss){var is=Ext.DomQuery.is,r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){var n=prev(ci);if(n&&is(n,ss)){r[++ri]=ci}}return r}}}}();Ext.query=Ext.DomQuery.select;Ext.util.DelayedTask=function(d,c,a){var e=this,g,b=function(){clearInterval(g);g=null;d.apply(c,a||[])};e.delay=function(i,k,j,h){e.cancel();d=k||d;c=j||c;a=h||a;g=setInterval(b,i)};e.cancel=function(){if(g){clearInterval(g);g=null}}};(function(){var h=document;Ext.Element=function(l,m){var n=typeof l=="string"?h.getElementById(l):l,o;if(!n){return null}o=n.id;if(!m&&o&&Ext.elCache[o]){return Ext.elCache[o].el}this.dom=n;this.id=o||Ext.id(n)};var d=Ext.DomHelper,e=Ext.Element,a=Ext.elCache;e.prototype={set:function(q,m){var n=this.dom,l,p,m=(m!==false)&&!!n.setAttribute;for(l in q){if(q.hasOwnProperty(l)){p=q[l];if(l=="style"){d.applyStyles(n,p)}else{if(l=="cls"){n.className=p}else{if(m){n.setAttribute(l,p)}else{n[l]=p}}}}}return this},defaultUnit:"px",is:function(l){return Ext.DomQuery.is(this.dom,l)},focus:function(o,n){var l=this,n=n||l.dom;try{if(Number(o)){l.focus.defer(o,null,[null,n])}else{n.focus()}}catch(m){}return l},blur:function(){try{this.dom.blur()}catch(l){}return this},getValue:function(l){var m=this.dom.value;return l?parseInt(m,10):m},addListener:function(l,o,n,m){Ext.EventManager.on(this.dom,l,o,n||this,m);return this},removeListener:function(l,n,m){Ext.EventManager.removeListener(this.dom,l,n,m||this);return this},removeAllListeners:function(){Ext.EventManager.removeAll(this.dom);return this},purgeAllListeners:function(){Ext.EventManager.purgeElement(this,true);return this},addUnits:function(l){if(l===""||l=="auto"||l===undefined){l=l||""}else{if(!isNaN(l)||!i.test(l)){l=l+(this.defaultUnit||"px")}}return l},load:function(m,n,l){Ext.Ajax.request(Ext.apply({params:n,url:m.url||m,callback:l,el:this.dom,indicatorText:m.indicatorText||""},Ext.isObject(m)?m:{}));return this},isBorderBox:function(){return Ext.isBorderBox||Ext.isForcedBorderBox||g[(this.dom.tagName||"").toLowerCase()]},remove:function(){var l=this,m=l.dom;if(m){delete l.dom;Ext.removeNode(m)}},hover:function(m,l,o,n){var p=this;p.on("mouseenter",m,o||p.dom,n);p.on("mouseleave",l,o||p.dom,n);return p},contains:function(l){return !l?false:Ext.lib.Dom.isAncestor(this.dom,l.dom?l.dom:l)},getAttributeNS:function(m,l){return this.getAttribute(l,m)},getAttribute:(function(){var p=document.createElement("table"),o=false,m="getAttribute" in p,l=/undefined|unknown/;if(m){try{p.getAttribute("ext:qtip")}catch(n){o=true}return function(q,s){var r=this.dom,t;if(r.getAttributeNS){t=r.getAttributeNS(s,q)||null}if(t==null){if(s){if(o&&r.tagName.toUpperCase()=="TABLE"){try{t=r.getAttribute(s+":"+q)}catch(u){t=""}}else{t=r.getAttribute(s+":"+q)}}else{t=r.getAttribute(q)||r[q]}}return t||""}}else{return function(q,s){var r=this.om,u,t;if(s){t=r[s+":"+q];u=l.test(typeof t)?undefined:t}else{u=r[q]}return u||""}}p=null})(),update:function(l){if(this.dom){this.dom.innerHTML=l}return this}};var k=e.prototype;e.addMethods=function(l){Ext.apply(k,l)};k.on=k.addListener;k.un=k.removeListener;k.autoBoxAdjust=true;var i=/\d+(px|em|%|en|ex|pt|in|cm|mm|pc)$/i,c;e.get=function(m){var l,p,o;if(!m){return null}if(typeof m=="string"){if(!(p=h.getElementById(m))){return null}if(a[m]&&a[m].el){l=a[m].el;l.dom=p}else{l=e.addToCache(new e(p))}return l}else{if(m.tagName){if(!(o=m.id)){o=Ext.id(m)}if(a[o]&&a[o].el){l=a[o].el;l.dom=m}else{l=e.addToCache(new e(m))}return l}else{if(m instanceof e){if(m!=c){if(Ext.isIE&&(m.id==undefined||m.id=="")){m.dom=m.dom}else{m.dom=h.getElementById(m.id)||m.dom}}return m}else{if(m.isComposite){return m}else{if(Ext.isArray(m)){return e.select(m)}else{if(m==h){if(!c){var n=function(){};n.prototype=e.prototype;c=new n();c.dom=h}return c}}}}}}return null};e.addToCache=function(l,m){m=m||l.id;a[m]={el:l,data:{},events:{}};return l};e.data=function(m,l,n){m=e.get(m);if(!m){return null}var o=a[m.id].data;if(arguments.length==2){return o[l]}else{return(o[l]=n)}};function j(){if(!Ext.enableGarbageCollector){clearInterval(e.collectorThreadId)}else{var l,n,q,p;for(l in a){p=a[l];if(p.skipGC){continue}n=p.el;q=n.dom;if(!q||!q.parentNode||(!q.offsetParent&&!h.getElementById(l))){if(Ext.enableListenerCollection){Ext.EventManager.removeAll(q)}delete a[l]}}if(Ext.isIE){var m={};for(l in a){m[l]=a[l]}a=Ext.elCache=m}}}e.collectorThreadId=setInterval(j,30000);var b=function(){};b.prototype=e.prototype;e.Flyweight=function(l){this.dom=l};e.Flyweight.prototype=new b();e.Flyweight.prototype.isFlyweight=true;e._flyweights={};e.fly=function(n,l){var m=null;l=l||"_global";if(n=Ext.getDom(n)){(e._flyweights[l]=e._flyweights[l]||new e.Flyweight()).dom=n;m=e._flyweights[l]}return m};Ext.get=e.get;Ext.fly=e.fly;var g=Ext.isStrict?{select:1}:{input:1,select:1,textarea:1};if(Ext.isIE||Ext.isGecko){g.button=1}})();Ext.Element.addMethods(function(){var d="parentNode",b="nextSibling",c="previousSibling",e=Ext.DomQuery,a=Ext.get;return{findParent:function(m,l,h){var j=this.dom,g=document.body,k=0,i;if(Ext.isGecko&&Object.prototype.toString.call(j)=="[object XULElement]"){return null}l=l||50;if(isNaN(l)){i=Ext.getDom(l);l=Number.MAX_VALUE}while(j&&j.nodeType==1&&k "+g,this.dom);return h?i:a(i)},parent:function(g,h){return this.matchNode(d,d,g,h)},next:function(g,h){return this.matchNode(b,b,g,h)},prev:function(g,h){return this.matchNode(c,c,g,h)},first:function(g,h){return this.matchNode(b,"firstChild",g,h)},last:function(g,h){return this.matchNode(c,"lastChild",g,h)},matchNode:function(h,k,g,i){var j=this.dom[k];while(j){if(j.nodeType==1&&(!g||e.is(j,g))){return !i?a(j):j}j=j[h]}return null}}}());Ext.Element.addMethods(function(){var c=Ext.getDom,a=Ext.get,b=Ext.DomHelper;return{appendChild:function(d){return a(d).appendTo(this)},appendTo:function(d){c(d).appendChild(this.dom);return this},insertBefore:function(d){(d=c(d)).parentNode.insertBefore(this.dom,d);return this},insertAfter:function(d){(d=c(d)).parentNode.insertBefore(this.dom,d.nextSibling);return this},insertFirst:function(e,d){e=e||{};if(e.nodeType||e.dom||typeof e=="string"){e=c(e);this.dom.insertBefore(e,this.dom.firstChild);return !d?a(e):e}else{return this.createChild(e,this.dom.firstChild,d)}},replace:function(d){d=a(d);this.insertBefore(d);d.remove();return this},replaceWith:function(d){var e=this;if(d.nodeType||d.dom||typeof d=="string"){d=c(d);e.dom.parentNode.insertBefore(d,e.dom)}else{d=b.insertBefore(e.dom,d)}delete Ext.elCache[e.id];Ext.removeNode(e.dom);e.id=Ext.id(e.dom=d);Ext.Element.addToCache(e.isFlyweight?new Ext.Element(e.dom):e);return e},createChild:function(e,d,g){e=e||{tag:"div"};return d?b.insertBefore(d,e,g!==true):b[!this.dom.firstChild?"overwrite":"append"](this.dom,e,g!==true)},wrap:function(d,e){var g=b.insertBefore(this.dom,d||{tag:"div"},!e);g.dom?g.dom.appendChild(this.dom):g.appendChild(this.dom);return g},insertHtml:function(e,g,d){var h=b.insertHtml(e,this.dom,g);return d?Ext.get(h):h}}}());Ext.Element.addMethods(function(){var A=Ext.supports,h={},x=/(-[a-z])/gi,s=document.defaultView,D=/alpha\(opacity=(.*)\)/i,l=/^\s+|\s+$/g,B=Ext.Element,u=/\s+/,b=/\w/g,d="padding",c="margin",y="border",t="-left",q="-right",w="-top",o="-bottom",j="-width",r=Math,z="hidden",e="isClipped",k="overflow",n="overflow-x",m="overflow-y",C="originalClip",i={l:y+t+j,r:y+q+j,t:y+w+j,b:y+o+j},g={l:d+t,r:d+q,t:d+w,b:d+o},a={l:c+t,r:c+q,t:c+w,b:c+o},E=Ext.Element.data;function p(F,G){return G.charAt(1).toUpperCase()}function v(F){return h[F]||(h[F]=F=="float"?(A.cssFloat?"cssFloat":"styleFloat"):F.replace(x,p))}return{adjustWidth:function(F){var G=this;var H=(typeof F=="number");if(H&&G.autoBoxAdjust&&!G.isBorderBox()){F-=(G.getBorderWidth("lr")+G.getPadding("lr"))}return(H&&F<0)?0:F},adjustHeight:function(F){var G=this;var H=(typeof F=="number");if(H&&G.autoBoxAdjust&&!G.isBorderBox()){F-=(G.getBorderWidth("tb")+G.getPadding("tb"))}return(H&&F<0)?0:F},addClass:function(J){var K=this,I,F,H,G=[];if(!Ext.isArray(J)){if(typeof J=="string"&&!this.hasClass(J)){K.dom.className+=" "+J}}else{for(I=0,F=J.length;I5?H.toLowerCase():G)},setStyle:function(I,H){var F,G;if(typeof I!="object"){F={};F[I]=H;I=F}for(G in I){H=I[G];G=="opacity"?this.setOpacity(H):this.dom.style[v(G)]=H}return this},setOpacity:function(G,F){var J=this,H=J.dom.style;if(!F||!J.anim){if(Ext.isIE){var I=G<1?"alpha(opacity="+G*100+")":"",K=H.filter.replace(D,"").replace(l,"");H.zoom=1;H.filter=K+(K.length>0?" ":"")+I}else{H.opacity=G}}else{J.anim({opacity:{to:G}},J.preanim(arguments,1),null,0.35,"easeIn")}return J},clearOpacity:function(){var F=this.dom.style;if(Ext.isIE){if(!Ext.isEmpty(F.filter)){F.filter=F.filter.replace(D,"").replace(l,"")}}else{F.opacity=F["-moz-opacity"]=F["-khtml-opacity"]=""}return this},getHeight:function(H){var G=this,J=G.dom,I=Ext.isIE&&G.isStyle("display","none"),F=r.max(J.offsetHeight,I?0:J.clientHeight)||0;F=!H?F:F-G.getBorderWidth("tb")-G.getPadding("tb");return F<0?0:F},getWidth:function(G){var H=this,J=H.dom,I=Ext.isIE&&H.isStyle("display","none"),F=r.max(J.offsetWidth,I?0:J.clientWidth)||0;F=!G?F:F-H.getBorderWidth("lr")-H.getPadding("lr");return F<0?0:F},setWidth:function(G,F){var H=this;G=H.adjustWidth(G);!F||!H.anim?H.dom.style.width=H.addUnits(G):H.anim({width:{to:G}},H.preanim(arguments,1));return H},setHeight:function(F,G){var H=this;F=H.adjustHeight(F);!G||!H.anim?H.dom.style.height=H.addUnits(F):H.anim({height:{to:F}},H.preanim(arguments,1));return H},getBorderWidth:function(F){return this.addStyles(F,i)},getPadding:function(F){return this.addStyles(F,g)},clip:function(){var F=this,G=F.dom;if(!E(G,e)){E(G,e,true);E(G,C,{o:F.getStyle(k),x:F.getStyle(n),y:F.getStyle(m)});F.setStyle(k,z);F.setStyle(n,z);F.setStyle(m,z)}return F},unclip:function(){var F=this,H=F.dom;if(E(H,e)){E(H,e,false);var G=E(H,C);if(G.o){F.setStyle(k,G.o)}if(G.x){F.setStyle(n,G.x)}if(G.y){F.setStyle(m,G.y)}}return F},addStyles:function(M,L){var J=0,K=M.match(b),I,H,G,F=K.length;for(G=0;Ga.clientHeight||a.scrollWidth>a.clientWidth},scrollTo:function(a,b){this.dom["scroll"+(/top/i.test(a)?"Top":"Left")]=b;return this},getScroll:function(){var i=this.dom,h=document,a=h.body,c=h.documentElement,b,g,e;if(i==h||i==a){if(Ext.isIE&&Ext.isStrict){b=c.scrollLeft;g=c.scrollTop}else{b=window.pageXOffset;g=window.pageYOffset}e={left:b||(a?a.scrollLeft:0),top:g||(a?a.scrollTop:0)}}else{e={left:i.scrollLeft,top:i.scrollTop}}return e}});Ext.Element.VISIBILITY=1;Ext.Element.DISPLAY=2;Ext.Element.OFFSETS=3;Ext.Element.ASCLASS=4;Ext.Element.visibilityCls="x-hide-nosize";Ext.Element.addMethods(function(){var e=Ext.Element,p="opacity",j="visibility",g="display",d="hidden",n="offsets",k="asclass",m="none",a="nosize",b="originalDisplay",c="visibilityMode",h="isVisible",i=e.data,l=function(r){var q=i(r,b);if(q===undefined){i(r,b,q="")}return q},o=function(r){var q=i(r,c);if(q===undefined){i(r,c,q=1)}return q};return{originalDisplay:"",visibilityMode:1,setVisibilityMode:function(q){i(this.dom,c,q);return this},animate:function(r,t,s,u,q){this.anim(r,{duration:t,callback:s,easing:u},q);return this},anim:function(t,u,r,w,s,q){r=r||"run";u=u||{};var v=this,x=Ext.lib.Anim[r](v.dom,t,(u.duration||w)||0.35,(u.easing||s)||"easeOut",function(){if(q){q.call(v)}if(u.callback){u.callback.call(u.scope||v,v,u)}},v);u.anim=x;return x},preanim:function(q,r){return !q[r]?false:(typeof q[r]=="object"?q[r]:{duration:q[r+1],callback:q[r+2],easing:q[r+3]})},isVisible:function(){var q=this,s=q.dom,r=i(s,h);if(typeof r=="boolean"){return r}r=!q.isStyle(j,d)&&!q.isStyle(g,m)&&!((o(s)==e.ASCLASS)&&q.hasClass(q.visibilityCls||e.visibilityCls));i(s,h,r);return r},setVisible:function(t,q){var w=this,r,y,x,v,u=w.dom,s=o(u);if(typeof q=="string"){switch(q){case g:s=e.DISPLAY;break;case j:s=e.VISIBILITY;break;case n:s=e.OFFSETS;break;case a:case k:s=e.ASCLASS;break}w.setVisibilityMode(s);q=false}if(!q||!w.anim){if(s==e.ASCLASS){w[t?"removeClass":"addClass"](w.visibilityCls||e.visibilityCls)}else{if(s==e.DISPLAY){return w.setDisplayed(t)}else{if(s==e.OFFSETS){if(!t){w.hideModeStyles={position:w.getStyle("position"),top:w.getStyle("top"),left:w.getStyle("left")};w.applyStyles({position:"absolute",top:"-10000px",left:"-10000px"})}else{w.applyStyles(w.hideModeStyles||{position:"",top:"",left:""});delete w.hideModeStyles}}else{w.fixDisplay();u.style.visibility=t?"visible":d}}}}else{if(t){w.setOpacity(0.01);w.setVisible(true)}w.anim({opacity:{to:(t?1:0)}},w.preanim(arguments,1),null,0.35,"easeIn",function(){t||w.setVisible(false).setOpacity(1)})}i(u,h,t);return w},hasMetrics:function(){var q=this.dom;return this.isVisible()||(o(q)==e.VISIBILITY)},toggle:function(q){var r=this;r.setVisible(!r.isVisible(),r.preanim(arguments,0));return r},setDisplayed:function(q){if(typeof q=="boolean"){q=q?l(this.dom):m}this.setStyle(g,q);return this},fixDisplay:function(){var q=this;if(q.isStyle(g,m)){q.setStyle(j,d);q.setStyle(g,l(this.dom));if(q.isStyle(g,m)){q.setStyle(g,"block")}}},hide:function(q){if(typeof q=="string"){this.setVisible(false,q);return this}this.setVisible(false,this.preanim(arguments,0));return this},show:function(q){if(typeof q=="string"){this.setVisible(true,q);return this}this.setVisible(true,this.preanim(arguments,0));return this}}}());(function(){var y=null,A=undefined,k=true,t=false,j="setX",h="setY",a="setXY",n="left",l="bottom",s="top",m="right",q="height",g="width",i="points",w="hidden",z="absolute",u="visible",e="motion",o="position",r="easeOut",d=new Ext.Element.Flyweight(),v={},x=function(B){return B||{}},p=function(B){d.dom=B;d.id=Ext.id(B);return d},c=function(B){if(!v[B]){v[B]=[]}return v[B]},b=function(C,B){v[C]=B};Ext.enableFx=k;Ext.Fx={switchStatements:function(C,D,B){return D.apply(this,B[C])},slideIn:function(H,E){E=x(E);var J=this,G=J.dom,M=G.style,O,B,L,D,C,M,I,N,K,F;H=H||"t";J.queueFx(E,function(){O=p(G).getXY();p(G).fixDisplay();B=p(G).getFxRestore();L={x:O[0],y:O[1],0:O[0],1:O[1],width:G.offsetWidth,height:G.offsetHeight};L.right=L.x+L.width;L.bottom=L.y+L.height;p(G).setWidth(L.width).setHeight(L.height);D=p(G).fxWrap(B.pos,E,w);M.visibility=u;M.position=z;function P(){p(G).fxUnwrap(D,B.pos,E);M.width=B.width;M.height=B.height;p(G).afterFx(E)}N={to:[L.x,L.y]};K={to:L.width};F={to:L.height};function Q(U,R,V,S,X,Z,ac,ab,aa,W,T){var Y={};p(U).setWidth(V).setHeight(S);if(p(U)[X]){p(U)[X](Z)}R[ac]=R[ab]="0";if(aa){Y.width=aa}if(W){Y.height=W}if(T){Y.points=T}return Y}I=p(G).switchStatements(H.toLowerCase(),Q,{t:[D,M,L.width,0,y,y,n,l,y,F,y],l:[D,M,0,L.height,y,y,m,s,K,y,y],r:[D,M,L.width,L.height,j,L.right,n,s,y,y,N],b:[D,M,L.width,L.height,h,L.bottom,n,s,y,F,N],tl:[D,M,0,0,y,y,m,l,K,F,N],bl:[D,M,0,0,h,L.y+L.height,m,s,K,F,N],br:[D,M,0,0,a,[L.right,L.bottom],n,s,K,F,N],tr:[D,M,0,0,j,L.x+L.width,n,l,K,F,N]});M.visibility=u;p(D).show();arguments.callee.anim=p(D).fxanim(I,E,e,0.5,r,P)});return J},slideOut:function(F,D){D=x(D);var H=this,E=H.dom,K=E.style,L=H.getXY(),C,B,I,J,G={to:0};F=F||"t";H.queueFx(D,function(){B=p(E).getFxRestore();I={x:L[0],y:L[1],0:L[0],1:L[1],width:E.offsetWidth,height:E.offsetHeight};I.right=I.x+I.width;I.bottom=I.y+I.height;p(E).setWidth(I.width).setHeight(I.height);C=p(E).fxWrap(B.pos,D,u);K.visibility=u;K.position=z;p(C).setWidth(I.width).setHeight(I.height);function M(){D.useDisplay?p(E).setDisplayed(t):p(E).hide();p(E).fxUnwrap(C,B.pos,D);K.width=B.width;K.height=B.height;p(E).afterFx(D)}function N(O,W,U,X,S,V,R,T,Q){var P={};O[W]=O[U]="0";P[X]=S;if(V){P[V]=R}if(T){P[T]=Q}return P}J=p(E).switchStatements(F.toLowerCase(),N,{t:[K,n,l,q,G],l:[K,m,s,g,G],r:[K,n,s,g,G,i,{to:[I.right,I.y]}],b:[K,n,s,q,G,i,{to:[I.x,I.bottom]}],tl:[K,m,l,g,G,q,G],bl:[K,m,s,g,G,q,G,i,{to:[I.x,I.bottom]}],br:[K,n,s,g,G,q,G,i,{to:[I.x+I.width,I.bottom]}],tr:[K,n,l,g,G,q,G,i,{to:[I.right,I.y]}]});arguments.callee.anim=p(C).fxanim(J,D,e,0.5,r,M)});return H},puff:function(H){H=x(H);var F=this,G=F.dom,C=G.style,D,B,E;F.queueFx(H,function(){D=p(G).getWidth();B=p(G).getHeight();p(G).clearOpacity();p(G).show();E=p(G).getFxRestore();function I(){H.useDisplay?p(G).setDisplayed(t):p(G).hide();p(G).clearOpacity();p(G).setPositioning(E.pos);C.width=E.width;C.height=E.height;C.fontSize="";p(G).afterFx(H)}arguments.callee.anim=p(G).fxanim({width:{to:p(G).adjustWidth(D*2)},height:{to:p(G).adjustHeight(B*2)},points:{by:[-D*0.5,-B*0.5]},opacity:{to:0},fontSize:{to:200,unit:"%"}},H,e,0.5,r,I)});return F},switchOff:function(F){F=x(F);var D=this,E=D.dom,B=E.style,C;D.queueFx(F,function(){p(E).clearOpacity();p(E).clip();C=p(E).getFxRestore();function G(){F.useDisplay?p(E).setDisplayed(t):p(E).hide();p(E).clearOpacity();p(E).setPositioning(C.pos);B.width=C.width;B.height=C.height;p(E).afterFx(F)}p(E).fxanim({opacity:{to:0.3}},y,y,0.1,y,function(){p(E).clearOpacity();(function(){p(E).fxanim({height:{to:1},points:{by:[0,p(E).getHeight()*0.5]}},F,e,0.3,"easeIn",G)}).defer(100)})});return D},highlight:function(D,H){H=x(H);var F=this,G=F.dom,B=H.attr||"backgroundColor",C={},E;F.queueFx(H,function(){p(G).clearOpacity();p(G).show();function I(){G.style[B]=E;p(G).afterFx(H)}E=G.style[B];C[B]={from:D||"ffff9c",to:H.endColor||p(G).getColor(B)||"ffffff"};arguments.callee.anim=p(G).fxanim(C,H,"color",1,"easeIn",I)});return F},frame:function(B,E,H){H=x(H);var D=this,G=D.dom,C,F;D.queueFx(H,function(){B=B||"#C3DAF9";if(B.length==6){B="#"+B}E=E||1;p(G).show();var L=p(G).getXY(),J={x:L[0],y:L[1],0:L[0],1:L[1],width:G.offsetWidth,height:G.offsetHeight},I=function(){C=p(document.body||document.documentElement).createChild({style:{position:z,"z-index":35000,border:"0px solid "+B}});return C.queueFx({},K)};arguments.callee.anim={isAnimated:true,stop:function(){E=0;C.stopFx()}};function K(){var M=Ext.isBorderBox?2:1;F=C.anim({top:{from:J.y,to:J.y-20},left:{from:J.x,to:J.x-20},borderWidth:{from:0,to:10},opacity:{from:1,to:0},height:{from:J.height,to:J.height+20*M},width:{from:J.width,to:J.width+20*M}},{duration:H.duration||1,callback:function(){C.remove();--E>0?I():p(G).afterFx(H)}});arguments.callee.anim={isAnimated:true,stop:function(){F.stop()}}}I()});return D},pause:function(D){var C=this.dom,B;this.queueFx({},function(){B=setTimeout(function(){p(C).afterFx({})},D*1000);arguments.callee.anim={isAnimated:true,stop:function(){clearTimeout(B);p(C).afterFx({})}}});return this},fadeIn:function(D){D=x(D);var B=this,C=B.dom,E=D.endOpacity||1;B.queueFx(D,function(){p(C).setOpacity(0);p(C).fixDisplay();C.style.visibility=u;arguments.callee.anim=p(C).fxanim({opacity:{to:E}},D,y,0.5,r,function(){if(E==1){p(C).clearOpacity()}p(C).afterFx(D)})});return B},fadeOut:function(E){E=x(E);var C=this,D=C.dom,B=D.style,F=E.endOpacity||0;C.queueFx(E,function(){arguments.callee.anim=p(D).fxanim({opacity:{to:F}},E,y,0.5,r,function(){if(F==0){Ext.Element.data(D,"visibilityMode")==Ext.Element.DISPLAY||E.useDisplay?B.display="none":B.visibility=w;p(D).clearOpacity()}p(D).afterFx(E)})});return C},scale:function(B,C,D){this.shift(Ext.apply({},D,{width:B,height:C}));return this},shift:function(D){D=x(D);var C=this.dom,B={};this.queueFx(D,function(){for(var E in D){if(D[E]!=A){B[E]={to:D[E]}}}B.width?B.width.to=p(C).adjustWidth(D.width):B;B.height?B.height.to=p(C).adjustWidth(D.height):B;if(B.x||B.y||B.xy){B.points=B.xy||{to:[B.x?B.x.to:p(C).getX(),B.y?B.y.to:p(C).getY()]}}arguments.callee.anim=p(C).fxanim(B,D,e,0.35,r,function(){p(C).afterFx(D)})});return this},ghost:function(E,C){C=x(C);var G=this,D=G.dom,J=D.style,H={opacity:{to:0},points:{}},K=H.points,B,I,F;E=E||"b";G.queueFx(C,function(){B=p(D).getFxRestore();I=p(D).getWidth();F=p(D).getHeight();function L(){C.useDisplay?p(D).setDisplayed(t):p(D).hide();p(D).clearOpacity();p(D).setPositioning(B.pos);J.width=B.width;J.height=B.height;p(D).afterFx(C)}K.by=p(D).switchStatements(E.toLowerCase(),function(N,M){return[N,M]},{t:[0,-F],l:[-I,0],r:[I,0],b:[0,F],tl:[-I,-F],bl:[-I,F],br:[I,F],tr:[I,-F]});arguments.callee.anim=p(D).fxanim(H,C,e,0.5,r,L)});return G},syncFx:function(){var B=this;B.fxDefaults=Ext.apply(B.fxDefaults||{},{block:t,concurrent:k,stopFx:t});return B},sequenceFx:function(){var B=this;B.fxDefaults=Ext.apply(B.fxDefaults||{},{block:t,concurrent:t,stopFx:t});return B},nextFx:function(){var B=c(this.dom.id)[0];if(B){B.call(this)}},hasActiveFx:function(){return c(this.dom.id)[0]},stopFx:function(B){var C=this,E=C.dom.id;if(C.hasActiveFx()){var D=c(E)[0];if(D&&D.anim){if(D.anim.isAnimated){b(E,[D]);D.anim.stop(B!==undefined?B:k)}else{b(E,[])}}}return C},beforeFx:function(B){if(this.hasActiveFx()&&!B.concurrent){if(B.stopFx){this.stopFx();return k}return t}return k},hasFxBlock:function(){var B=c(this.dom.id);return B&&B[0]&&B[0].block},queueFx:function(E,B){var C=p(this.dom);if(!C.hasFxBlock()){Ext.applyIf(E,C.fxDefaults);if(!E.concurrent){var D=C.beforeFx(E);B.block=E.block;c(C.dom.id).push(B);if(D){C.nextFx()}}else{B.call(C)}}return C},fxWrap:function(H,F,D){var E=this.dom,C,B;if(!F.wrap||!(C=Ext.getDom(F.wrap))){if(F.fixPosition){B=p(E).getXY()}var G=document.createElement("div");G.style.visibility=D;C=E.parentNode.insertBefore(G,E);p(C).setPositioning(H);if(p(C).isStyle(o,"static")){p(C).position("relative")}p(E).clearPositioning("auto");p(C).clip();C.appendChild(E);if(B){p(C).setXY(B)}}return C},fxUnwrap:function(C,F,E){var D=this.dom;p(D).clearPositioning();p(D).setPositioning(F);if(!E.wrap){var B=p(C).dom.parentNode;B.insertBefore(D,C);p(C).remove()}},getFxRestore:function(){var B=this.dom.style;return{pos:this.getPositioning(),width:B.width,height:B.height}},afterFx:function(C){var B=this.dom,D=B.id;if(C.afterStyle){p(B).setStyle(C.afterStyle)}if(C.afterCls){p(B).addClass(C.afterCls)}if(C.remove==k){p(B).remove()}if(C.callback){C.callback.call(C.scope,p(B))}if(!C.concurrent){c(D).shift();p(B).nextFx()}},fxanim:function(E,F,C,G,D,B){C=C||"run";F=F||{};var H=Ext.lib.Anim[C](this.dom,E,(F.duration||G)||0.35,(F.easing||D)||r,B,this);F.anim=H;return H}};Ext.Fx.resize=Ext.Fx.scale;Ext.Element.addMethods(Ext.Fx)})();Ext.CompositeElementLite=function(b,a){this.elements=[];this.add(b,a);this.el=new Ext.Element.Flyweight()};Ext.CompositeElementLite.prototype={isComposite:true,getElement:function(a){var b=this.el;b.dom=a;b.id=a.id;return b},transformElement:function(a){return Ext.getDom(a)},getCount:function(){return this.elements.length},add:function(d,b){var e=this,g=e.elements;if(!d){return this}if(typeof d=="string"){d=Ext.Element.selectorFunction(d,b)}else{if(d.isComposite){d=d.elements}else{if(!Ext.isIterable(d)){d=[d]}}}for(var c=0,a=d.length;c-1){c=Ext.getDom(c);if(a){g=this.elements[b];g.parentNode.insertBefore(c,g);Ext.removeNode(g)}this.elements.splice(b,1,c)}return this},clear:function(){this.elements=[]}};Ext.CompositeElementLite.prototype.on=Ext.CompositeElementLite.prototype.addListener;Ext.CompositeElementLite.importElementMethods=function(){var c,b=Ext.Element.prototype,a=Ext.CompositeElementLite.prototype;for(c in b){if(typeof b[c]=="function"){(function(d){a[d]=a[d]||function(){return this.invoke(d,arguments)}}).call(a,c)}}};Ext.CompositeElementLite.importElementMethods();if(Ext.DomQuery){Ext.Element.selectorFunction=Ext.DomQuery.select}Ext.Element.select=function(a,b){var c;if(typeof a=="string"){c=Ext.Element.selectorFunction(a,b)}else{if(a.length!==undefined){c=a}else{throw"Invalid selector"}}return new Ext.CompositeElementLite(c)};Ext.select=Ext.Element.select;(function(){var b="beforerequest",e="requestcomplete",d="requestexception",h=undefined,c="load",i="POST",a="GET",g=window;Ext.data.Connection=function(j){Ext.apply(this,j);this.addEvents(b,e,d);Ext.data.Connection.superclass.constructor.call(this)};Ext.extend(Ext.data.Connection,Ext.util.Observable,{timeout:30000,autoAbort:false,disableCaching:true,disableCachingParam:"_dc",request:function(n){var s=this;if(s.fireEvent(b,s,n)){if(n.el){if(!Ext.isEmpty(n.indicatorText)){s.indicatorText='
        '+n.indicatorText+"
        "}if(s.indicatorText){Ext.getDom(n.el).innerHTML=s.indicatorText}n.success=(Ext.isFunction(n.success)?n.success:function(){}).createInterceptor(function(o){Ext.getDom(n.el).innerHTML=o.responseText})}var l=n.params,k=n.url||s.url,j,q={success:s.handleResponse,failure:s.handleFailure,scope:s,argument:{options:n},timeout:Ext.num(n.timeout,s.timeout)},m,t;if(Ext.isFunction(l)){l=l.call(n.scope||g,n)}l=Ext.urlEncode(s.extraParams,Ext.isObject(l)?Ext.urlEncode(l):l);if(Ext.isFunction(k)){k=k.call(n.scope||g,n)}if((m=Ext.getDom(n.form))){k=k||m.action;if(n.isUpload||(/multipart\/form-data/i.test(m.getAttribute("enctype")))){return s.doFormUpload.call(s,n,l,k)}t=Ext.lib.Ajax.serializeForm(m);l=l?(l+"&"+t):t}j=n.method||s.method||((l||n.xmlData||n.jsonData)?i:a);if(j===a&&(s.disableCaching&&n.disableCaching!==false)||n.disableCaching===true){var r=n.disableCachingParam||s.disableCachingParam;k=Ext.urlAppend(k,r+"="+(new Date().getTime()))}n.headers=Ext.applyIf(n.headers||{},s.defaultHeaders||{});if(n.autoAbort===true||s.autoAbort){s.abort()}if((j==a||n.xmlData||n.jsonData)&&l){k=Ext.urlAppend(k,l);l=""}return(s.transId=Ext.lib.Ajax.request(j,k,q,l,n))}else{return n.callback?n.callback.apply(n.scope,[n,h,h]):null}},isLoading:function(j){return j?Ext.lib.Ajax.isCallInProgress(j):!!this.transId},abort:function(j){if(j||this.isLoading()){Ext.lib.Ajax.abort(j||this.transId)}},handleResponse:function(j){this.transId=false;var k=j.argument.options;j.argument=k?k.argument:null;this.fireEvent(e,this,j,k);if(k.success){k.success.call(k.scope,j,k)}if(k.callback){k.callback.call(k.scope,k,true,j)}},handleFailure:function(j,l){this.transId=false;var k=j.argument.options;j.argument=k?k.argument:null;this.fireEvent(d,this,j,k,l);if(k.failure){k.failure.call(k.scope,j,k)}if(k.callback){k.callback.call(k.scope,k,false,j)}},doFormUpload:function(q,j,k){var l=Ext.id(),v=document,r=v.createElement("iframe"),m=Ext.getDom(q.form),u=[],t,p="multipart/form-data",n={target:m.target,method:m.method,encoding:m.encoding,enctype:m.enctype,action:m.action};Ext.fly(r).set({id:l,name:l,cls:"x-hidden",src:Ext.SSL_SECURE_URL});v.body.appendChild(r);if(Ext.isIE){document.frames[l].name=l}Ext.fly(m).set({target:l,method:i,enctype:p,encoding:p,action:k||n.action});Ext.iterate(Ext.urlDecode(j,false),function(w,o){t=v.createElement("input");Ext.fly(t).set({type:"hidden",value:o,name:w});m.appendChild(t);u.push(t)});function s(){var x=this,w={responseText:"",responseXML:null,argument:q.argument},A,z;try{A=r.contentWindow.document||r.contentDocument||g.frames[l].document;if(A){if(A.body){if(/textarea/i.test((z=A.body.firstChild||{}).tagName)){w.responseText=z.value}else{w.responseText=A.body.innerHTML}}w.responseXML=A.XMLDocument||A}}catch(y){}Ext.EventManager.removeListener(r,c,s,x);x.fireEvent(e,x,w,q);function o(D,C,B){if(Ext.isFunction(D)){D.apply(C,B)}}o(q.success,q.scope,[w,q]);o(q.callback,q.scope,[q,true,w]);if(!x.debugUploads){setTimeout(function(){Ext.removeNode(r)},100)}}Ext.EventManager.on(r,c,s,this);m.submit();Ext.fly(m).set(n);Ext.each(u,function(o){Ext.removeNode(o)})}})})();Ext.Ajax=new Ext.data.Connection({autoAbort:false,serializeForm:function(a){return Ext.lib.Ajax.serializeForm(a)}});Ext.util.JSON=new (function(){var useHasOwn=!!{}.hasOwnProperty,isNative=function(){var useNative=null;return function(){if(useNative===null){useNative=Ext.USE_NATIVE_JSON&&window.JSON&&JSON.toString()=="[object JSON]"}return useNative}}(),pad=function(n){return n<10?"0"+n:n},doDecode=function(json){return json?eval("("+json+")"):""},doEncode=function(o){if(!Ext.isDefined(o)||o===null){return"null"}else{if(Ext.isArray(o)){return encodeArray(o)}else{if(Ext.isDate(o)){return Ext.util.JSON.encodeDate(o)}else{if(Ext.isString(o)){return encodeString(o)}else{if(typeof o=="number"){return isFinite(o)?String(o):"null"}else{if(Ext.isBoolean(o)){return String(o)}else{var a=["{"],b,i,v;for(i in o){if(!o.getElementsByTagName){if(!useHasOwn||o.hasOwnProperty(i)){v=o[i];switch(typeof v){case"undefined":case"function":case"unknown":break;default:if(b){a.push(",")}a.push(doEncode(i),":",v===null?"null":doEncode(v));b=true}}}}a.push("}");return a.join("")}}}}}}},m={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},encodeString=function(s){if(/["\\\x00-\x1f]/.test(s)){return'"'+s.replace(/([\x00-\x1f\\"])/g,function(a,b){var c=m[b];if(c){return c}c=b.charCodeAt();return"\\u00"+Math.floor(c/16).toString(16)+(c%16).toString(16)})+'"'}return'"'+s+'"'},encodeArray=function(o){var a=["["],b,i,l=o.length,v;for(i=0;i
        ';e.body.appendChild(g);d=g.lastChild;if((c=e.defaultView)){if(c.getComputedStyle(g.firstChild.firstChild,null).marginRight!="0px"){b.correctRightMargin=false}if(c.getComputedStyle(d,null).backgroundColor!="transparent"){b.correctTransparentColor=false}}b.cssFloat=!!d.style.cssFloat;e.body.removeChild(g)};if(Ext.isReady){a()}else{Ext.onReady(a)}})();Ext.EventObject=function(){var b=Ext.lib.Event,c=/(dbl)?click/,a={3:13,63234:37,63235:39,63232:38,63233:40,63276:33,63277:34,63272:46,63273:36,63275:35},d=Ext.isIE?{1:0,4:1,2:2}:{0:0,1:1,2:2};Ext.EventObjectImpl=function(g){if(g){this.setEvent(g.browserEvent||g)}};Ext.EventObjectImpl.prototype={setEvent:function(h){var g=this;if(h==g||(h&&h.browserEvent)){return h}g.browserEvent=h;if(h){g.button=h.button?d[h.button]:(h.which?h.which-1:-1);if(c.test(h.type)&&g.button==-1){g.button=0}g.type=h.type;g.shiftKey=h.shiftKey;g.ctrlKey=h.ctrlKey||h.metaKey||false;g.altKey=h.altKey;g.keyCode=h.keyCode;g.charCode=h.charCode;g.target=b.getTarget(h);g.xy=b.getXY(h)}else{g.button=-1;g.shiftKey=false;g.ctrlKey=false;g.altKey=false;g.keyCode=0;g.charCode=0;g.target=null;g.xy=[0,0]}return g},stopEvent:function(){var e=this;if(e.browserEvent){if(e.browserEvent.type=="mousedown"){Ext.EventManager.stoppedMouseDownEvent.fire(e)}b.stopEvent(e.browserEvent)}},preventDefault:function(){if(this.browserEvent){b.preventDefault(this.browserEvent)}},stopPropagation:function(){var e=this;if(e.browserEvent){if(e.browserEvent.type=="mousedown"){Ext.EventManager.stoppedMouseDownEvent.fire(e)}b.stopPropagation(e.browserEvent)}},getCharCode:function(){return this.charCode||this.keyCode},getKey:function(){return this.normalizeKey(this.keyCode||this.charCode)},normalizeKey:function(e){return Ext.isSafari?(a[e]||e):e},getPageX:function(){return this.xy[0]},getPageY:function(){return this.xy[1]},getXY:function(){return this.xy},getTarget:function(g,h,e){return g?Ext.fly(this.target).findParent(g,h,e):(e?Ext.get(this.target):this.target)},getRelatedTarget:function(){return this.browserEvent?b.getRelatedTarget(this.browserEvent):null},getWheelDelta:function(){var g=this.browserEvent;var h=0;if(g.wheelDelta){h=g.wheelDelta/120}else{if(g.detail){h=-g.detail/3}}return h},within:function(h,i,e){if(h){var g=this[i?"getRelatedTarget":"getTarget"]();return g&&((e?(g==Ext.getDom(h)):false)||Ext.fly(h).contains(g))}return false}};return new Ext.EventObjectImpl()}();Ext.Loader=Ext.apply({},{load:function(j,i,k,c){var k=k||this,g=document.getElementsByTagName("head")[0],b=document.createDocumentFragment(),a=j.length,h=0,e=this;var l=function(m){g.appendChild(e.buildScriptTag(j[m],d))};var d=function(){h++;if(a==h&&typeof i=="function"){i.call(k)}else{if(c===true){l(h)}}};if(c===true){l.call(this,0)}else{Ext.each(j,function(n,m){b.appendChild(this.buildScriptTag(n,d))},this);g.appendChild(b)}},buildScriptTag:function(b,c){var a=document.createElement("script");a.type="text/javascript";a.src=b;if(a.readyState){a.onreadystatechange=function(){if(a.readyState=="loaded"||a.readyState=="complete"){a.onreadystatechange=null;c()}}}else{a.onload=c}return a}});Ext.ns("Ext.grid","Ext.list","Ext.dd","Ext.tree","Ext.form","Ext.menu","Ext.state","Ext.layout.boxOverflow","Ext.app","Ext.ux","Ext.chart","Ext.direct","Ext.slider");Ext.apply(Ext,function(){var c=Ext,a=0,b=null;return{emptyFn:function(){},BLANK_IMAGE_URL:Ext.isIE6||Ext.isIE7||Ext.isAir?"http://www.extjs.com/s.gif":"data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==",extendX:function(d,e){return Ext.extend(d,e(d.prototype))},getDoc:function(){return Ext.get(document)},num:function(e,d){e=Number(Ext.isEmpty(e)||Ext.isArray(e)||typeof e=="boolean"||(typeof e=="string"&&e.trim().length==0)?NaN:e);return isNaN(e)?d:e},value:function(g,d,e){return Ext.isEmpty(g,e)?d:g},escapeRe:function(d){return d.replace(/([-.*+?^${}()|[\]\/\\])/g,"\\$1")},sequence:function(h,d,g,e){h[d]=h[d].createSequence(g,e)},addBehaviors:function(i){if(!Ext.isReady){Ext.onReady(function(){Ext.addBehaviors(i)})}else{var e={},h,d,g;for(d in i){if((h=d.split("@"))[1]){g=h[0];if(!e[g]){e[g]=Ext.select(g)}e[g].on(h[1],i[d])}}e=null}},getScrollBarWidth:function(g){if(!Ext.isReady){return 0}if(g===true||b===null){var i=Ext.getBody().createChild('
        '),h=i.child("div",true);var e=h.offsetWidth;i.setStyle("overflow",(Ext.isWebKit||Ext.isGecko)?"auto":"scroll");var d=h.offsetWidth;i.remove();b=e-d+2}return b},combine:function(){var g=arguments,e=g.length,j=[];for(var h=0;hh?1:-1};Ext.each(d,function(h){g=e(g,h)==1?g:h});return g},mean:function(d){return d.length>0?Ext.sum(d)/d.length:undefined},sum:function(d){var e=0;Ext.each(d,function(g){e+=g});return e},partition:function(d,e){var g=[[],[]];Ext.each(d,function(j,k,h){g[(e&&e(j,k,h))||(!e&&j)?0:1].push(j)});return g},invoke:function(d,e){var h=[],g=Array.prototype.slice.call(arguments,2);Ext.each(d,function(j,k){if(j&&typeof j[e]=="function"){h.push(j[e].apply(j,g))}else{h.push(undefined)}});return h},pluck:function(d,g){var e=[];Ext.each(d,function(h){e.push(h[g])});return e},zip:function(){var n=Ext.partition(arguments,function(i){return typeof i!="function"}),k=n[0],m=n[1][0],d=Ext.max(Ext.pluck(k,"length")),h=[];for(var l=0;l=a.left&&b.right<=a.right&&b.top>=a.top&&b.bottom<=a.bottom)},getArea:function(){var a=this;return((a.bottom-a.top)*(a.right-a.left))},intersect:function(h){var g=this,d=Math.max(g.top,h.top),e=Math.min(g.right,h.right),a=Math.min(g.bottom,h.bottom),c=Math.max(g.left,h.left);if(a>=d&&e>=c){return new Ext.lib.Region(d,e,a,c)}},union:function(h){var g=this,d=Math.min(g.top,h.top),e=Math.max(g.right,h.right),a=Math.max(g.bottom,h.bottom),c=Math.min(g.left,h.left);return new Ext.lib.Region(d,e,a,c)},constrainTo:function(b){var a=this;a.top=a.top.constrain(b.top,b.bottom);a.bottom=a.bottom.constrain(b.top,b.bottom);a.left=a.left.constrain(b.left,b.right);a.right=a.right.constrain(b.left,b.right);return a},adjust:function(d,c,a,g){var e=this;e.top+=d;e.left+=c;e.right+=g;e.bottom+=a;return e}};Ext.lib.Region.getRegion=function(e){var h=Ext.lib.Dom.getXY(e),d=h[1],g=h[0]+e.offsetWidth,a=h[1]+e.offsetHeight,c=h[0];return new Ext.lib.Region(d,g,a,c)};Ext.lib.Point=function(a,c){if(Ext.isArray(a)){c=a[1];a=a[0]}var b=this;b.x=b.right=b.left=b[0]=a;b.y=b.top=b.bottom=b[1]=c};Ext.lib.Point.prototype=new Ext.lib.Region();Ext.apply(Ext.DomHelper,function(){var e,a="afterbegin",h="afterend",i="beforebegin",d="beforeend",b=/tag|children|cn|html$/i;function g(m,p,n,q,l,j){m=Ext.getDom(m);var k;if(e.useDom){k=c(p,null);if(j){m.appendChild(k)}else{(l=="firstChild"?m:m.parentNode).insertBefore(k,m[l]||m)}}else{k=Ext.DomHelper.insertHtml(q,m,Ext.DomHelper.createHtml(p))}return n?Ext.get(k,true):k}function c(j,r){var k,u=document,p,s,m,t;if(Ext.isArray(j)){k=u.createDocumentFragment();for(var q=0,n=j.length;q0){return setTimeout(d,c)}d();return 0},createSequence:function(c,b,a){if(!Ext.isFunction(b)){return c}else{return function(){var d=c.apply(this||window,arguments);b.apply(a||this||window,arguments);return d}}}};Ext.defer=Ext.util.Functions.defer;Ext.createInterceptor=Ext.util.Functions.createInterceptor;Ext.createSequence=Ext.util.Functions.createSequence;Ext.createDelegate=Ext.util.Functions.createDelegate;Ext.apply(Ext.util.Observable.prototype,function(){function a(j){var i=(this.methodEvents=this.methodEvents||{})[j],d,c,g,h=this;if(!i){this.methodEvents[j]=i={};i.originalFn=this[j];i.methodName=j;i.before=[];i.after=[];var b=function(l,k,e){if((c=l.apply(k||h,e))!==undefined){if(typeof c=="object"){if(c.returnValue!==undefined){d=c.returnValue}else{d=c}g=!!c.cancel}else{if(c===false){g=true}else{d=c}}}};this[j]=function(){var l=Array.prototype.slice.call(arguments,0),k;d=c=undefined;g=false;for(var m=0,e=i.before.length;m=525:!((Ext.isGecko&&!Ext.isWindows)||Ext.isOpera);return{_unload:function(){Ext.EventManager.un(window,"resize",this.fireWindowResize,this);c.call(Ext.EventManager)},doResizeEvent:function(){var m=a.getViewHeight(),l=a.getViewWidth();if(h!=m||i!=l){d.fire(i=l,h=m)}},onWindowResize:function(n,m,l){if(!d){d=new Ext.util.Event();k=new Ext.util.DelayedTask(this.doResizeEvent);Ext.EventManager.on(window,"resize",this.fireWindowResize,this)}d.addListener(n,m,l)},fireWindowResize:function(){if(d){k.delay(100)}},onTextResize:function(o,n,l){if(!g){g=new Ext.util.Event();var m=new Ext.Element(document.createElement("div"));m.dom.className="x-text-resize";m.dom.innerHTML="X";m.appendTo(document.body);b=m.dom.offsetHeight;setInterval(function(){if(m.dom.offsetHeight!=b){g.fire(b,b=m.dom.offsetHeight)}},this.textResizeInterval)}g.addListener(o,n,l)},removeResizeListener:function(m,l){if(d){d.removeListener(m,l)}},fireResize:function(){if(d){d.fire(a.getViewWidth(),a.getViewHeight())}},textResizeInterval:50,ieDeferSrc:false,getKeyEvent:function(){return e?"keydown":"keypress"},useKeydown:e}}());Ext.EventManager.on=Ext.EventManager.addListener;Ext.apply(Ext.EventObjectImpl.prototype,{BACKSPACE:8,TAB:9,NUM_CENTER:12,ENTER:13,RETURN:13,SHIFT:16,CTRL:17,CONTROL:17,ALT:18,PAUSE:19,CAPS_LOCK:20,ESC:27,SPACE:32,PAGE_UP:33,PAGEUP:33,PAGE_DOWN:34,PAGEDOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,PRINT_SCREEN:44,INSERT:45,DELETE:46,ZERO:48,ONE:49,TWO:50,THREE:51,FOUR:52,FIVE:53,SIX:54,SEVEN:55,EIGHT:56,NINE:57,A:65,B:66,C:67,D:68,E:69,F:70,G:71,H:72,I:73,J:74,K:75,L:76,M:77,N:78,O:79,P:80,Q:81,R:82,S:83,T:84,U:85,V:86,W:87,X:88,Y:89,Z:90,CONTEXT_MENU:93,NUM_ZERO:96,NUM_ONE:97,NUM_TWO:98,NUM_THREE:99,NUM_FOUR:100,NUM_FIVE:101,NUM_SIX:102,NUM_SEVEN:103,NUM_EIGHT:104,NUM_NINE:105,NUM_MULTIPLY:106,NUM_PLUS:107,NUM_MINUS:109,NUM_PERIOD:110,NUM_DIVISION:111,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,isNavKeyPress:function(){var b=this,a=this.normalizeKey(b.keyCode);return(a>=33&&a<=40)||a==b.RETURN||a==b.TAB||a==b.ESC},isSpecialKey:function(){var a=this.normalizeKey(this.keyCode);return(this.type=="keypress"&&this.ctrlKey)||this.isNavKeyPress()||(a==this.BACKSPACE)||(a>=16&&a<=20)||(a>=44&&a<=46)},getPoint:function(){return new Ext.lib.Point(this.xy[0],this.xy[1])},hasModifier:function(){return((this.ctrlKey||this.altKey)||this.shiftKey)}});Ext.Element.addMethods({swallowEvent:function(a,b){var d=this;function c(g){g.stopPropagation();if(b){g.preventDefault()}}if(Ext.isArray(a)){Ext.each(a,function(g){d.on(g,c)});return d}d.on(a,c);return d},relayEvent:function(a,b){this.on(a,function(c){b.fireEvent(a,c)})},clean:function(b){var d=this,e=d.dom,g=e.firstChild,c=-1;if(Ext.Element.data(e,"isCleaned")&&b!==true){return d}while(g){var a=g.nextSibling;if(g.nodeType==3&&!(/\S/.test(g.nodeValue))){e.removeChild(g)}else{g.nodeIndex=++c}g=a}Ext.Element.data(e,"isCleaned",true);return d},load:function(){var a=this.getUpdater();a.update.apply(a,arguments);return this},getUpdater:function(){return this.updateManager||(this.updateManager=new Ext.Updater(this))},update:function(html,loadScripts,callback){if(!this.dom){return this}html=html||"";if(loadScripts!==true){this.dom.innerHTML=html;if(typeof callback=="function"){callback()}return this}var id=Ext.id(),dom=this.dom;html+='';Ext.lib.Event.onAvailable(id,function(){var DOC=document,hd=DOC.getElementsByTagName("head")[0],re=/(?:]*)?>)((\n|\r|.)*?)(?:<\/script>)/ig,srcRe=/\ssrc=([\'\"])(.*?)\1/i,typeRe=/\stype=([\'\"])(.*?)\1/i,match,attrs,srcMatch,typeMatch,el,s;while((match=re.exec(html))){attrs=match[1];srcMatch=attrs?attrs.match(srcRe):false;if(srcMatch&&srcMatch[2]){s=DOC.createElement("script");s.src=srcMatch[2];typeMatch=attrs.match(typeRe);if(typeMatch&&typeMatch[2]){s.type=typeMatch[2]}hd.appendChild(s)}else{if(match[2]&&match[2].length>0){if(window.execScript){window.execScript(match[2])}else{window.eval(match[2])}}}}el=DOC.getElementById(id);if(el){Ext.removeNode(el)}if(typeof callback=="function"){callback()}});dom.innerHTML=html.replace(/(?:)((\n|\r|.)*?)(?:<\/script>)/ig,"");return this},removeAllListeners:function(){this.removeAnchor();Ext.EventManager.removeAll(this.dom);return this},createProxy:function(a,e,d){a=(typeof a=="object")?a:{tag:"div",cls:a};var c=this,b=e?Ext.DomHelper.append(e,a,true):Ext.DomHelper.insertBefore(c.dom,a,true);if(d&&c.setBox&&c.getBox){b.setBox(c.getBox())}return b}});Ext.Element.prototype.getUpdateManager=Ext.Element.prototype.getUpdater;Ext.Element.addMethods({getAnchorXY:function(e,l,q){e=(e||"tl").toLowerCase();q=q||{};var k=this,b=k.dom==document.body||k.dom==document,n=q.width||b?Ext.lib.Dom.getViewWidth():k.getWidth(),i=q.height||b?Ext.lib.Dom.getViewHeight():k.getHeight(),p,a=Math.round,c=k.getXY(),m=k.getScroll(),j=b?m.left:!l?c[0]:0,g=b?m.top:!l?c[1]:0,d={c:[a(n*0.5),a(i*0.5)],t:[a(n*0.5),0],l:[0,a(i*0.5)],r:[n,a(i*0.5)],b:[a(n*0.5),i],tl:[0,0],bl:[0,i],br:[n,i],tr:[n,0]};p=d[e];return[p[0]+j,p[1]+g]},anchorTo:function(b,h,c,a,k,l){var i=this,e=i.dom,j=!Ext.isEmpty(k),d=function(){Ext.fly(e).alignTo(b,h,c,a);Ext.callback(l,Ext.fly(e))},g=this.getAnchor();this.removeAnchor();Ext.apply(g,{fn:d,scroll:j});Ext.EventManager.onWindowResize(d,null);if(j){Ext.EventManager.on(window,"scroll",d,null,{buffer:!isNaN(k)?k:50})}d.call(i);return i},removeAnchor:function(){var b=this,a=this.getAnchor();if(a&&a.fn){Ext.EventManager.removeResizeListener(a.fn);if(a.scroll){Ext.EventManager.un(window,"scroll",a.fn)}delete a.fn}return b},getAnchor:function(){var b=Ext.Element.data,c=this.dom;if(!c){return}var a=b(c,"_anchor");if(!a){a=b(c,"_anchor",{})}return a},getAlignToXY:function(g,A,B){g=Ext.get(g);if(!g||!g.dom){throw"Element.alignToXY with an element that doesn't exist"}B=B||[0,0];A=(!A||A=="?"?"tl-bl?":(!(/-/).test(A)&&A!==""?"tl-"+A:A||"tl-bl")).toLowerCase();var K=this,H=K.dom,M,L,n,l,s,F,v,t=Ext.lib.Dom.getViewWidth()-10,G=Ext.lib.Dom.getViewHeight()-10,b,i,j,k,u,z,N=document,J=N.documentElement,q=N.body,E=(J.scrollLeft||q.scrollLeft||0)+5,D=(J.scrollTop||q.scrollTop||0)+5,I=false,e="",a="",C=A.match(/^([a-z]+)-([a-z]+)(\?)?$/);if(!C){throw"Element.alignTo with an invalid alignment "+A}e=C[1];a=C[2];I=!!C[3];M=K.getAnchorXY(e,true);L=g.getAnchorXY(a,false);n=L[0]-M[0]+B[0];l=L[1]-M[1]+B[1];if(I){s=K.getWidth();F=K.getHeight();v=g.getRegion();b=e.charAt(0);i=e.charAt(e.length-1);j=a.charAt(0);k=a.charAt(a.length-1);u=((b=="t"&&j=="b")||(b=="b"&&j=="t"));z=((i=="r"&&k=="l")||(i=="l"&&k=="r"));if(n+s>t+E){n=z?v.left-s:t+E-s}if(nG+D){l=u?v.top-F:G+D-F}if(lB){p=B-q;m=true}if((o+C)>g){o=g-C;m=true}if(p"+String.format(Ext.Element.boxMarkup,c)+"
        "));Ext.DomQuery.selectNode("."+c+"-mc",d.dom).appendChild(this.dom);return d},setSize:function(e,c,d){var g=this;if(typeof e=="object"){c=e.height;e=e.width}e=g.adjustWidth(e);c=g.adjustHeight(c);if(!d||!g.anim){g.dom.style.width=g.addUnits(e);g.dom.style.height=g.addUnits(c)}else{g.anim({width:{to:e},height:{to:c}},g.preanim(arguments,2))}return g},getComputedHeight:function(){var d=this,c=Math.max(d.dom.offsetHeight,d.dom.clientHeight);if(!c){c=parseFloat(d.getStyle("height"))||0;if(!d.isBorderBox()){c+=d.getFrameWidth("tb")}}return c},getComputedWidth:function(){var c=Math.max(this.dom.offsetWidth,this.dom.clientWidth);if(!c){c=parseFloat(this.getStyle("width"))||0;if(!this.isBorderBox()){c+=this.getFrameWidth("lr")}}return c},getFrameWidth:function(d,c){return c&&this.isBorderBox()?0:(this.getPadding(d)+this.getBorderWidth(d))},addClassOnOver:function(c){this.hover(function(){Ext.fly(this,a).addClass(c)},function(){Ext.fly(this,a).removeClass(c)});return this},addClassOnFocus:function(c){this.on("focus",function(){Ext.fly(this,a).addClass(c)},this.dom);this.on("blur",function(){Ext.fly(this,a).removeClass(c)},this.dom);return this},addClassOnClick:function(c){var d=this.dom;this.on("mousedown",function(){Ext.fly(d,a).addClass(c);var g=Ext.getDoc(),e=function(){Ext.fly(d,a).removeClass(c);g.removeListener("mouseup",e)};g.on("mouseup",e)});return this},getViewSize:function(){var g=document,h=this.dom,c=(h==g||h==g.body);if(c){var e=Ext.lib.Dom;return{width:e.getViewWidth(),height:e.getViewHeight()}}else{return{width:h.clientWidth,height:h.clientHeight}}},getStyleSize:function(){var j=this,c,i,l=document,m=this.dom,e=(m==l||m==l.body),g=m.style;if(e){var k=Ext.lib.Dom;return{width:k.getViewWidth(),height:k.getViewHeight()}}if(g.width&&g.width!="auto"){c=parseFloat(g.width);if(j.isBorderBox()){c-=j.getFrameWidth("lr")}}if(g.height&&g.height!="auto"){i=parseFloat(g.height);if(j.isBorderBox()){i-=j.getFrameWidth("tb")}}return{width:c||j.getWidth(true),height:i||j.getHeight(true)}},getSize:function(c){return{width:this.getWidth(c),height:this.getHeight(c)}},repaint:function(){var c=this.dom;this.addClass("x-repaint");setTimeout(function(){Ext.fly(c).removeClass("x-repaint")},1);return this},unselectable:function(){this.dom.unselectable="on";return this.swallowEvent("selectstart",true).applyStyles("-moz-user-select:none;-khtml-user-select:none;").addClass("x-unselectable")},getMargins:function(d){var e=this,c,g={t:"top",l:"left",r:"right",b:"bottom"},h={};if(!d){for(c in e.margins){h[g[c]]=parseFloat(e.getStyle(e.margins[c]))||0}return h}else{return e.addStyles.call(e,d,e.margins)}}}}());Ext.Element.addMethods({setBox:function(e,g,b){var d=this,a=e.width,c=e.height;if((g&&!d.autoBoxAdjust)&&!d.isBorderBox()){a-=(d.getBorderWidth("lr")+d.getPadding("lr"));c-=(d.getBorderWidth("tb")+d.getPadding("tb"))}d.setBounds(e.x,e.y,a,c,d.animTest.call(d,arguments,b,2));return d},getBox:function(j,p){var m=this,v,e,o,d=m.getBorderWidth,q=m.getPadding,g,a,u,n;if(!p){v=m.getXY()}else{e=parseInt(m.getStyle("left"),10)||0;o=parseInt(m.getStyle("top"),10)||0;v=[e,o]}var c=m.dom,s=c.offsetWidth,i=c.offsetHeight,k;if(!j){k={x:v[0],y:v[1],0:v[0],1:v[1],width:s,height:i}}else{g=d.call(m,"l")+q.call(m,"l");a=d.call(m,"r")+q.call(m,"r");u=d.call(m,"t")+q.call(m,"t");n=d.call(m,"b")+q.call(m,"b");k={x:v[0]+g,y:v[1]+u,0:v[0]+g,1:v[1]+u,width:s-(g+a),height:i-(u+n)}}k.right=k.x+k.width;k.bottom=k.y+k.height;return k},move:function(j,b,c){var g=this,m=g.getXY(),k=m[0],i=m[1],d=[k-b,i],l=[k+b,i],h=[k,i-b],a=[k,i+b],e={l:d,left:d,r:l,right:l,t:h,top:h,up:h,b:a,bottom:a,down:a};j=j.toLowerCase();g.moveTo(e[j][0],e[j][1],g.animTest.call(g,arguments,c,2))},setLeftTop:function(d,c){var b=this,a=b.dom.style;a.left=b.addUnits(d);a.top=b.addUnits(c);return b},getRegion:function(){return Ext.lib.Dom.getRegion(this.dom)},setBounds:function(b,g,d,a,c){var e=this;if(!c||!e.anim){e.setSize(d,a);e.setLocation(b,g)}else{e.anim({points:{to:[b,g]},width:{to:e.adjustWidth(d)},height:{to:e.adjustHeight(a)}},e.preanim(arguments,4),"motion")}return e},setRegion:function(b,a){return this.setBounds(b.left,b.top,b.right-b.left,b.bottom-b.top,this.animTest.call(this,arguments,a,1))}});Ext.Element.addMethods({scrollTo:function(b,d,a){var e=/top/i.test(b),c=this,g=c.dom,h;if(!a||!c.anim){h="scroll"+(e?"Top":"Left");g[h]=d}else{h="scroll"+(e?"Left":"Top");c.anim({scroll:{to:e?[g[h],d]:[d,g[h]]}},c.preanim(arguments,2),"scroll")}return c},scrollIntoView:function(e,i){var p=Ext.getDom(e)||Ext.getBody().dom,h=this.dom,g=this.getOffsetsTo(p),k=g[0]+p.scrollLeft,u=g[1]+p.scrollTop,q=u+h.offsetHeight,d=k+h.offsetWidth,a=p.clientHeight,m=parseInt(p.scrollTop,10),s=parseInt(p.scrollLeft,10),j=m+a,n=s+p.clientWidth;if(h.offsetHeight>a||uj){p.scrollTop=q-a}}p.scrollTop=p.scrollTop;if(i!==false){if(h.offsetWidth>p.clientWidth||kn){p.scrollLeft=d-p.clientWidth}}p.scrollLeft=p.scrollLeft}return this},scrollChildIntoView:function(b,a){Ext.fly(b,"_scrollChildIntoView").scrollIntoView(this,a)},scroll:function(m,b,d){if(!this.isScrollable()){return false}var e=this.dom,g=e.scrollLeft,p=e.scrollTop,n=e.scrollWidth,k=e.scrollHeight,i=e.clientWidth,a=e.clientHeight,c=false,o,j={l:Math.min(g+b,n-i),r:o=Math.max(g-b,0),t:Math.max(p-b,0),b:Math.min(p+b,k-a)};j.d=j.b;j.u=j.t;m=m.substr(0,1);if((o=j[m])>-1){c=true;this.scrollTo(m=="l"||m=="r"?"left":"top",o,this.preanim(arguments,2))}return c}});Ext.Element.addMethods(function(){var d="visibility",b="display",a="hidden",h="none",c="x-masked",g="x-masked-relative",e=Ext.Element.data;return{isVisible:function(i){var j=!this.isStyle(d,a)&&!this.isStyle(b,h),k=this.dom.parentNode;if(i!==true||!j){return j}while(k&&!(/^body/i.test(k.tagName))){if(!Ext.fly(k,"_isVisible").isVisible()){return false}k=k.parentNode}return true},isDisplayed:function(){return !this.isStyle(b,h)},enableDisplayMode:function(i){this.setVisibilityMode(Ext.Element.DISPLAY);if(!Ext.isEmpty(i)){e(this.dom,"originalDisplay",i)}return this},mask:function(j,n){var p=this,l=p.dom,o=Ext.DomHelper,m="ext-el-mask-msg",i,q;if(!/^body/i.test(l.tagName)&&p.getStyle("position")=="static"){p.addClass(g)}if(i=e(l,"maskMsg")){i.remove()}if(i=e(l,"mask")){i.remove()}q=o.append(l,{cls:"ext-el-mask"},true);e(l,"mask",q);p.addClass(c);q.setDisplayed(true);if(typeof j=="string"){var k=o.append(l,{cls:m,cn:{tag:"div"}},true);e(l,"maskMsg",k);k.dom.className=n?m+" "+n:m;k.dom.firstChild.innerHTML=j;k.setDisplayed(true);k.center(p)}if(Ext.isIE&&!(Ext.isIE7&&Ext.isStrict)&&p.getStyle("height")=="auto"){q.setSize(undefined,p.getHeight())}return q},unmask:function(){var k=this,l=k.dom,i=e(l,"mask"),j=e(l,"maskMsg");if(i){if(j){j.remove();e(l,"maskMsg",undefined)}i.remove();e(l,"mask",undefined);k.removeClass([c,g])}},isMasked:function(){var i=e(this.dom,"mask");return i&&i.isVisible()},createShim:function(){var i=document.createElement("iframe"),j;i.frameBorder="0";i.className="ext-shim";i.src=Ext.SSL_SECURE_URL;j=Ext.get(this.dom.parentNode.insertBefore(i,this.dom));j.autoBoxAdjust=false;return j}}}());Ext.Element.addMethods({addKeyListener:function(b,d,c){var a;if(typeof b!="object"||Ext.isArray(b)){a={key:b,fn:d,scope:c}}else{a={key:b.key,shift:b.shift,ctrl:b.ctrl,alt:b.alt,fn:d,scope:c}}return new Ext.KeyMap(this,a)},addKeyMap:function(a){return new Ext.KeyMap(this,a)}});Ext.CompositeElementLite.importElementMethods();Ext.apply(Ext.CompositeElementLite.prototype,{addElements:function(c,a){if(!c){return this}if(typeof c=="string"){c=Ext.Element.selectorFunction(c,a)}var b=this.elements;Ext.each(c,function(d){b.push(Ext.get(d))});return this},first:function(){return this.item(0)},last:function(){return this.item(this.getCount()-1)},contains:function(a){return this.indexOf(a)!=-1},removeElement:function(d,e){var c=this,a=this.elements,b;Ext.each(d,function(g){if((b=(a[g]||a[g=c.indexOf(g)]))){if(e){if(b.dom){b.remove()}else{Ext.removeNode(b)}}a.splice(g,1)}});return this}});Ext.CompositeElement=Ext.extend(Ext.CompositeElementLite,{constructor:function(b,a){this.elements=[];this.add(b,a)},getElement:function(a){return a},transformElement:function(a){return Ext.get(a)}});Ext.Element.select=function(a,d,b){var c;if(typeof a=="string"){c=Ext.Element.selectorFunction(a,b)}else{if(a.length!==undefined){c=a}else{throw"Invalid selector"}}return(d===true)?new Ext.CompositeElement(c):new Ext.CompositeElementLite(c)};Ext.select=Ext.Element.select;Ext.UpdateManager=Ext.Updater=Ext.extend(Ext.util.Observable,function(){var b="beforeupdate",d="update",c="failure";function a(h){var i=this;i.transaction=null;if(h.argument.form&&h.argument.reset){try{h.argument.form.reset()}catch(j){}}if(i.loadScripts){i.renderer.render(i.el,h,i,g.createDelegate(i,[h]))}else{i.renderer.render(i.el,h,i);g.call(i,h)}}function g(h,i,j){this.fireEvent(i||d,this.el,h);if(Ext.isFunction(h.argument.callback)){h.argument.callback.call(h.argument.scope,this.el,Ext.isEmpty(j)?true:false,h,h.argument.options)}}function e(h){g.call(this,h,c,!!(this.transaction=null))}return{constructor:function(i,h){var j=this;i=Ext.get(i);if(!h&&i.updateManager){return i.updateManager}j.el=i;j.defaultUrl=null;j.addEvents(b,d,c);Ext.apply(j,Ext.Updater.defaults);j.transaction=null;j.refreshDelegate=j.refresh.createDelegate(j);j.updateDelegate=j.update.createDelegate(j);j.formUpdateDelegate=(j.formUpdate||function(){}).createDelegate(j);j.renderer=j.renderer||j.getDefaultRenderer();Ext.Updater.superclass.constructor.call(j)},setRenderer:function(h){this.renderer=h},getRenderer:function(){return this.renderer},getDefaultRenderer:function(){return new Ext.Updater.BasicRenderer()},setDefaultUrl:function(h){this.defaultUrl=h},getEl:function(){return this.el},update:function(i,n,p,l){var k=this,h,j;if(k.fireEvent(b,k.el,i,n)!==false){if(Ext.isObject(i)){h=i;i=h.url;n=n||h.params;p=p||h.callback;l=l||h.discardUrl;j=h.scope;if(!Ext.isEmpty(h.nocache)){k.disableCaching=h.nocache}if(!Ext.isEmpty(h.text)){k.indicatorText='
        '+h.text+"
        "}if(!Ext.isEmpty(h.scripts)){k.loadScripts=h.scripts}if(!Ext.isEmpty(h.timeout)){k.timeout=h.timeout}}k.showLoading();if(!l){k.defaultUrl=i}if(Ext.isFunction(i)){i=i.call(k)}var m=Ext.apply({},{url:i,params:(Ext.isFunction(n)&&j)?n.createDelegate(j):n,success:a,failure:e,scope:k,callback:undefined,timeout:(k.timeout*1000),disableCaching:k.disableCaching,argument:{options:h,url:i,form:null,callback:p,scope:j||window,params:n}},h);k.transaction=Ext.Ajax.request(m)}},formUpdate:function(k,h,j,l){var i=this;if(i.fireEvent(b,i.el,k,h)!==false){if(Ext.isFunction(h)){h=h.call(i)}k=Ext.getDom(k);i.transaction=Ext.Ajax.request({form:k,url:h,success:a,failure:e,scope:i,timeout:(i.timeout*1000),argument:{url:h,form:k,callback:l,reset:j}});i.showLoading.defer(1,i)}},startAutoRefresh:function(i,j,l,m,h){var k=this;if(h){k.update(j||k.defaultUrl,l,m,true)}if(k.autoRefreshProcId){clearInterval(k.autoRefreshProcId)}k.autoRefreshProcId=setInterval(k.update.createDelegate(k,[j||k.defaultUrl,l,m,true]),i*1000)},stopAutoRefresh:function(){if(this.autoRefreshProcId){clearInterval(this.autoRefreshProcId);delete this.autoRefreshProcId}},isAutoRefreshing:function(){return !!this.autoRefreshProcId},showLoading:function(){if(this.showLoadIndicator){this.el.dom.innerHTML=this.indicatorText}},abort:function(){if(this.transaction){Ext.Ajax.abort(this.transaction)}},isUpdating:function(){return this.transaction?Ext.Ajax.isLoading(this.transaction):false},refresh:function(h){if(this.defaultUrl){this.update(this.defaultUrl,null,h,true)}}}}());Ext.Updater.defaults={timeout:30,disableCaching:false,showLoadIndicator:true,indicatorText:'
        Loading...
        ',loadScripts:false,sslBlankUrl:Ext.SSL_SECURE_URL};Ext.Updater.updateElement=function(d,c,e,b){var a=Ext.get(d).getUpdater();Ext.apply(a,b);a.update(c,e,b?b.callback:null)};Ext.Updater.BasicRenderer=function(){};Ext.Updater.BasicRenderer.prototype={render:function(c,a,b,d){c.update(a.responseText,b.loadScripts,d)}};(function(){Date.useStrict=false;function b(d){var c=Array.prototype.slice.call(arguments,1);return d.replace(/\{(\d+)\}/g,function(e,g){return c[g]})}Date.formatCodeToRegex=function(d,c){var e=Date.parseCodes[d];if(e){e=typeof e=="function"?e():e;Date.parseCodes[d]=e}return e?Ext.applyIf({c:e.c?b(e.c,c||"{0}"):e.c},e):{g:0,c:null,s:Ext.escapeRe(d)}};var a=Date.formatCodeToRegex;Ext.apply(Date,{parseFunctions:{"M$":function(d,c){var e=new RegExp("\\/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\/");var g=(d||"").match(e);return g?new Date(((g[1]||"")+g[2])*1):null}},parseRegexes:[],formatFunctions:{"M$":function(){return"\\/Date("+this.getTime()+")\\/"}},y2kYear:50,MILLI:"ms",SECOND:"s",MINUTE:"mi",HOUR:"h",DAY:"d",MONTH:"mo",YEAR:"y",defaults:{},dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNumbers:{Jan:0,Feb:1,Mar:2,Apr:3,May:4,Jun:5,Jul:6,Aug:7,Sep:8,Oct:9,Nov:10,Dec:11},getShortMonthName:function(c){return Date.monthNames[c].substring(0,3)},getShortDayName:function(c){return Date.dayNames[c].substring(0,3)},getMonthNumber:function(c){return Date.monthNumbers[c.substring(0,1).toUpperCase()+c.substring(1,3).toLowerCase()]},formatContainsHourInfo:(function(){var d=/(\\.)/g,c=/([gGhHisucUOPZ]|M\$)/;return function(e){return c.test(e.replace(d,""))}})(),formatCodes:{d:"String.leftPad(this.getDate(), 2, '0')",D:"Date.getShortDayName(this.getDay())",j:"this.getDate()",l:"Date.dayNames[this.getDay()]",N:"(this.getDay() ? this.getDay() : 7)",S:"this.getSuffix()",w:"this.getDay()",z:"this.getDayOfYear()",W:"String.leftPad(this.getWeekOfYear(), 2, '0')",F:"Date.monthNames[this.getMonth()]",m:"String.leftPad(this.getMonth() + 1, 2, '0')",M:"Date.getShortMonthName(this.getMonth())",n:"(this.getMonth() + 1)",t:"this.getDaysInMonth()",L:"(this.isLeapYear() ? 1 : 0)",o:"(this.getFullYear() + (this.getWeekOfYear() == 1 && this.getMonth() > 0 ? +1 : (this.getWeekOfYear() >= 52 && this.getMonth() < 11 ? -1 : 0)))",Y:"String.leftPad(this.getFullYear(), 4, '0')",y:"('' + this.getFullYear()).substring(2, 4)",a:"(this.getHours() < 12 ? 'am' : 'pm')",A:"(this.getHours() < 12 ? 'AM' : 'PM')",g:"((this.getHours() % 12) ? this.getHours() % 12 : 12)",G:"this.getHours()",h:"String.leftPad((this.getHours() % 12) ? this.getHours() % 12 : 12, 2, '0')",H:"String.leftPad(this.getHours(), 2, '0')",i:"String.leftPad(this.getMinutes(), 2, '0')",s:"String.leftPad(this.getSeconds(), 2, '0')",u:"String.leftPad(this.getMilliseconds(), 3, '0')",O:"this.getGMTOffset()",P:"this.getGMTOffset(true)",T:"this.getTimezone()",Z:"(this.getTimezoneOffset() * -60)",c:function(){for(var k="Y-m-dTH:i:sP",h=[],g=0,d=k.length;g= 0 && y >= 0){","v = new Date(y < 100 ? 100 : y, 0, 1, h, i, s, ms).add(Date.YEAR, y < 100 ? y - 100 : 0);","v = !strict? v : (strict === true && (z <= 364 || (v.isLeapYear() && z <= 365))? v.add(Date.DAY, z) : null);","}else if(strict === true && !Date.isValid(y, m + 1, d, h, i, s, ms)){","v = null;","}else{","v = new Date(y < 100 ? 100 : y, m, d, h, i, s, ms).add(Date.YEAR, y < 100 ? y - 100 : 0);","}","}","}","if(v){","if(zz != null){","v = v.add(Date.SECOND, -v.getTimezoneOffset() * 60 - zz);","}else if(o){","v = v.add(Date.MINUTE, -v.getTimezoneOffset() + (sn == '+'? -1 : 1) * (hr * 60 + mn));","}","}","return v;"].join("\n");return function(m){var e=Date.parseRegexes.length,o=1,g=[],l=[],k=false,d="",j=0,h,n;for(;j Date.y2kYear ? 1900 + ty : 2000 + ty;\n",s:"(\\d{1,2})"},a:function(){return a("A")},A:{calcLast:true,g:1,c:"if (/(am)/i.test(results[{0}])) {\nif (!h || h == 12) { h = 0; }\n} else { if (!h || h < 12) { h = (h || 0) + 12; }}",s:"(AM|PM|am|pm)"},g:function(){return a("G")},G:{g:1,c:"h = parseInt(results[{0}], 10);\n",s:"(\\d{1,2})"},h:function(){return a("H")},H:{g:1,c:"h = parseInt(results[{0}], 10);\n",s:"(\\d{2})"},i:{g:1,c:"i = parseInt(results[{0}], 10);\n",s:"(\\d{2})"},s:{g:1,c:"s = parseInt(results[{0}], 10);\n",s:"(\\d{2})"},u:{g:1,c:"ms = results[{0}]; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n",s:"(\\d+)"},O:{g:1,c:["o = results[{0}];","var sn = o.substring(0,1),","hr = o.substring(1,3)*1 + Math.floor(o.substring(3,5) / 60),","mn = o.substring(3,5) % 60;","o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + String.leftPad(hr, 2, '0') + String.leftPad(mn, 2, '0')) : null;\n"].join("\n"),s:"([+-]\\d{4})"},P:{g:1,c:["o = results[{0}];","var sn = o.substring(0,1),","hr = o.substring(1,3)*1 + Math.floor(o.substring(4,6) / 60),","mn = o.substring(4,6) % 60;","o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + String.leftPad(hr, 2, '0') + String.leftPad(mn, 2, '0')) : null;\n"].join("\n"),s:"([+-]\\d{2}:\\d{2})"},T:{g:0,c:null,s:"[A-Z]{1,4}"},Z:{g:1,c:"zz = results[{0}] * 1;\nzz = (-43200 <= zz && zz <= 50400)? zz : null;\n",s:"([+-]?\\d{1,5})"},c:function(){var e=[],c=[a("Y",1),a("m",2),a("d",3),a("h",4),a("i",5),a("s",6),{c:"ms = results[7] || '0'; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n"},{c:["if(results[8]) {","if(results[8] == 'Z'){","zz = 0;","}else if (results[8].indexOf(':') > -1){",a("P",8).c,"}else{",a("O",8).c,"}","}"].join("\n")}];for(var g=0,d=c.length;g0?"-":"+")+String.leftPad(Math.floor(Math.abs(this.getTimezoneOffset())/60),2,"0")+(a?":":"")+String.leftPad(Math.abs(this.getTimezoneOffset()%60),2,"0")},getDayOfYear:function(){var b=0,e=this.clone(),a=this.getMonth(),c;for(c=0,e.setDate(1),e.setMonth(0);c28){a=Math.min(a,this.getFirstDateOfMonth().add("mo",c).getLastDateOfMonth().getDate())}e.setDate(a);e.setMonth(this.getMonth()+c);break;case Date.YEAR:e.setFullYear(this.getFullYear()+c);break}return e},between:function(c,a){var b=this.getTime();return c.getTime()<=b&&b<=a.getTime()}});Date.prototype.format=Date.prototype.dateFormat;if(Ext.isSafari&&(navigator.userAgent.match(/WebKit\/(\d+)/)[1]||NaN)<420){Ext.apply(Date.prototype,{_xMonth:Date.prototype.setMonth,_xDate:Date.prototype.setDate,setMonth:function(a){if(a<=-1){var d=Math.ceil(-a),c=Math.ceil(d/12),b=(d%12)?12-d%12:0;this.setFullYear(this.getFullYear()-c);return this._xMonth(b)}else{return this._xMonth(a)}},setDate:function(a){return this.setTime(this.getTime()-(this.getDate()-a)*86400000)}})}Ext.util.MixedCollection=function(b,a){this.items=[];this.map={};this.keys=[];this.length=0;this.addEvents("clear","add","replace","remove","sort");this.allowFunctions=b===true;if(a){this.getKey=a}Ext.util.MixedCollection.superclass.constructor.call(this)};Ext.extend(Ext.util.MixedCollection,Ext.util.Observable,{allowFunctions:false,add:function(b,c){if(arguments.length==1){c=arguments[0];b=this.getKey(c)}if(typeof b!="undefined"&&b!==null){var a=this.map[b];if(typeof a!="undefined"){return this.replace(b,c)}this.map[b]=c}this.length++;this.items.push(c);this.keys.push(b);this.fireEvent("add",this.length-1,c,b);return c},getKey:function(a){return a.id},replace:function(c,d){if(arguments.length==1){d=arguments[0];c=this.getKey(d)}var a=this.map[c];if(typeof c=="undefined"||c===null||typeof a=="undefined"){return this.add(c,d)}var b=this.indexOfKey(c);this.items[b]=d;this.map[c]=d;this.fireEvent("replace",c,a,d);return d},addAll:function(e){if(arguments.length>1||Ext.isArray(e)){var b=arguments.length>1?arguments:e;for(var d=0,a=b.length;d=this.length){return this.add(b,c)}this.length++;this.items.splice(a,0,c);if(typeof b!="undefined"&&b!==null){this.map[b]=c}this.keys.splice(a,0,b);this.fireEvent("add",a,c,b);return c},remove:function(a){return this.removeAt(this.indexOf(a))},removeAt:function(a){if(a=0){this.length--;var c=this.items[a];this.items.splice(a,1);var b=this.keys[a];if(typeof b!="undefined"){delete this.map[b]}this.keys.splice(a,1);this.fireEvent("remove",c,b);return c}return false},removeKey:function(a){return this.removeAt(this.indexOfKey(a))},getCount:function(){return this.length},indexOf:function(a){return this.items.indexOf(a)},indexOfKey:function(a){return this.keys.indexOf(a)},item:function(b){var a=this.map[b],c=a!==undefined?a:(typeof b=="number")?this.items[b]:undefined;return typeof c!="function"||this.allowFunctions?c:null},itemAt:function(a){return this.items[a]},key:function(a){return this.map[a]},contains:function(a){return this.indexOf(a)!=-1},containsKey:function(a){return typeof this.map[a]!="undefined"},clear:function(){this.length=0;this.items=[];this.keys=[];this.map={};this.fireEvent("clear")},first:function(){return this.items[0]},last:function(){return this.items[this.length-1]},_sort:function(k,a,j){var d,e,b=String(a).toUpperCase()=="DESC"?-1:1,h=[],l=this.keys,g=this.items;j=j||function(i,c){return i-c};for(d=0,e=g.length;de?1:(g=a;c--){d[d.length]=b[c]}}return d},filter:function(c,b,d,a){if(Ext.isEmpty(b,false)){return this.clone()}b=this.createValueMatcher(b,d,a);return this.filterBy(function(e){return e&&b.test(e[c])})},filterBy:function(g,e){var h=new Ext.util.MixedCollection();h.getKey=this.getKey;var b=this.keys,d=this.items;for(var c=0,a=d.length;c]+>/gi,stripScriptsRe=/(?:)((\n|\r|.)*?)(?:<\/script>)/ig,nl2brRe=/\r?\n/g;return{ellipsis:function(value,len,word){if(value&&value.length>len){if(word){var vs=value.substr(0,len-2),index=Math.max(vs.lastIndexOf(" "),vs.lastIndexOf("."),vs.lastIndexOf("!"),vs.lastIndexOf("?"));if(index==-1||index<(len-15)){return value.substr(0,len-3)+"..."}else{return vs.substr(0,index)+"..."}}else{return value.substr(0,len-3)+"..."}}return value},undef:function(value){return value!==undefined?value:""},defaultValue:function(value,defaultValue){return value!==undefined&&value!==""?value:defaultValue},htmlEncode:function(value){return !value?value:String(value).replace(/&/g,"&").replace(/>/g,">").replace(/").replace(/</g,"<").replace(/"/g,'"').replace(/&/g,"&")},trim:function(value){return String(value).replace(trimRe,"")},substr:function(value,start,length){return String(value).substr(start,length)},lowercase:function(value){return String(value).toLowerCase()},uppercase:function(value){return String(value).toUpperCase()},capitalize:function(value){return !value?value:value.charAt(0).toUpperCase()+value.substr(1).toLowerCase()},call:function(value,fn){if(arguments.length>2){var args=Array.prototype.slice.call(arguments,2);args.unshift(value);return eval(fn).apply(window,args)}else{return eval(fn).call(window,value)}},usMoney:function(v){v=(Math.round((v-0)*100))/100;v=(v==Math.floor(v))?v+".00":((v*10==Math.floor(v*10))?v+"0":v);v=String(v);var ps=v.split("."),whole=ps[0],sub=ps[1]?"."+ps[1]:".00",r=/(\d+)(\d{3})/;while(r.test(whole)){whole=whole.replace(r,"$1,$2")}v=whole+sub;if(v.charAt(0)=="-"){return"-$"+v.substr(1)}return"$"+v},date:function(v,format){if(!v){return""}if(!Ext.isDate(v)){v=new Date(Date.parse(v))}return v.dateFormat(format||"m/d/Y")},dateRenderer:function(format){return function(v){return Ext.util.Format.date(v,format)}},stripTags:function(v){return !v?v:String(v).replace(stripTagsRE,"")},stripScripts:function(v){return !v?v:String(v).replace(stripScriptsRe,"")},fileSize:function(size){if(size<1024){return size+" bytes"}else{if(size<1048576){return(Math.round(((size*10)/1024))/10)+" KB"}else{return(Math.round(((size*10)/1048576))/10)+" MB"}}},math:function(){var fns={};return function(v,a){if(!fns[a]){fns[a]=new Function("v","return v "+a+";")}return fns[a](v)}}(),round:function(value,precision){var result=Number(value);if(typeof precision=="number"){precision=Math.pow(10,precision);result=Math.round(value*precision)/precision}return result},number:function(v,format){if(!format){return v}v=Ext.num(v,NaN);if(isNaN(v)){return""}var comma=",",dec=".",i18n=false,neg=v<0;v=Math.abs(v);if(format.substr(format.length-2)=="/i"){format=format.substr(0,format.length-2);i18n=true;comma=".";dec=","}var hasComma=format.indexOf(comma)!=-1,psplit=(i18n?format.replace(/[^\d\,]/g,""):format.replace(/[^\d\.]/g,"")).split(dec);if(1")}}}();Ext.XTemplate=function(){Ext.XTemplate.superclass.constructor.apply(this,arguments);var y=this,j=y.html,q=/]*>((?:(?=([^<]+))\2|<(?!tpl\b[^>]*>))*?)<\/tpl>/,d=/^]*?for="(.*?)"/,v=/^]*?if="(.*?)"/,x=/^]*?exec="(.*?)"/,r,p=0,k=[],o="values",w="parent",l="xindex",n="xcount",e="return ",c="with(values){ ";j=["",j,""].join("");while((r=j.match(q))){var b=r[0].match(d),a=r[0].match(v),A=r[0].match(x),g=null,h=null,t=null,z=b&&b[1]?b[1]:"";if(a){g=a&&a[1]?a[1]:null;if(g){h=new Function(o,w,l,n,c+e+(Ext.util.Format.htmlDecode(g))+"; }")}}if(A){g=A&&A[1]?A[1]:null;if(g){t=new Function(o,w,l,n,c+(Ext.util.Format.htmlDecode(g))+"; }")}}if(z){switch(z){case".":z=new Function(o,w,c+e+o+"; }");break;case"..":z=new Function(o,w,c+e+w+"; }");break;default:z=new Function(o,w,c+e+z+"; }")}}k.push({id:p,target:z,exec:t,test:h,body:r[1]||""});j=j.replace(r[0],"{xtpl"+p+"}");++p}for(var u=k.length-1;u>=0;--u){y.compileTpl(k[u])}y.master=k[k.length-1];y.tpls=k};Ext.extend(Ext.XTemplate,Ext.Template,{re:/\{([\w\-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\\]\s?[\d\.\+\-\*\\\(\)]+)?\}/g,codeRe:/\{\[((?:\\\]|.|\n)*?)\]\}/g,applySubTemplate:function(a,k,j,d,c){var h=this,g,m=h.tpls[a],l,b=[];if((m.test&&!m.test.call(h,k,j,d,c))||(m.exec&&m.exec.call(h,k,j,d,c))){return""}l=m.target?m.target.call(h,k,j):k;g=l.length;j=m.target?k:j;if(m.target&&Ext.isArray(l)){for(var e=0,g=l.length;e=0;--g){d[k[g].selectorText.toLowerCase()]=k[g]}}catch(i){}},getRules:function(h){if(d===null||h){d={};var k=c.styleSheets;for(var j=0,g=k.length;j=37&&a<=40){b.stopEvent()}},destroy:function(){this.disable()},enable:function(){if(this.disabled){if(Ext.isSafari2){this.el.on("keyup",this.stopKeyUp,this)}this.el.on(this.isKeydown()?"keydown":"keypress",this.relay,this);this.disabled=false}},disable:function(){if(!this.disabled){if(Ext.isSafari2){this.el.un("keyup",this.stopKeyUp,this)}this.el.un(this.isKeydown()?"keydown":"keypress",this.relay,this);this.disabled=true}},setDisabled:function(a){this[a?"disable":"enable"]()},isKeydown:function(){return this.forceKeyDown||Ext.EventManager.useKeydown}};Ext.KeyMap=function(c,b,a){this.el=Ext.get(c);this.eventName=a||"keydown";this.bindings=[];if(b){this.addBinding(b)}this.enable()};Ext.KeyMap.prototype={stopEvent:false,addBinding:function(b){if(Ext.isArray(b)){Ext.each(b,function(j){this.addBinding(j)},this);return}var k=b.key,g=b.fn||b.handler,l=b.scope;if(b.stopEvent){this.stopEvent=b.stopEvent}if(typeof k=="string"){var h=[];var e=k.toUpperCase();for(var c=0,d=e.length;c2)?a[2]:null;var h=(i>3)?a[3]:"/";var d=(i>4)?a[4]:null;var g=(i>5)?a[5]:false;document.cookie=c+"="+escape(e)+((b===null)?"":("; expires="+b.toGMTString()))+((h===null)?"":("; path="+h))+((d===null)?"":("; domain="+d))+((g===true)?"; secure":"")},get:function(d){var b=d+"=";var g=b.length;var a=document.cookie.length;var e=0;var c=0;while(e0){return this.ownerCt.items.itemAt(a-1)}}return null},getBubbleTarget:function(){return this.ownerCt}});Ext.reg("component",Ext.Component);Ext.Action=Ext.extend(Object,{constructor:function(a){this.initialConfig=a;this.itemId=a.itemId=(a.itemId||a.id||Ext.id());this.items=[]},isAction:true,setText:function(a){this.initialConfig.text=a;this.callEach("setText",[a])},getText:function(){return this.initialConfig.text},setIconClass:function(a){this.initialConfig.iconCls=a;this.callEach("setIconClass",[a])},getIconClass:function(){return this.initialConfig.iconCls},setDisabled:function(a){this.initialConfig.disabled=a;this.callEach("setDisabled",[a])},enable:function(){this.setDisabled(false)},disable:function(){this.setDisabled(true)},isDisabled:function(){return this.initialConfig.disabled},setHidden:function(a){this.initialConfig.hidden=a;this.callEach("setVisible",[!a])},show:function(){this.setHidden(false)},hide:function(){this.setHidden(true)},isHidden:function(){return this.initialConfig.hidden},setHandler:function(b,a){this.initialConfig.handler=b;this.initialConfig.scope=a;this.callEach("setHandler",[b,a])},each:function(b,a){Ext.each(this.items,b,a)},callEach:function(e,b){var d=this.items;for(var c=0,a=d.length;cj+o.left){k=j-l-c;g=true}if((i+e)>d+o.top){i=d-e-c;g=true}if(k=m){i=m-e-5}}n=[k,i];this.storeXY(n);a.setXY.call(this,n);this.sync()}}return this},getConstrainOffset:function(){return this.shadowOffset},isVisible:function(){return this.visible},showAction:function(){this.visible=true;if(this.useDisplay===true){this.setDisplayed("")}else{if(this.lastXY){a.setXY.call(this,this.lastXY)}else{if(this.lastLT){a.setLeftTop.call(this,this.lastLT[0],this.lastLT[1])}}}},hideAction:function(){this.visible=false;if(this.useDisplay===true){this.setDisplayed(false)}else{this.setLeftTop(-10000,-10000)}},setVisible:function(i,h,k,l,j){if(i){this.showAction()}if(h&&i){var g=function(){this.sync(true);if(l){l()}}.createDelegate(this);a.setVisible.call(this,true,true,k,g,j)}else{if(!i){this.hideUnders(true)}var g=l;if(h){g=function(){this.hideAction();if(l){l()}}.createDelegate(this)}a.setVisible.call(this,i,h,k,g,j);if(i){this.sync(true)}else{if(!h){this.hideAction()}}}return this},storeXY:function(c){delete this.lastLT;this.lastXY=c},storeLeftTop:function(d,c){delete this.lastXY;this.lastLT=[d,c]},beforeFx:function(){this.beforeAction();return Ext.Layer.superclass.beforeFx.apply(this,arguments)},afterFx:function(){Ext.Layer.superclass.afterFx.apply(this,arguments);this.sync(this.isVisible())},beforeAction:function(){if(!this.updating&&this.shadow){this.shadow.hide()}},setLeft:function(c){this.storeLeftTop(c,this.getTop(true));a.setLeft.apply(this,arguments);this.sync();return this},setTop:function(c){this.storeLeftTop(this.getLeft(true),c);a.setTop.apply(this,arguments);this.sync();return this},setLeftTop:function(d,c){this.storeLeftTop(d,c);a.setLeftTop.apply(this,arguments);this.sync();return this},setXY:function(j,h,k,l,i){this.fixDisplay();this.beforeAction();this.storeXY(j);var g=this.createCB(l);a.setXY.call(this,j,h,k,g,i);if(!h){g()}return this},createCB:function(e){var d=this;return function(){d.constrainXY();d.sync(true);if(e){e()}}},setX:function(g,h,j,k,i){this.setXY([g,this.getY()],h,j,k,i);return this},setY:function(k,g,i,j,h){this.setXY([this.getX(),k],g,i,j,h);return this},setSize:function(j,k,i,m,n,l){this.beforeAction();var g=this.createCB(n);a.setSize.call(this,j,k,i,m,g,l);if(!i){g()}return this},setWidth:function(i,h,k,l,j){this.beforeAction();var g=this.createCB(l);a.setWidth.call(this,i,h,k,g,j);if(!h){g()}return this},setHeight:function(j,i,l,m,k){this.beforeAction();var g=this.createCB(m);a.setHeight.call(this,j,i,l,g,k);if(!i){g()}return this},setBounds:function(o,m,p,i,n,k,l,j){this.beforeAction();var g=this.createCB(l);if(!n){this.storeXY([o,m]);a.setXY.call(this,[o,m]);a.setSize.call(this,p,i,n,k,g,j);g()}else{a.setBounds.call(this,o,m,p,i,n,k,g,j)}return this},setZIndex:function(c){this.zindex=c;this.setStyle("z-index",c+2);if(this.shadow){this.shadow.setZIndex(c+1)}if(this.shim){this.shim.setStyle("z-index",c)}return this}})})();Ext.Shadow=function(d){Ext.apply(this,d);if(typeof this.mode!="string"){this.mode=this.defaultMode}var e=this.offset,c={h:0},b=Math.floor(this.offset/2);switch(this.mode.toLowerCase()){case"drop":c.w=0;c.l=c.t=e;c.t-=1;if(Ext.isIE){c.l-=this.offset+b;c.t-=this.offset+b;c.w-=b;c.h-=b;c.t+=1}break;case"sides":c.w=(e*2);c.l=-e;c.t=e-1;if(Ext.isIE){c.l-=(this.offset-b);c.t-=this.offset+b;c.l+=1;c.w-=(this.offset-b)*2;c.w-=b+1;c.h-=1}break;case"frame":c.w=c.h=(e*2);c.l=c.t=-e;c.t+=1;c.h-=2;if(Ext.isIE){c.l-=(this.offset-b);c.t-=(this.offset-b);c.l+=1;c.w-=(this.offset+b+1);c.h-=(this.offset+b);c.h+=1}break}this.adjusts=c};Ext.Shadow.prototype={offset:4,defaultMode:"drop",show:function(a){a=Ext.get(a);if(!this.el){this.el=Ext.Shadow.Pool.pull();if(this.el.dom.nextSibling!=a.dom){this.el.insertBefore(a)}}this.el.setStyle("z-index",this.zIndex||parseInt(a.getStyle("z-index"),10)-1);if(Ext.isIE){this.el.dom.style.filter="progid:DXImageTransform.Microsoft.alpha(opacity=50) progid:DXImageTransform.Microsoft.Blur(pixelradius="+(this.offset)+")"}this.realign(a.getLeft(true),a.getTop(true),a.getWidth(),a.getHeight());this.el.dom.style.display="block"},isVisible:function(){return this.el?true:false},realign:function(b,r,q,g){if(!this.el){return}var n=this.adjusts,k=this.el.dom,u=k.style,i=0,p=(q+n.w),e=(g+n.h),j=p+"px",o=e+"px",m,c;u.left=(b+n.l)+"px";u.top=(r+n.t)+"px";if(u.width!=j||u.height!=o){u.width=j;u.height=o;if(!Ext.isIE){m=k.childNodes;c=Math.max(0,(p-12))+"px";m[0].childNodes[1].style.width=c;m[1].childNodes[1].style.width=c;m[2].childNodes[1].style.width=c;m[1].style.height=Math.max(0,(e-12))+"px"}}},hide:function(){if(this.el){this.el.dom.style.display="none";Ext.Shadow.Pool.push(this.el);delete this.el}},setZIndex:function(a){this.zIndex=a;if(this.el){this.el.setStyle("z-index",a)}}};Ext.Shadow.Pool=function(){var b=[],a=Ext.isIE?'
        ':'
        ';return{pull:function(){var c=b.shift();if(!c){c=Ext.get(Ext.DomHelper.insertHtml("beforeBegin",document.body.firstChild,a));c.autoBoxAdjust=false}return c},push:function(c){b.push(c)}}}();Ext.BoxComponent=Ext.extend(Ext.Component,{initComponent:function(){Ext.BoxComponent.superclass.initComponent.call(this);this.addEvents("resize","move")},boxReady:false,deferHeight:false,setSize:function(b,d){if(typeof b=="object"){d=b.height;b=b.width}if(Ext.isDefined(b)&&Ext.isDefined(this.boxMinWidth)&&(bthis.boxMaxWidth)){b=this.boxMaxWidth}if(Ext.isDefined(d)&&Ext.isDefined(this.boxMaxHeight)&&(d>this.boxMaxHeight)){d=this.boxMaxHeight}if(!this.boxReady){this.width=b;this.height=d;return this}if(this.cacheSizes!==false&&this.lastSize&&this.lastSize.width==b&&this.lastSize.height==d){return this}this.lastSize={width:b,height:d};var c=this.adjustSize(b,d),g=c.width,a=c.height,e;if(g!==undefined||a!==undefined){e=this.getResizeEl();if(!this.deferHeight&&g!==undefined&&a!==undefined){e.setSize(g,a)}else{if(!this.deferHeight&&a!==undefined){e.setHeight(a)}else{if(g!==undefined){e.setWidth(g)}}}this.onResize(g,a,b,d);this.fireEvent("resize",this,g,a,b,d)}return this},setWidth:function(a){return this.setSize(a)},setHeight:function(a){return this.setSize(undefined,a)},getSize:function(){return this.getResizeEl().getSize()},getWidth:function(){return this.getResizeEl().getWidth()},getHeight:function(){return this.getResizeEl().getHeight()},getOuterSize:function(){var a=this.getResizeEl();return{width:a.getWidth()+a.getMargins("lr"),height:a.getHeight()+a.getMargins("tb")}},getPosition:function(a){var b=this.getPositionEl();if(a===true){return[b.getLeft(true),b.getTop(true)]}return this.xy||b.getXY()},getBox:function(a){var c=this.getPosition(a);var b=this.getSize();b.x=c[0];b.y=c[1];return b},updateBox:function(a){this.setSize(a.width,a.height);this.setPagePosition(a.x,a.y);return this},getResizeEl:function(){return this.resizeEl||this.el},setAutoScroll:function(a){if(this.rendered){this.getContentTarget().setOverflow(a?"auto":"")}this.autoScroll=a;return this},setPosition:function(a,g){if(a&&typeof a[1]=="number"){g=a[1];a=a[0]}this.x=a;this.y=g;if(!this.boxReady){return this}var b=this.adjustPosition(a,g);var e=b.x,d=b.y;var c=this.getPositionEl();if(e!==undefined||d!==undefined){if(e!==undefined&&d!==undefined){c.setLeftTop(e,d)}else{if(e!==undefined){c.setLeft(e)}else{if(d!==undefined){c.setTop(d)}}}this.onPosition(e,d);this.fireEvent("move",this,e,d)}return this},setPagePosition:function(a,c){if(a&&typeof a[1]=="number"){c=a[1];a=a[0]}this.pageX=a;this.pageY=c;if(!this.boxReady){return}if(a===undefined||c===undefined){return}var b=this.getPositionEl().translatePoints(a,c);this.setPosition(b.left,b.top);return this},afterRender:function(){Ext.BoxComponent.superclass.afterRender.call(this);if(this.resizeEl){this.resizeEl=Ext.get(this.resizeEl)}if(this.positionEl){this.positionEl=Ext.get(this.positionEl)}this.boxReady=true;Ext.isDefined(this.autoScroll)&&this.setAutoScroll(this.autoScroll);this.setSize(this.width,this.height);if(this.x||this.y){this.setPosition(this.x,this.y)}else{if(this.pageX||this.pageY){this.setPagePosition(this.pageX,this.pageY)}}},syncSize:function(){delete this.lastSize;this.setSize(this.autoWidth?undefined:this.getResizeEl().getWidth(),this.autoHeight?undefined:this.getResizeEl().getHeight());return this},onResize:function(d,b,a,c){},onPosition:function(a,b){},adjustSize:function(a,b){if(this.autoWidth){a="auto"}if(this.autoHeight){b="auto"}return{width:a,height:b}},adjustPosition:function(a,b){return{x:a,y:b}}});Ext.reg("box",Ext.BoxComponent);Ext.Spacer=Ext.extend(Ext.BoxComponent,{autoEl:"div"});Ext.reg("spacer",Ext.Spacer);Ext.SplitBar=function(c,e,b,d,a){this.el=Ext.get(c,true);this.el.dom.unselectable="on";this.resizingEl=Ext.get(e,true);this.orientation=b||Ext.SplitBar.HORIZONTAL;this.minSize=0;this.maxSize=2000;this.animate=false;this.useShim=false;this.shim=null;if(!a){this.proxy=Ext.SplitBar.createProxy(this.orientation)}else{this.proxy=Ext.get(a).dom}this.dd=new Ext.dd.DDProxy(this.el.dom.id,"XSplitBars",{dragElId:this.proxy.id});this.dd.b4StartDrag=this.onStartProxyDrag.createDelegate(this);this.dd.endDrag=this.onEndProxyDrag.createDelegate(this);this.dragSpecs={};this.adapter=new Ext.SplitBar.BasicLayoutAdapter();this.adapter.init(this);if(this.orientation==Ext.SplitBar.HORIZONTAL){this.placement=d||(this.el.getX()>this.resizingEl.getX()?Ext.SplitBar.LEFT:Ext.SplitBar.RIGHT);this.el.addClass("x-splitbar-h")}else{this.placement=d||(this.el.getY()>this.resizingEl.getY()?Ext.SplitBar.TOP:Ext.SplitBar.BOTTOM);this.el.addClass("x-splitbar-v")}this.addEvents("resize","moved","beforeresize","beforeapply");Ext.SplitBar.superclass.constructor.call(this)};Ext.extend(Ext.SplitBar,Ext.util.Observable,{onStartProxyDrag:function(a,e){this.fireEvent("beforeresize",this);this.overlay=Ext.DomHelper.append(document.body,{cls:"x-drag-overlay",html:" "},true);this.overlay.unselectable();this.overlay.setSize(Ext.lib.Dom.getViewWidth(true),Ext.lib.Dom.getViewHeight(true));this.overlay.show();Ext.get(this.proxy).setDisplayed("block");var c=this.adapter.getElementSize(this);this.activeMinSize=this.getMinimumSize();this.activeMaxSize=this.getMaximumSize();var d=c-this.activeMinSize;var b=Math.max(this.activeMaxSize-c,0);if(this.orientation==Ext.SplitBar.HORIZONTAL){this.dd.resetConstraints();this.dd.setXConstraint(this.placement==Ext.SplitBar.LEFT?d:b,this.placement==Ext.SplitBar.LEFT?b:d,this.tickSize);this.dd.setYConstraint(0,0)}else{this.dd.resetConstraints();this.dd.setXConstraint(0,0);this.dd.setYConstraint(this.placement==Ext.SplitBar.TOP?d:b,this.placement==Ext.SplitBar.TOP?b:d,this.tickSize)}this.dragSpecs.startSize=c;this.dragSpecs.startPoint=[a,e];Ext.dd.DDProxy.prototype.b4StartDrag.call(this.dd,a,e)},onEndProxyDrag:function(c){Ext.get(this.proxy).setDisplayed(false);var b=Ext.lib.Event.getXY(c);if(this.overlay){Ext.destroy(this.overlay);delete this.overlay}var a;if(this.orientation==Ext.SplitBar.HORIZONTAL){a=this.dragSpecs.startSize+(this.placement==Ext.SplitBar.LEFT?b[0]-this.dragSpecs.startPoint[0]:this.dragSpecs.startPoint[0]-b[0])}else{a=this.dragSpecs.startSize+(this.placement==Ext.SplitBar.TOP?b[1]-this.dragSpecs.startPoint[1]:this.dragSpecs.startPoint[1]-b[1])}a=Math.min(Math.max(a,this.activeMinSize),this.activeMaxSize);if(a!=this.dragSpecs.startSize){if(this.fireEvent("beforeapply",this,a)!==false){this.adapter.setElementSize(this,a);this.fireEvent("moved",this,a);this.fireEvent("resize",this,a)}}},getAdapter:function(){return this.adapter},setAdapter:function(a){this.adapter=a;this.adapter.init(this)},getMinimumSize:function(){return this.minSize},setMinimumSize:function(a){this.minSize=a},getMaximumSize:function(){return this.maxSize},setMaximumSize:function(a){this.maxSize=a},setCurrentSize:function(b){var a=this.animate;this.animate=false;this.adapter.setElementSize(this,b);this.animate=a},destroy:function(a){Ext.destroy(this.shim,Ext.get(this.proxy));this.dd.unreg();if(a){this.el.remove()}this.purgeListeners()}});Ext.SplitBar.createProxy=function(b){var c=new Ext.Element(document.createElement("div"));document.body.appendChild(c.dom);c.unselectable();var a="x-splitbar-proxy";c.addClass(a+" "+(b==Ext.SplitBar.HORIZONTAL?a+"-h":a+"-v"));return c.dom};Ext.SplitBar.BasicLayoutAdapter=function(){};Ext.SplitBar.BasicLayoutAdapter.prototype={init:function(a){},getElementSize:function(a){if(a.orientation==Ext.SplitBar.HORIZONTAL){return a.resizingEl.getWidth()}else{return a.resizingEl.getHeight()}},setElementSize:function(b,a,c){if(b.orientation==Ext.SplitBar.HORIZONTAL){if(!b.animate){b.resizingEl.setWidth(a);if(c){c(b,a)}}else{b.resizingEl.setWidth(a,true,0.1,c,"easeOut")}}else{if(!b.animate){b.resizingEl.setHeight(a);if(c){c(b,a)}}else{b.resizingEl.setHeight(a,true,0.1,c,"easeOut")}}}};Ext.SplitBar.AbsoluteLayoutAdapter=function(a){this.basic=new Ext.SplitBar.BasicLayoutAdapter();this.container=Ext.get(a)};Ext.SplitBar.AbsoluteLayoutAdapter.prototype={init:function(a){this.basic.init(a)},getElementSize:function(a){return this.basic.getElementSize(a)},setElementSize:function(b,a,c){this.basic.setElementSize(b,a,this.moveSplitter.createDelegate(this,[b]))},moveSplitter:function(a){var b=Ext.SplitBar;switch(a.placement){case b.LEFT:a.el.setX(a.resizingEl.getRight());break;case b.RIGHT:a.el.setStyle("right",(this.container.getWidth()-a.resizingEl.getLeft())+"px");break;case b.TOP:a.el.setY(a.resizingEl.getBottom());break;case b.BOTTOM:a.el.setY(a.resizingEl.getTop()-a.el.getHeight());break}}};Ext.SplitBar.VERTICAL=1;Ext.SplitBar.HORIZONTAL=2;Ext.SplitBar.LEFT=1;Ext.SplitBar.RIGHT=2;Ext.SplitBar.TOP=3;Ext.SplitBar.BOTTOM=4;Ext.Container=Ext.extend(Ext.BoxComponent,{bufferResize:50,autoDestroy:true,forceLayout:false,defaultType:"panel",resizeEvent:"resize",bubbleEvents:["add","remove"],initComponent:function(){Ext.Container.superclass.initComponent.call(this);this.addEvents("afterlayout","beforeadd","beforeremove","add","remove");var a=this.items;if(a){delete this.items;this.add(a)}},initItems:function(){if(!this.items){this.items=new Ext.util.MixedCollection(false,this.getComponentId);this.getLayout()}},setLayout:function(a){if(this.layout&&this.layout!=a){this.layout.setContainer(null)}this.layout=a;this.initItems();a.setContainer(this)},afterRender:function(){Ext.Container.superclass.afterRender.call(this);if(!this.layout){this.layout="auto"}if(Ext.isObject(this.layout)&&!this.layout.layout){this.layoutConfig=this.layout;this.layout=this.layoutConfig.type}if(Ext.isString(this.layout)){this.layout=new Ext.Container.LAYOUTS[this.layout.toLowerCase()](this.layoutConfig)}this.setLayout(this.layout);if(this.activeItem!==undefined&&this.layout.setActiveItem){var a=this.activeItem;delete this.activeItem;this.layout.setActiveItem(a)}if(!this.ownerCt){this.doLayout(false,true)}if(this.monitorResize===true){Ext.EventManager.onWindowResize(this.doLayout,this,[false])}},getLayoutTarget:function(){return this.el},getComponentId:function(a){return a.getItemId()},add:function(b){this.initItems();var e=arguments.length>1;if(e||Ext.isArray(b)){var a=[];Ext.each(e?arguments:b,function(h){a.push(this.add(h))},this);return a}var g=this.lookupComponent(this.applyDefaults(b));var d=this.items.length;if(this.fireEvent("beforeadd",this,g,d)!==false&&this.onBeforeAdd(g)!==false){this.items.add(g);g.onAdded(this,d);this.onAdd(g);this.fireEvent("add",this,g,d)}return g},onAdd:function(a){},onAdded:function(a,b){this.ownerCt=a;this.initRef();this.cascade(function(d){d.initRef()});this.fireEvent("added",this,a,b)},insert:function(e,b){var d=arguments,h=d.length,a=[],g,j;this.initItems();if(h>2){for(g=h-1;g>=1;--g){a.push(this.insert(e,d[g]))}return a}j=this.lookupComponent(this.applyDefaults(b));e=Math.min(e,this.items.length);if(this.fireEvent("beforeadd",this,j,e)!==false&&this.onBeforeAdd(j)!==false){if(j.ownerCt==this){this.items.remove(j)}this.items.insert(e,j);j.onAdded(this,e);this.onAdd(j);this.fireEvent("add",this,j,e)}return j},applyDefaults:function(b){var a=this.defaults;if(a){if(Ext.isFunction(a)){a=a.call(this,b)}if(Ext.isString(b)){b=Ext.ComponentMgr.get(b);Ext.apply(b,a)}else{if(!b.events){Ext.applyIf(b.isAction?b.initialConfig:b,a)}else{Ext.apply(b,a)}}}return b},onBeforeAdd:function(a){if(a.ownerCt){a.ownerCt.remove(a,false)}if(this.hideBorders===true){a.border=(a.border===true)}},remove:function(a,b){this.initItems();var d=this.getComponent(a);if(d&&this.fireEvent("beforeremove",this,d)!==false){this.doRemove(d,b);this.fireEvent("remove",this,d)}return d},onRemove:function(a){},doRemove:function(e,d){var b=this.layout,a=b&&this.rendered;if(a){b.onRemove(e)}this.items.remove(e);e.onRemoved();this.onRemove(e);if(d===true||(d!==false&&this.autoDestroy)){e.destroy()}if(a){b.afterRemove(e)}},removeAll:function(c){this.initItems();var e,g=[],b=[];this.items.each(function(h){g.push(h)});for(var d=0,a=g.length;d','','
        ','
        ',"
        ");a.disableFormats=true;return a.compile()})(),destroy:function(){if(this.resizeTask&&this.resizeTask.cancel){this.resizeTask.cancel()}if(this.container){this.container.un(this.container.resizeEvent,this.onResize,this)}if(!Ext.isEmpty(this.targetCls)){var a=this.container.getLayoutTarget();if(a){a.removeClass(this.targetCls)}}}});Ext.layout.AutoLayout=Ext.extend(Ext.layout.ContainerLayout,{type:"auto",monitorResize:true,onLayout:function(d,g){Ext.layout.AutoLayout.superclass.onLayout.call(this,d,g);var e=this.getRenderedItems(d),a=e.length,b,h;for(b=0;b0){b.setSize(a)}}});Ext.Container.LAYOUTS.fit=Ext.layout.FitLayout;Ext.layout.CardLayout=Ext.extend(Ext.layout.FitLayout,{deferredRender:false,layoutOnCardChange:false,renderHidden:true,type:"card",setActiveItem:function(d){var a=this.activeItem,b=this.container;d=b.getComponent(d);if(d&&a!=d){if(a){a.hide();if(a.hidden!==true){return false}a.fireEvent("deactivate",a)}var c=d.doLayout&&(this.layoutOnCardChange||!d.rendered);this.activeItem=d;delete d.deferLayout;d.show();this.layout();if(c){d.doLayout()}d.fireEvent("activate",d)}},renderAll:function(a,b){if(this.deferredRender){this.renderItem(this.activeItem,undefined,b)}else{Ext.layout.CardLayout.superclass.renderAll.call(this,a,b)}}});Ext.Container.LAYOUTS.card=Ext.layout.CardLayout;Ext.layout.AnchorLayout=Ext.extend(Ext.layout.ContainerLayout,{monitorResize:true,type:"anchor",defaultAnchor:"100%",parseAnchorRE:/^(r|right|b|bottom)$/i,getLayoutTargetSize:function(){var b=this.container.getLayoutTarget(),a={};if(b){a=b.getViewSize();if(Ext.isIE&&Ext.isStrict&&a.width==0){a=b.getStyleSize()}a.width-=b.getPadding("lr");a.height-=b.getPadding("tb")}return a},onLayout:function(m,w){Ext.layout.AnchorLayout.superclass.onLayout.call(this,m,w);var p=this.getLayoutTargetSize(),k=p.width,o=p.height,q=w.getStyle("overflow"),n=this.getRenderedItems(m),t=n.length,g=[],j,a,v,l,h,c,e,d,u=0,s,b;if(k<20&&o<20){return}if(m.anchorSize){if(typeof m.anchorSize=="number"){a=m.anchorSize}else{a=m.anchorSize.width;v=m.anchorSize.height}}else{a=m.initialConfig.width;v=m.initialConfig.height}for(s=0;s 
      ');b.disableFormats=true;b.compile();Ext.layout.BorderLayout.Region.prototype.toolTemplate=b}this.collapsedEl=this.targetEl.createChild({cls:"x-layout-collapsed x-layout-collapsed-"+this.position,id:this.panel.id+"-xcollapsed"});this.collapsedEl.enableDisplayMode("block");if(this.collapseMode=="mini"){this.collapsedEl.addClass("x-layout-cmini-"+this.position);this.miniCollapsedEl=this.collapsedEl.createChild({cls:"x-layout-mini x-layout-mini-"+this.position,html:" "});this.miniCollapsedEl.addClassOnOver("x-layout-mini-over");this.collapsedEl.addClassOnOver("x-layout-collapsed-over");this.collapsedEl.on("click",this.onExpandClick,this,{stopEvent:true})}else{if(this.collapsible!==false&&!this.hideCollapseTool){var a=this.expandToolEl=this.toolTemplate.append(this.collapsedEl.dom,{id:"expand-"+this.position},true);a.addClassOnOver("x-tool-expand-"+this.position+"-over");a.on("click",this.onExpandClick,this,{stopEvent:true})}if(this.floatable!==false||this.titleCollapse){this.collapsedEl.addClassOnOver("x-layout-collapsed-over");this.collapsedEl.on("click",this[this.floatable?"collapseClick":"onExpandClick"],this)}}}return this.collapsedEl},onExpandClick:function(a){if(this.isSlid){this.panel.expand(false)}else{this.panel.expand()}},onCollapseClick:function(a){this.panel.collapse()},beforeCollapse:function(c,a){this.lastAnim=a;if(this.splitEl){this.splitEl.hide()}this.getCollapsedEl().show();var b=this.panel.getEl();this.originalZIndex=b.getStyle("z-index");b.setStyle("z-index",100);this.isCollapsed=true;this.layout.layout()},onCollapse:function(a){this.panel.el.setStyle("z-index",1);if(this.lastAnim===false||this.panel.animCollapse===false){this.getCollapsedEl().dom.style.visibility="visible"}else{this.getCollapsedEl().slideIn(this.panel.slideAnchor,{duration:0.2})}this.state.collapsed=true;this.panel.saveState()},beforeExpand:function(a){if(this.isSlid){this.afterSlideIn()}var b=this.getCollapsedEl();this.el.show();if(this.position=="east"||this.position=="west"){this.panel.setSize(undefined,b.getHeight())}else{this.panel.setSize(b.getWidth(),undefined)}b.hide();b.dom.style.visibility="hidden";this.panel.el.setStyle("z-index",this.floatingZIndex)},onExpand:function(){this.isCollapsed=false;if(this.splitEl){this.splitEl.show()}this.layout.layout();this.panel.el.setStyle("z-index",this.originalZIndex);this.state.collapsed=false;this.panel.saveState()},collapseClick:function(a){if(this.isSlid){a.stopPropagation();this.slideIn()}else{a.stopPropagation();this.slideOut()}},onHide:function(){if(this.isCollapsed){this.getCollapsedEl().hide()}else{if(this.splitEl){this.splitEl.hide()}}},onShow:function(){if(this.isCollapsed){this.getCollapsedEl().show()}else{if(this.splitEl){this.splitEl.show()}}},isVisible:function(){return !this.panel.hidden},getMargins:function(){return this.isCollapsed&&this.cmargins?this.cmargins:this.margins},getSize:function(){return this.isCollapsed?this.getCollapsedEl().getSize():this.panel.getSize()},setPanel:function(a){this.panel=a},getMinWidth:function(){return this.minWidth},getMinHeight:function(){return this.minHeight},applyLayoutCollapsed:function(a){var b=this.getCollapsedEl();b.setLeftTop(a.x,a.y);b.setSize(a.width,a.height)},applyLayout:function(a){if(this.isCollapsed){this.applyLayoutCollapsed(a)}else{this.panel.setPosition(a.x,a.y);this.panel.setSize(a.width,a.height)}},beforeSlide:function(){this.panel.beforeEffect()},afterSlide:function(){this.panel.afterEffect()},initAutoHide:function(){if(this.autoHide!==false){if(!this.autoHideHd){this.autoHideSlideTask=new Ext.util.DelayedTask(this.slideIn,this);this.autoHideHd={mouseout:function(a){if(!a.within(this.el,true)){this.autoHideSlideTask.delay(500)}},mouseover:function(a){this.autoHideSlideTask.cancel()},scope:this}}this.el.on(this.autoHideHd);this.collapsedEl.on(this.autoHideHd)}},clearAutoHide:function(){if(this.autoHide!==false){this.el.un("mouseout",this.autoHideHd.mouseout);this.el.un("mouseover",this.autoHideHd.mouseover);this.collapsedEl.un("mouseout",this.autoHideHd.mouseout);this.collapsedEl.un("mouseover",this.autoHideHd.mouseover)}},clearMonitor:function(){Ext.getDoc().un("click",this.slideInIf,this)},slideOut:function(){if(this.isSlid||this.el.hasActiveFx()){return}this.isSlid=true;var b=this.panel.tools,c,a;if(b&&b.toggle){b.toggle.hide()}this.el.show();a=this.panel.collapsed;this.panel.collapsed=false;if(this.position=="east"||this.position=="west"){c=this.panel.deferHeight;this.panel.deferHeight=false;this.panel.setSize(undefined,this.collapsedEl.getHeight());this.panel.deferHeight=c}else{this.panel.setSize(this.collapsedEl.getWidth(),undefined)}this.panel.collapsed=a;this.restoreLT=[this.el.dom.style.left,this.el.dom.style.top];this.el.alignTo(this.collapsedEl,this.getCollapseAnchor());this.el.setStyle("z-index",this.floatingZIndex+2);this.panel.el.replaceClass("x-panel-collapsed","x-panel-floating");if(this.animFloat!==false){this.beforeSlide();this.el.slideIn(this.getSlideAnchor(),{callback:function(){this.afterSlide();this.initAutoHide();Ext.getDoc().on("click",this.slideInIf,this)},scope:this,block:true})}else{this.initAutoHide();Ext.getDoc().on("click",this.slideInIf,this)}},afterSlideIn:function(){this.clearAutoHide();this.isSlid=false;this.clearMonitor();this.el.setStyle("z-index","");this.panel.el.replaceClass("x-panel-floating","x-panel-collapsed");this.el.dom.style.left=this.restoreLT[0];this.el.dom.style.top=this.restoreLT[1];var a=this.panel.tools;if(a&&a.toggle){a.toggle.show()}},slideIn:function(a){if(!this.isSlid||this.el.hasActiveFx()){Ext.callback(a);return}this.isSlid=false;if(this.animFloat!==false){this.beforeSlide();this.el.slideOut(this.getSlideAnchor(),{callback:function(){this.el.hide();this.afterSlide();this.afterSlideIn();Ext.callback(a)},scope:this,block:true})}else{this.el.hide();this.afterSlideIn()}},slideInIf:function(a){if(!a.within(this.el)){this.slideIn()}},anchors:{west:"left",east:"right",north:"top",south:"bottom"},sanchors:{west:"l",east:"r",north:"t",south:"b"},canchors:{west:"tl-tr",east:"tr-tl",north:"tl-bl",south:"bl-tl"},getAnchor:function(){return this.anchors[this.position]},getCollapseAnchor:function(){return this.canchors[this.position]},getSlideAnchor:function(){return this.sanchors[this.position]},getAlignAdj:function(){var a=this.cmargins;switch(this.position){case"west":return[0,0];break;case"east":return[0,0];break;case"north":return[0,0];break;case"south":return[0,0];break}},getExpandAdj:function(){var b=this.collapsedEl,a=this.cmargins;switch(this.position){case"west":return[-(a.right+b.getWidth()+a.left),0];break;case"east":return[a.right+b.getWidth()+a.left,0];break;case"north":return[0,-(a.top+a.bottom+b.getHeight())];break;case"south":return[0,a.top+a.bottom+b.getHeight()];break}},destroy:function(){if(this.autoHideSlideTask&&this.autoHideSlideTask.cancel){this.autoHideSlideTask.cancel()}Ext.destroyMembers(this,"miniCollapsedEl","collapsedEl","expandToolEl")}};Ext.layout.BorderLayout.SplitRegion=function(b,a,c){Ext.layout.BorderLayout.SplitRegion.superclass.constructor.call(this,b,a,c);this.applyLayout=this.applyFns[c]};Ext.extend(Ext.layout.BorderLayout.SplitRegion,Ext.layout.BorderLayout.Region,{splitTip:"Drag to resize.",collapsibleSplitTip:"Drag to resize. Double click to hide.",useSplitTips:false,splitSettings:{north:{orientation:Ext.SplitBar.VERTICAL,placement:Ext.SplitBar.TOP,maxFn:"getVMaxSize",minProp:"minHeight",maxProp:"maxHeight"},south:{orientation:Ext.SplitBar.VERTICAL,placement:Ext.SplitBar.BOTTOM,maxFn:"getVMaxSize",minProp:"minHeight",maxProp:"maxHeight"},east:{orientation:Ext.SplitBar.HORIZONTAL,placement:Ext.SplitBar.RIGHT,maxFn:"getHMaxSize",minProp:"minWidth",maxProp:"maxWidth"},west:{orientation:Ext.SplitBar.HORIZONTAL,placement:Ext.SplitBar.LEFT,maxFn:"getHMaxSize",minProp:"minWidth",maxProp:"maxWidth"}},applyFns:{west:function(c){if(this.isCollapsed){return this.applyLayoutCollapsed(c)}var d=this.splitEl.dom,b=d.style;this.panel.setPosition(c.x,c.y);var a=d.offsetWidth;b.left=(c.x+c.width-a)+"px";b.top=(c.y)+"px";b.height=Math.max(0,c.height)+"px";this.panel.setSize(c.width-a,c.height)},east:function(c){if(this.isCollapsed){return this.applyLayoutCollapsed(c)}var d=this.splitEl.dom,b=d.style;var a=d.offsetWidth;this.panel.setPosition(c.x+a,c.y);b.left=(c.x)+"px";b.top=(c.y)+"px";b.height=Math.max(0,c.height)+"px";this.panel.setSize(c.width-a,c.height)},north:function(c){if(this.isCollapsed){return this.applyLayoutCollapsed(c)}var d=this.splitEl.dom,b=d.style;var a=d.offsetHeight;this.panel.setPosition(c.x,c.y);b.left=(c.x)+"px";b.top=(c.y+c.height-a)+"px";b.width=Math.max(0,c.width)+"px";this.panel.setSize(c.width,c.height-a)},south:function(c){if(this.isCollapsed){return this.applyLayoutCollapsed(c)}var d=this.splitEl.dom,b=d.style;var a=d.offsetHeight;this.panel.setPosition(c.x,c.y+a);b.left=(c.x)+"px";b.top=(c.y)+"px";b.width=Math.max(0,c.width)+"px";this.panel.setSize(c.width,c.height-a)}},render:function(a,c){Ext.layout.BorderLayout.SplitRegion.superclass.render.call(this,a,c);var d=this.position;this.splitEl=a.createChild({cls:"x-layout-split x-layout-split-"+d,html:" ",id:this.panel.id+"-xsplit"});if(this.collapseMode=="mini"){this.miniSplitEl=this.splitEl.createChild({cls:"x-layout-mini x-layout-mini-"+d,html:" "});this.miniSplitEl.addClassOnOver("x-layout-mini-over");this.miniSplitEl.on("click",this.onCollapseClick,this,{stopEvent:true})}var b=this.splitSettings[d];this.split=new Ext.SplitBar(this.splitEl.dom,c.el,b.orientation);this.split.tickSize=this.tickSize;this.split.placement=b.placement;this.split.getMaximumSize=this[b.maxFn].createDelegate(this);this.split.minSize=this.minSize||this[b.minProp];this.split.on("beforeapply",this.onSplitMove,this);this.split.useShim=this.useShim===true;this.maxSize=this.maxSize||this[b.maxProp];if(c.hidden){this.splitEl.hide()}if(this.useSplitTips){this.splitEl.dom.title=this.collapsible?this.collapsibleSplitTip:this.splitTip}if(this.collapsible){this.splitEl.on("dblclick",this.onCollapseClick,this)}},getSize:function(){if(this.isCollapsed){return this.collapsedEl.getSize()}var a=this.panel.getSize();if(this.position=="north"||this.position=="south"){a.height+=this.splitEl.dom.offsetHeight}else{a.width+=this.splitEl.dom.offsetWidth}return a},getHMaxSize:function(){var b=this.maxSize||10000;var a=this.layout.center;return Math.min(b,(this.el.getWidth()+a.el.getWidth())-a.getMinWidth())},getVMaxSize:function(){var b=this.maxSize||10000;var a=this.layout.center;return Math.min(b,(this.el.getHeight()+a.el.getHeight())-a.getMinHeight())},onSplitMove:function(b,a){var c=this.panel.getSize();this.lastSplitSize=a;if(this.position=="north"||this.position=="south"){this.panel.setSize(c.width,a);this.state.height=a}else{this.panel.setSize(a,c.height);this.state.width=a}this.layout.layout();this.panel.saveState();return false},getSplitBar:function(){return this.split},destroy:function(){Ext.destroy(this.miniSplitEl,this.split,this.splitEl);Ext.layout.BorderLayout.SplitRegion.superclass.destroy.call(this)}});Ext.Container.LAYOUTS.border=Ext.layout.BorderLayout;Ext.layout.FormLayout=Ext.extend(Ext.layout.AnchorLayout,{labelSeparator:":",trackLabels:true,type:"form",onRemove:function(d){Ext.layout.FormLayout.superclass.onRemove.call(this,d);if(this.trackLabels){d.un("show",this.onFieldShow,this);d.un("hide",this.onFieldHide,this)}var b=d.getPositionEl(),a=d.getItemCt&&d.getItemCt();if(d.rendered&&a){if(b&&b.dom){b.insertAfter(a)}Ext.destroy(a);Ext.destroyMembers(d,"label","itemCt");if(d.customItemCt){Ext.destroyMembers(d,"getItemCt","customItemCt")}}},setContainer:function(a){Ext.layout.FormLayout.superclass.setContainer.call(this,a);if(a.labelAlign){a.addClass("x-form-label-"+a.labelAlign)}if(a.hideLabels){Ext.apply(this,{labelStyle:"display:none",elementStyle:"padding-left:0;",labelAdjust:0})}else{this.labelSeparator=Ext.isDefined(a.labelSeparator)?a.labelSeparator:this.labelSeparator;a.labelWidth=a.labelWidth||100;if(Ext.isNumber(a.labelWidth)){var b=Ext.isNumber(a.labelPad)?a.labelPad:5;Ext.apply(this,{labelAdjust:a.labelWidth+b,labelStyle:"width:"+a.labelWidth+"px;",elementStyle:"padding-left:"+(a.labelWidth+b)+"px"})}if(a.labelAlign=="top"){Ext.apply(this,{labelStyle:"width:auto;",labelAdjust:0,elementStyle:"padding-left:0;"})}}},isHide:function(a){return a.hideLabel||this.container.hideLabels},onFieldShow:function(a){a.getItemCt().removeClass("x-hide-"+a.hideMode);if(a.isComposite){a.doLayout()}},onFieldHide:function(a){a.getItemCt().addClass("x-hide-"+a.hideMode)},getLabelStyle:function(e){var b="",c=[this.labelStyle,e];for(var d=0,a=c.length;d=b)||(this.cells[c]&&this.cells[c][a])){if(b&&a>=b){c++;a=0}else{a++}}return[a,c]},renderItem:function(e,a,d){if(!this.table){this.table=d.createChild(Ext.apply({tag:"table",cls:"x-table-layout",cellspacing:0,cn:{tag:"tbody"}},this.tableAttrs),null,true)}if(e&&!e.rendered){e.render(this.getNextCell(e));this.configureItem(e)}else{if(e&&!this.isValidParent(e,d)){var b=this.getNextCell(e);b.insertBefore(e.getPositionEl().dom,null);e.container=Ext.get(b);this.configureItem(e)}}},isValidParent:function(b,a){return b.getPositionEl().up("table",5).dom.parentNode===(a.dom||a)},destroy:function(){delete this.table;Ext.layout.TableLayout.superclass.destroy.call(this)}});Ext.Container.LAYOUTS.table=Ext.layout.TableLayout;Ext.layout.AbsoluteLayout=Ext.extend(Ext.layout.AnchorLayout,{extraCls:"x-abs-layout-item",type:"absolute",onLayout:function(a,b){b.position();this.paddingLeft=b.getPadding("l");this.paddingTop=b.getPadding("t");Ext.layout.AbsoluteLayout.superclass.onLayout.call(this,a,b)},adjustWidthAnchor:function(b,a){return b?b-a.getPosition(true)[0]+this.paddingLeft:b},adjustHeightAnchor:function(b,a){return b?b-a.getPosition(true)[1]+this.paddingTop:b}});Ext.Container.LAYOUTS.absolute=Ext.layout.AbsoluteLayout;Ext.layout.BoxLayout=Ext.extend(Ext.layout.ContainerLayout,{defaultMargins:{left:0,top:0,right:0,bottom:0},padding:"0",pack:"start",monitorResize:true,type:"box",scrollOffset:0,extraCls:"x-box-item",targetCls:"x-box-layout-ct",innerCls:"x-box-inner",constructor:function(a){Ext.layout.BoxLayout.superclass.constructor.call(this,a);if(Ext.isString(this.defaultMargins)){this.defaultMargins=this.parseMargins(this.defaultMargins)}var d=this.overflowHandler;if(typeof d=="string"){d={type:d}}var c="none";if(d&&d.type!=undefined){c=d.type}var b=Ext.layout.boxOverflow[c];if(b[this.type]){b=b[this.type]}this.overflowHandler=new b(this,d)},onLayout:function(b,h){Ext.layout.BoxLayout.superclass.onLayout.call(this,b,h);var d=this.getLayoutTargetSize(),i=this.getVisibleItems(b),c=this.calculateChildBoxes(i,d),g=c.boxes,j=c.meta;if(d.width>0){var k=this.overflowHandler,a=j.tooNarrow?"handleOverflow":"clearOverflow";var e=k[a](c,d);if(e){if(e.targetSize){d=e.targetSize}if(e.recalculate){i=this.getVisibleItems(b);c=this.calculateChildBoxes(i,d);g=c.boxes}}}this.layoutTargetLastSize=d;this.childBoxCache=c;this.updateInnerCtSize(d,c);this.updateChildBoxes(g);this.handleTargetOverflow(d,b,h)},updateChildBoxes:function(c){for(var b=0,e=c.length;b(None)
    ',constructor:function(a){Ext.layout.boxOverflow.Menu.superclass.constructor.apply(this,arguments);this.menuItems=[]},createInnerElements:function(){if(!this.afterCt){this.afterCt=this.layout.innerCt.insertSibling({cls:this.afterCls},"before")}},clearOverflow:function(a,g){var e=g.width+(this.afterCt?this.afterCt.getWidth():0),b=this.menuItems;this.hideTrigger();for(var c=0,d=b.length;ci.width;return l}},handleOverflow:function(d,h){this.showTrigger();var k=h.width-this.afterCt.getWidth(),l=d.boxes,e=0,r=false;for(var o=0,c=l.length;o=0;j--){var q=l[j].component,p=l[j].left+l[j].width;if(p>=k){this.menuItems.unshift({component:q,width:l[j].width});q.hide()}else{break}}}if(this.menuItems.length==0){this.hideTrigger()}return{targetSize:{height:h.height,width:k},recalculate:r}}});Ext.layout.boxOverflow.menu.hbox=Ext.layout.boxOverflow.HorizontalMenu;Ext.layout.boxOverflow.Scroller=Ext.extend(Ext.layout.boxOverflow.None,{animateScroll:true,scrollIncrement:100,wheelIncrement:3,scrollRepeatInterval:400,scrollDuration:0.4,beforeCls:"x-strip-left",afterCls:"x-strip-right",scrollerCls:"x-strip-scroller",beforeScrollerCls:"x-strip-scroller-left",afterScrollerCls:"x-strip-scroller-right",createWheelListener:function(){this.layout.innerCt.on({scope:this,mousewheel:function(a){a.stopEvent();this.scrollBy(a.getWheelDelta()*this.wheelIncrement*-1,false)}})},handleOverflow:function(a,b){this.createInnerElements();this.showScrollers()},clearOverflow:function(){this.hideScrollers()},showScrollers:function(){this.createScrollers();this.beforeScroller.show();this.afterScroller.show();this.updateScrollButtons()},hideScrollers:function(){if(this.beforeScroller!=undefined){this.beforeScroller.hide();this.afterScroller.hide()}},createScrollers:function(){if(!this.beforeScroller&&!this.afterScroller){var a=this.beforeCt.createChild({cls:String.format("{0} {1} ",this.scrollerCls,this.beforeScrollerCls)});var b=this.afterCt.createChild({cls:String.format("{0} {1}",this.scrollerCls,this.afterScrollerCls)});a.addClassOnOver(this.beforeScrollerCls+"-hover");b.addClassOnOver(this.afterScrollerCls+"-hover");a.setVisibilityMode(Ext.Element.DISPLAY);b.setVisibilityMode(Ext.Element.DISPLAY);this.beforeRepeater=new Ext.util.ClickRepeater(a,{interval:this.scrollRepeatInterval,handler:this.scrollLeft,scope:this});this.afterRepeater=new Ext.util.ClickRepeater(b,{interval:this.scrollRepeatInterval,handler:this.scrollRight,scope:this});this.beforeScroller=a;this.afterScroller=b}},destroy:function(){Ext.destroy(this.beforeScroller,this.afterScroller,this.beforeRepeater,this.afterRepeater,this.beforeCt,this.afterCt)},scrollBy:function(b,a){this.scrollTo(this.getScrollPosition()+b,a)},getItem:function(a){if(Ext.isString(a)){a=Ext.getCmp(a)}else{if(Ext.isNumber(a)){a=this.items[a]}}return a},getScrollAnim:function(){return{duration:this.scrollDuration,callback:this.updateScrollButtons,scope:this}},updateScrollButtons:function(){if(this.beforeScroller==undefined||this.afterScroller==undefined){return}var d=this.atExtremeBefore()?"addClass":"removeClass",c=this.atExtremeAfter()?"addClass":"removeClass",a=this.beforeScrollerCls+"-disabled",b=this.afterScrollerCls+"-disabled";this.beforeScroller[d](a);this.afterScroller[c](b);this.scrolling=false},atExtremeBefore:function(){return this.getScrollPosition()===0},scrollLeft:function(a){this.scrollBy(-this.scrollIncrement,a)},scrollRight:function(a){this.scrollBy(this.scrollIncrement,a)},scrollToItem:function(d,b){d=this.getItem(d);if(d!=undefined){var a=this.getItemVisibility(d);if(!a.fullyVisible){var c=d.getBox(true,true),e=c.x;if(a.hiddenRight){e-=(this.layout.innerCt.getWidth()-c.width)}this.scrollTo(e,b)}}},getItemVisibility:function(e){var d=this.getItem(e).getBox(true,true),a=d.x,c=d.x+d.width,g=this.getScrollPosition(),b=this.layout.innerCt.getWidth()+g;return{hiddenLeft:ab,fullyVisible:a>g&&c=this.getMaxScrollBottom()}});Ext.layout.boxOverflow.scroller.vbox=Ext.layout.boxOverflow.VerticalScroller;Ext.layout.boxOverflow.HorizontalScroller=Ext.extend(Ext.layout.boxOverflow.Scroller,{handleOverflow:function(a,b){Ext.layout.boxOverflow.HorizontalScroller.superclass.handleOverflow.apply(this,arguments);return{targetSize:{height:b.height,width:b.width-(this.beforeCt.getWidth()+this.afterCt.getWidth())}}},createInnerElements:function(){var a=this.layout.innerCt;if(!this.beforeCt){this.afterCt=a.insertSibling({cls:this.afterCls},"before");this.beforeCt=a.insertSibling({cls:this.beforeCls},"before");this.createWheelListener()}},scrollTo:function(a,b){var d=this.getScrollPosition(),c=a.constrain(0,this.getMaxScrollRight());if(c!=d&&!this.scrolling){if(b==undefined){b=this.animateScroll}this.layout.innerCt.scrollTo("left",c,b?this.getScrollAnim():false);if(b){this.scrolling=true}else{this.scrolling=false;this.updateScrollButtons()}}},getScrollPosition:function(){return parseInt(this.layout.innerCt.dom.scrollLeft,10)||0},getMaxScrollRight:function(){return this.layout.innerCt.dom.scrollWidth-this.layout.innerCt.getWidth()},atExtremeAfter:function(){return this.getScrollPosition()>=this.getMaxScrollRight()}});Ext.layout.boxOverflow.scroller.hbox=Ext.layout.boxOverflow.HorizontalScroller;Ext.layout.HBoxLayout=Ext.extend(Ext.layout.BoxLayout,{align:"top",type:"hbox",calculateChildBoxes:function(r,b){var F=r.length,R=this.padding,D=R.top,U=R.left,y=D+R.bottom,O=U+R.right,a=b.width-this.scrollOffset,e=b.height,o=Math.max(0,e-y),P=this.pack=="start",W=this.pack=="center",A=this.pack=="end",L=0,Q=0,T=0,l=0,X=0,H=[],k,J,M,V,w,j,S,I,c,x,q,N;for(S=0;Sa;var n=Math.max(0,a-L-O);if(p){for(S=0;S0){var C=[];for(var E=0,v=F;Ei.available?1:-1});for(var S=0,v=C.length;S0){I.top=D+q+(z/2)}}U+=I.width+w.right}return{boxes:H,meta:{maxHeight:Q,nonFlexWidth:L,desiredWidth:l,minimumWidth:X,shortfall:l-a,tooNarrow:p}}}});Ext.Container.LAYOUTS.hbox=Ext.layout.HBoxLayout;Ext.layout.VBoxLayout=Ext.extend(Ext.layout.BoxLayout,{align:"left",type:"vbox",calculateChildBoxes:function(o,b){var E=o.length,R=this.padding,C=R.top,V=R.left,x=C+R.bottom,O=V+R.right,a=b.width-this.scrollOffset,c=b.height,K=Math.max(0,a-O),P=this.pack=="start",X=this.pack=="center",z=this.pack=="end",k=0,u=0,U=0,L=0,m=0,G=[],h,I,N,W,t,g,T,H,S,w,n,d,r;for(T=0;Tc;var q=Math.max(0,(c-k-x));if(l){for(T=0,r=E;T0){var J=[];for(var D=0,r=E;Di.available?1:-1});for(var T=0,r=J.length;T0){H.left=V+w+(y/2)}}C+=H.height+t.bottom}return{boxes:G,meta:{maxWidth:u,nonFlexHeight:k,desiredHeight:L,minimumHeight:m,shortfall:L-c,tooNarrow:l}}}});Ext.Container.LAYOUTS.vbox=Ext.layout.VBoxLayout;Ext.layout.ToolbarLayout=Ext.extend(Ext.layout.ContainerLayout,{monitorResize:true,type:"toolbar",triggerWidth:18,noItemsMenuText:'
    (None)
    ',lastOverflow:false,tableHTML:['',"","",'",'","","","
    ','',"",'',"","
    ","
    ','',"","","","","","","
    ",'',"",'',"","
    ","
    ",'',"",'',"","
    ","
    ","
    "].join(""),onLayout:function(e,j){if(!this.leftTr){var h=e.buttonAlign=="center"?"center":"left";j.addClass("x-toolbar-layout-ct");j.insertHtml("beforeEnd",String.format(this.tableHTML,h));this.leftTr=j.child("tr.x-toolbar-left-row",true);this.rightTr=j.child("tr.x-toolbar-right-row",true);this.extrasTr=j.child("tr.x-toolbar-extras-row",true);if(this.hiddenItem==undefined){this.hiddenItems=[]}}var k=e.buttonAlign=="right"?this.rightTr:this.leftTr,l=e.items.items,d=0;for(var b=0,g=l.length,m;b=0&&(d=e[a]);a--){if(!d.firstChild){b.removeChild(d)}}},insertCell:function(e,b,a){var d=document.createElement("td");d.className="x-toolbar-cell";b.insertBefore(d,b.childNodes[a]||null);return d},hideItem:function(a){this.hiddenItems.push(a);a.xtbHidden=true;a.xtbWidth=a.getPositionEl().dom.parentNode.offsetWidth;a.hide()},unhideItem:function(a){a.show();a.xtbHidden=false;this.hiddenItems.remove(a)},getItemWidth:function(a){return a.hidden?(a.xtbWidth||0):a.getPositionEl().dom.parentNode.offsetWidth},fitToSize:function(k){if(this.container.enableOverflow===false){return}var b=k.dom.clientWidth,j=k.dom.firstChild.offsetWidth,m=b-this.triggerWidth,a=this.lastWidth||0,c=this.hiddenItems,e=c.length!=0,n=b>=a;this.lastWidth=b;if(j>b||(e&&n)){var l=this.container.items.items,h=l.length,d=0,o;for(var g=0;gm){if(!(o.hidden||o.xtbHidden)){this.hideItem(o)}}else{if(o.xtbHidden){this.unhideItem(o)}}}}}e=c.length!=0;if(e){this.initMore();if(!this.lastOverflow){this.container.fireEvent("overflowchange",this.container,true);this.lastOverflow=true}}else{if(this.more){this.clearMenu();this.more.destroy();delete this.more;if(this.lastOverflow){this.container.fireEvent("overflowchange",this.container,false);this.lastOverflow=false}}}},createMenuConfig:function(c,a){var b=Ext.apply({},c.initialConfig),d=c.toggleGroup;Ext.copyTo(b,c,["iconCls","icon","itemId","disabled","handler","scope","menu"]);Ext.apply(b,{text:c.overflowText||c.text,hideOnClick:a});if(d||c.enableToggle){Ext.apply(b,{group:d,checked:c.pressed,listeners:{checkchange:function(g,e){c.toggle(e)}}})}delete b.ownerCt;delete b.xtype;delete b.id;return b},addComponentToMenu:function(b,a){if(a instanceof Ext.Toolbar.Separator){b.add("-")}else{if(Ext.isFunction(a.isXType)){if(a.isXType("splitbutton")){b.add(this.createMenuConfig(a,true))}else{if(a.isXType("button")){b.add(this.createMenuConfig(a,!a.menu))}else{if(a.isXType("buttongroup")){a.items.each(function(c){this.addComponentToMenu(b,c)},this)}}}}}},clearMenu:function(){var a=this.moreMenu;if(a&&a.items){a.items.each(function(b){delete b.menu})}},beforeMoreShow:function(h){var b=this.container.items.items,a=b.length,g,e;var c=function(j,i){return j.isXType("buttongroup")&&!(i instanceof Ext.Toolbar.Separator)};this.clearMenu();h.removeAll();for(var d=0;d','','{altText}',"","")}if(g&&!g.rendered){if(Ext.isNumber(b)){b=e.dom.childNodes[b]}var d=this.getItemArgs(g);g.render(g.positionEl=b?this.itemTpl.insertBefore(b,d,true):this.itemTpl.append(e,d,true));g.positionEl.menuItemId=g.getItemId();if(!d.isMenuItem&&d.needsIcon){g.positionEl.addClass("x-menu-list-item-indent")}this.configureItem(g)}else{if(g&&!this.isValidParent(g,e)){if(Ext.isNumber(b)){b=e.dom.childNodes[b]}e.dom.insertBefore(g.getActionEl().dom,b||null)}}},getItemArgs:function(d){var a=d instanceof Ext.menu.Item,b=!(a||d instanceof Ext.menu.Separator);return{isMenuItem:a,needsIcon:b&&(d.icon||d.iconCls),icon:d.icon||Ext.BLANK_IMAGE_URL,iconCls:"x-menu-item-icon "+(d.iconCls||""),itemId:"x-menu-el-"+d.id,itemCls:"x-menu-list-item ",altText:d.altText||""}},isValidParent:function(b,a){return b.el.up("li.x-menu-list-item",5).dom.parentNode===(a.dom||a)},onLayout:function(a,b){Ext.layout.MenuLayout.superclass.onLayout.call(this,a,b);this.doAutoSize()},doAutoSize:function(){var c=this.container,a=c.width;if(c.floating){if(a){c.setWidth(a)}else{if(Ext.isIE){c.setWidth(Ext.isStrict&&(Ext.isIE7||Ext.isIE8||Ext.isIE9)?"auto":c.minWidth);var d=c.getEl(),b=d.dom.offsetWidth;c.setWidth(c.getLayoutTarget().getWidth()+d.getFrameWidth("lr"))}}}}});Ext.Container.LAYOUTS.menu=Ext.layout.MenuLayout;Ext.Viewport=Ext.extend(Ext.Container,{initComponent:function(){Ext.Viewport.superclass.initComponent.call(this);document.getElementsByTagName("html")[0].className+=" x-viewport";this.el=Ext.getBody();this.el.setHeight=Ext.emptyFn;this.el.setWidth=Ext.emptyFn;this.el.setSize=Ext.emptyFn;this.el.dom.scroll="no";this.allowDomMove=false;this.autoWidth=true;this.autoHeight=true;Ext.EventManager.onWindowResize(this.fireResize,this);this.renderTo=this.el},fireResize:function(a,b){this.fireEvent("resize",this,a,b,a,b)}});Ext.reg("viewport",Ext.Viewport);Ext.Panel=Ext.extend(Ext.Container,{baseCls:"x-panel",collapsedCls:"x-panel-collapsed",maskDisabled:true,animCollapse:Ext.enableFx,headerAsText:true,buttonAlign:"right",collapsed:false,collapseFirst:true,minButtonWidth:75,elements:"body",preventBodyReset:false,padding:undefined,resizeEvent:"bodyresize",toolTarget:"header",collapseEl:"bwrap",slideAnchor:"t",disabledClass:"",deferHeight:true,expandDefaults:{duration:0.25},collapseDefaults:{duration:0.25},initComponent:function(){Ext.Panel.superclass.initComponent.call(this);this.addEvents("bodyresize","titlechange","iconchange","collapse","expand","beforecollapse","beforeexpand","beforeclose","close","activate","deactivate");if(this.unstyled){this.baseCls="x-plain"}this.toolbars=[];if(this.tbar){this.elements+=",tbar";this.topToolbar=this.createToolbar(this.tbar);this.tbar=null}if(this.bbar){this.elements+=",bbar";this.bottomToolbar=this.createToolbar(this.bbar);this.bbar=null}if(this.header===true){this.elements+=",header";this.header=null}else{if(this.headerCfg||(this.title&&this.header!==false)){this.elements+=",header"}}if(this.footerCfg||this.footer===true){this.elements+=",footer";this.footer=null}if(this.buttons){this.fbar=this.buttons;this.buttons=null}if(this.fbar){this.createFbar(this.fbar)}if(this.autoLoad){this.on("render",this.doAutoLoad,this,{delay:10})}},createFbar:function(b){var a=this.minButtonWidth;this.elements+=",footer";this.fbar=this.createToolbar(b,{buttonAlign:this.buttonAlign,toolbarCls:"x-panel-fbar",enableOverflow:false,defaults:function(d){return{minWidth:d.minWidth||a}}});this.fbar.items.each(function(d){d.minWidth=d.minWidth||this.minButtonWidth},this);this.buttons=this.fbar.items.items},createToolbar:function(b,c){var a;if(Ext.isArray(b)){b={items:b}}a=b.events?Ext.apply(b,c):this.createComponent(Ext.apply({},b,c),"toolbar");this.toolbars.push(a);return a},createElement:function(a,c){if(this[a]){c.appendChild(this[a].dom);return}if(a==="bwrap"||this.elements.indexOf(a)!=-1){if(this[a+"Cfg"]){this[a]=Ext.fly(c).createChild(this[a+"Cfg"])}else{var b=document.createElement("div");b.className=this[a+"Cls"];this[a]=Ext.get(c.appendChild(b))}if(this[a+"CssClass"]){this[a].addClass(this[a+"CssClass"])}if(this[a+"Style"]){this[a].applyStyles(this[a+"Style"])}}},onRender:function(g,e){Ext.Panel.superclass.onRender.call(this,g,e);this.createClasses();var a=this.el,h=a.dom,k,i;if(this.collapsible&&!this.hideCollapseTool){this.tools=this.tools?this.tools.slice(0):[];this.tools[this.collapseFirst?"unshift":"push"]({id:"toggle",handler:this.toggleCollapse,scope:this})}if(this.tools){i=this.tools;this.elements+=(this.header!==false)?",header":""}this.tools={};a.addClass(this.baseCls);if(h.firstChild){this.header=a.down("."+this.headerCls);this.bwrap=a.down("."+this.bwrapCls);var j=this.bwrap?this.bwrap:a;this.tbar=j.down("."+this.tbarCls);this.body=j.down("."+this.bodyCls);this.bbar=j.down("."+this.bbarCls);this.footer=j.down("."+this.footerCls);this.fromMarkup=true}if(this.preventBodyReset===true){a.addClass("x-panel-reset")}if(this.cls){a.addClass(this.cls)}if(this.buttons){this.elements+=",footer"}if(this.frame){a.insertHtml("afterBegin",String.format(Ext.Element.boxMarkup,this.baseCls));this.createElement("header",h.firstChild.firstChild.firstChild);this.createElement("bwrap",h);k=this.bwrap.dom;var c=h.childNodes[1],b=h.childNodes[2];k.appendChild(c);k.appendChild(b);var l=k.firstChild.firstChild.firstChild;this.createElement("tbar",l);this.createElement("body",l);this.createElement("bbar",l);this.createElement("footer",k.lastChild.firstChild.firstChild);if(!this.footer){this.bwrap.dom.lastChild.className+=" x-panel-nofooter"}this.ft=Ext.get(this.bwrap.dom.lastChild);this.mc=Ext.get(l)}else{this.createElement("header",h);this.createElement("bwrap",h);k=this.bwrap.dom;this.createElement("tbar",k);this.createElement("body",k);this.createElement("bbar",k);this.createElement("footer",k);if(!this.header){this.body.addClass(this.bodyCls+"-noheader");if(this.tbar){this.tbar.addClass(this.tbarCls+"-noheader")}}}if(Ext.isDefined(this.padding)){this.body.setStyle("padding",this.body.addUnits(this.padding))}if(this.border===false){this.el.addClass(this.baseCls+"-noborder");this.body.addClass(this.bodyCls+"-noborder");if(this.header){this.header.addClass(this.headerCls+"-noborder")}if(this.footer){this.footer.addClass(this.footerCls+"-noborder")}if(this.tbar){this.tbar.addClass(this.tbarCls+"-noborder")}if(this.bbar){this.bbar.addClass(this.bbarCls+"-noborder")}}if(this.bodyBorder===false){this.body.addClass(this.bodyCls+"-noborder")}this.bwrap.enableDisplayMode("block");if(this.header){this.header.unselectable();if(this.headerAsText){this.header.dom.innerHTML=''+this.header.dom.innerHTML+"";if(this.iconCls){this.setIconClass(this.iconCls)}}}if(this.floating){this.makeFloating(this.floating)}if(this.collapsible&&this.titleCollapse&&this.header){this.mon(this.header,"click",this.toggleCollapse,this);this.header.setStyle("cursor","pointer")}if(i){this.addTool.apply(this,i)}if(this.fbar){this.footer.addClass("x-panel-btns");this.fbar.ownerCt=this;this.fbar.render(this.footer);this.footer.createChild({cls:"x-clear"})}if(this.tbar&&this.topToolbar){this.topToolbar.ownerCt=this;this.topToolbar.render(this.tbar)}if(this.bbar&&this.bottomToolbar){this.bottomToolbar.ownerCt=this;this.bottomToolbar.render(this.bbar)}},setIconClass:function(b){var a=this.iconCls;this.iconCls=b;if(this.rendered&&this.header){if(this.frame){this.header.addClass("x-panel-icon");this.header.replaceClass(a,this.iconCls)}else{var e=this.header,c=e.child("img.x-panel-inline-icon");if(c){Ext.fly(c).replaceClass(a,this.iconCls)}else{var d=e.child("span."+this.headerTextCls);if(d){Ext.DomHelper.insertBefore(d.dom,{tag:"img",alt:"",src:Ext.BLANK_IMAGE_URL,cls:"x-panel-inline-icon "+this.iconCls})}}}}this.fireEvent("iconchange",this,b,a)},makeFloating:function(a){this.floating=true;this.el=new Ext.Layer(Ext.apply({},a,{shadow:Ext.isDefined(this.shadow)?this.shadow:"sides",shadowOffset:this.shadowOffset,constrain:false,shim:this.shim===false?false:undefined}),this.el)},getTopToolbar:function(){return this.topToolbar},getBottomToolbar:function(){return this.bottomToolbar},getFooterToolbar:function(){return this.fbar},addButton:function(a,c,b){if(!this.fbar){this.createFbar([])}if(c){if(Ext.isString(a)){a={text:a}}a=Ext.apply({handler:c,scope:b},a)}return this.fbar.add(a)},addTool:function(){if(!this.rendered){if(!this.tools){this.tools=[]}Ext.each(arguments,function(a){this.tools.push(a)},this);return}if(!this[this.toolTarget]){return}if(!this.toolTemplate){var h=new Ext.Template('
     
    ');h.disableFormats=true;h.compile();Ext.Panel.prototype.toolTemplate=h}for(var g=0,d=arguments,c=d.length;g0){Ext.each(this.toolbars,function(c){c.doLayout(undefined,a)});this.syncHeight()}},syncHeight:function(){var b=this.toolbarHeight,c=this.body,a=this.lastSize.height,d;if(this.autoHeight||!Ext.isDefined(a)||a=="auto"){return}if(b!=this.getToolbarHeight()){b=Math.max(0,a-this.getFrameHeight());c.setHeight(b);d=c.getSize();this.toolbarHeight=this.getToolbarHeight();this.onBodyResize(d.width,d.height)}},onShow:function(){if(this.floating){return this.el.show()}Ext.Panel.superclass.onShow.call(this)},onHide:function(){if(this.floating){return this.el.hide()}Ext.Panel.superclass.onHide.call(this)},createToolHandler:function(c,a,d,b){return function(g){c.removeClass(d);if(a.stopEvent!==false){g.stopEvent()}if(a.handler){a.handler.call(a.scope||c,g,c,b,a)}}},afterRender:function(){if(this.floating&&!this.hidden){this.el.show()}if(this.title){this.setTitle(this.title)}Ext.Panel.superclass.afterRender.call(this);if(this.collapsed){this.collapsed=false;this.collapse(false)}this.initEvents()},getKeyMap:function(){if(!this.keyMap){this.keyMap=new Ext.KeyMap(this.el,this.keys)}return this.keyMap},initEvents:function(){if(this.keys){this.getKeyMap()}if(this.draggable){this.initDraggable()}if(this.toolbars.length>0){Ext.each(this.toolbars,function(a){a.doLayout();a.on({scope:this,afterlayout:this.syncHeight,remove:this.syncHeight})},this);this.syncHeight()}},initDraggable:function(){this.dd=new Ext.Panel.DD(this,Ext.isBoolean(this.draggable)?null:this.draggable)},beforeEffect:function(a){if(this.floating){this.el.beforeAction()}if(a!==false){this.el.addClass("x-panel-animated")}},afterEffect:function(a){this.syncShadow();this.el.removeClass("x-panel-animated")},createEffect:function(c,b,d){var e={scope:d,block:true};if(c===true){e.callback=b;return e}else{if(!c.callback){e.callback=b}else{e.callback=function(){b.call(d);Ext.callback(c.callback,c.scope)}}}return Ext.applyIf(e,c)},collapse:function(b){if(this.collapsed||this.el.hasFxBlock()||this.fireEvent("beforecollapse",this,b)===false){return}var a=b===true||(b!==false&&this.animCollapse);this.beforeEffect(a);this.onCollapse(a,b);return this},onCollapse:function(a,b){if(a){this[this.collapseEl].slideOut(this.slideAnchor,Ext.apply(this.createEffect(b||true,this.afterCollapse,this),this.collapseDefaults))}else{this[this.collapseEl].hide(this.hideMode);this.afterCollapse(false)}},afterCollapse:function(a){this.collapsed=true;this.el.addClass(this.collapsedCls);if(a!==false){this[this.collapseEl].hide(this.hideMode)}this.afterEffect(a);this.cascade(function(b){if(b.lastSize){b.lastSize={width:undefined,height:undefined}}});this.fireEvent("collapse",this)},expand:function(b){if(!this.collapsed||this.el.hasFxBlock()||this.fireEvent("beforeexpand",this,b)===false){return}var a=b===true||(b!==false&&this.animCollapse);this.el.removeClass(this.collapsedCls);this.beforeEffect(a);this.onExpand(a,b);return this},onExpand:function(a,b){if(a){this[this.collapseEl].slideIn(this.slideAnchor,Ext.apply(this.createEffect(b||true,this.afterExpand,this),this.expandDefaults))}else{this[this.collapseEl].show(this.hideMode);this.afterExpand(false)}},afterExpand:function(a){this.collapsed=false;if(a!==false){this[this.collapseEl].show(this.hideMode)}this.afterEffect(a);if(this.deferLayout){delete this.deferLayout;this.doLayout(true)}this.fireEvent("expand",this)},toggleCollapse:function(a){this[this.collapsed?"expand":"collapse"](a);return this},onDisable:function(){if(this.rendered&&this.maskDisabled){this.el.mask()}Ext.Panel.superclass.onDisable.call(this)},onEnable:function(){if(this.rendered&&this.maskDisabled){this.el.unmask()}Ext.Panel.superclass.onEnable.call(this)},onResize:function(g,d,c,e){var a=g,b=d;if(Ext.isDefined(a)||Ext.isDefined(b)){if(!this.collapsed){if(Ext.isNumber(a)){this.body.setWidth(a=this.adjustBodyWidth(a-this.getFrameWidth()))}else{if(a=="auto"){a=this.body.setWidth("auto").dom.offsetWidth}else{a=this.body.dom.offsetWidth}}if(this.tbar){this.tbar.setWidth(a);if(this.topToolbar){this.topToolbar.setSize(a)}}if(this.bbar){this.bbar.setWidth(a);if(this.bottomToolbar){this.bottomToolbar.setSize(a);if(Ext.isIE){this.bbar.setStyle("position","static");this.bbar.setStyle("position","")}}}if(this.footer){this.footer.setWidth(a);if(this.fbar){this.fbar.setSize(Ext.isIE?(a-this.footer.getFrameWidth("lr")):"auto")}}if(Ext.isNumber(b)){b=Math.max(0,b-this.getFrameHeight());this.body.setHeight(b)}else{if(b=="auto"){this.body.setHeight(b)}}if(this.disabled&&this.el._mask){this.el._mask.setSize(this.el.dom.clientWidth,this.el.getHeight())}}else{this.queuedBodySize={width:a,height:b};if(!this.queuedExpand&&this.allowQueuedExpand!==false){this.queuedExpand=true;this.on("expand",function(){delete this.queuedExpand;this.onResize(this.queuedBodySize.width,this.queuedBodySize.height)},this,{single:true})}}this.onBodyResize(a,b)}this.syncShadow();Ext.Panel.superclass.onResize.call(this,g,d,c,e)},onBodyResize:function(a,b){this.fireEvent("bodyresize",this,a,b)},getToolbarHeight:function(){var a=0;if(this.rendered){Ext.each(this.toolbars,function(b){a+=b.getHeight()},this)}return a},adjustBodyHeight:function(a){return a},adjustBodyWidth:function(a){return a},onPosition:function(){this.syncShadow()},getFrameWidth:function(){var b=this.el.getFrameWidth("lr")+this.bwrap.getFrameWidth("lr");if(this.frame){var a=this.bwrap.dom.firstChild;b+=(Ext.fly(a).getFrameWidth("l")+Ext.fly(a.firstChild).getFrameWidth("r"));b+=this.mc.getFrameWidth("lr")}return b},getFrameHeight:function(){var a=this.el.getFrameWidth("tb")+this.bwrap.getFrameWidth("tb");a+=(this.tbar?this.tbar.getHeight():0)+(this.bbar?this.bbar.getHeight():0);if(this.frame){a+=this.el.dom.firstChild.offsetHeight+this.ft.dom.offsetHeight+this.mc.getFrameWidth("tb")}else{a+=(this.header?this.header.getHeight():0)+(this.footer?this.footer.getHeight():0)}return a},getInnerWidth:function(){return this.getSize().width-this.getFrameWidth()},getInnerHeight:function(){return this.body.getHeight()},syncShadow:function(){if(this.floating){this.el.sync(true)}},getLayoutTarget:function(){return this.body},getContentTarget:function(){return this.body},setTitle:function(b,a){this.title=b;if(this.header&&this.headerAsText){this.header.child("span").update(b)}if(a){this.setIconClass(a)}this.fireEvent("titlechange",this,b);return this},getUpdater:function(){return this.body.getUpdater()},load:function(){var a=this.body.getUpdater();a.update.apply(a,arguments);return this},beforeDestroy:function(){Ext.Panel.superclass.beforeDestroy.call(this);if(this.header){this.header.removeAllListeners()}if(this.tools){for(var a in this.tools){Ext.destroy(this.tools[a])}}if(this.toolbars.length>0){Ext.each(this.toolbars,function(b){b.un("afterlayout",this.syncHeight,this);b.un("remove",this.syncHeight,this)},this)}if(Ext.isArray(this.buttons)){while(this.buttons.length){Ext.destroy(this.buttons[0])}}if(this.rendered){Ext.destroy(this.ft,this.header,this.footer,this.tbar,this.bbar,this.body,this.mc,this.bwrap,this.dd);if(this.fbar){Ext.destroy(this.fbar,this.fbar.el)}}Ext.destroy(this.toolbars)},createClasses:function(){this.headerCls=this.baseCls+"-header";this.headerTextCls=this.baseCls+"-header-text";this.bwrapCls=this.baseCls+"-bwrap";this.tbarCls=this.baseCls+"-tbar";this.bodyCls=this.baseCls+"-body";this.bbarCls=this.baseCls+"-bbar";this.footerCls=this.baseCls+"-footer"},createGhost:function(a,e,b){var d=document.createElement("div");d.className="x-panel-ghost "+(a?a:"");if(this.header){d.appendChild(this.el.dom.firstChild.cloneNode(true))}Ext.fly(d.appendChild(document.createElement("ul"))).setHeight(this.bwrap.getHeight());d.style.width=this.el.dom.offsetWidth+"px";if(!b){this.container.dom.appendChild(d)}else{Ext.getDom(b).appendChild(d)}if(e!==false&&this.el.useShim!==false){var c=new Ext.Layer({shadow:false,useDisplay:true,constrain:false},d);c.show();return c}else{return new Ext.Element(d)}},doAutoLoad:function(){var a=this.body.getUpdater();if(this.renderer){a.setRenderer(this.renderer)}a.update(Ext.isObject(this.autoLoad)?this.autoLoad:{url:this.autoLoad})},getTool:function(a){return this.tools[a]}});Ext.reg("panel",Ext.Panel);Ext.Editor=function(b,a){if(b.field){this.field=Ext.create(b.field,"textfield");a=Ext.apply({},b);delete a.field}else{this.field=b}Ext.Editor.superclass.constructor.call(this,a)};Ext.extend(Ext.Editor,Ext.Component,{allowBlur:true,value:"",alignment:"c-c?",offsets:[0,0],shadow:"frame",constrain:false,swallowKeys:true,completeOnEnter:true,cancelOnEsc:true,updateEl:false,initComponent:function(){Ext.Editor.superclass.initComponent.call(this);this.addEvents("beforestartedit","startedit","beforecomplete","complete","canceledit","specialkey")},onRender:function(b,a){this.el=new Ext.Layer({shadow:this.shadow,cls:"x-editor",parentEl:b,shim:this.shim,shadowOffset:this.shadowOffset||4,id:this.id,constrain:this.constrain});if(this.zIndex){this.el.setZIndex(this.zIndex)}this.el.setStyle("overflow",Ext.isGecko?"auto":"hidden");if(this.field.msgTarget!="title"){this.field.msgTarget="qtip"}this.field.inEditor=true;this.mon(this.field,{scope:this,blur:this.onBlur,specialkey:this.onSpecialKey});if(this.field.grow){this.mon(this.field,"autosize",this.el.sync,this.el,{delay:1})}this.field.render(this.el).show();this.field.getEl().dom.name="";if(this.swallowKeys){this.field.el.swallowEvent(["keypress","keydown"])}},onSpecialKey:function(g,d){var b=d.getKey(),a=this.completeOnEnter&&b==d.ENTER,c=this.cancelOnEsc&&b==d.ESC;if(a||c){d.stopEvent();if(a){this.completeEdit()}else{this.cancelEdit()}if(g.triggerBlur){g.triggerBlur()}}this.fireEvent("specialkey",g,d)},startEdit:function(b,c){if(this.editing){this.completeEdit()}this.boundEl=Ext.get(b);var a=c!==undefined?c:this.boundEl.dom.innerHTML;if(!this.rendered){this.render(this.parentEl||document.body)}if(this.fireEvent("beforestartedit",this,this.boundEl,a)!==false){this.startValue=a;this.field.reset();this.field.setValue(a);this.realign(true);this.editing=true;this.show()}},doAutoSize:function(){if(this.autoSize){var b=this.boundEl.getSize(),a=this.field.getSize();switch(this.autoSize){case"width":this.setSize(b.width,a.height);break;case"height":this.setSize(a.width,b.height);break;case"none":this.setSize(a.width,a.height);break;default:this.setSize(b.width,b.height)}}},setSize:function(a,b){delete this.field.lastSize;this.field.setSize(a,b);if(this.el){if(Ext.isGecko2||Ext.isOpera||(Ext.isIE7&&Ext.isStrict)){this.el.setSize(a,b)}this.el.sync()}},realign:function(a){if(a===true){this.doAutoSize()}this.el.alignTo(this.boundEl,this.alignment,this.offsets)},completeEdit:function(a){if(!this.editing){return}if(this.field.assertValue){this.field.assertValue()}var b=this.getValue();if(!this.field.isValid()){if(this.revertInvalid!==false){this.cancelEdit(a)}return}if(String(b)===String(this.startValue)&&this.ignoreNoChange){this.hideEdit(a);return}if(this.fireEvent("beforecomplete",this,b,this.startValue)!==false){b=this.getValue();if(this.updateEl&&this.boundEl){this.boundEl.update(b)}this.hideEdit(a);this.fireEvent("complete",this,b,this.startValue)}},onShow:function(){this.el.show();if(this.hideEl!==false){this.boundEl.hide()}this.field.show().focus(false,true);this.fireEvent("startedit",this.boundEl,this.startValue)},cancelEdit:function(a){if(this.editing){var b=this.getValue();this.setValue(this.startValue);this.hideEdit(a);this.fireEvent("canceledit",this,b,this.startValue)}},hideEdit:function(a){if(a!==true){this.editing=false;this.hide()}},onBlur:function(){if(this.allowBlur===true&&this.editing&&this.selectSameEditor!==true){this.completeEdit()}},onHide:function(){if(this.editing){this.completeEdit();return}this.field.blur();if(this.field.collapse){this.field.collapse()}this.el.hide();if(this.hideEl!==false){this.boundEl.show()}},setValue:function(a){this.field.setValue(a)},getValue:function(){return this.field.getValue()},beforeDestroy:function(){Ext.destroyMembers(this,"field");delete this.parentEl;delete this.boundEl}});Ext.reg("editor",Ext.Editor);Ext.ColorPalette=Ext.extend(Ext.Component,{itemCls:"x-color-palette",value:null,clickEvent:"click",ctype:"Ext.ColorPalette",allowReselect:false,colors:["000000","993300","333300","003300","003366","000080","333399","333333","800000","FF6600","808000","008000","008080","0000FF","666699","808080","FF0000","FF9900","99CC00","339966","33CCCC","3366FF","800080","969696","FF00FF","FFCC00","FFFF00","00FF00","00FFFF","00CCFF","993366","C0C0C0","FF99CC","FFCC99","FFFF99","CCFFCC","CCFFFF","99CCFF","CC99FF","FFFFFF"],initComponent:function(){Ext.ColorPalette.superclass.initComponent.call(this);this.addEvents("select");if(this.handler){this.on("select",this.handler,this.scope,true)}},onRender:function(b,a){this.autoEl={tag:"div",cls:this.itemCls};Ext.ColorPalette.superclass.onRender.call(this,b,a);var c=this.tpl||new Ext.XTemplate(' ');c.overwrite(this.el,this.colors);this.mon(this.el,this.clickEvent,this.handleClick,this,{delegate:"a"});if(this.clickEvent!="click"){this.mon(this.el,"click",Ext.emptyFn,this,{delegate:"a",preventDefault:true})}},afterRender:function(){Ext.ColorPalette.superclass.afterRender.call(this);if(this.value){var a=this.value;this.value=null;this.select(a,true)}},handleClick:function(b,a){b.preventDefault();if(!this.disabled){var d=a.className.match(/(?:^|\s)color-(.{6})(?:\s|$)/)[1];this.select(d.toUpperCase())}},select:function(b,a){b=b.replace("#","");if(b!=this.value||this.allowReselect){var c=this.el;if(this.value){c.child("a.color-"+this.value).removeClass("x-color-palette-sel")}c.child("a.color-"+b).addClass("x-color-palette-sel");this.value=b;if(a!==true){this.fireEvent("select",this,b)}}}});Ext.reg("colorpalette",Ext.ColorPalette);Ext.DatePicker=Ext.extend(Ext.BoxComponent,{todayText:"Today",okText:" OK ",cancelText:"Cancel",todayTip:"{0} (Spacebar)",minText:"This date is before the minimum date",maxText:"This date is after the maximum date",format:"m/d/y",disabledDaysText:"Disabled",disabledDatesText:"Disabled",monthNames:Date.monthNames,dayNames:Date.dayNames,nextText:"Next Month (Control+Right)",prevText:"Previous Month (Control+Left)",monthYearText:"Choose a month (Control+Up/Down to move years)",startDay:0,showToday:true,focusOnSelect:true,initHour:12,initComponent:function(){Ext.DatePicker.superclass.initComponent.call(this);this.value=this.value?this.value.clearTime(true):new Date().clearTime();this.addEvents("select");if(this.handler){this.on("select",this.handler,this.scope||this)}this.initDisabledDays()},initDisabledDays:function(){if(!this.disabledDatesRE&&this.disabledDates){var b=this.disabledDates,a=b.length-1,c="(?:";Ext.each(b,function(g,e){c+=Ext.isDate(g)?"^"+Ext.escapeRe(g.dateFormat(this.format))+"$":b[e];if(e!=a){c+="|"}},this);this.disabledDatesRE=new RegExp(c+")")}},setDisabledDates:function(a){if(Ext.isArray(a)){this.disabledDates=a;this.disabledDatesRE=null}else{this.disabledDatesRE=a}this.initDisabledDays();this.update(this.value,true)},setDisabledDays:function(a){this.disabledDays=a;this.update(this.value,true)},setMinDate:function(a){this.minDate=a;this.update(this.value,true)},setMaxDate:function(a){this.maxDate=a;this.update(this.value,true)},setValue:function(a){this.value=a.clearTime(true);this.update(this.value)},getValue:function(){return this.value},focus:function(){this.update(this.activeDate)},onEnable:function(a){Ext.DatePicker.superclass.onEnable.call(this);this.doDisabled(false);this.update(a?this.value:this.activeDate);if(Ext.isIE){this.el.repaint()}},onDisable:function(){Ext.DatePicker.superclass.onDisable.call(this);this.doDisabled(true);if(Ext.isIE&&!Ext.isIE8){Ext.each([].concat(this.textNodes,this.el.query("th span")),function(a){Ext.fly(a).repaint()})}},doDisabled:function(a){this.keyNav.setDisabled(a);this.prevRepeater.setDisabled(a);this.nextRepeater.setDisabled(a);if(this.showToday){this.todayKeyListener.setDisabled(a);this.todayBtn.setDisabled(a)}},onRender:function(e,b){var a=['','','",this.showToday?'':"",'
      
    '],c=this.dayNames,h;for(h=0;h<7;h++){var k=this.startDay+h;if(k>6){k=k-7}a.push("")}a[a.length]="";for(h=0;h<42;h++){if(h%7===0&&h!==0){a[a.length]=""}a[a.length]=''}a.push("
    ",c[k].substr(0,1),"
    ');var j=document.createElement("div");j.className="x-date-picker";j.innerHTML=a.join("");e.dom.insertBefore(j,b);this.el=Ext.get(j);this.eventEl=Ext.get(j.firstChild);this.prevRepeater=new Ext.util.ClickRepeater(this.el.child("td.x-date-left a"),{handler:this.showPrevMonth,scope:this,preventDefault:true,stopDefault:true});this.nextRepeater=new Ext.util.ClickRepeater(this.el.child("td.x-date-right a"),{handler:this.showNextMonth,scope:this,preventDefault:true,stopDefault:true});this.monthPicker=this.el.down("div.x-date-mp");this.monthPicker.enableDisplayMode("block");this.keyNav=new Ext.KeyNav(this.eventEl,{left:function(d){if(d.ctrlKey){this.showPrevMonth()}else{this.update(this.activeDate.add("d",-1))}},right:function(d){if(d.ctrlKey){this.showNextMonth()}else{this.update(this.activeDate.add("d",1))}},up:function(d){if(d.ctrlKey){this.showNextYear()}else{this.update(this.activeDate.add("d",-7))}},down:function(d){if(d.ctrlKey){this.showPrevYear()}else{this.update(this.activeDate.add("d",7))}},pageUp:function(d){this.showNextMonth()},pageDown:function(d){this.showPrevMonth()},enter:function(d){d.stopPropagation();return true},scope:this});this.el.unselectable();this.cells=this.el.select("table.x-date-inner tbody td");this.textNodes=this.el.query("table.x-date-inner tbody span");this.mbtn=new Ext.Button({text:" ",tooltip:this.monthYearText,renderTo:this.el.child("td.x-date-middle",true)});this.mbtn.el.child("em").addClass("x-btn-arrow");if(this.showToday){this.todayKeyListener=this.eventEl.addKeyListener(Ext.EventObject.SPACE,this.selectToday,this);var g=(new Date()).dateFormat(this.format);this.todayBtn=new Ext.Button({renderTo:this.el.child("td.x-date-bottom",true),text:String.format(this.todayText,g),tooltip:String.format(this.todayTip,g),handler:this.selectToday,scope:this})}this.mon(this.eventEl,"mousewheel",this.handleMouseWheel,this);this.mon(this.eventEl,"click",this.handleDateClick,this,{delegate:"a.x-date-date"});this.mon(this.mbtn,"click",this.showMonthPicker,this);this.onEnable(true)},createMonthPicker:function(){if(!this.monthPicker.dom.firstChild){var a=[''];for(var b=0;b<6;b++){a.push('",'",b===0?'':'')}a.push('","
    ',Date.getShortMonthName(b),"',Date.getShortMonthName(b+6),"
    ");this.monthPicker.update(a.join(""));this.mon(this.monthPicker,"click",this.onMonthClick,this);this.mon(this.monthPicker,"dblclick",this.onMonthDblClick,this);this.mpMonths=this.monthPicker.select("td.x-date-mp-month");this.mpYears=this.monthPicker.select("td.x-date-mp-year");this.mpMonths.each(function(c,d,e){e+=1;if((e%2)===0){c.dom.xmonth=5+Math.round(e*0.5)}else{c.dom.xmonth=Math.round((e-1)*0.5)}})}},showMonthPicker:function(){if(!this.disabled){this.createMonthPicker();var a=this.el.getSize();this.monthPicker.setSize(a);this.monthPicker.child("table").setSize(a);this.mpSelMonth=(this.activeDate||this.value).getMonth();this.updateMPMonth(this.mpSelMonth);this.mpSelYear=(this.activeDate||this.value).getFullYear();this.updateMPYear(this.mpSelYear);this.monthPicker.slideIn("t",{duration:0.2})}},updateMPYear:function(e){this.mpyear=e;var c=this.mpYears.elements;for(var b=1;b<=10;b++){var d=c[b-1],a;if((b%2)===0){a=e+Math.round(b*0.5);d.firstChild.innerHTML=a;d.xyear=a}else{a=e-(5-Math.round(b*0.5));d.firstChild.innerHTML=a;d.xyear=a}this.mpYears.item(b-1)[a==this.mpSelYear?"addClass":"removeClass"]("x-date-mp-sel")}},updateMPMonth:function(a){this.mpMonths.each(function(b,c,d){b[b.dom.xmonth==a?"addClass":"removeClass"]("x-date-mp-sel")})},selectMPMonth:function(a){},onMonthClick:function(g,b){g.stopEvent();var c=new Ext.Element(b),a;if(c.is("button.x-date-mp-cancel")){this.hideMonthPicker()}else{if(c.is("button.x-date-mp-ok")){var h=new Date(this.mpSelYear,this.mpSelMonth,(this.activeDate||this.value).getDate());if(h.getMonth()!=this.mpSelMonth){h=new Date(this.mpSelYear,this.mpSelMonth,1).getLastDateOfMonth()}this.update(h);this.hideMonthPicker()}else{if((a=c.up("td.x-date-mp-month",2))){this.mpMonths.removeClass("x-date-mp-sel");a.addClass("x-date-mp-sel");this.mpSelMonth=a.dom.xmonth}else{if((a=c.up("td.x-date-mp-year",2))){this.mpYears.removeClass("x-date-mp-sel");a.addClass("x-date-mp-sel");this.mpSelYear=a.dom.xyear}else{if(c.is("a.x-date-mp-prev")){this.updateMPYear(this.mpyear-10)}else{if(c.is("a.x-date-mp-next")){this.updateMPYear(this.mpyear+10)}}}}}}},onMonthDblClick:function(d,b){d.stopEvent();var c=new Ext.Element(b),a;if((a=c.up("td.x-date-mp-month",2))){this.update(new Date(this.mpSelYear,a.dom.xmonth,(this.activeDate||this.value).getDate()));this.hideMonthPicker()}else{if((a=c.up("td.x-date-mp-year",2))){this.update(new Date(a.dom.xyear,this.mpSelMonth,(this.activeDate||this.value).getDate()));this.hideMonthPicker()}}},hideMonthPicker:function(a){if(this.monthPicker){if(a===true){this.monthPicker.hide()}else{this.monthPicker.slideOut("t",{duration:0.2})}}},showPrevMonth:function(a){this.update(this.activeDate.add("mo",-1))},showNextMonth:function(a){this.update(this.activeDate.add("mo",1))},showPrevYear:function(){this.update(this.activeDate.add("y",-1))},showNextYear:function(){this.update(this.activeDate.add("y",1))},handleMouseWheel:function(a){a.stopEvent();if(!this.disabled){var b=a.getWheelDelta();if(b>0){this.showPrevMonth()}else{if(b<0){this.showNextMonth()}}}},handleDateClick:function(b,a){b.stopEvent();if(!this.disabled&&a.dateValue&&!Ext.fly(a.parentNode).hasClass("x-date-disabled")){this.cancelFocus=this.focusOnSelect===false;this.setValue(new Date(a.dateValue));delete this.cancelFocus;this.fireEvent("select",this,this.value)}},selectToday:function(){if(this.todayBtn&&!this.todayBtn.disabled){this.setValue(new Date().clearTime());this.fireEvent("select",this,this.value)}},update:function(G,A){if(this.rendered){var a=this.activeDate,p=this.isVisible();this.activeDate=G;if(!A&&a&&this.el){var o=G.getTime();if(a.getMonth()==G.getMonth()&&a.getFullYear()==G.getFullYear()){this.cells.removeClass("x-date-selected");this.cells.each(function(d){if(d.dom.firstChild.dateValue==o){d.addClass("x-date-selected");if(p&&!this.cancelFocus){Ext.fly(d.dom.firstChild).focus(50)}return false}},this);return}}var k=G.getDaysInMonth(),q=G.getFirstDateOfMonth(),g=q.getDay()-this.startDay;if(g<0){g+=7}k+=g;var B=G.add("mo",-1),h=B.getDaysInMonth()-g,e=this.cells.elements,r=this.textNodes,D=(new Date(B.getFullYear(),B.getMonth(),h,this.initHour)),C=new Date().clearTime().getTime(),v=G.clearTime(true).getTime(),u=this.minDate?this.minDate.clearTime(true):Number.NEGATIVE_INFINITY,y=this.maxDate?this.maxDate.clearTime(true):Number.POSITIVE_INFINITY,F=this.disabledDatesRE,s=this.disabledDatesText,I=this.disabledDays?this.disabledDays.join(""):false,E=this.disabledDaysText,z=this.format;if(this.showToday){var m=new Date().clearTime(),c=(my||(F&&z&&F.test(m.dateFormat(z)))||(I&&I.indexOf(m.getDay())!=-1));if(!this.disabled){this.todayBtn.setDisabled(c);this.todayKeyListener[c?"disable":"enable"]()}}var l=function(J,d){d.title="";var i=D.clearTime(true).getTime();d.firstChild.dateValue=i;if(i==C){d.className+=" x-date-today";d.title=J.todayText}if(i==v){d.className+=" x-date-selected";if(p){Ext.fly(d.firstChild).focus(50)}}if(iy){d.className=" x-date-disabled";d.title=J.maxText;return}if(I){if(I.indexOf(D.getDay())!=-1){d.title=E;d.className=" x-date-disabled"}}if(F&&z){var w=D.dateFormat(z);if(F.test(w)){d.title=s.replace("%0",w);d.className=" x-date-disabled"}}};var x=0;for(;x=a.value){d=a.value}}c.setValue(b,d,false);c.fireEvent("drag",c,g,this)},getNewValue:function(){var a=this.slider,b=a.innerEl.translatePoints(this.tracker.getXY());return Ext.util.Format.round(a.reverseValue(b.left),a.decimalPrecision)},onDragEnd:function(c){var a=this.slider,b=this.value;this.el.removeClass("x-slider-thumb-drag");this.dragging=false;a.fireEvent("dragend",a,c);if(this.dragStartValue!=b){a.fireEvent("changecomplete",a,b,this)}},destroy:function(){Ext.destroyMembers(this,"tracker","el")}});Ext.slider.MultiSlider=Ext.extend(Ext.BoxComponent,{vertical:false,minValue:0,maxValue:100,decimalPrecision:0,keyIncrement:1,increment:0,clickRange:[5,15],clickToChange:true,animate:true,constrainThumbs:true,topThumbZIndex:10000,initComponent:function(){if(!Ext.isDefined(this.value)){this.value=this.minValue}this.thumbs=[];Ext.slider.MultiSlider.superclass.initComponent.call(this);this.keyIncrement=Math.max(this.increment,this.keyIncrement);this.addEvents("beforechange","change","changecomplete","dragstart","drag","dragend");if(this.values==undefined||Ext.isEmpty(this.values)){this.values=[0]}var a=this.values;for(var b=0;bthis.clickRange[0]&&c.top=c){d+=c}else{if(a*2<-c){d-=c}}}return d.constrain(this.minValue,this.maxValue)},afterRender:function(){Ext.slider.MultiSlider.superclass.afterRender.apply(this,arguments);for(var c=0;ce?e:c.value}this.syncThumb()},setValue:function(d,c,b,g){var a=this.thumbs[d],e=a.el;c=this.normalizeValue(c);if(c!==a.value&&this.fireEvent("beforechange",this,c,a.value,a)!==false){a.value=c;if(this.rendered){this.moveThumb(d,this.translateValue(c),b!==false);this.fireEvent("change",this,c,a);if(g){this.fireEvent("changecomplete",this,c,a)}}}},translateValue:function(a){var b=this.getRatio();return(a*b)-(this.minValue*b)-this.halfThumb},reverseValue:function(b){var a=this.getRatio();return(b+(this.minValue*a))/a},moveThumb:function(d,c,b){var a=this.thumbs[d].el;if(!b||this.animate===false){a.setLeft(c)}else{a.shift({left:c,stopFx:true,duration:0.35})}},focus:function(){this.focusEl.focus(10)},onResize:function(c,e){var b=this.thumbs,a=b.length,d=0;for(;dthis.clickRange[0]&&c.left','
    ','
    ','
    ',"
     
    ","
    ","
    ",'
    ',"
     
    ","
    ","
    ","");this.el=a?c.insertBefore(a,{cls:this.baseCls},true):c.append(d,{cls:this.baseCls},true);if(this.id){this.el.dom.id=this.id}var b=this.el.dom.firstChild;this.progressBar=Ext.get(b.firstChild);if(this.textEl){this.textEl=Ext.get(this.textEl);delete this.textTopEl}else{this.textTopEl=Ext.get(this.progressBar.dom.firstChild);var e=Ext.get(b.childNodes[1]);this.textTopEl.setStyle("z-index",99).addClass("x-hidden");this.textEl=new Ext.CompositeElement([this.textTopEl.dom.firstChild,e.dom.firstChild]);this.textEl.setWidth(b.offsetWidth)}this.progressBar.setHeight(b.offsetHeight)},afterRender:function(){Ext.ProgressBar.superclass.afterRender.call(this);if(this.value){this.updateProgress(this.value,this.text)}else{this.updateText(this.text)}},updateProgress:function(c,d,b){this.value=c||0;if(d){this.updateText(d)}if(this.rendered&&!this.isDestroyed){var a=Math.floor(c*this.el.dom.firstChild.offsetWidth);this.progressBar.setWidth(a,b===true||(b!==false&&this.animate));if(this.textTopEl){this.textTopEl.removeClass("x-hidden").setWidth(a)}}this.fireEvent("update",this,c,d);return this},wait:function(b){if(!this.waitTimer){var a=this;b=b||{};this.updateText(b.text);this.waitTimer=Ext.TaskMgr.start({run:function(c){var d=b.increment||10;c-=1;this.updateProgress(((((c+d)%d)+1)*(100/d))*0.01,null,b.animate)},interval:b.interval||1000,duration:b.duration,onStop:function(){if(b.fn){b.fn.apply(b.scope||this)}this.reset()},scope:a})}return this},isWaiting:function(){return this.waitTimer!==null},updateText:function(a){this.text=a||" ";if(this.rendered){this.textEl.update(this.text)}return this},syncProgressBar:function(){if(this.value){this.updateProgress(this.value,this.text)}return this},setSize:function(a,c){Ext.ProgressBar.superclass.setSize.call(this,a,c);if(this.textTopEl){var b=this.el.dom.firstChild;this.textEl.setSize(b.offsetWidth,b.offsetHeight)}this.syncProgressBar();return this},reset:function(a){this.updateProgress(0);if(this.textTopEl){this.textTopEl.addClass("x-hidden")}this.clearTimer();if(a===true){this.hide()}return this},clearTimer:function(){if(this.waitTimer){this.waitTimer.onStop=null;Ext.TaskMgr.stop(this.waitTimer);this.waitTimer=null}},onDestroy:function(){this.clearTimer();if(this.rendered){if(this.textEl.isComposite){this.textEl.clear()}Ext.destroyMembers(this,"textEl","progressBar","textTopEl")}Ext.ProgressBar.superclass.onDestroy.call(this)}});Ext.reg("progress",Ext.ProgressBar);(function(){var a=Ext.EventManager;var b=Ext.lib.Dom;Ext.dd.DragDrop=function(e,c,d){if(e){this.init(e,c,d)}};Ext.dd.DragDrop.prototype={id:null,config:null,dragElId:null,handleElId:null,invalidHandleTypes:null,invalidHandleIds:null,invalidHandleClasses:null,startPageX:0,startPageY:0,groups:null,locked:false,lock:function(){this.locked=true},moveOnly:false,unlock:function(){this.locked=false},isTarget:true,padding:null,_domRef:null,__ygDragDrop:true,constrainX:false,constrainY:false,minX:0,maxX:0,minY:0,maxY:0,maintainOffset:false,xTicks:null,yTicks:null,primaryButtonOnly:true,available:false,hasOuterHandles:false,b4StartDrag:function(c,d){},startDrag:function(c,d){},b4Drag:function(c){},onDrag:function(c){},onDragEnter:function(c,d){},b4DragOver:function(c){},onDragOver:function(c,d){},b4DragOut:function(c){},onDragOut:function(c,d){},b4DragDrop:function(c){},onDragDrop:function(c,d){},onInvalidDrop:function(c){},b4EndDrag:function(c){},endDrag:function(c){},b4MouseDown:function(c){},onMouseDown:function(c){},onMouseUp:function(c){},onAvailable:function(){},defaultPadding:{left:0,right:0,top:0,bottom:0},constrainTo:function(j,h,o){if(Ext.isNumber(h)){h={left:h,right:h,top:h,bottom:h}}h=h||this.defaultPadding;var l=Ext.get(this.getEl()).getBox(),d=Ext.get(j),n=d.getScroll(),k,e=d.dom;if(e==document.body){k={x:n.left,y:n.top,width:Ext.lib.Dom.getViewWidth(),height:Ext.lib.Dom.getViewHeight()}}else{var m=d.getXY();k={x:m[0],y:m[1],width:e.clientWidth,height:e.clientHeight}}var i=l.y-k.y,g=l.x-k.x;this.resetConstraints();this.setXConstraint(g-(h.left||0),k.width-g-l.width-(h.right||0),this.xTickSize);this.setYConstraint(i-(h.top||0),k.height-i-l.height-(h.bottom||0),this.yTickSize)},getEl:function(){if(!this._domRef){this._domRef=Ext.getDom(this.id)}return this._domRef},getDragEl:function(){return Ext.getDom(this.dragElId)},init:function(e,c,d){this.initTarget(e,c,d);a.on(this.id,"mousedown",this.handleMouseDown,this)},initTarget:function(e,c,d){this.config=d||{};this.DDM=Ext.dd.DDM;this.groups={};if(typeof e!=="string"){e=Ext.id(e)}this.id=e;this.addToGroup((c)?c:"default");this.handleElId=e;this.setDragElId(e);this.invalidHandleTypes={A:"A"};this.invalidHandleIds={};this.invalidHandleClasses=[];this.applyConfig();this.handleOnAvailable()},applyConfig:function(){this.padding=this.config.padding||[0,0,0,0];this.isTarget=(this.config.isTarget!==false);this.maintainOffset=(this.config.maintainOffset);this.primaryButtonOnly=(this.config.primaryButtonOnly!==false)},handleOnAvailable:function(){this.available=true;this.resetConstraints();this.onAvailable()},setPadding:function(e,c,g,d){if(!c&&0!==c){this.padding=[e,e,e,e]}else{if(!g&&0!==g){this.padding=[e,c,e,c]}else{this.padding=[e,c,g,d]}}},setInitPosition:function(g,e){var h=this.getEl();if(!this.DDM.verifyEl(h)){return}var d=g||0;var c=e||0;var i=b.getXY(h);this.initPageX=i[0]-d;this.initPageY=i[1]-c;this.lastPageX=i[0];this.lastPageY=i[1];this.setStartPosition(i)},setStartPosition:function(d){var c=d||b.getXY(this.getEl());this.deltaSetXY=null;this.startPageX=c[0];this.startPageY=c[1]},addToGroup:function(c){this.groups[c]=true;this.DDM.regDragDrop(this,c)},removeFromGroup:function(c){if(this.groups[c]){delete this.groups[c]}this.DDM.removeDDFromGroup(this,c)},setDragElId:function(c){this.dragElId=c},setHandleElId:function(c){if(typeof c!=="string"){c=Ext.id(c)}this.handleElId=c;this.DDM.regHandle(this.id,c)},setOuterHandleElId:function(c){if(typeof c!=="string"){c=Ext.id(c)}a.on(c,"mousedown",this.handleMouseDown,this);this.setHandleElId(c);this.hasOuterHandles=true},unreg:function(){a.un(this.id,"mousedown",this.handleMouseDown);this._domRef=null;this.DDM._remove(this)},destroy:function(){this.unreg()},isLocked:function(){return(this.DDM.isLocked()||this.locked)},handleMouseDown:function(g,d){if(this.primaryButtonOnly&&g.button!=0){return}if(this.isLocked()){return}this.DDM.refreshCache(this.groups);var c=new Ext.lib.Point(Ext.lib.Event.getPageX(g),Ext.lib.Event.getPageY(g));if(!this.hasOuterHandles&&!this.DDM.isOverTarget(c,this)){}else{if(this.clickValidator(g)){this.setStartPosition();this.b4MouseDown(g);this.onMouseDown(g);this.DDM.handleMouseDown(g,this);this.DDM.stopEvent(g)}else{}}},clickValidator:function(d){var c=d.getTarget();return(this.isValidHandleChild(c)&&(this.id==this.handleElId||this.DDM.handleWasClicked(c,this.id)))},addInvalidHandleType:function(c){var d=c.toUpperCase();this.invalidHandleTypes[d]=d},addInvalidHandleId:function(c){if(typeof c!=="string"){c=Ext.id(c)}this.invalidHandleIds[c]=c},addInvalidHandleClass:function(c){this.invalidHandleClasses.push(c)},removeInvalidHandleType:function(c){var d=c.toUpperCase();delete this.invalidHandleTypes[d]},removeInvalidHandleId:function(c){if(typeof c!=="string"){c=Ext.id(c)}delete this.invalidHandleIds[c]},removeInvalidHandleClass:function(d){for(var e=0,c=this.invalidHandleClasses.length;e=this.minX;d=d-c){if(!e[d]){this.xTicks[this.xTicks.length]=d;e[d]=true}}for(d=this.initPageX;d<=this.maxX;d=d+c){if(!e[d]){this.xTicks[this.xTicks.length]=d;e[d]=true}}this.xTicks.sort(this.DDM.numericSort)},setYTicks:function(g,c){this.yTicks=[];this.yTickSize=c;var e={};for(var d=this.initPageY;d>=this.minY;d=d-c){if(!e[d]){this.yTicks[this.yTicks.length]=d;e[d]=true}}for(d=this.initPageY;d<=this.maxY;d=d+c){if(!e[d]){this.yTicks[this.yTicks.length]=d;e[d]=true}}this.yTicks.sort(this.DDM.numericSort)},setXConstraint:function(e,d,c){this.leftConstraint=e;this.rightConstraint=d;this.minX=this.initPageX-e;this.maxX=this.initPageX+d;if(c){this.setXTicks(this.initPageX,c)}this.constrainX=true},clearConstraints:function(){this.constrainX=false;this.constrainY=false;this.clearTicks()},clearTicks:function(){this.xTicks=null;this.yTicks=null;this.xTickSize=0;this.yTickSize=0},setYConstraint:function(c,e,d){this.topConstraint=c;this.bottomConstraint=e;this.minY=this.initPageY-c;this.maxY=this.initPageY+e;if(d){this.setYTicks(this.initPageY,d)}this.constrainY=true},resetConstraints:function(){if(this.initPageX||this.initPageX===0){var d=(this.maintainOffset)?this.lastPageX-this.initPageX:0;var c=(this.maintainOffset)?this.lastPageY-this.initPageY:0;this.setInitPosition(d,c)}else{this.setInitPosition()}if(this.constrainX){this.setXConstraint(this.leftConstraint,this.rightConstraint,this.xTickSize)}if(this.constrainY){this.setYConstraint(this.topConstraint,this.bottomConstraint,this.yTickSize)}},getTick:function(k,g){if(!g){return k}else{if(g[0]>=k){return g[0]}else{for(var d=0,c=g.length;d=k){var j=k-g[d];var h=g[e]-k;return(h>j)?g[d]:g[e]}}return g[g.length-1]}}},toString:function(){return("DragDrop "+this.id)}}})();if(!Ext.dd.DragDropMgr){Ext.dd.DragDropMgr=function(){var a=Ext.EventManager;return{ids:{},handleIds:{},dragCurrent:null,dragOvers:{},deltaX:0,deltaY:0,preventDefault:true,stopPropagation:true,initialized:false,locked:false,init:function(){this.initialized=true},POINT:0,INTERSECT:1,mode:0,_execOnAll:function(d,c){for(var e in this.ids){for(var b in this.ids[e]){var g=this.ids[e][b];if(!this.isTypeOfDD(g)){continue}g[d].apply(g,c)}}},_onLoad:function(){this.init();a.on(document,"mouseup",this.handleMouseUp,this,true);a.on(document,"mousemove",this.handleMouseMove,this,true);a.on(window,"unload",this._onUnload,this,true);a.on(window,"resize",this._onResize,this,true)},_onResize:function(b){this._execOnAll("resetConstraints",[])},lock:function(){this.locked=true},unlock:function(){this.locked=false},isLocked:function(){return this.locked},locationCache:{},useCache:true,clickPixelThresh:3,clickTimeThresh:350,dragThreshMet:false,clickTimeout:null,startX:0,startY:0,regDragDrop:function(c,b){if(!this.initialized){this.init()}if(!this.ids[b]){this.ids[b]={}}this.ids[b][c.id]=c},removeDDFromGroup:function(d,b){if(!this.ids[b]){this.ids[b]={}}var c=this.ids[b];if(c&&c[d.id]){delete c[d.id]}},_remove:function(c){for(var b in c.groups){if(b&&this.ids[b]&&this.ids[b][c.id]){delete this.ids[b][c.id]}}delete this.handleIds[c.id]},regHandle:function(c,b){if(!this.handleIds[c]){this.handleIds[c]={}}this.handleIds[c][b]=b},isDragDrop:function(b){return(this.getDDById(b))?true:false},getRelated:function(h,c){var g=[];for(var e in h.groups){for(var d in this.ids[e]){var b=this.ids[e][d];if(!this.isTypeOfDD(b)){continue}if(!c||b.isTarget){g[g.length]=b}}}return g},isLegalTarget:function(g,e){var c=this.getRelated(g,true);for(var d=0,b=c.length;dthis.clickPixelThresh||b>this.clickPixelThresh){this.startDrag(this.startX,this.startY)}}if(this.dragThreshMet){this.dragCurrent.b4Drag(d);this.dragCurrent.onDrag(d);if(!this.dragCurrent.moveOnly){this.fireEvents(d,false)}}this.stopEvent(d);return true},fireEvents:function(n,o){var q=this.dragCurrent;if(!q||q.isLocked()){return}var r=n.getPoint();var b=[];var g=[];var l=[];var j=[];var d=[];for(var h in this.dragOvers){var c=this.dragOvers[h];if(!this.isTypeOfDD(c)){continue}if(!this.isOverTarget(r,c,this.mode)){g.push(c)}b[h]=true;delete this.dragOvers[h]}for(var p in q.groups){if("string"!=typeof p){continue}for(h in this.ids[p]){var k=this.ids[p][h];if(!this.isTypeOfDD(k)){continue}if(k.isTarget&&!k.isLocked()&&((k!=q)||(q.ignoreSelf===false))){if(this.isOverTarget(r,k,this.mode)){if(o){j.push(k)}else{if(!b[k.id]){d.push(k)}else{l.push(k)}this.dragOvers[k.id]=k}}}}}if(this.mode){if(g.length){q.b4DragOut(n,g);q.onDragOut(n,g)}if(d.length){q.onDragEnter(n,d)}if(l.length){q.b4DragOver(n,l);q.onDragOver(n,l)}if(j.length){q.b4DragDrop(n,j);q.onDragDrop(n,j)}}else{var m=0;for(h=0,m=g.length;h2000){}else{setTimeout(b._addListeners,10);if(document&&document.body){b._timeoutCount+=1}}}},handleWasClicked:function(b,d){if(this.isHandle(d,b.id)){return true}else{var c=b.parentNode;while(c){if(this.isHandle(d,c.id)){return true}else{c=c.parentNode}}}return false}}}();Ext.dd.DDM=Ext.dd.DragDropMgr;Ext.dd.DDM._addListeners()}Ext.dd.DD=function(c,a,b){if(c){this.init(c,a,b)}};Ext.extend(Ext.dd.DD,Ext.dd.DragDrop,{scroll:true,autoOffset:function(c,b){var a=c-this.startPageX;var d=b-this.startPageY;this.setDelta(a,d)},setDelta:function(b,a){this.deltaX=b;this.deltaY=a},setDragElPos:function(c,b){var a=this.getDragEl();this.alignElWithMouse(a,c,b)},alignElWithMouse:function(c,h,g){var e=this.getTargetCoord(h,g);var b=c.dom?c:Ext.fly(c,"_dd");if(!this.deltaSetXY){var i=[e.x,e.y];b.setXY(i);var d=b.getLeft(true);var a=b.getTop(true);this.deltaSetXY=[d-e.x,a-e.y]}else{b.setLeftTop(e.x+this.deltaSetXY[0],e.y+this.deltaSetXY[1])}this.cachePosition(e.x,e.y);this.autoScroll(e.x,e.y,c.offsetHeight,c.offsetWidth);return e},cachePosition:function(b,a){if(b){this.lastPageX=b;this.lastPageY=a}else{var c=Ext.lib.Dom.getXY(this.getEl());this.lastPageX=c[0];this.lastPageY=c[1]}},autoScroll:function(l,k,e,m){if(this.scroll){var n=Ext.lib.Dom.getViewHeight();var b=Ext.lib.Dom.getViewWidth();var p=this.DDM.getScrollTop();var d=this.DDM.getScrollLeft();var j=e+k;var o=m+l;var i=(n+p-k-this.deltaY);var g=(b+d-l-this.deltaX);var c=40;var a=(document.all)?80:30;if(j>n&&i0&&k-pb&&g0&&l-dthis.maxX){a=this.maxX}}if(this.constrainY){if(dthis.maxY){d=this.maxY}}a=this.getTick(a,this.xTicks);d=this.getTick(d,this.yTicks);return{x:a,y:d}},applyConfig:function(){Ext.dd.DD.superclass.applyConfig.call(this);this.scroll=(this.config.scroll!==false)},b4MouseDown:function(a){this.autoOffset(a.getPageX(),a.getPageY())},b4Drag:function(a){this.setDragElPos(a.getPageX(),a.getPageY())},toString:function(){return("DD "+this.id)}});Ext.dd.DDProxy=function(c,a,b){if(c){this.init(c,a,b);this.initFrame()}};Ext.dd.DDProxy.dragElId="ygddfdiv";Ext.extend(Ext.dd.DDProxy,Ext.dd.DD,{resizeFrame:true,centerFrame:false,createFrame:function(){var b=this;var a=document.body;if(!a||!a.firstChild){setTimeout(function(){b.createFrame()},50);return}var d=this.getDragEl();if(!d){d=document.createElement("div");d.id=this.dragElId;var c=d.style;c.position="absolute";c.visibility="hidden";c.cursor="move";c.border="2px solid #aaa";c.zIndex=999;a.insertBefore(d,a.firstChild)}},initFrame:function(){this.createFrame()},applyConfig:function(){Ext.dd.DDProxy.superclass.applyConfig.call(this);this.resizeFrame=(this.config.resizeFrame!==false);this.centerFrame=(this.config.centerFrame);this.setDragElId(this.config.dragElId||Ext.dd.DDProxy.dragElId)},showFrame:function(e,d){var c=this.getEl();var a=this.getDragEl();var b=a.style;this._resizeProxy();if(this.centerFrame){this.setDelta(Math.round(parseInt(b.width,10)/2),Math.round(parseInt(b.height,10)/2))}this.setDragElPos(e,d);Ext.fly(a).show()},_resizeProxy:function(){if(this.resizeFrame){var a=this.getEl();Ext.fly(this.getDragEl()).setSize(a.offsetWidth,a.offsetHeight)}},b4MouseDown:function(b){var a=b.getPageX();var c=b.getPageY();this.autoOffset(a,c);this.setDragElPos(a,c)},b4StartDrag:function(a,b){this.showFrame(a,b)},b4EndDrag:function(a){Ext.fly(this.getDragEl()).hide()},endDrag:function(c){var b=this.getEl();var a=this.getDragEl();a.style.visibility="";this.beforeMove();b.style.visibility="hidden";Ext.dd.DDM.moveToEl(b,a);a.style.visibility="hidden";b.style.visibility="";this.afterDrag()},beforeMove:function(){},afterDrag:function(){},toString:function(){return("DDProxy "+this.id)}});Ext.dd.DDTarget=function(c,a,b){if(c){this.initTarget(c,a,b)}};Ext.extend(Ext.dd.DDTarget,Ext.dd.DragDrop,{getDragEl:Ext.emptyFn,isValidHandleChild:Ext.emptyFn,startDrag:Ext.emptyFn,endDrag:Ext.emptyFn,onDrag:Ext.emptyFn,onDragDrop:Ext.emptyFn,onDragEnter:Ext.emptyFn,onDragOut:Ext.emptyFn,onDragOver:Ext.emptyFn,onInvalidDrop:Ext.emptyFn,onMouseDown:Ext.emptyFn,onMouseUp:Ext.emptyFn,setXConstraint:Ext.emptyFn,setYConstraint:Ext.emptyFn,resetConstraints:Ext.emptyFn,clearConstraints:Ext.emptyFn,clearTicks:Ext.emptyFn,setInitPosition:Ext.emptyFn,setDragElId:Ext.emptyFn,setHandleElId:Ext.emptyFn,setOuterHandleElId:Ext.emptyFn,addInvalidHandleClass:Ext.emptyFn,addInvalidHandleId:Ext.emptyFn,addInvalidHandleType:Ext.emptyFn,removeInvalidHandleClass:Ext.emptyFn,removeInvalidHandleId:Ext.emptyFn,removeInvalidHandleType:Ext.emptyFn,toString:function(){return("DDTarget "+this.id)}});Ext.dd.DragTracker=Ext.extend(Ext.util.Observable,{active:false,tolerance:5,autoStart:false,constructor:function(a){Ext.apply(this,a);this.addEvents("mousedown","mouseup","mousemove","dragstart","dragend","drag");this.dragRegion=new Ext.lib.Region(0,0,0,0);if(this.el){this.initEl(this.el)}Ext.dd.DragTracker.superclass.constructor.call(this,a)},initEl:function(a){this.el=Ext.get(a);a.on("mousedown",this.onMouseDown,this,this.delegate?{delegate:this.delegate}:undefined)},destroy:function(){this.el.un("mousedown",this.onMouseDown,this);delete this.el},onMouseDown:function(b,a){if(this.fireEvent("mousedown",this,b)!==false&&this.onBeforeStart(b)!==false){this.startXY=this.lastXY=b.getXY();this.dragTarget=this.delegate?a:this.el.dom;if(this.preventDefault!==false){b.preventDefault()}Ext.getDoc().on({scope:this,mouseup:this.onMouseUp,mousemove:this.onMouseMove,selectstart:this.stopSelect});if(this.autoStart){this.timer=this.triggerStart.defer(this.autoStart===true?1000:this.autoStart,this,[b])}}},onMouseMove:function(d,c){if(this.active&&Ext.isIE&&!d.browserEvent.button){d.preventDefault();this.onMouseUp(d);return}d.preventDefault();var b=d.getXY(),a=this.startXY;this.lastXY=b;if(!this.active){if(Math.abs(a[0]-b[0])>this.tolerance||Math.abs(a[1]-b[1])>this.tolerance){this.triggerStart(d)}else{return}}this.fireEvent("mousemove",this,d);this.onDrag(d);this.fireEvent("drag",this,d)},onMouseUp:function(c){var b=Ext.getDoc(),a=this.active;b.un("mousemove",this.onMouseMove,this);b.un("mouseup",this.onMouseUp,this);b.un("selectstart",this.stopSelect,this);c.preventDefault();this.clearStart();this.active=false;delete this.elRegion;this.fireEvent("mouseup",this,c);if(a){this.onEnd(c);this.fireEvent("dragend",this,c)}},triggerStart:function(a){this.clearStart();this.active=true;this.onStart(a);this.fireEvent("dragstart",this,a)},clearStart:function(){if(this.timer){clearTimeout(this.timer);delete this.timer}},stopSelect:function(a){a.stopEvent();return false},onBeforeStart:function(a){},onStart:function(a){},onDrag:function(a){},onEnd:function(a){},getDragTarget:function(){return this.dragTarget},getDragCt:function(){return this.el},getXY:function(a){return a?this.constrainModes[a].call(this,this.lastXY):this.lastXY},getOffset:function(c){var b=this.getXY(c),a=this.startXY;return[a[0]-b[0],a[1]-b[1]]},constrainModes:{point:function(b){if(!this.elRegion){this.elRegion=this.getDragCt().getRegion()}var a=this.dragRegion;a.left=b[0];a.top=b[1];a.right=b[0];a.bottom=b[1];a.constrainTo(this.elRegion);return[a.left,a.top]}}});Ext.dd.ScrollManager=function(){var c=Ext.dd.DragDropMgr;var e={};var b=null;var i={};var h=function(l){b=null;a()};var j=function(){if(c.dragCurrent){c.refreshCache(c.dragCurrent.groups)}};var d=function(){if(c.dragCurrent){var l=Ext.dd.ScrollManager;var m=i.el.ddScrollConfig?i.el.ddScrollConfig.increment:l.increment;if(!l.animate){if(i.el.scroll(i.dir,m)){j()}}else{i.el.scroll(i.dir,m,true,l.animDuration,j)}}};var a=function(){if(i.id){clearInterval(i.id)}i.id=0;i.el=null;i.dir=""};var g=function(m,l){a();i.el=m;i.dir=l;var o=m.ddScrollConfig?m.ddScrollConfig.ddGroup:undefined,n=(m.ddScrollConfig&&m.ddScrollConfig.frequency)?m.ddScrollConfig.frequency:Ext.dd.ScrollManager.frequency;if(o===undefined||c.dragCurrent.ddGroup==o){i.id=setInterval(d,n)}};var k=function(o,q){if(q||!c.dragCurrent){return}var s=Ext.dd.ScrollManager;if(!b||b!=c.dragCurrent){b=c.dragCurrent;s.refreshCache()}var t=Ext.lib.Event.getXY(o);var u=new Ext.lib.Point(t[0],t[1]);for(var m in e){var n=e[m],l=n._region;var p=n.ddScrollConfig?n.ddScrollConfig:s;if(l&&l.contains(u)&&n.isScrollable()){if(l.bottom-u.y<=p.vthresh){if(i.el!=n){g(n,"down")}return}else{if(l.right-u.x<=p.hthresh){if(i.el!=n){g(n,"left")}return}else{if(u.y-l.top<=p.vthresh){if(i.el!=n){g(n,"up")}return}else{if(u.x-l.left<=p.hthresh){if(i.el!=n){g(n,"right")}return}}}}}}a()};c.fireEvents=c.fireEvents.createSequence(k,c);c.stopDrag=c.stopDrag.createSequence(h,c);return{register:function(n){if(Ext.isArray(n)){for(var m=0,l=n.length;m]+>/gi,asText:function(a){return String(a).replace(this.stripTagsRE,"")},asUCText:function(a){return String(a).toUpperCase().replace(this.stripTagsRE,"")},asUCString:function(a){return String(a).toUpperCase()},asDate:function(a){if(!a){return 0}if(Ext.isDate(a)){return a.getTime()}return Date.parse(String(a))},asFloat:function(a){var b=parseFloat(String(a).replace(/,/g,""));return isNaN(b)?0:b},asInt:function(a){var b=parseInt(String(a).replace(/,/g,""),10);return isNaN(b)?0:b}};Ext.data.Record=function(a,b){this.id=(b||b===0)?b:Ext.data.Record.id(this);this.data=a||{}};Ext.data.Record.create=function(e){var c=Ext.extend(Ext.data.Record,{});var d=c.prototype;d.fields=new Ext.util.MixedCollection(false,function(g){return g.name});for(var b=0,a=e.length;b-1){a.join(null);this.data.removeAt(b)}if(this.pruneModifiedRecords){this.modified.remove(a)}if(this.snapshot){this.snapshot.remove(a)}if(b>-1){this.fireEvent("remove",this,a,b)}},removeAt:function(a){this.remove(this.getAt(a))},removeAll:function(b){var a=[];this.each(function(c){a.push(c)});this.clearData();if(this.snapshot){this.snapshot.clear()}if(this.pruneModifiedRecords){this.modified=[]}if(b!==true){this.fireEvent("clear",this,a)}},onClear:function(b,a){Ext.each(a,function(d,c){this.destroyRecord(this,d,c)},this)},insert:function(d,c){var e,a,b;c=[].concat(c);for(e=0,a=c.length;e=0;d--){if(b[d].phantom===true){var a=b.splice(d,1).shift();if(a.isValid()){g.push(a)}}else{if(!b[d].isValid()){b.splice(d,1)}}}if(g.length){h.push(["create",g])}if(b.length){h.push(["update",b])}}j=h.length;if(j){e=++this.batchCounter;for(d=0;d=0;b--){this.modified.splice(this.modified.indexOf(a[b]),1)}}else{this.modified.splice(this.modified.indexOf(a),1)}},reMap:function(b){if(Ext.isArray(b)){for(var d=0,a=b.length;d=0;c--){this.insert(b[c].lastIndex,b[c])}}},handleException:function(a){Ext.handleError(a)},reload:function(a){this.load(Ext.applyIf(a||{},this.lastOptions))},loadRecords:function(b,l,h){var e,g;if(this.isDestroyed===true){return}if(!b||h===false){if(h!==false){this.fireEvent("load",this,[],l)}if(l.callback){l.callback.call(l.scope||this,[],l,false,b)}return}var a=b.records,j=b.totalRecords||a.length;if(!l||l.add!==true){if(this.pruneModifiedRecords){this.modified=[]}for(e=0,g=a.length;e-1){this.doUpdate(d)}else{k.push(d);++c}}this.totalLength=Math.max(j,this.data.length+c);this.add(k)}this.fireEvent("load",this,a,l);if(l.callback){l.callback.call(l.scope||this,a,l,true)}},loadData:function(c,a){var b=this.reader.readRecords(c);this.loadRecords(b,{add:a},true)},getCount:function(){return this.data.length||0},getTotalCount:function(){return this.totalLength||0},getSortState:function(){return this.sortInfo},applySort:function(){if((this.sortInfo||this.multiSortInfo)&&!this.remoteSort){this.sortData()}},sortData:function(){var a=this.hasMultiSort?this.multiSortInfo:this.sortInfo,k=a.direction||"ASC",h=a.sorters,c=[];if(!this.hasMultiSort){h=[{direction:k,field:a.field}]}for(var d=0,b=h.length;d1){for(var p=1,o=c.length;ph?1:(i=0;b--){if(Ext.isArray(c)){this.realize(a.splice(b,1).shift(),c.splice(b,1).shift())}else{this.realize(a.splice(b,1).shift(),c)}}}else{if(Ext.isArray(c)&&c.length==1){c=c.shift()}if(!this.isData(c)){throw new Ext.data.DataReader.Error("realize",a)}a.phantom=false;a._phid=a.id;a.id=this.getId(c);a.data=c;a.commit();a.store.reMap(a)}},update:function(a,c){if(Ext.isArray(a)){for(var b=a.length-1;b>=0;b--){if(Ext.isArray(c)){this.update(a.splice(b,1).shift(),c.splice(b,1).shift())}else{this.update(a.splice(b,1).shift(),c)}}}else{if(Ext.isArray(c)&&c.length==1){c=c.shift()}if(this.isData(c)){a.data=Ext.apply(a.data,c)}a.commit()}},extractData:function(k,a){var j=(this instanceof Ext.data.JsonReader)?"json":"node";var c=[];if(this.isData(k)&&!(this instanceof Ext.data.XmlReader)){k=[k]}var h=this.recordType.prototype.fields,o=h.items,m=h.length,c=[];if(a===true){var l=this.recordType;for(var e=0;e=0){return new Function("obj","return obj"+(b>0?".":"")+c)}return function(d){return d[c]}}}(),extractValues:function(h,d,a){var g,c={};for(var e=0;e<\u003fxml version="{version}" encoding="{encoding}"\u003f><{documentRoot}><{name}>{value}<{root}><{parent.record}><{name}>{value}',render:function(b,c,a){c=this.toArray(c);b.xmlData=this.tpl.applyTemplate({version:this.xmlVersion,encoding:this.xmlEncoding,documentRoot:(c.length>0||this.forceDocumentRoot===true)?this.documentRoot:false,record:this.meta.record,root:this.root,baseParams:c,records:(Ext.isArray(a[0]))?a:[a]})},createRecord:function(a){return this.toArray(this.toHash(a))},updateRecord:function(a){return this.toArray(this.toHash(a))},destroyRecord:function(b){var a={};a[this.meta.idProperty]=b.id;return this.toArray(a)}});Ext.data.XmlReader=function(a,b){a=a||{};Ext.applyIf(a,{idProperty:a.idProperty||a.idPath||a.id,successProperty:a.successProperty||a.success});Ext.data.XmlReader.superclass.constructor.call(this,a,b||a.fields)};Ext.extend(Ext.data.XmlReader,Ext.data.DataReader,{read:function(a){var b=a.responseXML;if(!b){throw {message:"XmlReader.read: XML Document not available"}}return this.readRecords(b)},readRecords:function(d){this.xmlData=d;var a=d.documentElement||d,c=Ext.DomQuery,g=0,e=true;if(this.meta.totalProperty){g=this.getTotal(a,0)}if(this.meta.successProperty){e=this.getSuccess(a)}var b=this.extractData(c.select(this.meta.record,a),true);return{success:e,records:b,totalRecords:g||b.length}},readResponse:function(g,b){var e=Ext.DomQuery,h=b.responseXML,a=h.documentElement||h;var c=new Ext.data.Response({action:g,success:this.getSuccess(a),message:this.getMessage(a),data:this.extractData(e.select(this.meta.record,a)||e.select(this.meta.root,a),false),raw:h});if(Ext.isEmpty(c.success)){throw new Ext.data.DataReader.Error("successProperty-response",this.meta.successProperty)}if(g===Ext.data.Api.actions.create){var d=Ext.isDefined(c.data);if(d&&Ext.isEmpty(c.data)){throw new Ext.data.JsonReader.Error("root-empty",this.meta.root)}else{if(!d){throw new Ext.data.JsonReader.Error("root-undefined-response",this.meta.root)}}}return c},getSuccess:function(){return true},buildExtractors:function(){if(this.ef){return}var l=this.meta,h=this.recordType,e=h.prototype.fields,k=e.items,j=e.length;if(l.totalProperty){this.getTotal=this.createAccessor(l.totalProperty)}if(l.successProperty){this.getSuccess=this.createAccessor(l.successProperty)}if(l.messageProperty){this.getMessage=this.createAccessor(l.messageProperty)}this.getRoot=function(g){return(!Ext.isEmpty(g[this.meta.record]))?g[this.meta.record]:g[this.meta.root]};if(l.idPath||l.idProperty){var d=this.createAccessor(l.idPath||l.idProperty);this.getId=function(g){var i=d(g)||g.id;return(i===undefined||i==="")?null:i}}else{this.getId=function(){return null}}var c=[];for(var b=0;b0&&c[0].field==this.groupField){c.shift()}this.groupField=e;this.groupDir=d;this.applyGroupField();var b=function(){this.fireEvent("groupchange",this,this.getGroupState())};if(this.groupOnSort){this.sort(e,d);b.call(this);return}if(this.remoteGroup){this.on("load",b,this,{single:true});this.reload()}else{this.sort(c);b.call(this)}},sort:function(h,c){if(this.remoteSort){return Ext.data.GroupingStore.superclass.sort.call(this,h,c)}var g=[];if(Ext.isArray(arguments[0])){g=arguments[0]}else{if(h==undefined){g=this.sortInfo?[this.sortInfo]:[]}else{var e=this.fields.get(h);if(!e){return false}var b=e.name,a=this.sortInfo||null,d=this.sortToggle?this.sortToggle[b]:null;if(!c){if(a&&a.field==b){c=(this.sortToggle[b]||"ASC").toggle("ASC","DESC")}else{c=e.sortDir}}this.sortToggle[b]=c;this.sortInfo={field:b,direction:c};g=[this.sortInfo]}}if(this.groupField){g.unshift({direction:this.groupDir,field:this.groupField})}return this.multiSort.call(this,g,c)},applyGroupField:function(){if(this.remoteGroup){if(!this.baseParams){this.baseParams={}}Ext.apply(this.baseParams,{groupBy:this.groupField,groupDir:this.groupDir});var a=this.lastOptions;if(a&&a.params){a.params.groupDir=this.groupDir;delete a.params.groupBy}}},applyGrouping:function(a){if(this.groupField!==false){this.groupBy(this.groupField,true,this.groupDir);return true}else{if(a===true){this.fireEvent("datachanged",this)}return false}},getGroupState:function(){return this.groupOnSort&&this.groupField!==false?(this.sortInfo?this.sortInfo.field:undefined):this.groupField}});Ext.reg("groupingstore",Ext.data.GroupingStore);Ext.data.DirectProxy=function(a){Ext.apply(this,a);if(typeof this.paramOrder=="string"){this.paramOrder=this.paramOrder.split(/[\s,|]/)}Ext.data.DirectProxy.superclass.constructor.call(this,a)};Ext.extend(Ext.data.DirectProxy,Ext.data.DataProxy,{paramOrder:undefined,paramsAsHash:true,directFn:undefined,doRequest:function(b,c,a,e,k,l,n){var j=[],h=this.api[b]||this.directFn;switch(b){case Ext.data.Api.actions.create:j.push(a.jsonData);break;case Ext.data.Api.actions.read:if(h.directCfg.method.len>0){if(this.paramOrder){for(var d=0,g=this.paramOrder.length;d1){for(var d=0,b=c.length;d0){this.doSend(a==1?this.callBuffer[0]:this.callBuffer);this.callBuffer=[]}},queueTransaction:function(a){if(a.form){this.processForm(a);return}this.callBuffer.push(a);if(this.enableBuffer){if(!this.callTask){this.callTask=new Ext.util.DelayedTask(this.combineAndSend,this)}this.callTask.delay(Ext.isNumber(this.enableBuffer)?this.enableBuffer:10)}else{this.combineAndSend()}},doCall:function(i,a,b){var h=null,e=b[a.len],g=b[a.len+1];if(a.len!==0){h=b.slice(0,a.len)}var d=new Ext.Direct.Transaction({provider:this,args:b,action:i,method:a.name,data:h,cb:g&&Ext.isFunction(e)?e.createDelegate(g):e});if(this.fireEvent("beforecall",this,d,a)!==false){Ext.Direct.addTransaction(d);this.queueTransaction(d);this.fireEvent("call",this,d,a)}},doForm:function(j,b,g,i,e){var d=new Ext.Direct.Transaction({provider:this,action:j,method:b.name,args:[g,i,e],cb:e&&Ext.isFunction(i)?i.createDelegate(e):i,isForm:true});if(this.fireEvent("beforecall",this,d,b)!==false){Ext.Direct.addTransaction(d);var a=String(g.getAttribute("enctype")).toLowerCase()=="multipart/form-data",h={extTID:d.tid,extAction:j,extMethod:b.name,extType:"rpc",extUpload:String(a)};Ext.apply(d,{form:Ext.getDom(g),isUpload:a,params:i&&Ext.isObject(i.params)?Ext.apply(h,i.params):h});this.fireEvent("call",this,d,b);this.processForm(d)}},processForm:function(a){Ext.Ajax.request({url:this.url,params:a.params,callback:this.onData,scope:this,form:a.form,isUpload:a.isUpload,ts:a})},createMethod:function(d,a){var b;if(!a.formHandler){b=function(){this.doCall(d,a,Array.prototype.slice.call(arguments,0))}.createDelegate(this)}else{b=function(e,g,c){this.doForm(d,a,e,g,c)}.createDelegate(this)}b.directCfg={action:d,method:a};return b},getTransaction:function(a){return a&&a.tid?Ext.Direct.getTransaction(a.tid):null},doCallback:function(c,g){var d=g.status?"success":"failure";if(c&&c.cb){var b=c.cb,a=Ext.isDefined(g.result)?g.result:g.data;if(Ext.isFunction(b)){b(a,g)}else{Ext.callback(b[d],b.scope,[a,g]);Ext.callback(b.callback,b.scope,[a,g])}}}});Ext.Direct.PROVIDERS.remoting=Ext.direct.RemotingProvider;Ext.Resizable=Ext.extend(Ext.util.Observable,{constructor:function(d,e){this.el=Ext.get(d);if(e&&e.wrap){e.resizeChild=this.el;this.el=this.el.wrap(typeof e.wrap=="object"?e.wrap:{cls:"xresizable-wrap"});this.el.id=this.el.dom.id=e.resizeChild.id+"-rzwrap";this.el.setStyle("overflow","hidden");this.el.setPositioning(e.resizeChild.getPositioning());e.resizeChild.clearPositioning();if(!e.width||!e.height){var g=e.resizeChild.getSize();this.el.setSize(g.width,g.height)}if(e.pinned&&!e.adjustments){e.adjustments="auto"}}this.proxy=this.el.createProxy({tag:"div",cls:"x-resizable-proxy",id:this.el.id+"-rzproxy"},Ext.getBody());this.proxy.unselectable();this.proxy.enableDisplayMode("block");Ext.apply(this,e);if(this.pinned){this.disableTrackOver=true;this.el.addClass("x-resizable-pinned")}var k=this.el.getStyle("position");if(k!="absolute"&&k!="fixed"){this.el.setStyle("position","relative")}if(!this.handles){this.handles="s,e,se";if(this.multiDirectional){this.handles+=",n,w"}}if(this.handles=="all"){this.handles="n s e w ne nw se sw"}var o=this.handles.split(/\s*?[,;]\s*?| /);var c=Ext.Resizable.positions;for(var j=0,l=o.length;j0){if(a>(e/2)){d=c+(e-a)}else{d=c-a}}return Math.max(b,d)},resizeElement:function(){var a=this.proxy.getBox();if(this.updateBox){this.el.setBox(a,false,this.animate,this.duration,null,this.easing)}else{this.el.setSize(a.width,a.height,this.animate,this.duration,null,this.easing)}this.updateChildSize();if(!this.dynamic){this.proxy.hide()}if(this.draggable&&this.constrainTo){this.dd.resetConstraints();this.dd.constrainTo(this.constrainTo)}return a},constrain:function(b,c,a,d){if(b-cd){c=b-d}}return c},onMouseMove:function(z){if(this.enabled&&this.activeHandle){try{if(this.resizeRegion&&!this.resizeRegion.contains(z.getPoint())){return}var t=this.curSize||this.startBox,l=this.startBox.x,k=this.startBox.y,c=l,b=k,m=t.width,u=t.height,d=m,o=u,n=this.minWidth,A=this.minHeight,s=this.maxWidth,D=this.maxHeight,i=this.widthIncrement,a=this.heightIncrement,B=z.getXY(),r=-(this.startPoint[0]-Math.max(this.minX,B[0])),p=-(this.startPoint[1]-Math.max(this.minY,B[1])),j=this.activeHandle.position,E,g;switch(j){case"east":m+=r;m=Math.min(Math.max(n,m),s);break;case"south":u+=p;u=Math.min(Math.max(A,u),D);break;case"southeast":m+=r;u+=p;m=Math.min(Math.max(n,m),s);u=Math.min(Math.max(A,u),D);break;case"north":p=this.constrain(u,p,A,D);k+=p;u-=p;break;case"west":r=this.constrain(m,r,n,s);l+=r;m-=r;break;case"northeast":m+=r;m=Math.min(Math.max(n,m),s);p=this.constrain(u,p,A,D);k+=p;u-=p;break;case"northwest":r=this.constrain(m,r,n,s);p=this.constrain(u,p,A,D);k+=p;u-=p;l+=r;m-=r;break;case"southwest":r=this.constrain(m,r,n,s);u+=p;u=Math.min(Math.max(A,u),D);l+=r;m-=r;break}var q=this.snap(m,i,n);var C=this.snap(u,a,A);if(q!=m||C!=u){switch(j){case"northeast":k-=C-u;break;case"north":k-=C-u;break;case"southwest":l-=q-m;break;case"west":l-=q-m;break;case"northwest":l-=q-m;k-=C-u;break}m=q;u=C}if(this.preserveRatio){switch(j){case"southeast":case"east":u=o*(m/d);u=Math.min(Math.max(A,u),D);m=d*(u/o);break;case"south":m=d*(u/o);m=Math.min(Math.max(n,m),s);u=o*(m/d);break;case"northeast":m=d*(u/o);m=Math.min(Math.max(n,m),s);u=o*(m/d);break;case"north":E=m;m=d*(u/o);m=Math.min(Math.max(n,m),s);u=o*(m/d);l+=(E-m)/2;break;case"southwest":u=o*(m/d);u=Math.min(Math.max(A,u),D);E=m;m=d*(u/o);l+=E-m;break;case"west":g=u;u=o*(m/d);u=Math.min(Math.max(A,u),D);k+=(g-u)/2;E=m;m=d*(u/o);l+=E-m;break;case"northwest":E=m;g=u;u=o*(m/d);u=Math.min(Math.max(A,u),D);m=d*(u/o);k+=g-u;l+=E-m;break}}this.proxy.setBounds(l,k,m,u);if(this.dynamic){this.resizeElement()}}catch(v){}}},handleOver:function(){if(this.enabled){this.el.addClass("x-resizable-over")}},handleOut:function(){if(!this.resizing){this.el.removeClass("x-resizable-over")}},getEl:function(){return this.el},getResizeChild:function(){return this.resizeChild},destroy:function(b){Ext.destroy(this.dd,this.overlay,this.proxy);this.overlay=null;this.proxy=null;var c=Ext.Resizable.positions;for(var a in c){if(typeof c[a]!="function"&&this[c[a]]){this[c[a]].destroy()}}if(b){this.el.update("");Ext.destroy(this.el);this.el=null}this.purgeListeners()},syncHandleHeight:function(){var a=this.el.getHeight(true);if(this.west){this.west.el.setHeight(a)}if(this.east){this.east.el.setHeight(a)}}});Ext.Resizable.positions={n:"north",s:"south",e:"east",w:"west",se:"southeast",sw:"southwest",nw:"northwest",ne:"northeast"};Ext.Resizable.Handle=Ext.extend(Object,{constructor:function(d,g,c,e,a){if(!this.tpl){var b=Ext.DomHelper.createTemplate({tag:"div",cls:"x-resizable-handle x-resizable-handle-{0}"});b.compile();Ext.Resizable.Handle.prototype.tpl=b}this.position=g;this.rz=d;this.el=this.tpl.append(d.el.dom,[this.position],true);this.el.unselectable();if(e){this.el.setOpacity(0)}if(!Ext.isEmpty(a)){this.el.addClass(a)}this.el.on("mousedown",this.onMouseDown,this);if(!c){this.el.on({scope:this,mouseover:this.onMouseOver,mouseout:this.onMouseOut})}},afterResize:function(a){},onMouseDown:function(a){this.rz.onMouseDown(this,a)},onMouseOver:function(a){this.rz.handleOver(this,a)},onMouseOut:function(a){this.rz.handleOut(this,a)},destroy:function(){Ext.destroy(this.el);this.el=null}});Ext.Window=Ext.extend(Ext.Panel,{baseCls:"x-window",resizable:true,draggable:true,closable:true,closeAction:"close",constrain:false,constrainHeader:false,plain:false,minimizable:false,maximizable:false,minHeight:100,minWidth:200,expandOnShow:true,showAnimDuration:0.25,hideAnimDuration:0.25,collapsible:false,initHidden:undefined,hidden:true,elements:"header,body",frame:true,floating:true,initComponent:function(){this.initTools();Ext.Window.superclass.initComponent.call(this);this.addEvents("resize","maximize","minimize","restore");if(Ext.isDefined(this.initHidden)){this.hidden=this.initHidden}if(this.hidden===false){this.hidden=true;this.show()}},getState:function(){return Ext.apply(Ext.Window.superclass.getState.call(this)||{},this.getBox(true))},onRender:function(b,a){Ext.Window.superclass.onRender.call(this,b,a);if(this.plain){this.el.addClass("x-window-plain")}this.focusEl=this.el.createChild({tag:"a",href:"#",cls:"x-dlg-focus",tabIndex:"-1",html:" "});this.focusEl.swallowEvent("click",true);this.proxy=this.el.createProxy("x-window-proxy");this.proxy.enableDisplayMode("block");if(this.modal){this.mask=this.container.createChild({cls:"ext-el-mask"},this.el.dom);this.mask.enableDisplayMode("block");this.mask.hide();this.mon(this.mask,"click",this.focus,this)}if(this.maximizable){this.mon(this.header,"dblclick",this.toggleMaximize,this)}},initEvents:function(){Ext.Window.superclass.initEvents.call(this);if(this.animateTarget){this.setAnimateTarget(this.animateTarget)}if(this.resizable){this.resizer=new Ext.Resizable(this.el,{minWidth:this.minWidth,minHeight:this.minHeight,handles:this.resizeHandles||"all",pinned:true,resizeElement:this.resizerAction,handleCls:"x-window-handle"});this.resizer.window=this;this.mon(this.resizer,"beforeresize",this.beforeResize,this)}if(this.draggable){this.header.addClass("x-window-draggable")}this.mon(this.el,"mousedown",this.toFront,this);this.manager=this.manager||Ext.WindowMgr;this.manager.register(this);if(this.maximized){this.maximized=false;this.maximize()}if(this.closable){var a=this.getKeyMap();a.on(27,this.onEsc,this);a.disable()}},initDraggable:function(){this.dd=new Ext.Window.DD(this)},onEsc:function(a,b){if(this.activeGhost){this.unghost()}b.stopEvent();this[this.closeAction]()},beforeDestroy:function(){if(this.rendered){this.hide();this.clearAnchor();Ext.destroy(this.focusEl,this.resizer,this.dd,this.proxy,this.mask)}Ext.Window.superclass.beforeDestroy.call(this)},onDestroy:function(){if(this.manager){this.manager.unregister(this)}Ext.Window.superclass.onDestroy.call(this)},initTools:function(){if(this.minimizable){this.addTool({id:"minimize",handler:this.minimize.createDelegate(this,[])})}if(this.maximizable){this.addTool({id:"maximize",handler:this.maximize.createDelegate(this,[])});this.addTool({id:"restore",handler:this.restore.createDelegate(this,[]),hidden:true})}if(this.closable){this.addTool({id:"close",handler:this[this.closeAction].createDelegate(this,[])})}},resizerAction:function(){var a=this.proxy.getBox();this.proxy.hide();this.window.handleResize(a);return a},beforeResize:function(){this.resizer.minHeight=Math.max(this.minHeight,this.getFrameHeight()+40);this.resizer.minWidth=Math.max(this.minWidth,this.getFrameWidth()+40);this.resizeBox=this.el.getBox()},updateHandles:function(){if(Ext.isIE&&this.resizer){this.resizer.syncHandleHeight();this.el.repaint()}},handleResize:function(b){var a=this.resizeBox;if(a.x!=b.x||a.y!=b.y){this.updateBox(b)}else{this.setSize(b);if(Ext.isIE6&&Ext.isStrict){this.doLayout()}}this.focus();this.updateHandles();this.saveState()},focus:function(){var e=this.focusEl,a=this.defaultButton,c=typeof a,d,b;if(Ext.isDefined(a)){if(Ext.isNumber(a)&&this.fbar){e=this.fbar.items.get(a)}else{if(Ext.isString(a)){e=Ext.getCmp(a)}else{e=a}}d=e.getEl();b=Ext.getDom(this.container);if(d&&b){if(b!=document.body&&!Ext.lib.Region.getRegion(b).contains(Ext.lib.Region.getRegion(d.dom))){return}}}e=e||this.focusEl;e.focus.defer(10,e)},setAnimateTarget:function(a){a=Ext.get(a);this.animateTarget=a},beforeShow:function(){delete this.el.lastXY;delete this.el.lastLT;if(this.x===undefined||this.y===undefined){var a=this.el.getAlignToXY(this.container,"c-c");var b=this.el.translatePoints(a[0],a[1]);this.x=this.x===undefined?b.left:this.x;this.y=this.y===undefined?b.top:this.y}this.el.setLeftTop(this.x,this.y);if(this.expandOnShow){this.expand(false)}if(this.modal){Ext.getBody().addClass("x-body-masked");this.mask.setSize(Ext.lib.Dom.getViewWidth(true),Ext.lib.Dom.getViewHeight(true));this.mask.show()}},show:function(c,a,b){if(!this.rendered){this.render(Ext.getBody())}if(this.hidden===false){this.toFront();return this}if(this.fireEvent("beforeshow",this)===false){return this}if(a){this.on("show",a,b,{single:true})}this.hidden=false;if(Ext.isDefined(c)){this.setAnimateTarget(c)}this.beforeShow();if(this.animateTarget){this.animShow()}else{this.afterShow()}return this},afterShow:function(b){if(this.isDestroyed){return false}this.proxy.hide();this.el.setStyle("display","block");this.el.show();if(this.maximized){this.fitContainer()}if(Ext.isMac&&Ext.isGecko2){this.cascade(this.setAutoScroll)}if(this.monitorResize||this.modal||this.constrain||this.constrainHeader){Ext.EventManager.onWindowResize(this.onWindowResize,this)}this.doConstrain();this.doLayout();if(this.keyMap){this.keyMap.enable()}this.toFront();this.updateHandles();if(b&&(Ext.isIE||Ext.isWebKit)){var a=this.getSize();this.onResize(a.width,a.height)}this.onShow();this.fireEvent("show",this)},animShow:function(){this.proxy.show();this.proxy.setBox(this.animateTarget.getBox());this.proxy.setOpacity(0);var a=this.getBox();this.el.setStyle("display","none");this.proxy.shift(Ext.apply(a,{callback:this.afterShow.createDelegate(this,[true],false),scope:this,easing:"easeNone",duration:this.showAnimDuration,opacity:0.5}))},hide:function(c,a,b){if(this.hidden||this.fireEvent("beforehide",this)===false){return this}if(a){this.on("hide",a,b,{single:true})}this.hidden=true;if(c!==undefined){this.setAnimateTarget(c)}if(this.modal){this.mask.hide();Ext.getBody().removeClass("x-body-masked")}if(this.animateTarget){this.animHide()}else{this.el.hide();this.afterHide()}return this},afterHide:function(){this.proxy.hide();if(this.monitorResize||this.modal||this.constrain||this.constrainHeader){Ext.EventManager.removeResizeListener(this.onWindowResize,this)}if(this.keyMap){this.keyMap.disable()}this.onHide();this.fireEvent("hide",this)},animHide:function(){this.proxy.setOpacity(0.5);this.proxy.show();var a=this.getBox(false);this.proxy.setBox(a);this.el.hide();this.proxy.shift(Ext.apply(this.animateTarget.getBox(),{callback:this.afterHide,scope:this,duration:this.hideAnimDuration,easing:"easeNone",opacity:0}))},onShow:Ext.emptyFn,onHide:Ext.emptyFn,onWindowResize:function(){if(this.maximized){this.fitContainer()}if(this.modal){this.mask.setSize("100%","100%");var a=this.mask.dom.offsetHeight;this.mask.setSize(Ext.lib.Dom.getViewWidth(true),Ext.lib.Dom.getViewHeight(true))}this.doConstrain()},doConstrain:function(){if(this.constrain||this.constrainHeader){var b;if(this.constrain){b={right:this.el.shadowOffset,left:this.el.shadowOffset,bottom:this.el.shadowOffset}}else{var a=this.getSize();b={right:-(a.width-100),bottom:-(a.height-25+this.el.getConstrainOffset())}}var c=this.el.getConstrainToXY(this.container,true,b);if(c){this.setPosition(c[0],c[1])}}},ghost:function(a){var c=this.createGhost(a);var b=this.getBox(true);c.setLeftTop(b.x,b.y);c.setWidth(b.width);this.el.hide();this.activeGhost=c;return c},unghost:function(b,a){if(!this.activeGhost){return}if(b!==false){this.el.show();this.focus.defer(10,this);if(Ext.isMac&&Ext.isGecko2){this.cascade(this.setAutoScroll)}}if(a!==false){this.setPosition(this.activeGhost.getLeft(true),this.activeGhost.getTop(true))}this.activeGhost.hide();this.activeGhost.remove();delete this.activeGhost},minimize:function(){this.fireEvent("minimize",this);return this},close:function(){if(this.fireEvent("beforeclose",this)!==false){if(this.hidden){this.doClose()}else{this.hide(null,this.doClose,this)}}},doClose:function(){this.fireEvent("close",this);this.destroy()},maximize:function(){if(!this.maximized){this.expand(false);this.restoreSize=this.getSize();this.restorePos=this.getPosition(true);if(this.maximizable){this.tools.maximize.hide();this.tools.restore.show()}this.maximized=true;this.el.disableShadow();if(this.dd){this.dd.lock()}if(this.collapsible){this.tools.toggle.hide()}this.el.addClass("x-window-maximized");this.container.addClass("x-window-maximized-ct");this.setPosition(0,0);this.fitContainer();this.fireEvent("maximize",this)}return this},restore:function(){if(this.maximized){var a=this.tools;this.el.removeClass("x-window-maximized");if(a.restore){a.restore.hide()}if(a.maximize){a.maximize.show()}this.setPosition(this.restorePos[0],this.restorePos[1]);this.setSize(this.restoreSize.width,this.restoreSize.height);delete this.restorePos;delete this.restoreSize;this.maximized=false;this.el.enableShadow(true);if(this.dd){this.dd.unlock()}if(this.collapsible&&a.toggle){a.toggle.show()}this.container.removeClass("x-window-maximized-ct");this.doConstrain();this.fireEvent("restore",this)}return this},toggleMaximize:function(){return this[this.maximized?"restore":"maximize"]()},fitContainer:function(){var a=this.container.getViewSize(false);this.setSize(a.width,a.height)},setZIndex:function(a){if(this.modal){this.mask.setStyle("z-index",a)}this.el.setZIndex(++a);a+=5;if(this.resizer){this.resizer.proxy.setStyle("z-index",++a)}this.lastZIndex=a},alignTo:function(b,a,c){var d=this.el.getAlignToXY(b,a,c);this.setPagePosition(d[0],d[1]);return this},anchorTo:function(c,e,d,b){this.clearAnchor();this.anchorTarget={el:c,alignment:e,offsets:d};Ext.EventManager.onWindowResize(this.doAnchor,this);var a=typeof b;if(a!="undefined"){Ext.EventManager.on(window,"scroll",this.doAnchor,this,{buffer:a=="number"?b:50})}return this.doAnchor()},doAnchor:function(){var a=this.anchorTarget;this.alignTo(a.el,a.alignment,a.offsets);return this},clearAnchor:function(){if(this.anchorTarget){Ext.EventManager.removeResizeListener(this.doAnchor,this);Ext.EventManager.un(window,"scroll",this.doAnchor,this);delete this.anchorTarget}return this},toFront:function(a){if(this.manager.bringToFront(this)){if(!a||!a.getTarget().focus){this.focus()}}return this},setActive:function(a){if(a){if(!this.maximized){this.el.enableShadow(true)}this.fireEvent("activate",this)}else{this.el.disableShadow();this.fireEvent("deactivate",this)}},toBack:function(){this.manager.sendToBack(this);return this},center:function(){var a=this.el.getAlignToXY(this.container,"c-c");this.setPagePosition(a[0],a[1]);return this}});Ext.reg("window",Ext.Window);Ext.Window.DD=Ext.extend(Ext.dd.DD,{constructor:function(a){this.win=a;Ext.Window.DD.superclass.constructor.call(this,a.el.id,"WindowDD-"+a.id);this.setHandleElId(a.header.id);this.scroll=false},moveOnly:true,headerOffsets:[100,25],startDrag:function(){var a=this.win;this.proxy=a.ghost(a.initialConfig.cls);if(a.constrain!==false){var c=a.el.shadowOffset;this.constrainTo(a.container,{right:c,left:c,bottom:c})}else{if(a.constrainHeader!==false){var b=this.proxy.getSize();this.constrainTo(a.container,{right:-(b.width-this.headerOffsets[0]),bottom:-(b.height-this.headerOffsets[1])})}}},b4Drag:Ext.emptyFn,onDrag:function(a){this.alignElWithMouse(this.proxy,a.getPageX(),a.getPageY())},endDrag:function(a){this.win.unghost();this.win.saveState()}});Ext.WindowGroup=function(){var g={};var d=[];var e=null;var c=function(j,i){return(!j._lastAccess||j._lastAccess0){l.sort(c);var k=l[0].manager.zseed;for(var m=0;m=0;--j){if(!d[j].hidden){b(d[j]);return}}b(null)};return{zseed:9000,register:function(i){if(i.manager){i.manager.unregister(i)}i.manager=this;g[i.id]=i;d.push(i);i.on("hide",a)},unregister:function(i){delete i.manager;delete g[i.id];i.un("hide",a);d.remove(i)},get:function(i){return typeof i=="object"?i:g[i]},bringToFront:function(i){i=this.get(i);if(i!=e){i._lastAccess=new Date().getTime();h();return true}return false},sendToBack:function(i){i=this.get(i);i._lastAccess=-(new Date().getTime());h();return i},hideAll:function(){for(var i in g){if(g[i]&&typeof g[i]!="function"&&g[i].isVisible()){g[i].hide()}}},getActive:function(){return e},getBy:function(l,k){var m=[];for(var j=d.length-1;j>=0;--j){var n=d[j];if(l.call(k||n,n)!==false){m.push(n)}}return m},each:function(j,i){for(var k in g){if(g[k]&&typeof g[k]!="function"){if(j.call(i||g[k],g[k])===false){return}}}}}};Ext.WindowMgr=new Ext.WindowGroup();Ext.MessageBox=function(){var u,b,q,t,h,l,s,a,n,p,j,g,r,v,o,i="",d="",m=["ok","yes","no","cancel"];var c=function(x){r[x].blur();if(u.isVisible()){u.hide();w();Ext.callback(b.fn,b.scope||window,[x,v.dom.value,b],1)}};var w=function(){if(b&&b.cls){u.el.removeClass(b.cls)}n.reset()};var e=function(z,x,y){if(b&&b.closable!==false){u.hide();w()}if(y){y.stopEvent()}};var k=function(x){var z=0,y;if(!x){Ext.each(m,function(A){r[A].hide()});return z}u.footer.dom.style.display="";Ext.iterate(r,function(A,B){y=x[A];if(y){B.show();B.setText(Ext.isString(y)?y:Ext.MessageBox.buttonText[A]);z+=B.getEl().getWidth()+15}else{B.hide()}});return z};return{getDialog:function(x){if(!u){var z=[];r={};Ext.each(m,function(A){z.push(r[A]=new Ext.Button({text:this.buttonText[A],handler:c.createCallback(A),hideMode:"offsets"}))},this);u=new Ext.Window({autoCreate:true,title:x,resizable:false,constrain:true,constrainHeader:true,minimizable:false,maximizable:false,stateful:false,modal:true,shim:true,buttonAlign:"center",width:400,height:100,minHeight:80,plain:true,footer:true,closable:true,close:function(){if(b&&b.buttons&&b.buttons.no&&!b.buttons.cancel){c("no")}else{c("cancel")}},fbar:new Ext.Toolbar({items:z,enableOverflow:false})});u.render(document.body);u.getEl().addClass("x-window-dlg");q=u.mask;h=u.body.createChild({html:'

    '});j=Ext.get(h.dom.firstChild);var y=h.dom.childNodes[1];l=Ext.get(y.firstChild);s=Ext.get(y.childNodes[2].firstChild);s.enableDisplayMode();s.addKeyListener([10,13],function(){if(u.isVisible()&&b&&b.buttons){if(b.buttons.ok){c("ok")}else{if(b.buttons.yes){c("yes")}}}});a=Ext.get(y.childNodes[2].childNodes[1]);a.enableDisplayMode();n=new Ext.ProgressBar({renderTo:h});h.createChild({cls:"x-clear"})}return u},updateText:function(A){if(!u.isVisible()&&!b.width){u.setSize(this.maxWidth,100)}l.update(A?A+" ":" ");var y=d!=""?(j.getWidth()+j.getMargins("lr")):0,C=l.getWidth()+l.getMargins("lr"),z=u.getFrameWidth("lr"),B=u.body.getFrameWidth("lr"),x;x=Math.max(Math.min(b.width||y+C+z+B,b.maxWidth||this.maxWidth),Math.max(b.minWidth||this.minWidth,o||0));if(b.prompt===true){v.setWidth(x-y-z-B)}if(b.progress===true||b.wait===true){n.setSize(x-y-z-B)}if(Ext.isIE&&x==o){x+=4}l.update(A||" ");u.setSize(x,"auto").center();return this},updateProgress:function(y,x,z){n.updateProgress(y,x);if(z){this.updateText(z)}return this},isVisible:function(){return u&&u.isVisible()},hide:function(){var x=u?u.activeGhost:null;if(this.isVisible()||x){u.hide();w();if(x){u.unghost(false,false)}}return this},show:function(A){if(this.isVisible()){this.hide()}b=A;var B=this.getDialog(b.title||" ");B.setTitle(b.title||" ");var x=(b.closable!==false&&b.progress!==true&&b.wait!==true);B.tools.close.setDisplayed(x);v=s;b.prompt=b.prompt||(b.multiline?true:false);if(b.prompt){if(b.multiline){s.hide();a.show();a.setHeight(Ext.isNumber(b.multiline)?b.multiline:this.defaultTextHeight);v=a}else{s.show();a.hide()}}else{s.hide();a.hide()}v.dom.value=b.value||"";if(b.prompt){B.focusEl=v}else{var z=b.buttons;var y=null;if(z&&z.ok){y=r.ok}else{if(z&&z.yes){y=r.yes}}if(y){B.focusEl=y}}if(Ext.isDefined(b.iconCls)){B.setIconClass(b.iconCls)}this.setIcon(Ext.isDefined(b.icon)?b.icon:i);o=k(b.buttons);n.setVisible(b.progress===true||b.wait===true);this.updateProgress(0,b.progressText);this.updateText(b.msg);if(b.cls){B.el.addClass(b.cls)}B.proxyDrag=b.proxyDrag===true;B.modal=b.modal!==false;B.mask=b.modal!==false?q:false;if(!B.isVisible()){document.body.appendChild(u.el.dom);B.setAnimateTarget(b.animEl);B.on("show",function(){if(x===true){B.keyMap.enable()}else{B.keyMap.disable()}},this,{single:true});B.show(b.animEl)}if(b.wait===true){n.wait(b.waitConfig)}return this},setIcon:function(x){if(!u){i=x;return}i=undefined;if(x&&x!=""){j.removeClass("x-hidden");j.replaceClass(d,x);h.addClass("x-dlg-icon");d=x}else{j.replaceClass(d,"x-hidden");h.removeClass("x-dlg-icon");d=""}return this},progress:function(z,y,x){this.show({title:z,msg:y,buttons:false,progress:true,closable:false,minWidth:this.minProgressWidth,progressText:x});return this},wait:function(z,y,x){this.show({title:y,msg:z,buttons:false,closable:false,wait:true,modal:true,minWidth:this.minProgressWidth,waitConfig:x});return this},alert:function(A,z,y,x){this.show({title:A,msg:z,buttons:this.OK,fn:y,scope:x,minWidth:this.minWidth});return this},confirm:function(A,z,y,x){this.show({title:A,msg:z,buttons:this.YESNO,fn:y,scope:x,icon:this.QUESTION,minWidth:this.minWidth});return this},prompt:function(C,B,z,y,x,A){this.show({title:C,msg:B,buttons:this.OKCANCEL,fn:z,minWidth:this.minPromptWidth,scope:y,prompt:true,multiline:x,value:A});return this},OK:{ok:true},CANCEL:{cancel:true},OKCANCEL:{ok:true,cancel:true},YESNO:{yes:true,no:true},YESNOCANCEL:{yes:true,no:true,cancel:true},INFO:"ext-mb-info",WARNING:"ext-mb-warning",QUESTION:"ext-mb-question",ERROR:"ext-mb-error",defaultTextHeight:75,maxWidth:600,minWidth:100,minProgressWidth:250,minPromptWidth:250,buttonText:{ok:"OK",cancel:"Cancel",yes:"Yes",no:"No"}}}();Ext.Msg=Ext.MessageBox;Ext.dd.PanelProxy=Ext.extend(Object,{constructor:function(a,b){this.panel=a;this.id=this.panel.id+"-ddproxy";Ext.apply(this,b)},insertProxy:true,setStatus:Ext.emptyFn,reset:Ext.emptyFn,update:Ext.emptyFn,stop:Ext.emptyFn,sync:Ext.emptyFn,getEl:function(){return this.ghost},getGhost:function(){return this.ghost},getProxy:function(){return this.proxy},hide:function(){if(this.ghost){if(this.proxy){this.proxy.remove();delete this.proxy}this.panel.el.dom.style.display="";this.ghost.remove();delete this.ghost}},show:function(){if(!this.ghost){this.ghost=this.panel.createGhost(this.panel.initialConfig.cls,undefined,Ext.getBody());this.ghost.setXY(this.panel.el.getXY());if(this.insertProxy){this.proxy=this.panel.el.insertSibling({cls:"x-panel-dd-spacer"});this.proxy.setSize(this.panel.getSize())}this.panel.el.dom.style.display="none"}},repair:function(b,c,a){this.hide();if(typeof c=="function"){c.call(a||this)}},moveProxy:function(a,b){if(this.proxy){a.insertBefore(this.proxy.dom,b)}}});Ext.Panel.DD=Ext.extend(Ext.dd.DragSource,{constructor:function(b,a){this.panel=b;this.dragData={panel:b};this.proxy=new Ext.dd.PanelProxy(b,a);Ext.Panel.DD.superclass.constructor.call(this,b.el,a);var d=b.header,c=b.body;if(d){this.setHandleElId(d.id);c=b.header}c.setStyle("cursor","move");this.scroll=false},showFrame:Ext.emptyFn,startDrag:Ext.emptyFn,b4StartDrag:function(a,b){this.proxy.show()},b4MouseDown:function(b){var a=b.getPageX(),c=b.getPageY();this.autoOffset(a,c)},onInitDrag:function(a,b){this.onStartDrag(a,b);return true},createFrame:Ext.emptyFn,getDragEl:function(a){return this.proxy.ghost.dom},endDrag:function(a){this.proxy.hide();this.panel.saveState()},autoOffset:function(a,b){a-=this.startPageX;b-=this.startPageY;this.setDelta(a,b)}});Ext.state.Provider=Ext.extend(Ext.util.Observable,{constructor:function(){this.addEvents("statechange");this.state={};Ext.state.Provider.superclass.constructor.call(this)},get:function(b,a){return typeof this.state[b]=="undefined"?a:this.state[b]},clear:function(a){delete this.state[a];this.fireEvent("statechange",this,a,null)},set:function(a,b){this.state[a]=b;this.fireEvent("statechange",this,a,b)},decodeValue:function(b){var e=/^(a|n|d|b|s|o|e)\:(.*)$/,h=e.exec(unescape(b)),d,c,a,g;if(!h||!h[1]){return}c=h[1];a=h[2];switch(c){case"e":return null;case"n":return parseFloat(a);case"d":return new Date(Date.parse(a));case"b":return(a=="1");case"a":d=[];if(a!=""){Ext.each(a.split("^"),function(i){d.push(this.decodeValue(i))},this)}return d;case"o":d={};if(a!=""){Ext.each(a.split("^"),function(i){g=i.split("=");d[g[0]]=this.decodeValue(g[1])},this)}return d;default:return a}},encodeValue:function(c){var b,g="",e=0,a,d;if(c==null){return"e:1"}else{if(typeof c=="number"){b="n:"+c}else{if(typeof c=="boolean"){b="b:"+(c?"1":"0")}else{if(Ext.isDate(c)){b="d:"+c.toGMTString()}else{if(Ext.isArray(c)){for(a=c.length;e-1){var e=this.isSelected(b),c=this.all.elements[b],d=this.bufferRender([a],b)[0];this.all.replaceElement(b,d,true);if(e){this.selected.replaceElement(c,d);this.all.item(b).addClass(this.selectedClass)}this.updateIndexes(b,b)}},onAdd:function(g,d,e){if(this.all.getCount()===0){this.refresh();return}var c=this.bufferRender(d,e),h,b=this.all.elements;if(e0){if(!b){this.selected.removeClass(this.selectedClass)}this.selected.clear();this.last=false;if(!a){this.fireEvent("selectionchange",this,this.selected.elements)}}},isSelected:function(a){return this.selected.contains(this.getNode(a))},deselect:function(a){if(this.isSelected(a)){a=this.getNode(a);this.selected.removeElement(a);if(this.last==a.viewIndex){this.last=false}Ext.fly(a).removeClass(this.selectedClass);this.fireEvent("selectionchange",this,this.selected.elements)}},select:function(d,g,b){if(Ext.isArray(d)){if(!g){this.clearSelections(true)}for(var c=0,a=d.length;c=a&&d[c];c--){b.push(d[c])}}return b},indexOf:function(a){a=this.getNode(a);if(Ext.isNumber(a.viewIndex)){return a.viewIndex}return this.all.indexOf(a)},onBeforeLoad:function(){if(this.loadingText){this.clearSelections(false,true);this.getTemplateTarget().update('
    '+this.loadingText+"
    ");this.all.clear()}},onDestroy:function(){this.all.clear();this.selected.clear();Ext.DataView.superclass.onDestroy.call(this);this.bindStore(null)}});Ext.DataView.prototype.setStore=Ext.DataView.prototype.bindStore;Ext.reg("dataview",Ext.DataView);Ext.list.ListView=Ext.extend(Ext.DataView,{itemSelector:"dl",selectedClass:"x-list-selected",overClass:"x-list-over",scrollOffset:undefined,columnResize:true,columnSort:true,maxColumnWidth:Ext.isIE?99:100,initComponent:function(){if(this.columnResize){this.colResizer=new Ext.list.ColumnResizer(this.colResizer);this.colResizer.init(this)}if(this.columnSort){this.colSorter=new Ext.list.Sorter(this.columnSort);this.colSorter.init(this)}if(!this.internalTpl){this.internalTpl=new Ext.XTemplate('
    ','','
    ',"{header}","
    ","
    ",'
    ',"
    ",'
    ',"
    ")}if(!this.tpl){this.tpl=new Ext.XTemplate('',"
    ",'','
    ',' class="{cls}">',"{[values.tpl.apply(parent)]}","
    ","
    ",'
    ',"
    ","
    ")}var l=this.columns,h=0,k=0,m=l.length,b=[];for(var g=0;gthis.maxColumnWidth){n.width-=(h-this.maxColumnWidth)/100}k++}b.push(n)}l=this.columns=b;if(k10)){b.style.width=d;g.style.width=d}else{b.style.width=c+"px";g.style.width=c+"px";setTimeout(function(){if((a.offsetWidth-a.clientWidth)>10){b.style.width=d;g.style.width=d}},10)}}if(Ext.isNumber(e)){a.style.height=Math.max(0,e-g.parentNode.offsetHeight)+"px"}},updateIndexes:function(){Ext.list.ListView.superclass.updateIndexes.apply(this,arguments);this.verifyInternalSize()},findHeaderIndex:function(g){g=g.dom||g;var a=g.parentNode,d=a.parentNode.childNodes,b=0,e;for(;e=d[b];b++){if(e==a){return b}}return -1},setHdWidths:function(){var d=this.innerHd.dom.getElementsByTagName("div"),c=0,b=this.columns,a=b.length;for(;c','','{text}',"");d.disableFormats=true;d.compile();Ext.TabPanel.prototype.itemTpl=d}this.items.each(this.initTab,this)},afterRender:function(){Ext.TabPanel.superclass.afterRender.call(this);if(this.autoTabs){this.readTabs(false)}if(this.activeTab!==undefined){var a=Ext.isObject(this.activeTab)?this.activeTab:this.items.get(this.activeTab);delete this.activeTab;this.setActiveTab(a)}},initEvents:function(){Ext.TabPanel.superclass.initEvents.call(this);this.mon(this.strip,{scope:this,mousedown:this.onStripMouseDown,contextmenu:this.onStripContextMenu});if(this.enableTabScroll){this.mon(this.strip,"mousewheel",this.onWheel,this)}},findTargets:function(c){var b=null,a=c.getTarget("li:not(.x-tab-edge)",this.strip);if(a){b=this.getComponent(a.id.split(this.idDelimiter)[1]);if(b.disabled){return{close:null,item:null,el:null}}}return{close:c.getTarget(".x-tab-strip-close",this.strip),item:b,el:a}},onStripMouseDown:function(b){if(b.button!==0){return}b.preventDefault();var a=this.findTargets(b);if(a.close){if(a.item.fireEvent("beforeclose",a.item)!==false){a.item.fireEvent("close",a.item);this.remove(a.item)}return}if(a.item&&a.item!=this.activeTab){this.setActiveTab(a.item)}},onStripContextMenu:function(b){b.preventDefault();var a=this.findTargets(b);if(a.item){this.fireEvent("contextmenu",this,a.item,b)}},readTabs:function(d){if(d===true){this.items.each(function(h){this.remove(h)},this)}var c=this.el.query(this.autoTabSelector);for(var b=0,a=c.length;b0){this.setActiveTab(0)}else{this.setActiveTab(null)}}}if(!this.destroying){this.delegateUpdates()}},onBeforeShowItem:function(a){if(a!=this.activeTab){this.setActiveTab(a);return false}},onItemDisabled:function(b){var a=this.getTabEl(b);if(a){Ext.fly(a).addClass("x-item-disabled")}this.stack.remove(b)},onItemEnabled:function(b){var a=this.getTabEl(b);if(a){Ext.fly(a).removeClass("x-item-disabled")}},onItemTitleChanged:function(b){var a=this.getTabEl(b);if(a){Ext.fly(a).child("span.x-tab-strip-text",true).innerHTML=b.title}},onItemIconChanged:function(d,a,c){var b=this.getTabEl(d);if(b){b=Ext.get(b);b.child("span.x-tab-strip-text").replaceClass(c,a);b[Ext.isEmpty(a)?"removeClass":"addClass"]("x-tab-with-icon")}},getTabEl:function(a){var b=this.getComponent(a);return b?b.tabEl:null},onResize:function(){Ext.TabPanel.superclass.onResize.apply(this,arguments);this.delegateUpdates()},beginUpdate:function(){this.suspendUpdates=true},endUpdate:function(){this.suspendUpdates=false;this.delegateUpdates()},hideTabStripItem:function(b){b=this.getComponent(b);var a=this.getTabEl(b);if(a){a.style.display="none";this.delegateUpdates()}this.stack.remove(b)},unhideTabStripItem:function(b){b=this.getComponent(b);var a=this.getTabEl(b);if(a){a.style.display="";this.delegateUpdates()}},delegateUpdates:function(){var a=this.rendered;if(this.suspendUpdates){return}if(this.resizeTabs&&a){this.autoSizeTabs()}if(this.enableTabScroll&&a){this.autoScrollTabs()}},autoSizeTabs:function(){var h=this.items.length,b=this.tabPosition!="bottom"?"header":"footer",c=this[b].dom.offsetWidth,a=this[b].dom.clientWidth;if(!this.resizeTabs||h<1||!a){return}var k=Math.max(Math.min(Math.floor((a-4)/h)-this.tabMargin,this.tabWidth),this.minTabWidth);this.lastTabWidth=k;var m=this.strip.query("li:not(.x-tab-edge)");for(var e=0,j=m.length;e20?c:20);if(!this.scrolling){if(!this.scrollLeft){this.createScrollers()}else{this.scrollLeft.show();this.scrollRight.show()}}this.scrolling=true;if(i>(a-c)){e.scrollLeft=a-c}else{this.scrollToTab(this.activeTab,false)}this.updateScrollButtons()}},createScrollers:function(){this.pos.addClass("x-tab-scrolling-"+this.tabPosition);var c=this.stripWrap.dom.offsetHeight;var a=this.pos.insertFirst({cls:"x-tab-scroller-left"});a.setHeight(c);a.addClassOnOver("x-tab-scroller-left-over");this.leftRepeater=new Ext.util.ClickRepeater(a,{interval:this.scrollRepeatInterval,handler:this.onScrollLeft,scope:this});this.scrollLeft=a;var b=this.pos.insertFirst({cls:"x-tab-scroller-right"});b.setHeight(c);b.addClassOnOver("x-tab-scroller-right-over");this.rightRepeater=new Ext.util.ClickRepeater(b,{interval:this.scrollRepeatInterval,handler:this.onScrollRight,scope:this});this.scrollRight=b},getScrollWidth:function(){return this.edge.getOffsetsTo(this.stripWrap)[0]+this.getScrollPos()},getScrollPos:function(){return parseInt(this.stripWrap.dom.scrollLeft,10)||0},getScrollArea:function(){return parseInt(this.stripWrap.dom.clientWidth,10)||0},getScrollAnim:function(){return{duration:this.scrollDuration,callback:this.updateScrollButtons,scope:this}},getScrollIncrement:function(){return this.scrollIncrement||(this.resizeTabs?this.lastTabWidth+2:100)},scrollToTab:function(e,a){if(!e){return}var c=this.getTabEl(e),h=this.getScrollPos(),d=this.getScrollArea(),g=Ext.fly(c).getOffsetsTo(this.stripWrap)[0]+h,b=g+c.offsetWidth;if(g(h+d)){this.scrollTo(b-d,a)}}},scrollTo:function(b,a){this.stripWrap.scrollTo("left",b,a?this.getScrollAnim():false);if(!a){this.updateScrollButtons()}},onWheel:function(g){var h=g.getWheelDelta()*this.wheelIncrement*-1;g.stopEvent();var i=this.getScrollPos(),c=i+h,a=this.getScrollWidth()-this.getScrollArea();var b=Math.max(0,Math.min(a,c));if(b!=i){this.scrollTo(b,false)}},onScrollRight:function(){var a=this.getScrollWidth()-this.getScrollArea(),c=this.getScrollPos(),b=Math.min(a,c+this.getScrollIncrement());if(b!=c){this.scrollTo(b,this.animScroll)}},onScrollLeft:function(){var b=this.getScrollPos(),a=Math.max(0,b-this.getScrollIncrement());if(a!=b){this.scrollTo(a,this.animScroll)}},updateScrollButtons:function(){var a=this.getScrollPos();this.scrollLeft[a===0?"addClass":"removeClass"]("x-tab-scroller-left-disabled");this.scrollRight[a>=(this.getScrollWidth()-this.getScrollArea())?"addClass":"removeClass"]("x-tab-scroller-right-disabled")},beforeDestroy:function(){Ext.destroy(this.leftRepeater,this.rightRepeater);this.deleteMembers("strip","edge","scrollLeft","scrollRight","stripWrap");this.activeTab=null;Ext.TabPanel.superclass.beforeDestroy.apply(this)}});Ext.reg("tabpanel",Ext.TabPanel);Ext.TabPanel.prototype.activate=Ext.TabPanel.prototype.setActiveTab;Ext.TabPanel.AccessStack=function(){var a=[];return{add:function(b){a.push(b);if(a.length>10){a.shift()}},remove:function(e){var d=[];for(var c=0,b=a.length;c','  ','  ','  ',"");Ext.Button.buttonTemplate.compile()}this.template=Ext.Button.buttonTemplate}var b,d=this.getTemplateArgs();if(a){b=this.template.insertBefore(a,d,true)}else{b=this.template.append(c,d,true)}this.btnEl=b.child(this.buttonSelector);this.mon(this.btnEl,{scope:this,focus:this.onFocus,blur:this.onBlur});this.initButtonEl(b,this.btnEl);Ext.ButtonToggleMgr.register(this)},initButtonEl:function(b,c){this.el=b;this.setIcon(this.icon);this.setText(this.text);this.setIconClass(this.iconCls);if(Ext.isDefined(this.tabIndex)){c.dom.tabIndex=this.tabIndex}if(this.tooltip){this.setTooltip(this.tooltip,true)}if(this.handleMouseEvents){this.mon(b,{scope:this,mouseover:this.onMouseOver,mousedown:this.onMouseDown})}if(this.menu){this.mon(this.menu,{scope:this,show:this.onMenuShow,hide:this.onMenuHide})}if(this.repeat){var a=new Ext.util.ClickRepeater(b,Ext.isObject(this.repeat)?this.repeat:{});this.mon(a,"click",this.onRepeatClick,this)}else{this.mon(b,this.clickEvent,this.onClick,this)}},afterRender:function(){Ext.Button.superclass.afterRender.call(this);this.useSetClass=true;this.setButtonClass();this.doc=Ext.getDoc();this.doAutoWidth()},setIconClass:function(a){this.iconCls=a;if(this.el){this.btnEl.dom.className="";this.btnEl.addClass(["x-btn-text",a||""]);this.setButtonClass()}return this},setTooltip:function(b,a){if(this.rendered){if(!a){this.clearTip()}if(Ext.isObject(b)){Ext.QuickTips.register(Ext.apply({target:this.btnEl.id},b));this.tooltip=b}else{this.btnEl.dom[this.tooltipType]=b}}else{this.tooltip=b}return this},clearTip:function(){if(Ext.isObject(this.tooltip)){Ext.QuickTips.unregister(this.btnEl)}},beforeDestroy:function(){if(this.rendered){this.clearTip()}if(this.menu&&this.destroyMenu!==false){Ext.destroy(this.btnEl,this.menu)}Ext.destroy(this.repeater)},onDestroy:function(){if(this.rendered){this.doc.un("mouseover",this.monitorMouseOver,this);this.doc.un("mouseup",this.onMouseUp,this);delete this.doc;delete this.btnEl;Ext.ButtonToggleMgr.unregister(this)}Ext.Button.superclass.onDestroy.call(this)},doAutoWidth:function(){if(this.autoWidth!==false&&this.el&&this.text&&this.width===undefined){this.el.setWidth("auto");if(Ext.isIE7&&Ext.isStrict){var a=this.btnEl;if(a&&a.getWidth()>20){a.clip();a.setWidth(Ext.util.TextMetrics.measure(a,this.text).width+a.getFrameWidth("lr"))}}if(this.minWidth){if(this.el.getWidth()a}else{return c.getPageY()>this.btnEl.getRegion().bottom}},onClick:function(b,a){b.preventDefault();if(!this.disabled){if(this.isClickOnArrow(b)){if(this.menu&&!this.menu.isVisible()&&!this.ignoreNextClick){this.showMenu()}this.fireEvent("arrowclick",this,b);if(this.arrowHandler){this.arrowHandler.call(this.scope||this,this,b)}}else{this.doToggle();this.fireEvent("click",this,b);if(this.handler){this.handler.call(this.scope||this,this,b)}}}},isMenuTriggerOver:function(a){return this.menu&&a.target.tagName==this.arrowSelector},isMenuTriggerOut:function(b,a){return this.menu&&b.target.tagName!=this.arrowSelector}});Ext.reg("splitbutton",Ext.SplitButton);Ext.CycleButton=Ext.extend(Ext.SplitButton,{getItemText:function(a){if(a&&this.showText===true){var b="";if(this.prependText){b+=this.prependText}b+=a.text;return b}return undefined},setActiveItem:function(c,a){if(!Ext.isObject(c)){c=this.menu.getComponent(c)}if(c){if(!this.rendered){this.text=this.getItemText(c);this.iconCls=c.iconCls}else{var b=this.getItemText(c);if(b){this.setText(b)}this.setIconClass(c.iconCls)}this.activeItem=c;if(!c.checked){c.setChecked(true,a)}if(this.forceIcon){this.setIconClass(this.forceIcon)}if(!a){this.fireEvent("change",this,c)}}},getActiveItem:function(){return this.activeItem},initComponent:function(){this.addEvents("change");if(this.changeHandler){this.on("change",this.changeHandler,this.scope||this);delete this.changeHandler}this.itemCount=this.items.length;this.menu={cls:"x-cycle-menu",items:[]};var a=0;Ext.each(this.items,function(c,b){Ext.apply(c,{group:c.group||this.id,itemIndex:b,checkHandler:this.checkHandler,scope:this,checked:c.checked||false});this.menu.items.push(c);if(c.checked){a=b}},this);Ext.CycleButton.superclass.initComponent.call(this);this.on("click",this.toggleSelected,this);this.setActiveItem(a,true)},checkHandler:function(a,b){if(b){this.setActiveItem(a)}},toggleSelected:function(){var a=this.menu;a.render();if(!a.hasLayout){a.doLayout()}var d,b;for(var c=1;c"){b=new a.Fill()}else{b=new a.TextItem(b)}}}this.applyDefaults(b)}else{if(b.isFormField||b.render){b=this.createComponent(b)}else{if(b.tag){b=new a.Item({autoEl:b})}else{if(b.tagName){b=new a.Item({el:b})}else{if(Ext.isObject(b)){b=b.xtype?this.createComponent(b):this.constructButton(b)}}}}}return b},applyDefaults:function(e){if(!Ext.isString(e)){e=Ext.Toolbar.superclass.applyDefaults.call(this,e);var b=this.internalDefaults;if(e.events){Ext.applyIf(e.initialConfig,b);Ext.apply(e,b)}else{Ext.applyIf(e,b)}}return e},addSeparator:function(){return this.add(new a.Separator())},addSpacer:function(){return this.add(new a.Spacer())},addFill:function(){this.add(new a.Fill())},addElement:function(b){return this.addItem(new a.Item({el:b}))},addItem:function(b){return this.add.apply(this,arguments)},addButton:function(c){if(Ext.isArray(c)){var e=[];for(var d=0,b=c.length;d");this.items.push(this.displayItem=new a.TextItem({}))}Ext.PagingToolbar.superclass.initComponent.call(this);this.addEvents("change","beforechange");this.on("afterlayout",this.onFirstLayout,this,{single:true});this.cursor=0;this.bindStore(this.store,true)},onFirstLayout:function(){if(this.dsLoaded){this.onLoad.apply(this,this.dsLoaded)}},updateInfo:function(){if(this.displayItem){var b=this.store.getCount();var c=b==0?this.emptyMsg:String.format(this.displayMsg,this.cursor+1,this.cursor+b,this.store.getTotalCount());this.displayItem.setText(c)}},onLoad:function(b,e,j){if(!this.rendered){this.dsLoaded=[b,e,j];return}var g=this.getParams();this.cursor=(j.params&&j.params[g.start])?j.params[g.start]:0;var i=this.getPageData(),c=i.activePage,h=i.pages;this.afterTextItem.setText(String.format(this.afterPageText,i.pages));this.inputItem.setValue(c);this.first.setDisabled(c==1);this.prev.setDisabled(c==1);this.next.setDisabled(c==h);this.last.setDisabled(c==h);this.refresh.enable();this.updateInfo();this.fireEvent("change",this,i)},getPageData:function(){var b=this.store.getTotalCount();return{total:b,activePage:Math.ceil((this.cursor+this.pageSize)/this.pageSize),pages:b=1&g<=j.pages){i.setValue(g)}}}}}},getParams:function(){return this.paramNames||this.store.paramNames},beforeLoad:function(){if(this.rendered&&this.refresh){this.refresh.disable()}},doLoad:function(d){var c={},b=this.getParams();c[b.start]=d;c[b.limit]=this.pageSize;if(this.fireEvent("beforechange",this,c)!==false){this.store.load({params:c})}},moveFirst:function(){this.doLoad(0)},movePrevious:function(){this.doLoad(Math.max(0,this.cursor-this.pageSize))},moveNext:function(){this.doLoad(this.cursor+this.pageSize)},moveLast:function(){var c=this.store.getTotalCount(),b=c%this.pageSize;this.doLoad(b?(c-b):c-this.pageSize)},doRefresh:function(){this.doLoad(this.cursor)},bindStore:function(c,d){var b;if(!d&&this.store){if(c!==this.store&&this.store.autoDestroy){this.store.destroy()}else{this.store.un("beforeload",this.beforeLoad,this);this.store.un("load",this.onLoad,this);this.store.un("exception",this.onLoadError,this)}if(!c){this.store=null}}if(c){c=Ext.StoreMgr.lookup(c);c.on({scope:this,beforeload:this.beforeLoad,load:this.onLoad,exception:this.onLoadError});b=true}this.store=c;if(b){this.onLoad(c,null,{})}},unbind:function(b){this.bindStore(null)},bind:function(b){this.bindStore(b)},onDestroy:function(){this.bindStore(null);Ext.PagingToolbar.superclass.onDestroy.call(this)}})})();Ext.reg("paging",Ext.PagingToolbar);Ext.History=(function(){var e,c;var k=false;var d;function g(){var l=location.href,m=l.indexOf("#"),n=m>=0?l.substr(m+1):null;if(Ext.isGecko){n=decodeURIComponent(n)}return n}function a(){c.value=d}function h(l){d=l;Ext.History.fireEvent("change",l)}function i(m){var l=['
    ',Ext.util.Format.htmlEncode(m),"
    "].join("");try{var o=e.contentWindow.document;o.open();o.write(l);o.close();return true}catch(n){return false}}function b(){if(!e.contentWindow||!e.contentWindow.document){setTimeout(b,10);return}var o=e.contentWindow.document;var m=o.getElementById("state");var l=m?m.innerText:null;var n=g();setInterval(function(){o=e.contentWindow.document;m=o.getElementById("state");var q=m?m.innerText:null;var p=g();if(q!==l){l=q;h(l);location.hash=l;n=l;a()}else{if(p!==n){n=p;i(p)}}},50);k=true;Ext.History.fireEvent("ready",Ext.History)}function j(){d=c.value?c.value:g();if(Ext.isIE){b()}else{var l=g();setInterval(function(){var m=g();if(m!==l){l=m;h(l);a()}},50);k=true;Ext.History.fireEvent("ready",Ext.History)}}return{fieldId:"x-history-field",iframeId:"x-history-frame",events:{},init:function(m,l){if(k){Ext.callback(m,l,[this]);return}if(!Ext.isReady){Ext.onReady(function(){Ext.History.init(m,l)});return}c=Ext.getDom(Ext.History.fieldId);if(Ext.isIE){e=Ext.getDom(Ext.History.iframeId)}this.addEvents("ready","change");if(m){this.on("ready",m,l,{single:true})}j()},add:function(l,m){if(m!==false){if(this.getToken()==l){return true}}if(Ext.isIE){return i(l)}else{location.hash=l;return true}},back:function(){history.go(-1)},forward:function(){history.go(1)},getToken:function(){return k?d:g()}}})();Ext.apply(Ext.History,new Ext.util.Observable());Ext.Tip=Ext.extend(Ext.Panel,{minWidth:40,maxWidth:300,shadow:"sides",defaultAlign:"tl-bl?",autoRender:true,quickShowInterval:250,frame:true,hidden:true,baseCls:"x-tip",floating:{shadow:true,shim:true,useDisplay:true,constrain:false},autoHeight:true,closeAction:"hide",initComponent:function(){Ext.Tip.superclass.initComponent.call(this);if(this.closable&&!this.title){this.elements+=",header"}},afterRender:function(){Ext.Tip.superclass.afterRender.call(this);if(this.closable){this.addTool({id:"close",handler:this[this.closeAction],scope:this})}},showAt:function(a){Ext.Tip.superclass.show.call(this);if(this.measureWidth!==false&&(!this.initialConfig||typeof this.initialConfig.width!="number")){this.doAutoWidth()}if(this.constrainPosition){a=this.el.adjustForConstraints(a)}this.setPagePosition(a[0],a[1])},doAutoWidth:function(a){a=a||0;var b=this.body.getTextWidth();if(this.title){b=Math.max(b,this.header.child("span").getTextWidth(this.title))}b+=this.getFrameWidth()+(this.closable?20:0)+this.body.getPadding("lr")+a;this.setWidth(b.constrain(this.minWidth,this.maxWidth));if(Ext.isIE7&&!this.repainted){this.el.repaint();this.repainted=true}},showBy:function(a,b){if(!this.rendered){this.render(Ext.getBody())}this.showAt(this.el.getAlignToXY(a,b||this.defaultAlign))},initDraggable:function(){this.dd=new Ext.Tip.DD(this,typeof this.draggable=="boolean"?null:this.draggable);this.header.addClass("x-tip-draggable")}});Ext.reg("tip",Ext.Tip);Ext.Tip.DD=function(b,a){Ext.apply(this,a);this.tip=b;Ext.Tip.DD.superclass.constructor.call(this,b.el.id,"WindowDD-"+b.id);this.setHandleElId(b.header.id);this.scroll=false};Ext.extend(Ext.Tip.DD,Ext.dd.DD,{moveOnly:true,scroll:false,headerOffsets:[100,25],startDrag:function(){this.tip.el.disableShadow()},endDrag:function(a){this.tip.el.enableShadow(true)}});Ext.ToolTip=Ext.extend(Ext.Tip,{showDelay:500,hideDelay:200,dismissDelay:5000,trackMouse:false,anchorToTarget:true,anchorOffset:0,targetCounter:0,constrainPosition:false,initComponent:function(){Ext.ToolTip.superclass.initComponent.call(this);this.lastActive=new Date();this.initTarget(this.target);this.origAnchor=this.anchor},onRender:function(b,a){Ext.ToolTip.superclass.onRender.call(this,b,a);this.anchorCls="x-tip-anchor-"+this.getAnchorPosition();this.anchorEl=this.el.createChild({cls:"x-tip-anchor "+this.anchorCls})},afterRender:function(){Ext.ToolTip.superclass.afterRender.call(this);this.anchorEl.setStyle("z-index",this.el.getZIndex()+1).setVisibilityMode(Ext.Element.DISPLAY)},initTarget:function(c){var a;if((a=Ext.get(c))){if(this.target){var b=Ext.get(this.target);this.mun(b,"mouseover",this.onTargetOver,this);this.mun(b,"mouseout",this.onTargetOut,this);this.mun(b,"mousemove",this.onMouseMove,this)}this.mon(a,{mouseover:this.onTargetOver,mouseout:this.onTargetOut,mousemove:this.onMouseMove,scope:this});this.target=a}if(this.anchor){this.anchorTarget=this.target}},onMouseMove:function(b){var a=this.delegate?b.getTarget(this.delegate):this.triggerElement=true;if(a){this.targetXY=b.getXY();if(a===this.triggerElement){if(!this.hidden&&this.trackMouse){this.setPagePosition(this.getTargetXY())}}else{this.hide();this.lastActive=new Date(0);this.onTargetOver(b)}}else{if(!this.closable&&this.isVisible()){this.hide()}}},getTargetXY:function(){if(this.delegate){this.anchorTarget=this.triggerElement}if(this.anchor){this.targetCounter++;var c=this.getOffsets(),l=(this.anchorToTarget&&!this.trackMouse)?this.el.getAlignToXY(this.anchorTarget,this.getAnchorAlign()):this.targetXY,a=Ext.lib.Dom.getViewWidth()-5,h=Ext.lib.Dom.getViewHeight()-5,i=document.documentElement,e=document.body,k=(i.scrollLeft||e.scrollLeft||0)+5,j=(i.scrollTop||e.scrollTop||0)+5,b=[l[0]+c[0],l[1]+c[1]],g=this.getSize();this.anchorEl.removeClass(this.anchorCls);if(this.targetCounter<2){if(b[0]a){if(this.anchorToTarget){this.defaultAlign="r-l";if(this.mouseOffset){this.mouseOffset[0]*=-1}}this.anchor="right";return this.getTargetXY()}if(b[1]h){if(this.anchorToTarget){this.defaultAlign="b-t";if(this.mouseOffset){this.mouseOffset[1]*=-1}}this.anchor="bottom";return this.getTargetXY()}}this.anchorCls="x-tip-anchor-"+this.getAnchorPosition();this.anchorEl.addClass(this.anchorCls);this.targetCounter=0;return b}else{var d=this.getMouseOffset();return[this.targetXY[0]+d[0],this.targetXY[1]+d[1]]}},getMouseOffset:function(){var a=this.anchor?[0,0]:[15,18];if(this.mouseOffset){a[0]+=this.mouseOffset[0];a[1]+=this.mouseOffset[1]}return a},getAnchorPosition:function(){if(this.anchor){this.tipAnchor=this.anchor.charAt(0)}else{var a=this.defaultAlign.match(/^([a-z]+)-([a-z]+)(\?)?$/);if(!a){throw"AnchorTip.defaultAlign is invalid"}this.tipAnchor=a[1].charAt(0)}switch(this.tipAnchor){case"t":return"top";case"b":return"bottom";case"r":return"right"}return"left"},getAnchorAlign:function(){switch(this.anchor){case"top":return"tl-bl";case"left":return"tl-tr";case"right":return"tr-tl";default:return"bl-tl"}},getOffsets:function(){var b,a=this.getAnchorPosition().charAt(0);if(this.anchorToTarget&&!this.trackMouse){switch(a){case"t":b=[0,9];break;case"b":b=[0,-13];break;case"r":b=[-13,0];break;default:b=[9,0];break}}else{switch(a){case"t":b=[-15-this.anchorOffset,30];break;case"b":b=[-19-this.anchorOffset,-13-this.el.dom.offsetHeight];break;case"r":b=[-15-this.el.dom.offsetWidth,-13-this.anchorOffset];break;default:b=[25,-13-this.anchorOffset];break}}var c=this.getMouseOffset();b[0]+=c[0];b[1]+=c[1];return b},onTargetOver:function(b){if(this.disabled||b.within(this.target.dom,true)){return}var a=b.getTarget(this.delegate);if(a){this.triggerElement=a;this.clearTimer("hide");this.targetXY=b.getXY();this.delayShow()}},delayShow:function(){if(this.hidden&&!this.showTimer){if(this.lastActive.getElapsed()=c){d=c-b-5}}return{x:a,y:d}},beforeDestroy:function(){this.clearTimers();Ext.destroy(this.anchorEl);delete this.anchorEl;delete this.target;delete this.anchorTarget;delete this.triggerElement;Ext.ToolTip.superclass.beforeDestroy.call(this)},onDestroy:function(){Ext.getDoc().un("mousedown",this.onDocMouseDown,this);Ext.ToolTip.superclass.onDestroy.call(this)}});Ext.reg("tooltip",Ext.ToolTip);Ext.QuickTip=Ext.extend(Ext.ToolTip,{interceptTitles:false,tagConfig:{namespace:"ext",attribute:"qtip",width:"qwidth",target:"target",title:"qtitle",hide:"hide",cls:"qclass",align:"qalign",anchor:"anchor"},initComponent:function(){this.target=this.target||Ext.getDoc();this.targets=this.targets||{};Ext.QuickTip.superclass.initComponent.call(this)},register:function(e){var h=Ext.isArray(e)?e:arguments;for(var g=0,a=h.length;g1){var d=function(i,h){if(i&&h){var j=h.findChild(a,b);if(j){j.select();if(g){g(true,j)}}else{if(g){g(false,j)}}}else{if(g){g(false,j)}}};this.expandPath(c.join(this.pathSeparator),a,d)}else{this.root.select();if(g){g(true,this.root)}}},getTreeEl:function(){return this.body},onRender:function(b,a){Ext.tree.TreePanel.superclass.onRender.call(this,b,a);this.el.addClass("x-tree");this.innerCt=this.body.createChild({tag:"ul",cls:"x-tree-root-ct "+(this.useArrows?"x-tree-arrows":this.lines?"x-tree-lines":"x-tree-no-lines")})},initEvents:function(){Ext.tree.TreePanel.superclass.initEvents.call(this);if(this.containerScroll){Ext.dd.ScrollManager.register(this.body)}if((this.enableDD||this.enableDrop)&&!this.dropZone){this.dropZone=new Ext.tree.TreeDropZone(this,this.dropConfig||{ddGroup:this.ddGroup||"TreeDD",appendOnly:this.ddAppendOnly===true})}if((this.enableDD||this.enableDrag)&&!this.dragZone){this.dragZone=new Ext.tree.TreeDragZone(this,this.dragConfig||{ddGroup:this.ddGroup||"TreeDD",scroll:this.ddScroll})}this.getSelectionModel().init(this)},afterRender:function(){Ext.tree.TreePanel.superclass.afterRender.call(this);this.renderRoot()},beforeDestroy:function(){if(this.rendered){Ext.dd.ScrollManager.unregister(this.body);Ext.destroy(this.dropZone,this.dragZone)}this.destroyRoot();Ext.destroy(this.loader);this.nodeHash=this.root=this.loader=null;Ext.tree.TreePanel.superclass.beforeDestroy.call(this)},destroyRoot:function(){if(this.root&&this.root.destroy){this.root.destroy(true)}}});Ext.tree.TreePanel.nodeTypes={};Ext.reg("treepanel",Ext.tree.TreePanel);Ext.tree.TreeEventModel=function(a){this.tree=a;this.tree.on("render",this.initEvents,this)};Ext.tree.TreeEventModel.prototype={initEvents:function(){var a=this.tree;if(a.trackMouseOver!==false){a.mon(a.innerCt,{scope:this,mouseover:this.delegateOver,mouseout:this.delegateOut})}a.mon(a.getTreeEl(),{scope:this,click:this.delegateClick,dblclick:this.delegateDblClick,contextmenu:this.delegateContextMenu})},getNode:function(b){var a;if(a=b.getTarget(".x-tree-node-el",10)){var c=Ext.fly(a,"_treeEvents").getAttribute("tree-node-id","ext");if(c){return this.tree.getNodeById(c)}}return null},getNodeTarget:function(b){var a=b.getTarget(".x-tree-node-icon",1);if(!a){a=b.getTarget(".x-tree-node-el",6)}return a},delegateOut:function(b,a){if(!this.beforeEvent(b)){return}if(b.getTarget(".x-tree-ec-icon",1)){var c=this.getNode(b);this.onIconOut(b,c);if(c==this.lastEcOver){delete this.lastEcOver}}if((a=this.getNodeTarget(b))&&!b.within(a,true)){this.onNodeOut(b,this.getNode(b))}},delegateOver:function(b,a){if(!this.beforeEvent(b)){return}if(Ext.isGecko&&!this.trackingDoc){Ext.getBody().on("mouseover",this.trackExit,this);this.trackingDoc=true}if(this.lastEcOver){this.onIconOut(b,this.lastEcOver);delete this.lastEcOver}if(b.getTarget(".x-tree-ec-icon",1)){this.lastEcOver=this.getNode(b);this.onIconOver(b,this.lastEcOver)}if(a=this.getNodeTarget(b)){this.onNodeOver(b,this.getNode(b))}},trackExit:function(a){if(this.lastOverNode){if(this.lastOverNode.ui&&!a.within(this.lastOverNode.ui.getEl())){this.onNodeOut(a,this.lastOverNode)}delete this.lastOverNode;Ext.getBody().un("mouseover",this.trackExit,this);this.trackingDoc=false}},delegateClick:function(b,a){if(this.beforeEvent(b)){if(b.getTarget("input[type=checkbox]",1)){this.onCheckboxClick(b,this.getNode(b))}else{if(b.getTarget(".x-tree-ec-icon",1)){this.onIconClick(b,this.getNode(b))}else{if(this.getNodeTarget(b)){this.onNodeClick(b,this.getNode(b))}}}}else{this.checkContainerEvent(b,"click")}},delegateDblClick:function(b,a){if(this.beforeEvent(b)){if(this.getNodeTarget(b)){this.onNodeDblClick(b,this.getNode(b))}}else{this.checkContainerEvent(b,"dblclick")}},delegateContextMenu:function(b,a){if(this.beforeEvent(b)){if(this.getNodeTarget(b)){this.onNodeContextMenu(b,this.getNode(b))}}else{this.checkContainerEvent(b,"contextmenu")}},checkContainerEvent:function(b,a){if(this.disabled){b.stopEvent();return false}this.onContainerEvent(b,a)},onContainerEvent:function(b,a){this.tree.fireEvent("container"+a,this.tree,b)},onNodeClick:function(b,a){a.ui.onClick(b)},onNodeOver:function(b,a){this.lastOverNode=a;a.ui.onOver(b)},onNodeOut:function(b,a){a.ui.onOut(b)},onIconOver:function(b,a){a.ui.addClass("x-tree-ec-over")},onIconOut:function(b,a){a.ui.removeClass("x-tree-ec-over")},onIconClick:function(b,a){a.ui.ecClick(b)},onCheckboxClick:function(b,a){a.ui.onCheckChange(b)},onNodeDblClick:function(b,a){a.ui.onDblClick(b)},onNodeContextMenu:function(b,a){a.ui.onContextMenu(b)},beforeEvent:function(b){var a=this.getNode(b);if(this.disabled||!a||!a.ui){b.stopEvent();return false}return true},disable:function(){this.disabled=true},enable:function(){this.disabled=false}};Ext.tree.DefaultSelectionModel=Ext.extend(Ext.util.Observable,{constructor:function(a){this.selNode=null;this.addEvents("selectionchange","beforeselect");Ext.apply(this,a);Ext.tree.DefaultSelectionModel.superclass.constructor.call(this)},init:function(a){this.tree=a;a.mon(a.getTreeEl(),"keydown",this.onKeyDown,this);a.on("click",this.onNodeClick,this)},onNodeClick:function(a,b){this.select(a)},select:function(c,a){if(!Ext.fly(c.ui.wrap).isVisible()&&a){return a.call(this,c)}var b=this.selNode;if(c==b){c.ui.onSelectedChange(true)}else{if(this.fireEvent("beforeselect",this,c,b)!==false){if(b&&b.ui){b.ui.onSelectedChange(false)}this.selNode=c;c.ui.onSelectedChange(true);this.fireEvent("selectionchange",this,c,b)}}return c},unselect:function(b,a){if(this.selNode==b){this.clearSelections(a)}},clearSelections:function(a){var b=this.selNode;if(b){b.ui.onSelectedChange(false);this.selNode=null;if(a!==true){this.fireEvent("selectionchange",this,null)}}return b},getSelectedNode:function(){return this.selNode},isSelected:function(a){return this.selNode==a},selectPrevious:function(a){if(!(a=a||this.selNode||this.lastSelNode)){return null}var c=a.previousSibling;if(c){if(!c.isExpanded()||c.childNodes.length<1){return this.select(c,this.selectPrevious)}else{var b=c.lastChild;while(b&&b.isExpanded()&&Ext.fly(b.ui.wrap).isVisible()&&b.childNodes.length>0){b=b.lastChild}return this.select(b,this.selectPrevious)}}else{if(a.parentNode&&(this.tree.rootVisible||!a.parentNode.isRoot)){return this.select(a.parentNode,this.selectPrevious)}}return null},selectNext:function(b){if(!(b=b||this.selNode||this.lastSelNode)){return null}if(b.firstChild&&b.isExpanded()&&Ext.fly(b.ui.wrap).isVisible()){return this.select(b.firstChild,this.selectNext)}else{if(b.nextSibling){return this.select(b.nextSibling,this.selectNext)}else{if(b.parentNode){var a=null;b.parentNode.bubble(function(){if(this.nextSibling){a=this.getOwnerTree().selModel.select(this.nextSibling,this.selectNext);return false}});return a}}}return null},onKeyDown:function(c){var b=this.selNode||this.lastSelNode;var d=this;if(!b){return}var a=c.getKey();switch(a){case c.DOWN:c.stopEvent();this.selectNext();break;case c.UP:c.stopEvent();this.selectPrevious();break;case c.RIGHT:c.preventDefault();if(b.hasChildNodes()){if(!b.isExpanded()){b.expand()}else{if(b.firstChild){this.select(b.firstChild,c)}}}break;case c.LEFT:c.preventDefault();if(b.hasChildNodes()&&b.isExpanded()){b.collapse()}else{if(b.parentNode&&(this.tree.rootVisible||b.parentNode!=this.tree.getRootNode())){this.select(b.parentNode,c)}}break}}});Ext.tree.MultiSelectionModel=Ext.extend(Ext.util.Observable,{constructor:function(a){this.selNodes=[];this.selMap={};this.addEvents("selectionchange");Ext.apply(this,a);Ext.tree.MultiSelectionModel.superclass.constructor.call(this)},init:function(a){this.tree=a;a.mon(a.getTreeEl(),"keydown",this.onKeyDown,this);a.on("click",this.onNodeClick,this)},onNodeClick:function(a,b){if(b.ctrlKey&&this.isSelected(a)){this.unselect(a)}else{this.select(a,b,b.ctrlKey)}},select:function(a,c,b){if(b!==true){this.clearSelections(true)}if(this.isSelected(a)){this.lastSelNode=a;return a}this.selNodes.push(a);this.selMap[a.id]=a;this.lastSelNode=a;a.ui.onSelectedChange(true);this.fireEvent("selectionchange",this,this.selNodes);return a},unselect:function(b){if(this.selMap[b.id]){b.ui.onSelectedChange(false);var c=this.selNodes;var a=c.indexOf(b);if(a!=-1){this.selNodes.splice(a,1)}delete this.selMap[b.id];this.fireEvent("selectionchange",this,this.selNodes)}},clearSelections:function(b){var d=this.selNodes;if(d.length>0){for(var c=0,a=d.length;c0},isExpandable:function(){return this.attributes.expandable||this.hasChildNodes()},appendChild:function(e){var g=false;if(Ext.isArray(e)){g=e}else{if(arguments.length>1){g=arguments}}if(g){for(var d=0,a=g.length;d0){var g=d?function(){e.apply(d,arguments)}:e;c.sort(g);for(var b=0;b
    ','',this.indentMarkup,"",'','',g?('':"/>")):"",'',e.text,"
    ",'',""].join("");if(l!==true&&e.nextSibling&&(b=e.nextSibling.ui.getEl())){this.wrap=Ext.DomHelper.insertHtml("beforeBegin",b,d)}else{this.wrap=Ext.DomHelper.insertHtml("beforeEnd",j,d)}this.elNode=this.wrap.childNodes[0];this.ctNode=this.wrap.childNodes[1];var i=this.elNode.childNodes;this.indentNode=i[0];this.ecNode=i[1];this.iconNode=i[2];var h=3;if(g){this.checkbox=i[3];this.checkbox.defaultChecked=this.checkbox.checked;h++}this.anchor=i[h];this.textNode=i[h].firstChild},getHref:function(a){return Ext.isEmpty(a)?(Ext.isGecko?"":"#"):a},getAnchor:function(){return this.anchor},getTextEl:function(){return this.textNode},getIconEl:function(){return this.iconNode},isChecked:function(){return this.checkbox?this.checkbox.checked:false},updateExpandIcon:function(){if(this.rendered){var g=this.node,d,c,a=g.isLast()?"x-tree-elbow-end":"x-tree-elbow",e=g.hasChildNodes();if(e||g.attributes.expandable){if(g.expanded){a+="-minus";d="x-tree-node-collapsed";c="x-tree-node-expanded"}else{a+="-plus";d="x-tree-node-expanded";c="x-tree-node-collapsed"}if(this.wasLeaf){this.removeClass("x-tree-node-leaf");this.wasLeaf=false}if(this.c1!=d||this.c2!=c){Ext.fly(this.elNode).replaceClass(d,c);this.c1=d;this.c2=c}}else{if(!this.wasLeaf){Ext.fly(this.elNode).replaceClass("x-tree-node-expanded","x-tree-node-collapsed");delete this.c1;delete this.c2;this.wasLeaf=true}}var b="x-tree-ec-icon "+a;if(this.ecc!=b){this.ecNode.className=b;this.ecc=b}}},onIdChange:function(a){if(this.rendered){this.elNode.setAttribute("ext:tree-node-id",a)}},getChildIndent:function(){if(!this.childIndent){var a=[],b=this.node;while(b){if(!b.isRoot||(b.isRoot&&b.ownerTree.rootVisible)){if(!b.isLast()){a.unshift('')}else{a.unshift('')}}b=b.parentNode}this.childIndent=a.join("")}return this.childIndent},renderIndent:function(){if(this.rendered){var a="",b=this.node.parentNode;if(b){a=b.ui.getChildIndent()}if(this.indentMarkup!=a){this.indentNode.innerHTML=a;this.indentMarkup=a}this.updateExpandIcon()}},destroy:function(){if(this.elNode){Ext.dd.Registry.unregister(this.elNode.id)}Ext.each(["textnode","anchor","checkbox","indentNode","ecNode","iconNode","elNode","ctNode","wrap","holder"],function(a){if(this[a]){Ext.fly(this[a]).remove();delete this[a]}},this);delete this.node}});Ext.tree.RootTreeNodeUI=Ext.extend(Ext.tree.TreeNodeUI,{render:function(){if(!this.rendered){var a=this.node.ownerTree.innerCt.dom;this.node.expanded=true;a.innerHTML='
    ';this.wrap=this.ctNode=a.firstChild}},collapse:Ext.emptyFn,expand:Ext.emptyFn});Ext.tree.TreeLoader=function(a){this.baseParams={};Ext.apply(this,a);this.addEvents("beforeload","load","loadexception");Ext.tree.TreeLoader.superclass.constructor.call(this);if(Ext.isString(this.paramOrder)){this.paramOrder=this.paramOrder.split(/[\s,|]/)}};Ext.extend(Ext.tree.TreeLoader,Ext.util.Observable,{uiProviders:{},clearOnLoad:true,paramOrder:undefined,paramsAsHash:false,nodeParameter:"node",directFn:undefined,load:function(b,c,a){if(this.clearOnLoad){while(b.firstChild){b.removeChild(b.firstChild)}}if(this.doPreload(b)){this.runCallback(c,a||b,[b])}else{if(this.directFn||this.dataUrl||this.url){this.requestData(b,c,a||b)}}},doPreload:function(d){if(d.attributes.children){if(d.childNodes.length<1){var c=d.attributes.children;d.beginUpdate();for(var b=0,a=c.length;b-1){c=[]}for(var d=0,a=b.length;dp){return e?-1:1}}return 0}},doSort:function(a){a.sort(this.sortFn)},updateSort:function(a,b){if(b.childrenRendered){this.doSort.defer(1,this,[b])}},updateSortParent:function(a){var b=a.parentNode;if(b&&b.childrenRendered){this.doSort.defer(1,this,[b])}}});if(Ext.dd.DropZone){Ext.tree.TreeDropZone=function(a,b){this.allowParentInsert=b.allowParentInsert||false;this.allowContainerDrop=b.allowContainerDrop||false;this.appendOnly=b.appendOnly||false;Ext.tree.TreeDropZone.superclass.constructor.call(this,a.getTreeEl(),b);this.tree=a;this.dragOverData={};this.lastInsertClass="x-tree-no-status"};Ext.extend(Ext.tree.TreeDropZone,Ext.dd.DropZone,{ddGroup:"TreeDD",expandDelay:1000,expandNode:function(a){if(a.hasChildNodes()&&!a.isExpanded()){a.expand(false,null,this.triggerCacheRefresh.createDelegate(this))}},queueExpand:function(a){this.expandProcId=this.expandNode.defer(this.expandDelay,this,[a])},cancelExpand:function(){if(this.expandProcId){clearTimeout(this.expandProcId);this.expandProcId=false}},isValidDropPoint:function(a,k,i,d,c){if(!a||!c){return false}var g=a.node;var h=c.node;if(!(g&&g.isTarget&&k)){return false}if(k=="append"&&g.allowChildren===false){return false}if((k=="above"||k=="below")&&(g.parentNode&&g.parentNode.allowChildren===false)){return false}if(h&&(g==h||h.contains(g))){return false}var b=this.dragOverData;b.tree=this.tree;b.target=g;b.data=c;b.point=k;b.source=i;b.rawEvent=d;b.dropNode=h;b.cancel=false;var j=this.tree.fireEvent("nodedragover",b);return b.cancel===false&&j!==false},getDropPoint:function(h,g,l){var m=g.node;if(m.isRoot){return m.allowChildren!==false?"append":false}var c=g.ddel;var o=Ext.lib.Dom.getY(c),j=o+c.offsetHeight;var i=Ext.lib.Event.getPageY(h);var k=m.allowChildren===false||m.isLeaf();if(this.appendOnly||m.parentNode.allowChildren===false){return k?false:"append"}var d=false;if(!this.allowParentInsert){d=m.hasChildNodes()&&m.isExpanded()}var a=(j-o)/(k?2:3);if(i>=o&&i<(o+a)){return"above"}else{if(!d&&(k||i>=j-a&&i<=j)){return"below"}else{return"append"}}},onNodeEnter:function(d,a,c,b){this.cancelExpand()},onContainerOver:function(a,c,b){if(this.allowContainerDrop&&this.isValidDropPoint({ddel:this.tree.getRootNode().ui.elNode,node:this.tree.getRootNode()},"append",a,c,b)){return this.dropAllowed}return this.dropNotAllowed},onNodeOver:function(b,i,h,g){var k=this.getDropPoint(h,b,i);var c=b.node;if(!this.expandProcId&&k=="append"&&c.hasChildNodes()&&!b.node.isExpanded()){this.queueExpand(c)}else{if(k!="append"){this.cancelExpand()}}var d=this.dropNotAllowed;if(this.isValidDropPoint(b,k,i,h,g)){if(k){var a=b.ddel;var j;if(k=="above"){d=b.node.isFirst()?"x-tree-drop-ok-above":"x-tree-drop-ok-between";j="x-tree-drag-insert-above"}else{if(k=="below"){d=b.node.isLast()?"x-tree-drop-ok-below":"x-tree-drop-ok-between";j="x-tree-drag-insert-below"}else{d="x-tree-drop-ok-append";j="x-tree-drag-append"}}if(this.lastInsertClass!=j){Ext.fly(a).replaceClass(this.lastInsertClass,j);this.lastInsertClass=j}}}return d},onNodeOut:function(d,a,c,b){this.cancelExpand();this.removeDropIndicators(d)},onNodeDrop:function(i,b,h,d){var a=this.getDropPoint(h,i,b);var g=i.node;g.ui.startDrop();if(!this.isValidDropPoint(i,a,b,h,d)){g.ui.endDrop();return false}var c=d.node||(b.getTreeNode?b.getTreeNode(d,g,a,h):null);return this.processDrop(g,d,a,b,h,c)},onContainerDrop:function(a,g,c){if(this.allowContainerDrop&&this.isValidDropPoint({ddel:this.tree.getRootNode().ui.elNode,node:this.tree.getRootNode()},"append",a,g,c)){var d=this.tree.getRootNode();d.ui.startDrop();var b=c.node||(a.getTreeNode?a.getTreeNode(c,d,"append",g):null);return this.processDrop(d,c,"append",a,g,b)}return false},processDrop:function(j,h,b,a,i,d){var g={tree:this.tree,target:j,data:h,point:b,source:a,rawEvent:i,dropNode:d,cancel:!d,dropStatus:false};var c=this.tree.fireEvent("beforenodedrop",g);if(c===false||g.cancel===true||!g.dropNode){j.ui.endDrop();return g.dropStatus}j=g.target;if(b=="append"&&!j.isExpanded()){j.expand(false,null,function(){this.completeDrop(g)}.createDelegate(this))}else{this.completeDrop(g)}return true},completeDrop:function(h){var d=h.dropNode,e=h.point,c=h.target;if(!Ext.isArray(d)){d=[d]}var g;for(var b=0,a=d.length;bd.offsetLeft){e.scrollLeft=d.offsetLeft}var a=Math.min(this.maxWidth,(e.clientWidth>20?e.clientWidth:e.offsetWidth)-Math.max(0,d.offsetLeft-e.scrollLeft)-5);this.setSize(a,"")},triggerEdit:function(a,c){this.completeEdit();if(a.attributes.editable!==false){this.editNode=a;if(this.tree.autoScroll){Ext.fly(a.ui.getEl()).scrollIntoView(this.tree.body)}var b=a.text||"";if(!Ext.isGecko&&Ext.isEmpty(a.text)){a.setText(" ")}this.autoEditTimer=this.startEdit.defer(this.editDelay,this,[a.ui.textNode,b]);return false}},bindScroll:function(){this.tree.getTreeEl().on("scroll",this.cancelEdit,this)},beforeNodeClick:function(a,b){clearTimeout(this.autoEditTimer);if(this.tree.getSelectionModel().isSelected(a)){b.stopEvent();return this.triggerEdit(a)}},onNodeDblClick:function(a,b){clearTimeout(this.autoEditTimer)},updateNode:function(a,b){this.tree.getTreeEl().un("scroll",this.cancelEdit,this);this.editNode.setText(b)},onHide:function(){Ext.tree.TreeEditor.superclass.onHide.call(this);if(this.editNode){this.editNode.ui.focus.defer(50,this.editNode.ui)}},onSpecialKey:function(c,b){var a=b.getKey();if(a==b.ESC){b.stopEvent();this.cancelEdit()}else{if(a==b.ENTER&&!b.hasModifier()){b.stopEvent();this.completeEdit()}}},onDestroy:function(){clearTimeout(this.autoEditTimer);Ext.tree.TreeEditor.superclass.onDestroy.call(this);var a=this.tree;a.un("beforeclick",this.beforeNodeClick,this);a.un("dblclick",this.onNodeDblClick,this)}}); -/* SWFObject v2.2 - is released under the MIT License -*/ -var swfobject=function(){var E="undefined",s="object",T="Shockwave Flash",X="ShockwaveFlash.ShockwaveFlash",r="application/x-shockwave-flash",S="SWFObjectExprInst",y="onreadystatechange",P=window,k=document,u=navigator,U=false,V=[i],p=[],O=[],J=[],m,R,F,C,K=false,a=false,o,H,n=true,N=function(){var ab=typeof k.getElementById!=E&&typeof k.getElementsByTagName!=E&&typeof k.createElement!=E,ai=u.userAgent.toLowerCase(),Z=u.platform.toLowerCase(),af=Z?(/win/).test(Z):/win/.test(ai),ad=Z?(/mac/).test(Z):/mac/.test(ai),ag=/webkit/.test(ai)?parseFloat(ai.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,Y=!+"\v1",ah=[0,0,0],ac=null;if(typeof u.plugins!=E&&typeof u.plugins[T]==s){ac=u.plugins[T].description;if(ac&&!(typeof u.mimeTypes!=E&&u.mimeTypes[r]&&!u.mimeTypes[r].enabledPlugin)){U=true;Y=false;ac=ac.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ah[0]=parseInt(ac.replace(/^(.*)\..*$/,"$1"),10);ah[1]=parseInt(ac.replace(/^.*\.(.*)\s.*$/,"$1"),10);ah[2]=/[a-zA-Z]/.test(ac)?parseInt(ac.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof P.ActiveXObject!=E){try{var ae=new ActiveXObject(X);if(ae){ac=ae.GetVariable("$version");if(ac){Y=true;ac=ac.split(" ")[1].split(",");ah=[parseInt(ac[0],10),parseInt(ac[1],10),parseInt(ac[2],10)]}}}catch(aa){}}}return{w3:ab,pv:ah,wk:ag,ie:Y,win:af,mac:ad}}(),l=function(){if(!N.w3){return}if((typeof k.readyState!=E&&k.readyState=="complete")||(typeof k.readyState==E&&(k.getElementsByTagName("body")[0]||k.body))){g()}if(!K){if(typeof k.addEventListener!=E){k.addEventListener("DOMContentLoaded",g,false)}if(N.ie&&N.win){k.attachEvent(y,function(){if(k.readyState=="complete"){k.detachEvent(y,arguments.callee);g()}});if(P==top){(function(){if(K){return}try{k.documentElement.doScroll("left")}catch(Y){setTimeout(arguments.callee,0);return}g()})()}}if(N.wk){(function(){if(K){return}if(!(/loaded|complete/).test(k.readyState)){setTimeout(arguments.callee,0);return}g()})()}t(g)}}();function g(){if(K){return}try{var aa=k.getElementsByTagName("body")[0].appendChild(D("span"));aa.parentNode.removeChild(aa)}catch(ab){return}K=true;var Y=V.length;for(var Z=0;Z0){for(var ag=0;ag0){var af=c(Z);if(af){if(G(p[ag].swfVersion)&&!(N.wk&&N.wk<312)){x(Z,true);if(ac){ab.success=true;ab.ref=A(Z);ac(ab)}}else{if(p[ag].expressInstall&&B()){var aj={};aj.data=p[ag].expressInstall;aj.width=af.getAttribute("width")||"0";aj.height=af.getAttribute("height")||"0";if(af.getAttribute("class")){aj.styleclass=af.getAttribute("class")}if(af.getAttribute("align")){aj.align=af.getAttribute("align")}var ai={};var Y=af.getElementsByTagName("param");var ad=Y.length;for(var ae=0;ae'}}ab.outerHTML='"+ag+"";O[O.length]=aj.id;Y=c(aj.id)}else{var aa=D(s);aa.setAttribute("type",r);for(var ad in aj){if(aj[ad]!=Object.prototype[ad]){if(ad.toLowerCase()=="styleclass"){aa.setAttribute("class",aj[ad])}else{if(ad.toLowerCase()!="classid"){aa.setAttribute(ad,aj[ad])}}}}for(var ac in ah){if(ah[ac]!=Object.prototype[ac]&&ac.toLowerCase()!="movie"){e(aa,ac,ah[ac])}}ab.parentNode.replaceChild(aa,ab);Y=aa}}return Y}function e(aa,Y,Z){var ab=D("param");ab.setAttribute("name",Y);ab.setAttribute("value",Z);aa.appendChild(ab)}function z(Z){var Y=c(Z);if(Y&&Y.nodeName=="OBJECT"){if(N.ie&&N.win){Y.style.display="none";(function(){if(Y.readyState==4){b(Z)}else{setTimeout(arguments.callee,10)}})()}else{Y.parentNode.removeChild(Y)}}}function b(aa){var Z=c(aa);if(Z){for(var Y in Z){if(typeof Z[Y]=="function"){Z[Y]=null}}Z.parentNode.removeChild(Z)}}function c(aa){var Y=null;try{Y=k.getElementById(aa)}catch(Z){}return Y}function D(Y){return k.createElement(Y)}function j(aa,Y,Z){aa.attachEvent(Y,Z);J[J.length]=[aa,Y,Z]}function G(aa){var Z=N.pv,Y=aa.split(".");Y[0]=parseInt(Y[0],10);Y[1]=parseInt(Y[1],10)||0;Y[2]=parseInt(Y[2],10)||0;return(Z[0]>Y[0]||(Z[0]==Y[0]&&Z[1]>Y[1])||(Z[0]==Y[0]&&Z[1]==Y[1]&&Z[2]>=Y[2]))?true:false}function w(ad,Z,ae,ac){if(N.ie&&N.mac){return}var ab=k.getElementsByTagName("head")[0];if(!ab){return}var Y=(ae&&typeof ae=="string")?ae:"screen";if(ac){o=null;H=null}if(!o||H!=Y){var aa=D("style");aa.setAttribute("type","text/css");aa.setAttribute("media",Y);o=ab.appendChild(aa);if(N.ie&&N.win&&typeof k.styleSheets!=E&&k.styleSheets.length>0){o=k.styleSheets[k.styleSheets.length-1]}H=Y}if(N.ie&&N.win){if(o&&typeof o.addRule==s){o.addRule(ad,Z)}}else{if(o&&typeof k.createTextNode!=E){o.appendChild(k.createTextNode(ad+" {"+Z+"}"))}}}function x(aa,Y){if(!n){return}var Z=Y?"visible":"hidden";if(K&&c(aa)){c(aa).style.visibility=Z}else{w("#"+aa,"visibility:"+Z)}}function M(Z){var aa=/[\\\"<>\.;]/;var Y=aa.exec(Z)!=null;return Y&&typeof encodeURIComponent!=E?encodeURIComponent(Z):Z}var d=function(){if(N.ie&&N.win){window.attachEvent("onunload",function(){var ad=J.length;for(var ac=0;ac0){for(h=0;h-1&&e.position=="left"){e.position="bottom"}return e},onDestroy:function(){Ext.chart.CartesianChart.superclass.onDestroy.call(this);Ext.each(this.labelFn,function(a){this.removeFnProxy(a)},this)}});Ext.reg("cartesianchart",Ext.chart.CartesianChart);Ext.chart.LineChart=Ext.extend(Ext.chart.CartesianChart,{type:"line"});Ext.reg("linechart",Ext.chart.LineChart);Ext.chart.ColumnChart=Ext.extend(Ext.chart.CartesianChart,{type:"column"});Ext.reg("columnchart",Ext.chart.ColumnChart);Ext.chart.StackedColumnChart=Ext.extend(Ext.chart.CartesianChart,{type:"stackcolumn"});Ext.reg("stackedcolumnchart",Ext.chart.StackedColumnChart);Ext.chart.BarChart=Ext.extend(Ext.chart.CartesianChart,{type:"bar"});Ext.reg("barchart",Ext.chart.BarChart);Ext.chart.StackedBarChart=Ext.extend(Ext.chart.CartesianChart,{type:"stackbar"});Ext.reg("stackedbarchart",Ext.chart.StackedBarChart);Ext.chart.Axis=function(a){Ext.apply(this,a)};Ext.chart.Axis.prototype={type:null,orientation:"horizontal",reverse:false,labelFunction:null,hideOverlappingLabels:true,labelSpacing:2};Ext.chart.NumericAxis=Ext.extend(Ext.chart.Axis,{type:"numeric",minimum:NaN,maximum:NaN,majorUnit:NaN,minorUnit:NaN,snapToUnits:true,alwaysShowZero:true,scale:"linear",roundMajorUnit:true,calculateByLabelSize:true,position:"left",adjustMaximumByMajorUnit:true,adjustMinimumByMajorUnit:true});Ext.chart.TimeAxis=Ext.extend(Ext.chart.Axis,{type:"time",minimum:null,maximum:null,majorUnit:NaN,majorTimeUnit:null,minorUnit:NaN,minorTimeUnit:null,snapToUnits:true,stackingEnabled:false,calculateByLabelSize:true});Ext.chart.CategoryAxis=Ext.extend(Ext.chart.Axis,{type:"category",categoryNames:null,calculateCategoryCount:false});Ext.chart.Series=function(a){Ext.apply(this,a)};Ext.chart.Series.prototype={type:null,displayName:null};Ext.chart.CartesianSeries=Ext.extend(Ext.chart.Series,{xField:null,yField:null,showInLegend:true,axis:"primary"});Ext.chart.ColumnSeries=Ext.extend(Ext.chart.CartesianSeries,{type:"column"});Ext.chart.LineSeries=Ext.extend(Ext.chart.CartesianSeries,{type:"line"});Ext.chart.BarSeries=Ext.extend(Ext.chart.CartesianSeries,{type:"bar"});Ext.chart.PieSeries=Ext.extend(Ext.chart.Series,{type:"pie",dataField:null,categoryField:null});Ext.menu.Menu=Ext.extend(Ext.Container,{minWidth:120,shadow:"sides",subMenuAlign:"tl-tr?",defaultAlign:"tl-bl?",allowOtherMenus:false,ignoreParentClicks:false,enableScrolling:true,maxHeight:null,scrollIncrement:24,showSeparator:true,defaultOffsets:[0,0],plain:false,floating:true,zIndex:15000,hidden:true,layout:"menu",hideMode:"offsets",scrollerHeight:8,autoLayout:true,defaultType:"menuitem",bufferResize:false,initComponent:function(){if(Ext.isArray(this.initialConfig)){Ext.apply(this,{items:this.initialConfig})}this.addEvents("click","mouseover","mouseout","itemclick");Ext.menu.MenuMgr.register(this);if(this.floating){Ext.EventManager.onWindowResize(this.hide,this)}else{if(this.initialConfig.hidden!==false){this.hidden=false}this.internalDefaults={hideOnClick:false}}Ext.menu.Menu.superclass.initComponent.call(this);if(this.autoLayout){var a=this.doLayout.createDelegate(this,[]);this.on({add:a,remove:a})}},getLayoutTarget:function(){return this.ul},onRender:function(b,a){if(!b){b=Ext.getBody()}var c={id:this.getId(),cls:"x-menu "+((this.floating)?"x-menu-floating x-layer ":"")+(this.cls||"")+(this.plain?" x-menu-plain":"")+(this.showSeparator?"":" x-menu-nosep"),style:this.style,cn:[{tag:"a",cls:"x-menu-focus",href:"#",onclick:"return false;",tabIndex:"-1"},{tag:"ul",cls:"x-menu-list"}]};if(this.floating){this.el=new Ext.Layer({shadow:this.shadow,dh:c,constrain:false,parentEl:b,zindex:this.zIndex})}else{this.el=b.createChild(c)}Ext.menu.Menu.superclass.onRender.call(this,b,a);if(!this.keyNav){this.keyNav=new Ext.menu.MenuNav(this)}this.focusEl=this.el.child("a.x-menu-focus");this.ul=this.el.child("ul.x-menu-list");this.mon(this.ul,{scope:this,click:this.onClick,mouseover:this.onMouseOver,mouseout:this.onMouseOut});if(this.enableScrolling){this.mon(this.el,{scope:this,delegate:".x-menu-scroller",click:this.onScroll,mouseover:this.deactivateActive})}},findTargetItem:function(b){var a=b.getTarget(".x-menu-list-item",this.ul,true);if(a&&a.menuItemId){return this.items.get(a.menuItemId)}},onClick:function(b){var a=this.findTargetItem(b);if(a){if(a.isFormField){this.setActiveItem(a)}else{if(a instanceof Ext.menu.BaseItem){if(a.menu&&this.ignoreParentClicks){a.expandMenu();b.preventDefault()}else{if(a.onClick){a.onClick(b);this.fireEvent("click",this,a,b)}}}}}},setActiveItem:function(a,b){if(a!=this.activeItem){this.deactivateActive();if((this.activeItem=a).isFormField){a.focus()}else{a.activate(b)}}else{if(b){a.expandMenu()}}},deactivateActive:function(){var b=this.activeItem;if(b){if(b.isFormField){if(b.collapse){b.collapse()}}else{b.deactivate()}delete this.activeItem}},tryActivate:function(g,e){var b=this.items;for(var c=g,a=b.length;c>=0&&c=a.scrollHeight){this.onScrollerOut(null,b)}},onScrollerIn:function(d,b){var a=this.ul.dom,c=Ext.fly(b).is(".x-menu-scroller-top");if(c?a.scrollTop>0:a.scrollTop+this.activeMaxc){b=c;a=i-h}else{if(bb&&b>0){this.activeMax=b-this.scrollerHeight*2-this.el.getFrameWidth("tb")-Ext.num(this.el.shadowOffset,0);this.ul.setHeight(this.activeMax);this.createScrollers();this.el.select(".x-menu-scroller").setDisplayed("")}else{this.ul.setHeight(d);this.el.select(".x-menu-scroller").setDisplayed("none")}this.ul.dom.scrollTop=0;return a},createScrollers:function(){if(!this.scroller){this.scroller={pos:0,top:this.el.insertFirst({tag:"div",cls:"x-menu-scroller x-menu-scroller-top",html:" "}),bottom:this.el.createChild({tag:"div",cls:"x-menu-scroller x-menu-scroller-bottom",html:" "})};this.scroller.top.hover(this.onScrollerIn,this.onScrollerOut,this);this.scroller.topRepeater=new Ext.util.ClickRepeater(this.scroller.top,{listeners:{click:this.onScroll.createDelegate(this,[null,this.scroller.top],false)}});this.scroller.bottom.hover(this.onScrollerIn,this.onScrollerOut,this);this.scroller.bottomRepeater=new Ext.util.ClickRepeater(this.scroller.bottom,{listeners:{click:this.onScroll.createDelegate(this,[null,this.scroller.bottom],false)}})}},onLayout:function(){if(this.isVisible()){if(this.enableScrolling){this.constrainScroll(this.el.getTop())}if(this.floating){this.el.sync()}}},focus:function(){if(!this.hidden){this.doFocus.defer(50,this)}},doFocus:function(){if(!this.hidden){this.focusEl.focus()}},hide:function(a){if(!this.isDestroyed){this.deepHide=a;Ext.menu.Menu.superclass.hide.call(this);delete this.deepHide}},onHide:function(){Ext.menu.Menu.superclass.onHide.call(this);this.deactivateActive();if(this.el&&this.floating){this.el.hide()}var a=this.parentMenu;if(this.deepHide===true&&a){if(a.floating){a.hide(true)}else{a.deactivateActive()}}},lookupComponent:function(a){if(Ext.isString(a)){a=(a=="separator"||a=="-")?new Ext.menu.Separator():new Ext.menu.TextItem(a);this.applyDefaults(a)}else{if(Ext.isObject(a)){a=this.getMenuItem(a)}else{if(a.tagName||a.el){a=new Ext.BoxComponent({el:a})}}}return a},applyDefaults:function(b){if(!Ext.isString(b)){b=Ext.menu.Menu.superclass.applyDefaults.call(this,b);var a=this.internalDefaults;if(a){if(b.events){Ext.applyIf(b.initialConfig,a);Ext.apply(b,a)}else{Ext.applyIf(b,a)}}}return b},getMenuItem:function(a){a.ownerCt=this;if(!a.isXType){if(!a.xtype&&Ext.isBoolean(a.checked)){return new Ext.menu.CheckItem(a)}return Ext.create(a,this.defaultType)}return a},addSeparator:function(){return this.add(new Ext.menu.Separator())},addElement:function(a){return this.add(new Ext.menu.BaseItem({el:a}))},addItem:function(a){return this.add(a)},addMenuItem:function(a){return this.add(this.getMenuItem(a))},addText:function(a){return this.add(new Ext.menu.TextItem(a))},onDestroy:function(){Ext.EventManager.removeResizeListener(this.hide,this);var a=this.parentMenu;if(a&&a.activeChild==this){delete a.activeChild}delete this.parentMenu;Ext.menu.Menu.superclass.onDestroy.call(this);Ext.menu.MenuMgr.unregister(this);if(this.keyNav){this.keyNav.disable()}var b=this.scroller;if(b){Ext.destroy(b.topRepeater,b.bottomRepeater,b.top,b.bottom)}Ext.destroy(this.el,this.focusEl,this.ul)}});Ext.reg("menu",Ext.menu.Menu);Ext.menu.MenuNav=Ext.extend(Ext.KeyNav,function(){function a(d,c){if(!c.tryActivate(c.items.indexOf(c.activeItem)-1,-1)){c.tryActivate(c.items.length-1,-1)}}function b(d,c){if(!c.tryActivate(c.items.indexOf(c.activeItem)+1,1)){c.tryActivate(0,1)}}return{constructor:function(c){Ext.menu.MenuNav.superclass.constructor.call(this,c.el);this.scope=this.menu=c},doRelay:function(g,d){var c=g.getKey();if(this.menu.activeItem&&this.menu.activeItem.isFormField&&c!=g.TAB){return false}if(!this.menu.activeItem&&g.isNavKeyPress()&&c!=g.SPACE&&c!=g.RETURN){this.menu.tryActivate(0,1);return false}return d.call(this.scope||this,g,this.menu)},tab:function(d,c){d.stopEvent();if(d.shiftKey){a(d,c)}else{b(d,c)}},up:a,down:b,right:function(d,c){if(c.activeItem){c.activeItem.expandMenu(true)}},left:function(d,c){c.hide();if(c.parentMenu&&c.parentMenu.activeItem){c.parentMenu.activeItem.activate()}},enter:function(d,c){if(c.activeItem){d.stopPropagation();c.activeItem.onClick(d);c.fireEvent("click",this,c.activeItem);return true}}}}());Ext.menu.MenuMgr=function(){var h,e,b,d={},a=false,l=new Date();function n(){h={};e=new Ext.util.MixedCollection();b=Ext.getDoc().addKeyListener(27,j);b.disable()}function j(){if(e&&e.length>0){var o=e.clone();o.each(function(p){p.hide()});return true}return false}function g(o){e.remove(o);if(e.length<1){b.disable();Ext.getDoc().un("mousedown",m);a=false}}function k(o){var p=e.last();l=new Date();e.add(o);if(!a){b.enable();Ext.getDoc().on("mousedown",m);a=true}if(o.parentMenu){o.getEl().setZIndex(parseInt(o.parentMenu.getEl().getStyle("z-index"),10)+3);o.parentMenu.activeChild=o}else{if(p&&!p.isDestroyed&&p.isVisible()){o.getEl().setZIndex(parseInt(p.getEl().getStyle("z-index"),10)+3)}}}function c(o){if(o.activeChild){o.activeChild.hide()}if(o.autoHideTimer){clearTimeout(o.autoHideTimer);delete o.autoHideTimer}}function i(o){var p=o.parentMenu;if(!p&&!o.allowOtherMenus){j()}else{if(p&&p.activeChild){p.activeChild.hide()}}}function m(o){if(l.getElapsed()>50&&e.length>0&&!o.getTarget(".x-menu")){j()}}return{hideAll:function(){return j()},register:function(o){if(!h){n()}h[o.id]=o;o.on({beforehide:c,hide:g,beforeshow:i,show:k})},get:function(o){if(typeof o=="string"){if(!h){return null}return h[o]}else{if(o.events){return o}else{if(typeof o.length=="number"){return new Ext.menu.Menu({items:o})}else{return Ext.create(o,"menu")}}}},unregister:function(o){delete h[o.id];o.un("beforehide",c);o.un("hide",g);o.un("beforeshow",i);o.un("show",k)},registerCheckable:function(o){var p=o.group;if(p){if(!d[p]){d[p]=[]}d[p].push(o)}},unregisterCheckable:function(o){var p=o.group;if(p){d[p].remove(o)}},onCheckChange:function(q,r){if(q.group&&r){var t=d[q.group],p=0,o=t.length,s;for(;p',' target="{hrefTarget}"',"",">",'{altText}','{text}',"")}var c=this.getTemplateArgs();this.el=b?this.itemTpl.insertBefore(b,c,true):this.itemTpl.append(d,c,true);this.iconEl=this.el.child("img.x-menu-item-icon");this.textEl=this.el.child(".x-menu-item-text");if(!this.href){this.mon(this.el,"click",Ext.emptyFn,null,{preventDefault:true})}Ext.menu.Item.superclass.onRender.call(this,d,b)},getTemplateArgs:function(){return{id:this.id,cls:this.itemCls+(this.menu?" x-menu-item-arrow":"")+(this.cls?" "+this.cls:""),href:this.href||"#",hrefTarget:this.hrefTarget,icon:this.icon||Ext.BLANK_IMAGE_URL,iconCls:this.iconCls||"",text:this.itemText||this.text||" ",altText:this.altText||""}},setText:function(a){this.text=a||" ";if(this.rendered){this.textEl.update(this.text);this.parentMenu.layout.doAutoSize()}},setIconClass:function(a){var b=this.iconCls;this.iconCls=a;if(this.rendered){this.iconEl.replaceClass(b,this.iconCls)}},beforeDestroy:function(){clearTimeout(this.showTimer);clearTimeout(this.hideTimer);if(this.menu){delete this.menu.ownerCt;this.menu.destroy()}Ext.menu.Item.superclass.beforeDestroy.call(this)},handleClick:function(a){if(!this.href){a.stopEvent()}Ext.menu.Item.superclass.handleClick.apply(this,arguments)},activate:function(a){if(Ext.menu.Item.superclass.activate.apply(this,arguments)){this.focus();if(a){this.expandMenu()}}return true},shouldDeactivate:function(a){if(Ext.menu.Item.superclass.shouldDeactivate.call(this,a)){if(this.menu&&this.menu.isVisible()){return !this.menu.getEl().getRegion().contains(a.getPoint())}return true}return false},deactivate:function(){Ext.menu.Item.superclass.deactivate.apply(this,arguments);this.hideMenu()},expandMenu:function(a){if(!this.disabled&&this.menu){clearTimeout(this.hideTimer);delete this.hideTimer;if(!this.menu.isVisible()&&!this.showTimer){this.showTimer=this.deferExpand.defer(this.showDelay,this,[a])}else{if(this.menu.isVisible()&&a){this.menu.tryActivate(0,1)}}}},deferExpand:function(a){delete this.showTimer;this.menu.show(this.container,this.parentMenu.subMenuAlign||"tl-tr?",this.parentMenu);if(a){this.menu.tryActivate(0,1)}},hideMenu:function(){clearTimeout(this.showTimer);delete this.showTimer;if(!this.hideTimer&&this.menu&&this.menu.isVisible()){this.hideTimer=this.deferHide.defer(this.hideDelay,this)}},deferHide:function(){delete this.hideTimer;if(this.menu.over){this.parentMenu.setActiveItem(this,false)}else{this.menu.hide()}}});Ext.reg("menuitem",Ext.menu.Item);Ext.menu.CheckItem=Ext.extend(Ext.menu.Item,{itemCls:"x-menu-item x-menu-check-item",groupClass:"x-menu-group-item",checked:false,ctype:"Ext.menu.CheckItem",initComponent:function(){Ext.menu.CheckItem.superclass.initComponent.call(this);this.addEvents("beforecheckchange","checkchange");if(this.checkHandler){this.on("checkchange",this.checkHandler,this.scope)}Ext.menu.MenuMgr.registerCheckable(this)},onRender:function(a){Ext.menu.CheckItem.superclass.onRender.apply(this,arguments);if(this.group){this.el.addClass(this.groupClass)}if(this.checked){this.checked=false;this.setChecked(true,true)}},destroy:function(){Ext.menu.MenuMgr.unregisterCheckable(this);Ext.menu.CheckItem.superclass.destroy.apply(this,arguments)},setChecked:function(b,a){var c=a===true;if(this.checked!=b&&(c||this.fireEvent("beforecheckchange",this,b)!==false)){Ext.menu.MenuMgr.onCheckChange(this,b);if(this.container){this.container[b?"addClass":"removeClass"]("x-menu-item-checked")}this.checked=b;if(!c){this.fireEvent("checkchange",this,b)}}},handleClick:function(a){if(!this.disabled&&!(this.checked&&this.group)){this.setChecked(!this.checked)}Ext.menu.CheckItem.superclass.handleClick.apply(this,arguments)}});Ext.reg("menucheckitem",Ext.menu.CheckItem);Ext.menu.DateMenu=Ext.extend(Ext.menu.Menu,{enableScrolling:false,hideOnClick:true,pickerId:null,cls:"x-date-menu",initComponent:function(){this.on("beforeshow",this.onBeforeShow,this);if(this.strict=(Ext.isIE7&&Ext.isStrict)){this.on("show",this.onShow,this,{single:true,delay:20})}Ext.apply(this,{plain:true,showSeparator:false,items:this.picker=new Ext.DatePicker(Ext.applyIf({internalRender:this.strict||!Ext.isIE,ctCls:"x-menu-date-item",id:this.pickerId},this.initialConfig))});this.picker.purgeListeners();Ext.menu.DateMenu.superclass.initComponent.call(this);this.relayEvents(this.picker,["select"]);this.on("show",this.picker.focus,this.picker);this.on("select",this.menuHide,this);if(this.handler){this.on("select",this.handler,this.scope||this)}},menuHide:function(){if(this.hideOnClick){this.hide(true)}},onBeforeShow:function(){if(this.picker){this.picker.hideMonthPicker(true)}},onShow:function(){var a=this.picker.getEl();a.setWidth(a.getWidth())}});Ext.reg("datemenu",Ext.menu.DateMenu);Ext.menu.ColorMenu=Ext.extend(Ext.menu.Menu,{enableScrolling:false,hideOnClick:true,cls:"x-color-menu",paletteId:null,initComponent:function(){Ext.apply(this,{plain:true,showSeparator:false,items:this.palette=new Ext.ColorPalette(Ext.applyIf({id:this.paletteId},this.initialConfig))});this.palette.purgeListeners();Ext.menu.ColorMenu.superclass.initComponent.call(this);this.relayEvents(this.palette,["select"]);this.on("select",this.menuHide,this);if(this.handler){this.on("select",this.handler,this.scope||this)}},menuHide:function(){if(this.hideOnClick){this.hide(true)}}});Ext.reg("colormenu",Ext.menu.ColorMenu);Ext.form.Field=Ext.extend(Ext.BoxComponent,{invalidClass:"x-form-invalid",invalidText:"The value in this field is invalid",focusClass:"x-form-focus",validationEvent:"keyup",validateOnBlur:true,validationDelay:250,defaultAutoCreate:{tag:"input",type:"text",size:"20",autocomplete:"off"},fieldClass:"x-form-field",msgTarget:"qtip",msgFx:"normal",readOnly:false,disabled:false,submitValue:true,isFormField:true,msgDisplay:"",hasFocus:false,initComponent:function(){Ext.form.Field.superclass.initComponent.call(this);this.addEvents("focus","blur","specialkey","change","invalid","valid")},getName:function(){return this.rendered&&this.el.dom.name?this.el.dom.name:this.name||this.id||""},onRender:function(c,a){if(!this.el){var b=this.getAutoCreate();if(!b.name){b.name=this.name||this.id}if(this.inputType){b.type=this.inputType}this.autoEl=b}Ext.form.Field.superclass.onRender.call(this,c,a);if(this.submitValue===false){this.el.dom.removeAttribute("name")}var d=this.el.dom.type;if(d){if(d=="password"){d="text"}this.el.addClass("x-form-"+d)}if(this.readOnly){this.setReadOnly(true)}if(this.tabIndex!==undefined){this.el.dom.setAttribute("tabIndex",this.tabIndex)}this.el.addClass([this.fieldClass,this.cls])},getItemCt:function(){return this.itemCt},initValue:function(){if(this.value!==undefined){this.setValue(this.value)}else{if(!Ext.isEmpty(this.el.dom.value)&&this.el.dom.value!=this.emptyText){this.setValue(this.el.dom.value)}}this.originalValue=this.getValue()},isDirty:function(){if(this.disabled||!this.rendered){return false}return String(this.getValue())!==String(this.originalValue)},setReadOnly:function(a){if(this.rendered){this.el.dom.readOnly=a}this.readOnly=a},afterRender:function(){Ext.form.Field.superclass.afterRender.call(this);this.initEvents();this.initValue()},fireKey:function(a){if(a.isSpecialKey()){this.fireEvent("specialkey",this,a)}},reset:function(){this.setValue(this.originalValue);this.clearInvalid()},initEvents:function(){this.mon(this.el,Ext.EventManager.getKeyEvent(),this.fireKey,this);this.mon(this.el,"focus",this.onFocus,this);this.mon(this.el,"blur",this.onBlur,this,this.inEditor?{buffer:10}:null)},preFocus:Ext.emptyFn,onFocus:function(){this.preFocus();if(this.focusClass){this.el.addClass(this.focusClass)}if(!this.hasFocus){this.hasFocus=true;this.startValue=this.getValue();this.fireEvent("focus",this)}},beforeBlur:Ext.emptyFn,onBlur:function(){this.beforeBlur();if(this.focusClass){this.el.removeClass(this.focusClass)}this.hasFocus=false;if(this.validationEvent!==false&&(this.validateOnBlur||this.validationEvent=="blur")){this.validate()}var a=this.getValue();if(String(a)!==String(this.startValue)){this.fireEvent("change",this,a,this.startValue)}this.fireEvent("blur",this);this.postBlur()},postBlur:Ext.emptyFn,isValid:function(a){if(this.disabled){return true}var c=this.preventMark;this.preventMark=a===true;var b=this.validateValue(this.processValue(this.getRawValue()),a);this.preventMark=c;return b},validate:function(){if(this.disabled||this.validateValue(this.processValue(this.getRawValue()))){this.clearInvalid();return true}return false},processValue:function(a){return a},validateValue:function(b){var a=this.getErrors(b)[0];if(a==undefined){return true}else{this.markInvalid(a);return false}},getErrors:function(){return[]},getActiveError:function(){return this.activeError||""},markInvalid:function(c){if(this.rendered&&!this.preventMark){c=c||this.invalidText;var a=this.getMessageHandler();if(a){a.mark(this,c)}else{if(this.msgTarget){this.el.addClass(this.invalidClass);var b=Ext.getDom(this.msgTarget);if(b){b.innerHTML=c;b.style.display=this.msgDisplay}}}}this.setActiveError(c)},clearInvalid:function(){if(this.rendered&&!this.preventMark){this.el.removeClass(this.invalidClass);var a=this.getMessageHandler();if(a){a.clear(this)}else{if(this.msgTarget){this.el.removeClass(this.invalidClass);var b=Ext.getDom(this.msgTarget);if(b){b.innerHTML="";b.style.display="none"}}}}this.unsetActiveError()},setActiveError:function(b,a){this.activeError=b;if(a!==true){this.fireEvent("invalid",this,b)}},unsetActiveError:function(a){delete this.activeError;if(a!==true){this.fireEvent("valid",this)}},getMessageHandler:function(){return Ext.form.MessageTargets[this.msgTarget]},getErrorCt:function(){return this.el.findParent(".x-form-element",5,true)||this.el.findParent(".x-form-field-wrap",5,true)},alignErrorEl:function(){this.errorEl.setWidth(this.getErrorCt().getWidth(true)-20)},alignErrorIcon:function(){this.errorIcon.alignTo(this.el,"tl-tr",[2,0])},getRawValue:function(){var a=this.rendered?this.el.getValue():Ext.value(this.value,"");if(a===this.emptyText){a=""}return a},getValue:function(){if(!this.rendered){return this.value}var a=this.el.getValue();if(a===this.emptyText||a===undefined){a=""}return a},setRawValue:function(a){return this.rendered?(this.el.dom.value=(Ext.isEmpty(a)?"":a)):""},setValue:function(a){this.value=a;if(this.rendered){this.el.dom.value=(Ext.isEmpty(a)?"":a);this.validate()}return this},append:function(a){this.setValue([this.getValue(),a].join(""))}});Ext.form.MessageTargets={qtip:{mark:function(a,b){a.el.addClass(a.invalidClass);a.el.dom.qtip=b;a.el.dom.qclass="x-form-invalid-tip";if(Ext.QuickTips){Ext.QuickTips.enable()}},clear:function(a){a.el.removeClass(a.invalidClass);a.el.dom.qtip=""}},title:{mark:function(a,b){a.el.addClass(a.invalidClass);a.el.dom.title=b},clear:function(a){a.el.dom.title=""}},under:{mark:function(b,c){b.el.addClass(b.invalidClass);if(!b.errorEl){var a=b.getErrorCt();if(!a){b.el.dom.title=c;return}b.errorEl=a.createChild({cls:"x-form-invalid-msg"});b.on("resize",b.alignErrorEl,b);b.on("destroy",function(){Ext.destroy(this.errorEl)},b)}b.alignErrorEl();b.errorEl.update(c);Ext.form.Field.msgFx[b.msgFx].show(b.errorEl,b)},clear:function(a){a.el.removeClass(a.invalidClass);if(a.errorEl){Ext.form.Field.msgFx[a.msgFx].hide(a.errorEl,a)}else{a.el.dom.title=""}}},side:{mark:function(b,c){b.el.addClass(b.invalidClass);if(!b.errorIcon){var a=b.getErrorCt();if(!a){b.el.dom.title=c;return}b.errorIcon=a.createChild({cls:"x-form-invalid-icon"});if(b.ownerCt){b.ownerCt.on("afterlayout",b.alignErrorIcon,b);b.ownerCt.on("expand",b.alignErrorIcon,b)}b.on("resize",b.alignErrorIcon,b);b.on("destroy",function(){Ext.destroy(this.errorIcon)},b)}b.alignErrorIcon();b.errorIcon.dom.qtip=c;b.errorIcon.dom.qclass="x-form-invalid-tip";b.errorIcon.show()},clear:function(a){a.el.removeClass(a.invalidClass);if(a.errorIcon){a.errorIcon.dom.qtip="";a.errorIcon.hide()}else{a.el.dom.title=""}}}};Ext.form.Field.msgFx={normal:{show:function(a,b){a.setDisplayed("block")},hide:function(a,b){a.setDisplayed(false).update("")}},slide:{show:function(a,b){a.slideIn("t",{stopFx:true})},hide:function(a,b){a.slideOut("t",{stopFx:true,useDisplay:true})}},slideRight:{show:function(a,b){a.fixDisplay();a.alignTo(b.el,"tl-tr");a.slideIn("l",{stopFx:true})},hide:function(a,b){a.slideOut("l",{stopFx:true,useDisplay:true})}}};Ext.reg("field",Ext.form.Field);Ext.form.TextField=Ext.extend(Ext.form.Field,{grow:false,growMin:30,growMax:800,vtype:null,maskRe:null,disableKeyFilter:false,allowBlank:true,minLength:0,maxLength:Number.MAX_VALUE,minLengthText:"The minimum length for this field is {0}",maxLengthText:"The maximum length for this field is {0}",selectOnFocus:false,blankText:"This field is required",validator:null,regex:null,regexText:"",emptyText:null,emptyClass:"x-form-empty-field",initComponent:function(){Ext.form.TextField.superclass.initComponent.call(this);this.addEvents("autosize","keydown","keyup","keypress")},initEvents:function(){Ext.form.TextField.superclass.initEvents.call(this);if(this.validationEvent=="keyup"){this.validationTask=new Ext.util.DelayedTask(this.validate,this);this.mon(this.el,"keyup",this.filterValidation,this)}else{if(this.validationEvent!==false&&this.validationEvent!="blur"){this.mon(this.el,this.validationEvent,this.validate,this,{buffer:this.validationDelay})}}if(this.selectOnFocus||this.emptyText){this.mon(this.el,"mousedown",this.onMouseDown,this);if(this.emptyText){this.applyEmptyText()}}if(this.maskRe||(this.vtype&&this.disableKeyFilter!==true&&(this.maskRe=Ext.form.VTypes[this.vtype+"Mask"]))){this.mon(this.el,"keypress",this.filterKeys,this)}if(this.grow){this.mon(this.el,"keyup",this.onKeyUpBuffered,this,{buffer:50});this.mon(this.el,"click",this.autoSize,this)}if(this.enableKeyEvents){this.mon(this.el,{scope:this,keyup:this.onKeyUp,keydown:this.onKeyDown,keypress:this.onKeyPress})}},onMouseDown:function(a){if(!this.hasFocus){this.mon(this.el,"mouseup",Ext.emptyFn,this,{single:true,preventDefault:true})}},processValue:function(a){if(this.stripCharsRe){var b=a.replace(this.stripCharsRe,"");if(b!==a){this.setRawValue(b);return b}}return a},filterValidation:function(a){if(!a.isNavKeyPress()){this.validationTask.delay(this.validationDelay)}},onDisable:function(){Ext.form.TextField.superclass.onDisable.call(this);if(Ext.isIE){this.el.dom.unselectable="on"}},onEnable:function(){Ext.form.TextField.superclass.onEnable.call(this);if(Ext.isIE){this.el.dom.unselectable=""}},onKeyUpBuffered:function(a){if(this.doAutoSize(a)){this.autoSize()}},doAutoSize:function(a){return !a.isNavKeyPress()},onKeyUp:function(a){this.fireEvent("keyup",this,a)},onKeyDown:function(a){this.fireEvent("keydown",this,a)},onKeyPress:function(a){this.fireEvent("keypress",this,a)},reset:function(){Ext.form.TextField.superclass.reset.call(this);this.applyEmptyText()},applyEmptyText:function(){if(this.rendered&&this.emptyText&&this.getRawValue().length<1&&!this.hasFocus){this.setRawValue(this.emptyText);this.el.addClass(this.emptyClass)}},preFocus:function(){var a=this.el,b;if(this.emptyText){if(a.dom.value==this.emptyText){this.setRawValue("");b=true}a.removeClass(this.emptyClass)}if(this.selectOnFocus||b){a.dom.select()}},postBlur:function(){this.applyEmptyText()},filterKeys:function(b){if(b.ctrlKey){return}var a=b.getKey();if(Ext.isGecko&&(b.isNavKeyPress()||a==b.BACKSPACE||(a==b.DELETE&&b.button==-1))){return}var c=String.fromCharCode(b.getCharCode());if(!Ext.isGecko&&b.isSpecialKey()&&!c){return}if(!this.maskRe.test(c)){b.stopEvent()}},setValue:function(a){if(this.emptyText&&this.el&&!Ext.isEmpty(a)){this.el.removeClass(this.emptyClass)}Ext.form.TextField.superclass.setValue.apply(this,arguments);this.applyEmptyText();this.autoSize();return this},getErrors:function(a){var d=Ext.form.TextField.superclass.getErrors.apply(this,arguments);a=Ext.isDefined(a)?a:this.processValue(this.getRawValue());if(Ext.isFunction(this.validator)){var c=this.validator(a);if(c!==true){d.push(c)}}if(a.length<1||a===this.emptyText){if(this.allowBlank){return d}else{d.push(this.blankText)}}if(!this.allowBlank&&(a.length<1||a===this.emptyText)){d.push(this.blankText)}if(a.lengththis.maxLength){d.push(String.format(this.maxLengthText,this.maxLength))}if(this.vtype){var b=Ext.form.VTypes;if(!b[this.vtype](a,this)){d.push(this.vtypeText||b[this.vtype+"Text"])}}if(this.regex&&!this.regex.test(a)){d.push(this.regexText)}return d},selectText:function(h,a){var c=this.getRawValue();var e=false;if(c.length>0){h=h===undefined?0:h;a=a===undefined?c.length:a;var g=this.el.dom;if(g.setSelectionRange){g.setSelectionRange(h,a)}else{if(g.createTextRange){var b=g.createTextRange();b.moveStart("character",h);b.moveEnd("character",a-c.length);b.select()}}e=Ext.isGecko||Ext.isOpera}else{e=true}if(e){this.focus()}},autoSize:function(){if(!this.grow||!this.rendered){return}if(!this.metrics){this.metrics=Ext.util.TextMetrics.createInstance(this.el)}var c=this.el;var b=c.dom.value;var e=document.createElement("div");e.appendChild(document.createTextNode(b));b=e.innerHTML;Ext.removeNode(e);e=null;b+=" ";var a=Math.min(this.growMax,Math.max(this.metrics.getWidth(b)+10,this.growMin));this.el.setWidth(a);this.fireEvent("autosize",this,a)},onDestroy:function(){if(this.validationTask){this.validationTask.cancel();this.validationTask=null}Ext.form.TextField.superclass.onDestroy.call(this)}});Ext.reg("textfield",Ext.form.TextField);Ext.form.TriggerField=Ext.extend(Ext.form.TextField,{defaultAutoCreate:{tag:"input",type:"text",size:"16",autocomplete:"off"},hideTrigger:false,editable:true,readOnly:false,wrapFocusClass:"x-trigger-wrap-focus",autoSize:Ext.emptyFn,monitorTab:true,deferHeight:true,mimicing:false,actionMode:"wrap",defaultTriggerWidth:17,onResize:function(a,c){Ext.form.TriggerField.superclass.onResize.call(this,a,c);var b=this.getTriggerWidth();if(Ext.isNumber(a)){this.el.setWidth(a-b)}this.wrap.setWidth(this.el.getWidth()+b)},getTriggerWidth:function(){var a=this.trigger.getWidth();if(!this.hideTrigger&&!this.readOnly&&a===0){a=this.defaultTriggerWidth}return a},alignErrorIcon:function(){if(this.wrap){this.errorIcon.alignTo(this.wrap,"tl-tr",[2,0])}},onRender:function(b,a){this.doc=Ext.isIE?Ext.getBody():Ext.getDoc();Ext.form.TriggerField.superclass.onRender.call(this,b,a);this.wrap=this.el.wrap({cls:"x-form-field-wrap x-form-field-trigger-wrap"});this.trigger=this.wrap.createChild(this.triggerConfig||{tag:"img",src:Ext.BLANK_IMAGE_URL,alt:"",cls:"x-form-trigger "+this.triggerClass});this.initTrigger();if(!this.width){this.wrap.setWidth(this.el.getWidth()+this.trigger.getWidth())}this.resizeEl=this.positionEl=this.wrap},getWidth:function(){return(this.el.getWidth()+this.trigger.getWidth())},updateEditState:function(){if(this.rendered){if(this.readOnly){this.el.dom.readOnly=true;this.el.addClass("x-trigger-noedit");this.mun(this.el,"click",this.onTriggerClick,this);this.trigger.setDisplayed(false)}else{if(!this.editable){this.el.dom.readOnly=true;this.el.addClass("x-trigger-noedit");this.mon(this.el,"click",this.onTriggerClick,this)}else{this.el.dom.readOnly=false;this.el.removeClass("x-trigger-noedit");this.mun(this.el,"click",this.onTriggerClick,this)}this.trigger.setDisplayed(!this.hideTrigger)}this.onResize(this.width||this.wrap.getWidth())}},setHideTrigger:function(a){if(a!=this.hideTrigger){this.hideTrigger=a;this.updateEditState()}},setEditable:function(a){if(a!=this.editable){this.editable=a;this.updateEditState()}},setReadOnly:function(a){if(a!=this.readOnly){this.readOnly=a;this.updateEditState()}},afterRender:function(){Ext.form.TriggerField.superclass.afterRender.call(this);this.updateEditState()},initTrigger:function(){this.mon(this.trigger,"click",this.onTriggerClick,this,{preventDefault:true});this.trigger.addClassOnOver("x-form-trigger-over");this.trigger.addClassOnClick("x-form-trigger-click")},onDestroy:function(){Ext.destroy(this.trigger,this.wrap);if(this.mimicing){this.doc.un("mousedown",this.mimicBlur,this)}delete this.doc;Ext.form.TriggerField.superclass.onDestroy.call(this)},onFocus:function(){Ext.form.TriggerField.superclass.onFocus.call(this);if(!this.mimicing){this.wrap.addClass(this.wrapFocusClass);this.mimicing=true;this.doc.on("mousedown",this.mimicBlur,this,{delay:10});if(this.monitorTab){this.on("specialkey",this.checkTab,this)}}},checkTab:function(a,b){if(b.getKey()==b.TAB){this.triggerBlur()}},onBlur:Ext.emptyFn,mimicBlur:function(a){if(!this.isDestroyed&&!this.wrap.contains(a.target)&&this.validateBlur(a)){this.triggerBlur()}},triggerBlur:function(){this.mimicing=false;this.doc.un("mousedown",this.mimicBlur,this);if(this.monitorTab&&this.el){this.un("specialkey",this.checkTab,this)}Ext.form.TriggerField.superclass.onBlur.call(this);if(this.wrap){this.wrap.removeClass(this.wrapFocusClass)}},beforeBlur:Ext.emptyFn,validateBlur:function(a){return true},onTriggerClick:Ext.emptyFn});Ext.form.TwinTriggerField=Ext.extend(Ext.form.TriggerField,{initComponent:function(){Ext.form.TwinTriggerField.superclass.initComponent.call(this);this.triggerConfig={tag:"span",cls:"x-form-twin-triggers",cn:[{tag:"img",src:Ext.BLANK_IMAGE_URL,alt:"",cls:"x-form-trigger "+this.trigger1Class},{tag:"img",src:Ext.BLANK_IMAGE_URL,alt:"",cls:"x-form-trigger "+this.trigger2Class}]}},getTrigger:function(a){return this.triggers[a]},afterRender:function(){Ext.form.TwinTriggerField.superclass.afterRender.call(this);var c=this.triggers,b=0,a=c.length;for(;b")}}d.innerHTML=a;b=Math.min(this.growMax,Math.max(d.offsetHeight,this.growMin));if(b!=this.lastHeight){this.lastHeight=b;this.el.setHeight(b);this.fireEvent("autosize",this,b)}}});Ext.reg("textarea",Ext.form.TextArea);Ext.form.NumberField=Ext.extend(Ext.form.TextField,{fieldClass:"x-form-field x-form-num-field",allowDecimals:true,decimalSeparator:".",decimalPrecision:2,allowNegative:true,minValue:Number.NEGATIVE_INFINITY,maxValue:Number.MAX_VALUE,minText:"The minimum value for this field is {0}",maxText:"The maximum value for this field is {0}",nanText:"{0} is not a valid number",baseChars:"0123456789",autoStripChars:false,initEvents:function(){var a=this.baseChars+"";if(this.allowDecimals){a+=this.decimalSeparator}if(this.allowNegative){a+="-"}a=Ext.escapeRe(a);this.maskRe=new RegExp("["+a+"]");if(this.autoStripChars){this.stripCharsRe=new RegExp("[^"+a+"]","gi")}Ext.form.NumberField.superclass.initEvents.call(this)},getErrors:function(b){var c=Ext.form.NumberField.superclass.getErrors.apply(this,arguments);b=Ext.isDefined(b)?b:this.processValue(this.getRawValue());if(b.length<1){return c}b=String(b).replace(this.decimalSeparator,".");if(isNaN(b)){c.push(String.format(this.nanText,b))}var a=this.parseValue(b);if(athis.maxValue){c.push(String.format(this.maxText,this.maxValue))}return c},getValue:function(){return this.fixPrecision(this.parseValue(Ext.form.NumberField.superclass.getValue.call(this)))},setValue:function(a){a=Ext.isNumber(a)?a:parseFloat(String(a).replace(this.decimalSeparator,"."));a=this.fixPrecision(a);a=isNaN(a)?"":String(a).replace(".",this.decimalSeparator);return Ext.form.NumberField.superclass.setValue.call(this,a)},setMinValue:function(a){this.minValue=Ext.num(a,Number.NEGATIVE_INFINITY)},setMaxValue:function(a){this.maxValue=Ext.num(a,Number.MAX_VALUE)},parseValue:function(a){a=parseFloat(String(a).replace(this.decimalSeparator,"."));return isNaN(a)?"":a},fixPrecision:function(b){var a=isNaN(b);if(!this.allowDecimals||this.decimalPrecision==-1||a||!b){return a?"":b}return parseFloat(parseFloat(b).toFixed(this.decimalPrecision))},beforeBlur:function(){var a=this.parseValue(this.getRawValue());if(!Ext.isEmpty(a)){this.setValue(a)}}});Ext.reg("numberfield",Ext.form.NumberField);Ext.form.DateField=Ext.extend(Ext.form.TriggerField,{format:"m/d/Y",altFormats:"m/d/Y|n/j/Y|n/j/y|m/j/y|n/d/y|m/j/Y|n/d/Y|m-d-y|m-d-Y|m/d|m-d|md|mdy|mdY|d|Y-m-d|n-j|n/j",disabledDaysText:"Disabled",disabledDatesText:"Disabled",minText:"The date in this field must be equal to or after {0}",maxText:"The date in this field must be equal to or before {0}",invalidText:"{0} is not a valid date - it must be in the format {1}",triggerClass:"x-form-date-trigger",showToday:true,startDay:0,defaultAutoCreate:{tag:"input",type:"text",size:"10",autocomplete:"off"},initTime:"12",initTimeFormat:"H",safeParse:function(b,c){if(Date.formatContainsHourInfo(c)){return Date.parseDate(b,c)}else{var a=Date.parseDate(b+" "+this.initTime,c+" "+this.initTimeFormat);if(a){return a.clearTime()}}},initComponent:function(){Ext.form.DateField.superclass.initComponent.call(this);this.addEvents("select");if(Ext.isString(this.minValue)){this.minValue=this.parseDate(this.minValue)}if(Ext.isString(this.maxValue)){this.maxValue=this.parseDate(this.maxValue)}this.disabledDatesRE=null;this.initDisabledDays()},initEvents:function(){Ext.form.DateField.superclass.initEvents.call(this);this.keyNav=new Ext.KeyNav(this.el,{down:function(a){this.onTriggerClick()},scope:this,forceKeyDown:true})},initDisabledDays:function(){if(this.disabledDates){var b=this.disabledDates,a=b.length-1,c="(?:";Ext.each(b,function(g,e){c+=Ext.isDate(g)?"^"+Ext.escapeRe(g.dateFormat(this.format))+"$":b[e];if(e!=a){c+="|"}},this);this.disabledDatesRE=new RegExp(c+")")}},setDisabledDates:function(a){this.disabledDates=a;this.initDisabledDays();if(this.menu){this.menu.picker.setDisabledDates(this.disabledDatesRE)}},setDisabledDays:function(a){this.disabledDays=a;if(this.menu){this.menu.picker.setDisabledDays(a)}},setMinValue:function(a){this.minValue=(Ext.isString(a)?this.parseDate(a):a);if(this.menu){this.menu.picker.setMinDate(this.minValue)}},setMaxValue:function(a){this.maxValue=(Ext.isString(a)?this.parseDate(a):a);if(this.menu){this.menu.picker.setMaxDate(this.maxValue)}},getErrors:function(e){var h=Ext.form.DateField.superclass.getErrors.apply(this,arguments);e=this.formatDate(e||this.processValue(this.getRawValue()));if(e.length<1){return h}var c=e;e=this.parseDate(e);if(!e){h.push(String.format(this.invalidText,c,this.format));return h}var g=e.getTime();if(this.minValue&&gthis.maxValue.clearTime().getTime()){h.push(String.format(this.maxText,this.formatDate(this.maxValue)))}if(this.disabledDays){var a=e.getDay();for(var b=0;b
    {'+this.displayField+"}
    "}this.view=new Ext.DataView({applyTo:this.innerList,tpl:this.tpl,singleSelect:true,selectedClass:this.selectedClass,itemSelector:this.itemSelector||"."+a+"-item",emptyText:this.listEmptyText,deferEmptyText:false});this.mon(this.view,{containerclick:this.onViewClick,click:this.onViewClick,scope:this});this.bindStore(this.store,true);if(this.resizable){this.resizer=new Ext.Resizable(this.list,{pinned:true,handles:"se"});this.mon(this.resizer,"resize",function(g,d,e){this.maxHeight=e-this.handleHeight-this.list.getFrameWidth("tb")-this.assetHeight;this.listWidth=d;this.innerList.setWidth(d-this.list.getFrameWidth("lr"));this.restrictHeight()},this);this[this.pageSize?"footer":"innerList"].setStyle("margin-bottom",this.handleHeight+"px")}}},getListParent:function(){return document.body},getStore:function(){return this.store},bindStore:function(a,b){if(this.store&&!b){if(this.store!==a&&this.store.autoDestroy){this.store.destroy()}else{this.store.un("beforeload",this.onBeforeLoad,this);this.store.un("load",this.onLoad,this);this.store.un("exception",this.collapse,this)}if(!a){this.store=null;if(this.view){this.view.bindStore(null)}if(this.pageTb){this.pageTb.bindStore(null)}}}if(a){if(!b){this.lastQuery=null;if(this.pageTb){this.pageTb.bindStore(a)}}this.store=Ext.StoreMgr.lookup(a);this.store.on({scope:this,beforeload:this.onBeforeLoad,load:this.onLoad,exception:this.collapse});if(this.view){this.view.bindStore(a)}}},reset:function(){if(this.clearFilterOnReset&&this.mode=="local"){this.store.clearFilter()}Ext.form.ComboBox.superclass.reset.call(this)},initEvents:function(){Ext.form.ComboBox.superclass.initEvents.call(this);this.keyNav=new Ext.KeyNav(this.el,{up:function(a){this.inKeyMode=true;this.selectPrev()},down:function(a){if(!this.isExpanded()){this.onTriggerClick()}else{this.inKeyMode=true;this.selectNext()}},enter:function(a){this.onViewClick()},esc:function(a){this.collapse()},tab:function(a){if(this.forceSelection===true){this.collapse()}else{this.onViewClick(false)}return true},scope:this,doRelay:function(c,b,a){if(a=="down"||this.scope.isExpanded()){var d=Ext.KeyNav.prototype.doRelay.apply(this,arguments);if(!Ext.isIE&&Ext.EventManager.useKeydown){this.scope.fireKey(c)}return d}return true},forceKeyDown:true,defaultEventAction:"stopEvent"});this.queryDelay=Math.max(this.queryDelay||10,this.mode=="local"?10:250);this.dqTask=new Ext.util.DelayedTask(this.initQuery,this);if(this.typeAhead){this.taTask=new Ext.util.DelayedTask(this.onTypeAhead,this)}if(!this.enableKeyEvents){this.mon(this.el,"keyup",this.onKeyUp,this)}},onDestroy:function(){if(this.dqTask){this.dqTask.cancel();this.dqTask=null}this.bindStore(null);Ext.destroy(this.resizer,this.view,this.pageTb,this.list);Ext.destroyMembers(this,"hiddenField");Ext.form.ComboBox.superclass.onDestroy.call(this)},fireKey:function(a){if(!this.isExpanded()){Ext.form.ComboBox.superclass.fireKey.call(this,a)}},onResize:function(a,b){Ext.form.ComboBox.superclass.onResize.apply(this,arguments);if(!isNaN(a)&&this.isVisible()&&this.list){this.doResize(a)}else{this.bufferSize=a}},doResize:function(a){if(!Ext.isDefined(this.listWidth)){var b=Math.max(a,this.minListWidth);this.list.setWidth(b);this.innerList.setWidth(b-this.list.getFrameWidth("lr"))}},onEnable:function(){Ext.form.ComboBox.superclass.onEnable.apply(this,arguments);if(this.hiddenField){this.hiddenField.disabled=false}},onDisable:function(){Ext.form.ComboBox.superclass.onDisable.apply(this,arguments);if(this.hiddenField){this.hiddenField.disabled=true}},onBeforeLoad:function(){if(!this.hasFocus){return}this.innerList.update(this.loadingText?'
    '+this.loadingText+"
    ":"");this.restrictHeight();this.selectedIndex=-1},onLoad:function(){if(!this.hasFocus){return}if(this.store.getCount()>0||this.listEmptyText){this.expand();this.restrictHeight();if(this.lastQuery==this.allQuery){if(this.editable){this.el.dom.select()}if(this.autoSelect!==false&&!this.selectByValue(this.value,true)){this.select(0,true)}}else{if(this.autoSelect!==false){this.selectNext()}if(this.typeAhead&&this.lastKey!=Ext.EventObject.BACKSPACE&&this.lastKey!=Ext.EventObject.DELETE){this.taTask.delay(this.typeAheadDelay)}}}else{this.collapse()}},onTypeAhead:function(){if(this.store.getCount()>0){var b=this.store.getAt(0);var c=b.data[this.displayField];var a=c.length;var d=this.getRawValue().length;if(d!=a){this.setRawValue(c);this.selectText(d,c.length)}}},assertValue:function(){var b=this.getRawValue(),a;if(this.valueField&&Ext.isDefined(this.value)){a=this.findRecord(this.valueField,this.value)}if(!a||a.get(this.displayField)!=b){a=this.findRecord(this.displayField,b)}if(!a&&this.forceSelection){if(b.length>0&&b!=this.emptyText){this.el.dom.value=Ext.value(this.lastSelectionText,"");this.applyEmptyText()}else{this.clearValue()}}else{if(a&&this.valueField){if(this.value==b){return}b=a.get(this.valueField||this.displayField)}this.setValue(b)}},onSelect:function(a,b){if(this.fireEvent("beforeselect",this,a,b)!==false){this.setValue(a.data[this.valueField||this.displayField]);this.collapse();this.fireEvent("select",this,a,b)}},getName:function(){var a=this.hiddenField;return a&&a.name?a.name:this.hiddenName||Ext.form.ComboBox.superclass.getName.call(this)},getValue:function(){if(this.valueField){return Ext.isDefined(this.value)?this.value:""}else{return Ext.form.ComboBox.superclass.getValue.call(this)}},clearValue:function(){if(this.hiddenField){this.hiddenField.value=""}this.setRawValue("");this.lastSelectionText="";this.applyEmptyText();this.value=""},setValue:function(a){var c=a;if(this.valueField){var b=this.findRecord(this.valueField,a);if(b){c=b.data[this.displayField]}else{if(Ext.isDefined(this.valueNotFoundText)){c=this.valueNotFoundText}}}this.lastSelectionText=c;if(this.hiddenField){this.hiddenField.value=Ext.value(a,"")}Ext.form.ComboBox.superclass.setValue.call(this,c);this.value=a;return this},findRecord:function(c,b){var a;if(this.store.getCount()>0){this.store.each(function(d){if(d.data[c]==b){a=d;return false}})}return a},onViewMove:function(b,a){this.inKeyMode=false},onViewOver:function(d,b){if(this.inKeyMode){return}var c=this.view.findItemFromChild(b);if(c){var a=this.view.indexOf(c);this.select(a,false)}},onViewClick:function(b){var a=this.view.getSelectedIndexes()[0],c=this.store,d=c.getAt(a);if(d){this.onSelect(d,a)}else{this.collapse()}if(b!==false){this.el.focus()}},restrictHeight:function(){this.innerList.dom.style.height="";var b=this.innerList.dom,e=this.list.getFrameWidth("tb")+(this.resizable?this.handleHeight:0)+this.assetHeight,c=Math.max(b.clientHeight,b.offsetHeight,b.scrollHeight),a=this.getPosition()[1]-Ext.getBody().getScroll().top,g=Ext.lib.Dom.getViewHeight()-a-this.getSize().height,d=Math.max(a,g,this.minHeight||0)-this.list.shadowOffset-e-5;c=Math.min(c,d,this.maxHeight);this.innerList.setHeight(c);this.list.beginUpdate();this.list.setHeight(c+e);this.list.alignTo.apply(this.list,[this.el].concat(this.listAlign));this.list.endUpdate()},isExpanded:function(){return this.list&&this.list.isVisible()},selectByValue:function(a,c){if(!Ext.isEmpty(a,true)){var b=this.findRecord(this.valueField||this.displayField,a);if(b){this.select(this.store.indexOf(b),c);return true}}return false},select:function(a,c){this.selectedIndex=a;this.view.select(a);if(c!==false){var b=this.view.getNode(a);if(b){this.innerList.scrollChildIntoView(b,false)}}},selectNext:function(){var a=this.store.getCount();if(a>0){if(this.selectedIndex==-1){this.select(0)}else{if(this.selectedIndex0){if(this.selectedIndex==-1){this.select(0)}else{if(this.selectedIndex!==0){this.select(this.selectedIndex-1)}}}},onKeyUp:function(b){var a=b.getKey();if(this.editable!==false&&this.readOnly!==true&&(a==b.BACKSPACE||!b.isSpecialKey())){this.lastKey=a;this.dqTask.delay(this.queryDelay)}Ext.form.ComboBox.superclass.onKeyUp.call(this,b)},validateBlur:function(){return !this.list||!this.list.isVisible()},initQuery:function(){this.doQuery(this.getRawValue())},beforeBlur:function(){this.assertValue()},postBlur:function(){Ext.form.ComboBox.superclass.postBlur.call(this);this.collapse();this.inKeyMode=false},doQuery:function(c,b){c=Ext.isEmpty(c)?"":c;var a={query:c,forceAll:b,combo:this,cancel:false};if(this.fireEvent("beforequery",a)===false||a.cancel){return false}c=a.query;b=a.forceAll;if(b===true||(c.length>=this.minChars)){if(this.lastQuery!==c){this.lastQuery=c;if(this.mode=="local"){this.selectedIndex=-1;if(b){this.store.clearFilter()}else{this.store.filter(this.displayField,c)}this.onLoad()}else{this.store.baseParams[this.queryParam]=c;this.store.load({params:this.getParams(c)});this.expand()}}else{this.selectedIndex=-1;this.onLoad()}}},getParams:function(a){var b={},c=this.store.paramNames;if(this.pageSize){b[c.start]=0;b[c.limit]=this.pageSize}return b},collapse:function(){if(!this.isExpanded()){return}this.list.hide();Ext.getDoc().un("mousewheel",this.collapseIf,this);Ext.getDoc().un("mousedown",this.collapseIf,this);this.fireEvent("collapse",this)},collapseIf:function(a){if(!this.isDestroyed&&!a.within(this.wrap)&&!a.within(this.list)){this.collapse()}},expand:function(){if(this.isExpanded()||!this.hasFocus){return}if(this.title||this.pageSize){this.assetHeight=0;if(this.title){this.assetHeight+=this.header.getHeight()}if(this.pageSize){this.assetHeight+=this.footer.getHeight()}}if(this.bufferSize){this.doResize(this.bufferSize);delete this.bufferSize}this.list.alignTo.apply(this.list,[this.el].concat(this.listAlign));this.list.setZIndex(this.getZIndex());this.list.show();if(Ext.isGecko2){this.innerList.setOverflow("auto")}this.mon(Ext.getDoc(),{scope:this,mousewheel:this.collapseIf,mousedown:this.collapseIf});this.fireEvent("expand",this)},onTriggerClick:function(){if(this.readOnly||this.disabled){return}if(this.isExpanded()){this.collapse();this.el.focus()}else{this.onFocus({});if(this.triggerAction=="all"){this.doQuery(this.allQuery,true)}else{this.doQuery(this.getRawValue())}this.el.focus()}}});Ext.reg("combo",Ext.form.ComboBox);Ext.form.Checkbox=Ext.extend(Ext.form.Field,{focusClass:undefined,fieldClass:"x-form-field",checked:false,boxLabel:" ",defaultAutoCreate:{tag:"input",type:"checkbox",autocomplete:"off"},actionMode:"wrap",initComponent:function(){Ext.form.Checkbox.superclass.initComponent.call(this);this.addEvents("check")},onResize:function(){Ext.form.Checkbox.superclass.onResize.apply(this,arguments);if(!this.boxLabel&&!this.fieldLabel){this.el.alignTo(this.wrap,"c-c")}},initEvents:function(){Ext.form.Checkbox.superclass.initEvents.call(this);this.mon(this.el,{scope:this,click:this.onClick,change:this.onClick})},markInvalid:Ext.emptyFn,clearInvalid:Ext.emptyFn,onRender:function(b,a){Ext.form.Checkbox.superclass.onRender.call(this,b,a);if(this.inputValue!==undefined){this.el.dom.value=this.inputValue}this.wrap=this.el.wrap({cls:"x-form-check-wrap"});if(this.boxLabel){this.wrap.createChild({tag:"label",htmlFor:this.el.id,cls:"x-form-cb-label",html:this.boxLabel})}if(this.checked){this.setValue(true)}else{this.checked=this.el.dom.checked}if(Ext.isIE&&!Ext.isStrict){this.wrap.repaint()}this.resizeEl=this.positionEl=this.wrap},onDestroy:function(){Ext.destroy(this.wrap);Ext.form.Checkbox.superclass.onDestroy.call(this)},initValue:function(){this.originalValue=this.getValue()},getValue:function(){if(this.rendered){return this.el.dom.checked}return this.checked},onClick:function(){if(this.el.dom.checked!=this.checked){this.setValue(this.el.dom.checked)}},setValue:function(a){var c=this.checked,b=this.inputValue;if(a===false){this.checked=false}else{this.checked=(a===true||a==="true"||a=="1"||(b?a==b:String(a).toLowerCase()=="on"))}if(this.rendered){this.el.dom.checked=this.checked;this.el.dom.defaultChecked=this.checked}if(c!=this.checked){this.fireEvent("check",this,this.checked);if(this.handler){this.handler.call(this.scope||this,this,this.checked)}}return this}});Ext.reg("checkbox",Ext.form.Checkbox);Ext.form.CheckboxGroup=Ext.extend(Ext.form.Field,{columns:"auto",vertical:false,allowBlank:true,blankText:"You must select at least one item in this group",defaultType:"checkbox",groupCls:"x-form-check-group",initComponent:function(){this.addEvents("change");this.on("change",this.validate,this);Ext.form.CheckboxGroup.superclass.initComponent.call(this)},onRender:function(j,g){if(!this.el){var p={autoEl:{id:this.id},cls:this.groupCls,layout:"column",renderTo:j,bufferResize:false};var a={xtype:"container",defaultType:this.defaultType,layout:"form",defaults:{hideLabel:true,anchor:"100%"}};if(this.items[0].items){Ext.apply(p,{layoutConfig:{columns:this.items.length},defaults:this.defaults,items:this.items});for(var e=0,m=this.items.length;e0&&e%r==0){o++}if(this.items[e].fieldLabel){this.items[e].hideLabel=false}n[o].items.push(this.items[e])}}else{for(var e=0,m=this.items.length;e-1){b.setValue(true)}})},getBox:function(b){var a=null;this.eachItem(function(c){if(b==c||c.dataIndex==b||c.id==b||c.getName()==b){a=c;return false}});return a},getValue:function(){var a=[];this.eachItem(function(b){if(b.checked){a.push(b)}});return a},eachItem:function(b,a){if(this.items&&this.items.each){this.items.each(b,a||this)}},getRawValue:Ext.emptyFn,setRawValue:Ext.emptyFn});Ext.reg("checkboxgroup",Ext.form.CheckboxGroup);Ext.form.CompositeField=Ext.extend(Ext.form.Field,{defaultMargins:"0 5 0 0",skipLastItemMargin:true,isComposite:true,combineErrors:true,labelConnector:", ",initComponent:function(){var g=[],b=this.items,e;for(var d=0,c=b.length;d")},sortErrors:function(){var a=this.items;this.fieldErrors.sort("ASC",function(g,d){var c=function(b){return function(i){return i.getName()==b}};var h=a.findIndexBy(c(g.field)),e=a.findIndexBy(c(d.field));return h1){var a=this.getBox(c);if(a){a.setValue(b);if(a.checked){this.eachItem(function(d){if(d!==a){d.setValue(false)}})}}}else{this.setValueForItem(c)}},setValueForItem:function(a){a=String(a).split(",")[0];this.eachItem(function(b){b.setValue(a==b.inputValue)})},fireChecked:function(){if(!this.checkTask){this.checkTask=new Ext.util.DelayedTask(this.bufferChecked,this)}this.checkTask.delay(10)},bufferChecked:function(){var a=null;this.eachItem(function(b){if(b.checked){a=b;return false}});this.fireEvent("change",this,a)},onDestroy:function(){if(this.checkTask){this.checkTask.cancel();this.checkTask=null}Ext.form.RadioGroup.superclass.onDestroy.call(this)}});Ext.reg("radiogroup",Ext.form.RadioGroup);Ext.form.Hidden=Ext.extend(Ext.form.Field,{inputType:"hidden",shouldLayout:false,onRender:function(){Ext.form.Hidden.superclass.onRender.apply(this,arguments)},initEvents:function(){this.originalValue=this.getValue()},setSize:Ext.emptyFn,setWidth:Ext.emptyFn,setHeight:Ext.emptyFn,setPosition:Ext.emptyFn,setPagePosition:Ext.emptyFn,markInvalid:Ext.emptyFn,clearInvalid:Ext.emptyFn});Ext.reg("hidden",Ext.form.Hidden);Ext.form.BasicForm=Ext.extend(Ext.util.Observable,{constructor:function(b,a){Ext.apply(this,a);if(Ext.isString(this.paramOrder)){this.paramOrder=this.paramOrder.split(/[\s,|]/)}this.items=new Ext.util.MixedCollection(false,function(c){return c.getItemId()});this.addEvents("beforeaction","actionfailed","actioncomplete");if(b){this.initEl(b)}Ext.form.BasicForm.superclass.constructor.call(this)},timeout:30,paramOrder:undefined,paramsAsHash:false,waitTitle:"Please Wait...",activeAction:null,trackResetOnLoad:false,initEl:function(a){this.el=Ext.get(a);this.id=this.el.id||Ext.id();if(!this.standardSubmit){this.el.on("submit",this.onSubmit,this)}this.el.addClass("x-form")},getEl:function(){return this.el},onSubmit:function(a){a.stopEvent()},destroy:function(a){if(a!==true){this.items.each(function(b){Ext.destroy(b)});Ext.destroy(this.el)}this.items.clear();this.purgeListeners()},isValid:function(){var a=true;this.items.each(function(b){if(!b.validate()){a=false}});return a},isDirty:function(){var a=false;this.items.each(function(b){if(b.isDirty()){a=true;return false}});return a},doAction:function(b,a){if(Ext.isString(b)){b=new Ext.form.Action.ACTION_TYPES[b](this,a)}if(this.fireEvent("beforeaction",this,b)!==false){this.beforeAction(b);b.run.defer(100,b)}return this},submit:function(b){b=b||{};if(this.standardSubmit){var a=b.clientValidation===false||this.isValid();if(a){var c=this.el.dom;if(this.url&&Ext.isEmpty(c.action)){c.action=this.url}c.submit()}return a}var d=String.format("{0}submit",this.api?"direct":"");this.doAction(d,b);return this},load:function(a){var b=String.format("{0}load",this.api?"direct":"");this.doAction(b,a);return this},updateRecord:function(b){b.beginEdit();var a=b.fields,d,c;a.each(function(e){d=this.findField(e.name);if(d){c=d.getValue();if(Ext.type(c)!==false&&c.getGroupValue){c=c.getGroupValue()}else{if(d.eachItem){c=[];d.eachItem(function(g){c.push(g.getValue())})}}b.set(e.name,c)}},this);b.endEdit();return this},loadRecord:function(a){this.setValues(a.data);return this},beforeAction:function(a){this.items.each(function(c){if(c.isFormField&&c.syncValue){c.syncValue()}});var b=a.options;if(b.waitMsg){if(this.waitMsgTarget===true){this.el.mask(b.waitMsg,"x-mask-loading")}else{if(this.waitMsgTarget){this.waitMsgTarget=Ext.get(this.waitMsgTarget);this.waitMsgTarget.mask(b.waitMsg,"x-mask-loading")}else{Ext.MessageBox.wait(b.waitMsg,b.waitTitle||this.waitTitle)}}}},afterAction:function(a,c){this.activeAction=null;var b=a.options;if(b.waitMsg){if(this.waitMsgTarget===true){this.el.unmask()}else{if(this.waitMsgTarget){this.waitMsgTarget.unmask()}else{Ext.MessageBox.updateProgress(1);Ext.MessageBox.hide()}}}if(c){if(b.reset){this.reset()}Ext.callback(b.success,b.scope,[this,a]);this.fireEvent("actioncomplete",this,a)}else{Ext.callback(b.failure,b.scope,[this,a]);this.fireEvent("actionfailed",this,a)}},findField:function(c){var b=this.items.get(c);if(!Ext.isObject(b)){var a=function(d){if(d.isFormField){if(d.dataIndex==c||d.id==c||d.getName()==c){b=d;return false}else{if(d.isComposite){return d.items.each(a)}else{if(d instanceof Ext.form.CheckboxGroup&&d.rendered){return d.eachItem(a)}}}}};this.items.each(a)}return b||null},markInvalid:function(h){if(Ext.isArray(h)){for(var c=0,a=h.length;c':">"),c,"")}return d.join("")},createToolbar:function(e){var c=[];var a=Ext.QuickTips&&Ext.QuickTips.isEnabled();function d(j,h,i){return{itemId:j,cls:"x-btn-icon",iconCls:"x-edit-"+j,enableToggle:h!==false,scope:e,handler:i||e.relayBtnCmd,clickEvent:"mousedown",tooltip:a?e.buttonTips[j]||undefined:undefined,overflowText:e.buttonTips[j].title||undefined,tabIndex:-1}}if(this.enableFont&&!Ext.isSafari2){var g=new Ext.Toolbar.Item({autoEl:{tag:"select",cls:"x-font-select",html:this.createFontOptions()}});c.push(g,"-")}if(this.enableFormat){c.push(d("bold"),d("italic"),d("underline"))}if(this.enableFontSize){c.push("-",d("increasefontsize",false,this.adjustFont),d("decreasefontsize",false,this.adjustFont))}if(this.enableColors){c.push("-",{itemId:"forecolor",cls:"x-btn-icon",iconCls:"x-edit-forecolor",clickEvent:"mousedown",tooltip:a?e.buttonTips.forecolor||undefined:undefined,tabIndex:-1,menu:new Ext.menu.ColorMenu({allowReselect:true,focus:Ext.emptyFn,value:"000000",plain:true,listeners:{scope:this,select:function(i,h){this.execCmd("forecolor",Ext.isWebKit||Ext.isIE?"#"+h:h);this.deferFocus()}},clickEvent:"mousedown"})},{itemId:"backcolor",cls:"x-btn-icon",iconCls:"x-edit-backcolor",clickEvent:"mousedown",tooltip:a?e.buttonTips.backcolor||undefined:undefined,tabIndex:-1,menu:new Ext.menu.ColorMenu({focus:Ext.emptyFn,value:"FFFFFF",plain:true,allowReselect:true,listeners:{scope:this,select:function(i,h){if(Ext.isGecko){this.execCmd("useCSS",false);this.execCmd("hilitecolor",h);this.execCmd("useCSS",true);this.deferFocus()}else{this.execCmd(Ext.isOpera?"hilitecolor":"backcolor",Ext.isWebKit||Ext.isIE?"#"+h:h);this.deferFocus()}}},clickEvent:"mousedown"})})}if(this.enableAlignments){c.push("-",d("justifyleft"),d("justifycenter"),d("justifyright"))}if(!Ext.isSafari2){if(this.enableLinks){c.push("-",d("createlink",false,this.createLink))}if(this.enableLists){c.push("-",d("insertorderedlist"),d("insertunorderedlist"))}if(this.enableSourceEdit){c.push("-",d("sourceedit",true,function(h){this.toggleSourceEdit(!this.sourceEditMode)}))}}var b=new Ext.Toolbar({renderTo:this.wrap.dom.firstChild,items:c});if(g){this.fontSelect=g.el;this.mon(this.fontSelect,"change",function(){var h=this.fontSelect.dom.value;this.relayCmd("fontname",h);this.deferFocus()},this)}this.mon(b.el,"click",function(h){h.preventDefault()});this.tb=b;this.tb.doLayout()},onDisable:function(){this.wrap.mask();Ext.form.HtmlEditor.superclass.onDisable.call(this)},onEnable:function(){this.wrap.unmask();Ext.form.HtmlEditor.superclass.onEnable.call(this)},setReadOnly:function(b){Ext.form.HtmlEditor.superclass.setReadOnly.call(this,b);if(this.initialized){if(Ext.isIE){this.getEditorBody().contentEditable=!b}else{this.setDesignMode(!b)}var a=this.getEditorBody();if(a){a.style.cursor=this.readOnly?"default":"text"}this.disableItems(b)}},getDocMarkup:function(){var a=Ext.fly(this.iframe).getHeight()-this.iframePad*2;return String.format('',this.iframePad,a)},getEditorBody:function(){var a=this.getDoc();return a.body||a.documentElement},getDoc:function(){return Ext.isIE?this.getWin().document:(this.iframe.contentDocument||this.getWin().document)},getWin:function(){return Ext.isIE?this.iframe.contentWindow:window.frames[this.iframe.name]},onRender:function(b,a){Ext.form.HtmlEditor.superclass.onRender.call(this,b,a);this.el.dom.style.border="0 none";this.el.dom.setAttribute("tabIndex",-1);this.el.addClass("x-hidden");if(Ext.isIE){this.el.applyStyles("margin-top:-1px;margin-bottom:-1px;")}this.wrap=this.el.wrap({cls:"x-html-editor-wrap",cn:{cls:"x-html-editor-tb"}});this.createToolbar(this);this.disableItems(true);this.tb.doLayout();this.createIFrame();if(!this.width){var c=this.el.getSize();this.setSize(c.width,this.height||c.height)}this.resizeEl=this.positionEl=this.wrap},createIFrame:function(){var a=document.createElement("iframe");a.name=Ext.id();a.frameBorder="0";a.style.overflow="auto";a.src=Ext.SSL_SECURE_URL;this.wrap.dom.appendChild(a);this.iframe=a;this.monitorTask=Ext.TaskMgr.start({run:this.checkDesignMode,scope:this,interval:100})},initFrame:function(){Ext.TaskMgr.stop(this.monitorTask);var b=this.getDoc();this.win=this.getWin();b.open();b.write(this.getDocMarkup());b.close();var a={run:function(){var c=this.getDoc();if(c.body||c.readyState=="complete"){Ext.TaskMgr.stop(a);this.setDesignMode(true);this.initEditor.defer(10,this)}},interval:10,duration:10000,scope:this};Ext.TaskMgr.start(a)},checkDesignMode:function(){if(this.wrap&&this.wrap.dom.offsetWidth){var a=this.getDoc();if(!a){return}if(!a.editorInitialized||this.getDesignMode()!="on"){this.initFrame()}}},setDesignMode:function(b){var a=this.getDoc();if(a){if(this.readOnly){b=false}a.designMode=(/on|true/i).test(String(b).toLowerCase())?"on":"off"}},getDesignMode:function(){var a=this.getDoc();if(!a){return""}return String(a.designMode).toLowerCase()},disableItems:function(a){if(this.fontSelect){this.fontSelect.dom.disabled=a}this.tb.items.each(function(b){if(b.getItemId()!="sourceedit"){b.setDisabled(a)}})},onResize:function(b,c){Ext.form.HtmlEditor.superclass.onResize.apply(this,arguments);if(this.el&&this.iframe){if(Ext.isNumber(b)){var e=b-this.wrap.getFrameWidth("lr");this.el.setWidth(e);this.tb.setWidth(e);this.iframe.style.width=Math.max(e,0)+"px"}if(Ext.isNumber(c)){var a=c-this.wrap.getFrameWidth("tb")-this.tb.el.getHeight();this.el.setHeight(a);this.iframe.style.height=Math.max(a,0)+"px";var d=this.getEditorBody();if(d){d.style.height=Math.max((a-(this.iframePad*2)),0)+"px"}}}},toggleSourceEdit:function(b){var d,a;if(b===undefined){b=!this.sourceEditMode}this.sourceEditMode=b===true;var c=this.tb.getComponent("sourceedit");if(c.pressed!==this.sourceEditMode){c.toggle(this.sourceEditMode);if(!c.xtbHidden){return}}if(this.sourceEditMode){this.previousSize=this.getSize();d=Ext.get(this.iframe).getHeight();this.disableItems(true);this.syncValue();this.iframe.className="x-hidden";this.el.removeClass("x-hidden");this.el.dom.removeAttribute("tabIndex");this.el.focus();this.el.dom.style.height=d+"px"}else{a=parseInt(this.el.dom.style.height,10);if(this.initialized){this.disableItems(this.readOnly)}this.pushValue();this.iframe.className="";this.el.addClass("x-hidden");this.el.dom.setAttribute("tabIndex",-1);this.deferFocus();this.setSize(this.previousSize);delete this.previousSize;this.iframe.style.height=a+"px"}this.fireEvent("editmodechange",this,this.sourceEditMode)},createLink:function(){var a=prompt(this.createLinkText,this.defaultLinkValue);if(a&&a!="http://"){this.relayCmd("createlink",a)}},initEvents:function(){this.originalValue=this.getValue()},markInvalid:Ext.emptyFn,clearInvalid:Ext.emptyFn,setValue:function(a){Ext.form.HtmlEditor.superclass.setValue.call(this,a);this.pushValue();return this},cleanHtml:function(a){a=String(a);if(Ext.isWebKit){a=a.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi,"")}if(a.charCodeAt(0)==this.defaultValue.replace(/\D/g,"")){a=a.substring(1)}return a},syncValue:function(){if(this.initialized){var d=this.getEditorBody();var c=d.innerHTML;if(Ext.isWebKit){var b=d.getAttribute("style");var a=b.match(/text-align:(.*?);/i);if(a&&a[1]){c='
    '+c+"
    "}}c=this.cleanHtml(c);if(this.fireEvent("beforesync",this,c)!==false){this.el.dom.value=c;this.fireEvent("sync",this,c)}}},getValue:function(){this[this.sourceEditMode?"pushValue":"syncValue"]();return Ext.form.HtmlEditor.superclass.getValue.call(this)},pushValue:function(){if(this.initialized){var a=this.el.dom.value;if(!this.activated&&a.length<1){a=this.defaultValue}if(this.fireEvent("beforepush",this,a)!==false){this.getEditorBody().innerHTML=a;if(Ext.isGecko){this.setDesignMode(false);this.setDesignMode(true)}this.fireEvent("push",this,a)}}},deferFocus:function(){this.focus.defer(10,this)},focus:function(){if(this.win&&!this.sourceEditMode){this.win.focus()}else{this.el.focus()}},initEditor:function(){try{var c=this.getEditorBody(),a=this.el.getStyles("font-size","font-family","background-image","background-repeat","background-color","color"),g,b;a["background-attachment"]="fixed";c.bgProperties="fixed";Ext.DomHelper.applyStyles(c,a);g=this.getDoc();if(g){try{Ext.EventManager.removeAll(g)}catch(d){}}b=this.onEditorEvent.createDelegate(this);Ext.EventManager.on(g,{mousedown:b,dblclick:b,click:b,keyup:b,buffer:100});if(Ext.isGecko){Ext.EventManager.on(g,"keypress",this.applyCommand,this)}if(Ext.isIE||Ext.isWebKit||Ext.isOpera){Ext.EventManager.on(g,"keydown",this.fixKeys,this)}g.editorInitialized=true;this.initialized=true;this.pushValue();this.setReadOnly(this.readOnly);this.fireEvent("initialize",this)}catch(d){}},beforeDestroy:function(){if(this.monitorTask){Ext.TaskMgr.stop(this.monitorTask)}if(this.rendered){Ext.destroy(this.tb);var b=this.getDoc();if(b){try{Ext.EventManager.removeAll(b);for(var c in b){delete b[c]}}catch(a){}}if(this.wrap){this.wrap.dom.innerHTML="";this.wrap.remove()}}Ext.form.HtmlEditor.superclass.beforeDestroy.call(this)},onFirstFocus:function(){this.activated=true;this.disableItems(this.readOnly);if(Ext.isGecko){this.win.focus();var a=this.win.getSelection();if(!a.focusNode||a.focusNode.nodeType!=3){var b=a.getRangeAt(0);b.selectNodeContents(this.getEditorBody());b.collapse(true);this.deferFocus()}try{this.execCmd("useCSS",true);this.execCmd("styleWithCSS",false)}catch(c){}}this.fireEvent("activate",this)},adjustFont:function(b){var d=b.getItemId()=="increasefontsize"?1:-1,c=this.getDoc(),a=parseInt(c.queryCommandValue("FontSize")||2,10);if((Ext.isSafari&&!Ext.isSafari2)||Ext.isChrome||Ext.isAir){if(a<=10){a=1+d}else{if(a<=13){a=2+d}else{if(a<=16){a=3+d}else{if(a<=18){a=4+d}else{if(a<=24){a=5+d}else{a=6+d}}}}}a=a.constrain(1,6)}else{if(Ext.isSafari){d*=2}a=Math.max(1,a+d)+(Ext.isSafari?"px":0)}this.execCmd("FontSize",a)},onEditorEvent:function(a){this.updateToolbar()},updateToolbar:function(){if(this.readOnly){return}if(!this.activated){this.onFirstFocus();return}var b=this.tb.items.map,c=this.getDoc();if(this.enableFont&&!Ext.isSafari2){var a=(c.queryCommandValue("FontName")||this.defaultFont).toLowerCase();if(a!=this.fontSelect.dom.value){this.fontSelect.dom.value=a}}if(this.enableFormat){b.bold.toggle(c.queryCommandState("bold"));b.italic.toggle(c.queryCommandState("italic"));b.underline.toggle(c.queryCommandState("underline"))}if(this.enableAlignments){b.justifyleft.toggle(c.queryCommandState("justifyleft"));b.justifycenter.toggle(c.queryCommandState("justifycenter"));b.justifyright.toggle(c.queryCommandState("justifyright"))}if(!Ext.isSafari2&&this.enableLists){b.insertorderedlist.toggle(c.queryCommandState("insertorderedlist"));b.insertunorderedlist.toggle(c.queryCommandState("insertunorderedlist"))}Ext.menu.MenuMgr.hideAll();this.syncValue()},relayBtnCmd:function(a){this.relayCmd(a.getItemId())},relayCmd:function(b,a){(function(){this.focus();this.execCmd(b,a);this.updateToolbar()}).defer(10,this)},execCmd:function(b,a){var c=this.getDoc();c.execCommand(b,false,a===undefined?null:a);this.syncValue()},applyCommand:function(b){if(b.ctrlKey){var d=b.getCharCode(),a;if(d>0){d=String.fromCharCode(d);switch(d){case"b":a="bold";break;case"i":a="italic";break;case"u":a="underline";break}if(a){this.win.focus();this.execCmd(a);this.deferFocus();b.preventDefault()}}}},insertAtCursor:function(c){if(!this.activated){return}if(Ext.isIE){this.win.focus();var b=this.getDoc(),a=b.selection.createRange();if(a){a.pasteHTML(c);this.syncValue();this.deferFocus()}}else{this.win.focus();this.execCmd("InsertHTML",c);this.deferFocus()}},fixKeys:function(){if(Ext.isIE){return function(g){var a=g.getKey(),d=this.getDoc(),b;if(a==g.TAB){g.stopEvent();b=d.selection.createRange();if(b){b.collapse(true);b.pasteHTML("    ");this.deferFocus()}}else{if(a==g.ENTER){b=d.selection.createRange();if(b){var c=b.parentElement();if(!c||c.tagName.toLowerCase()!="li"){g.stopEvent();b.pasteHTML("
    ");b.collapse(false);b.select()}}}}}}else{if(Ext.isOpera){return function(b){var a=b.getKey();if(a==b.TAB){b.stopEvent();this.win.focus();this.execCmd("InsertHTML","    ");this.deferFocus()}}}else{if(Ext.isWebKit){return function(b){var a=b.getKey();if(a==b.TAB){b.stopEvent();this.execCmd("InsertText","\t");this.deferFocus()}else{if(a==b.ENTER){b.stopEvent();this.execCmd("InsertHtml","

    ");this.deferFocus()}}}}}}}(),getToolbar:function(){return this.tb},buttonTips:{bold:{title:"Bold (Ctrl+B)",text:"Make the selected text bold.",cls:"x-html-editor-tip"},italic:{title:"Italic (Ctrl+I)",text:"Make the selected text italic.",cls:"x-html-editor-tip"},underline:{title:"Underline (Ctrl+U)",text:"Underline the selected text.",cls:"x-html-editor-tip"},increasefontsize:{title:"Grow Text",text:"Increase the font size.",cls:"x-html-editor-tip"},decreasefontsize:{title:"Shrink Text",text:"Decrease the font size.",cls:"x-html-editor-tip"},backcolor:{title:"Text Highlight Color",text:"Change the background color of the selected text.",cls:"x-html-editor-tip"},forecolor:{title:"Font Color",text:"Change the color of the selected text.",cls:"x-html-editor-tip"},justifyleft:{title:"Align Text Left",text:"Align text to the left.",cls:"x-html-editor-tip"},justifycenter:{title:"Center Text",text:"Center text in the editor.",cls:"x-html-editor-tip"},justifyright:{title:"Align Text Right",text:"Align text to the right.",cls:"x-html-editor-tip"},insertunorderedlist:{title:"Bullet List",text:"Start a bulleted list.",cls:"x-html-editor-tip"},insertorderedlist:{title:"Numbered List",text:"Start a numbered list.",cls:"x-html-editor-tip"},createlink:{title:"Hyperlink",text:"Make the selected text a hyperlink.",cls:"x-html-editor-tip"},sourceedit:{title:"Source Edit",text:"Switch to source editing mode.",cls:"x-html-editor-tip"}}});Ext.reg("htmleditor",Ext.form.HtmlEditor);Ext.form.TimeField=Ext.extend(Ext.form.ComboBox,{minValue:undefined,maxValue:undefined,minText:"The time in this field must be equal to or after {0}",maxText:"The time in this field must be equal to or before {0}",invalidText:"{0} is not a valid time",format:"g:i A",altFormats:"g:ia|g:iA|g:i a|g:i A|h:i|g:i|H:i|ga|ha|gA|h a|g a|g A|gi|hi|gia|hia|g|H|gi a|hi a|giA|hiA|gi A|hi A",increment:15,mode:"local",triggerAction:"all",typeAhead:false,initDate:"1/1/2008",initDateFormat:"j/n/Y",initComponent:function(){if(Ext.isDefined(this.minValue)){this.setMinValue(this.minValue,true)}if(Ext.isDefined(this.maxValue)){this.setMaxValue(this.maxValue,true)}if(!this.store){this.generateStore(true)}Ext.form.TimeField.superclass.initComponent.call(this)},setMinValue:function(b,a){this.setLimit(b,true,a);return this},setMaxValue:function(b,a){this.setLimit(b,false,a);return this},generateStore:function(b){var c=this.minValue||new Date(this.initDate).clearTime(),a=this.maxValue||new Date(this.initDate).clearTime().add("mi",(24*60)-1),d=[];while(c<=a){d.push(c.dateFormat(this.format));c=c.add("mi",this.increment)}this.bindStore(d,b)},setLimit:function(b,g,a){var e;if(Ext.isString(b)){e=this.parseDate(b)}else{if(Ext.isDate(b)){e=b}}if(e){var c=new Date(this.initDate).clearTime();c.setHours(e.getHours(),e.getMinutes(),e.getSeconds(),e.getMilliseconds());this[g?"minValue":"maxValue"]=c;if(!a){this.generateStore()}}},getValue:function(){var a=Ext.form.TimeField.superclass.getValue.call(this);return this.formatDate(this.parseDate(a))||""},setValue:function(a){return Ext.form.TimeField.superclass.setValue.call(this,this.formatDate(this.parseDate(a)))},validateValue:Ext.form.DateField.prototype.validateValue,formatDate:Ext.form.DateField.prototype.formatDate,parseDate:function(h){if(!h||Ext.isDate(h)){return h}var j=this.initDate+" ",g=this.initDateFormat+" ",b=Date.parseDate(j+h,g+this.format),c=this.altFormats;if(!b&&c){if(!this.altFormatsArray){this.altFormatsArray=c.split("|")}for(var e=0,d=this.altFormatsArray,a=d.length;e=0){if(!d){c=g-1}d=false;while(c>=0){if(e.call(j||this,k,c,i)===true){return[k,c]}c--}k--}}else{if(c>=g){k++;d=false}while(k','
    ','
    ','
    ','
    {header}
    ',"
    ",'
    ',"
    ",'
    ','
    {body}
    ','',"
    ","
    ",'
     
    ','
     
    ',""),headerTpl:new Ext.Template('',"",'{cells}',"","
    "),bodyTpl:new Ext.Template("{rows}"),cellTpl:new Ext.Template('','
    {value}
    ',""),initTemplates:function(){var c=this.templates||{},d,b,g=new Ext.Template('','
    ',this.grid.enableHdMenu?'':"","{value}",'',"
    ",""),a=['','','
    {body}
    ',"",""].join(""),e=['',"","{cells}",this.enableRowBody?a:"","","
    "].join("");Ext.applyIf(c,{hcell:g,cell:this.cellTpl,body:this.bodyTpl,header:this.headerTpl,master:this.masterTpl,row:new Ext.Template('
    '+e+"
    "),rowInner:new Ext.Template(e)});for(b in c){d=c[b];if(d&&Ext.isFunction(d.compile)&&!d.compiled){d.disableFormats=true;d.compile()}}this.templates=c;this.colRe=new RegExp("x-grid3-td-([^\\s]+)","")},fly:function(a){if(!this._flyweight){this._flyweight=new Ext.Element.Flyweight(document.body)}this._flyweight.dom=a;return this._flyweight},getEditorParent:function(){return this.scroller.dom},initElements:function(){var b=Ext.Element,d=Ext.get(this.grid.getGridEl().dom.firstChild),e=new b(d.child("div.x-grid3-viewport")),c=new b(e.child("div.x-grid3-header")),a=new b(e.child("div.x-grid3-scroller"));if(this.grid.hideHeaders){c.setDisplayed(false)}if(this.forceFit){a.setStyle("overflow-x","hidden")}Ext.apply(this,{el:d,mainWrap:e,scroller:a,mainHd:c,innerHd:c.child("div.x-grid3-header-inner").dom,mainBody:new b(b.fly(a).child("div.x-grid3-body")),focusEl:new b(b.fly(a).child("a")),resizeMarker:new b(d.child("div.x-grid3-resize-marker")),resizeProxy:new b(d.child("div.x-grid3-resize-proxy"))});this.focusEl.swallowEvent("click",true)},getRows:function(){return this.hasRows()?this.mainBody.dom.childNodes:[]},findCell:function(a){if(!a){return false}return this.fly(a).findParent(this.cellSelector,this.cellSelectorDepth)},findCellIndex:function(d,c){var b=this.findCell(d),a;if(b){a=this.fly(b).hasClass(c);if(!c||a){return this.getCellIndex(b)}}return false},getCellIndex:function(b){if(b){var a=b.className.match(this.colRe);if(a&&a[1]){return this.cm.getIndexById(a[1])}}return false},findHeaderCell:function(b){var a=this.findCell(b);return a&&this.fly(a).hasClass(this.hdCls)?a:null},findHeaderIndex:function(a){return this.findCellIndex(a,this.hdCls)},findRow:function(a){if(!a){return false}return this.fly(a).findParent(this.rowSelector,this.rowSelectorDepth)},findRowIndex:function(a){var b=this.findRow(a);return b?b.rowIndex:false},findRowBody:function(a){if(!a){return false}return this.fly(a).findParent(this.rowBodySelector,this.rowBodySelectorDepth)},getRow:function(a){return this.getRows()[a]},getCell:function(b,a){return Ext.fly(this.getRow(b)).query(this.cellSelector)[a]},getHeaderCell:function(a){return this.mainHd.dom.getElementsByTagName("td")[a]},addRowClass:function(b,a){var c=this.getRow(b);if(c){this.fly(c).addClass(a)}},removeRowClass:function(c,a){var b=this.getRow(c);if(b){this.fly(b).removeClass(a)}},removeRow:function(a){Ext.removeNode(this.getRow(a));this.syncFocusEl(a)},removeRows:function(c,a){var b=this.mainBody.dom,d;for(d=c;d<=a;d++){Ext.removeNode(b.childNodes[c])}this.syncFocusEl(c)},getScrollState:function(){var a=this.scroller.dom;return{left:a.scrollLeft,top:a.scrollTop}},restoreScroll:function(a){var b=this.scroller.dom;b.scrollLeft=a.left;b.scrollTop=a.top},scrollToTop:function(){var a=this.scroller.dom;a.scrollTop=0;a.scrollLeft=0},syncScroll:function(){this.syncHeaderScroll();var a=this.scroller.dom;this.grid.fireEvent("bodyscroll",a.scrollLeft,a.scrollTop)},syncHeaderScroll:function(){var a=this.innerHd,b=this.scroller.dom.scrollLeft;a.scrollLeft=b;a.scrollLeft=b},updateSortIcon:function(d,c){var a=this.sortClasses,b=a[c=="DESC"?1:0],e=this.mainHd.select("td").removeClass(a);e.item(d).addClass(b)},updateAllColumnWidths:function(){var e=this.getTotalWidth(),k=this.cm.getColumnCount(),m=this.getRows(),g=m.length,b=[],l,a,h,d,c;for(d=0;d=this.ds.getCount()){return null}d=(d!==undefined?d:0);var c=this.getRow(h),b=this.cm,e=b.getColumnCount(),a;if(!(g===false&&d===0)){while(dm){n.scrollTop=q-a}}if(e!==false){var l=parseInt(h.offsetLeft,10),j=l+h.offsetWidth,i=parseInt(n.scrollLeft,10),b=i+n.clientWidth;if(lb){n.scrollLeft=j-n.clientWidth}}}return this.getResolvedXY(r)},insertRows:function(a,i,e,h){var d=a.getCount()-1;if(!h&&i===0&&e>=d){this.fireEvent("beforerowsinserted",this,i,e);this.refresh();this.fireEvent("rowsinserted",this,i,e)}else{if(!h){this.fireEvent("beforerowsinserted",this,i,e)}var b=this.renderRows(i,e),g=this.getRow(i);if(g){if(i===0){Ext.fly(this.getRow(0)).removeClass(this.firstRowCls)}Ext.DomHelper.insertHtml("beforeBegin",g,b)}else{var c=this.getRow(d-1);if(c){Ext.fly(c).removeClass(this.lastRowCls)}Ext.DomHelper.insertHtml("beforeEnd",this.mainBody.dom,b)}if(!h){this.processRows(i);this.fireEvent("rowsinserted",this,i,e)}else{if(i===0||i>=d){Ext.fly(this.getRow(i)).addClass(i===0?this.firstRowCls:this.lastRowCls)}}}this.syncFocusEl(i)},deleteRows:function(a,c,b){if(a.getRowCount()<1){this.refresh()}else{this.fireEvent("beforerowsdeleted",this,c,b);this.removeRows(c,b);this.processRows(c);this.fireEvent("rowsdeleted",this,c,b)}},getColumnStyle:function(b,d){var a=this.cm,g=a.config,c=d?"":g[b].css||"",e=g[b].align;c+=String.format("width: {0};",this.getColumnWidth(b));if(a.isHidden(b)){c+="display: none; "}if(e){c+=String.format("text-align: {0};",e)}return c},getColumnWidth:function(b){var c=this.cm.getColumnWidth(b),a=this.borderWidth;if(Ext.isNumber(c)){if(Ext.isBorderBox||(Ext.isWebKit&&!Ext.isSafari2)){return c+"px"}else{return Math.max(c-a,0)+"px"}}else{return c}},getTotalWidth:function(){return this.cm.getTotalWidth()+"px"},fitColumns:function(g,j,h){var a=this.grid,l=this.cm,s=l.getTotalWidth(false),q=this.getGridInnerWidth(),r=q-s,c=[],o=0,n=0,u,d,p;if(q<20||r===0){return false}var e=l.getColumnCount(true),m=l.getColumnCount(false),b=e-(Ext.isNumber(h)?1:0);if(b===0){b=1;h=undefined}for(p=0;pq){var t=(b==e)?o:h,k=Math.max(1,l.getColumnWidth(t)-(s-q));l.setColumnWidth(t,k,true)}if(g!==true){this.updateAllColumnWidths()}return true},autoExpand:function(k){var a=this.grid,i=this.cm,e=this.getGridInnerWidth(),c=i.getTotalWidth(false),g=a.autoExpandColumn;if(!this.userResized&&g){if(e!=c){var j=i.getIndexById(g),b=i.getColumnWidth(j),h=e-c+b,d=Math.min(Math.max(h,a.autoExpandMin),a.autoExpandMax);if(b!=d){i.setColumnWidth(j,d,true);if(k!==true){this.updateColumnWidth(j,d)}}}}},getGridInnerWidth:function(){return this.grid.getGridEl().getWidth(true)-this.getScrollOffset()},getColumnData:function(){var e=[],c=this.cm,g=c.getColumnCount(),a=this.ds.fields,d,b;for(d=0;d'+this.emptyText+"")}},updateHeaderSortState:function(){var b=this.ds.getSortState();if(!b){return}if(!this.sortState||(this.sortState.field!=b.field||this.sortState.direction!=b.direction)){this.grid.fireEvent("sortchange",this.grid,b)}this.sortState=b;var c=this.cm.findColumnIndex(b.field);if(c!=-1){var a=b.direction;this.updateSortIcon(c,a)}},clearHeaderSortState:function(){if(!this.sortState){return}this.grid.fireEvent("sortchange",this.grid,null);this.mainHd.select("td").removeClass(this.sortClasses);delete this.sortState},destroy:function(){var j=this,a=j.grid,d=a.getGridEl(),i=j.dragZone,g=j.splitZone,h=j.columnDrag,e=j.columnDrop,k=j.scrollToTopTask,c,b;if(k&&k.cancel){k.cancel()}Ext.destroyMembers(j,"colMenu","hmenu");j.initData(null,null);j.purgeListeners();Ext.fly(j.innerHd).un("click",j.handleHdDown,j);if(a.enableColumnMove){c=h.dragData;b=h.proxy;Ext.destroy(h.el,b.ghost,b.el,e.el,e.proxyTop,e.proxyBottom,c.ddel,c.header);if(b.anim){Ext.destroy(b.anim)}delete b.ghost;delete c.ddel;delete c.header;h.destroy();delete Ext.dd.DDM.locationCache[h.id];delete h._domRef;delete e.proxyTop;delete e.proxyBottom;e.destroy();delete Ext.dd.DDM.locationCache["gridHeader"+d.id];delete e._domRef;delete Ext.dd.DDM.ids[e.ddGroup]}if(g){g.destroy();delete g._domRef;delete Ext.dd.DDM.ids["gridSplitters"+d.id]}Ext.fly(j.innerHd).removeAllListeners();Ext.removeNode(j.innerHd);delete j.innerHd;Ext.destroy(j.el,j.mainWrap,j.mainHd,j.scroller,j.mainBody,j.focusEl,j.resizeMarker,j.resizeProxy,j.activeHdBtn,j._flyweight,i,g);delete a.container;if(i){i.destroy()}Ext.dd.DDM.currentTarget=null;delete Ext.dd.DDM.locationCache[d.id];Ext.EventManager.removeResizeListener(j.onWindowResize,j)},onDenyColumnHide:function(){},render:function(){if(this.autoFill){var a=this.grid.ownerCt;if(a&&a.getLayout()){a.on("afterlayout",function(){this.fitColumns(true,true);this.updateHeaders();this.updateHeaderSortState()},this,{single:true})}}else{if(this.forceFit){this.fitColumns(true,false)}else{if(this.grid.autoExpandColumn){this.autoExpand(true)}}}this.grid.getGridEl().dom.innerHTML=this.renderUI();this.afterRenderUI()},initData:function(a,e){var b=this;if(b.ds){var d=b.ds;d.un("add",b.onAdd,b);d.un("load",b.onLoad,b);d.un("clear",b.onClear,b);d.un("remove",b.onRemove,b);d.un("update",b.onUpdate,b);d.un("datachanged",b.onDataChange,b);if(d!==a&&d.autoDestroy){d.destroy()}}if(a){a.on({scope:b,load:b.onLoad,add:b.onAdd,remove:b.onRemove,update:b.onUpdate,clear:b.onClear,datachanged:b.onDataChange})}if(b.cm){var c=b.cm;c.un("configchange",b.onColConfigChange,b);c.un("widthchange",b.onColWidthChange,b);c.un("headerchange",b.onHeaderChange,b);c.un("hiddenchange",b.onHiddenChange,b);c.un("columnmoved",b.onColumnMove,b)}if(e){delete b.lastViewWidth;e.on({scope:b,configchange:b.onColConfigChange,widthchange:b.onColWidthChange,headerchange:b.onHeaderChange,hiddenchange:b.onHiddenChange,columnmoved:b.onColumnMove})}b.ds=a;b.cm=e},onDataChange:function(){this.refresh(true);this.updateHeaderSortState();this.syncFocusEl(0)},onClear:function(){this.refresh();this.syncFocusEl(0)},onUpdate:function(b,a){this.refreshRow(a)},onAdd:function(b,a,c){this.insertRows(b,c,c+(a.length-1))},onRemove:function(b,a,c,d){if(d!==true){this.fireEvent("beforerowremoved",this,c,a)}this.removeRow(c);if(d!==true){this.processRows(c);this.applyEmptyText();this.fireEvent("rowremoved",this,c,a)}},onLoad:function(){if(Ext.isGecko){if(!this.scrollToTopTask){this.scrollToTopTask=new Ext.util.DelayedTask(this.scrollToTop,this)}this.scrollToTopTask.delay(1)}else{this.scrollToTop()}},onColWidthChange:function(a,b,c){this.updateColumnWidth(b,c)},onHeaderChange:function(a,b,c){this.updateHeaders()},onHiddenChange:function(a,b,c){this.updateColumnHidden(b,c)},onColumnMove:function(a,c,b){this.indexMap=null;this.refresh(true);this.restoreScroll(this.getScrollState());this.afterMove(b);this.grid.fireEvent("columnmove",c,b)},onColConfigChange:function(){delete this.lastViewWidth;this.indexMap=null;this.refresh(true)},initUI:function(a){a.on("headerclick",this.onHeaderClick,this)},initEvents:Ext.emptyFn,onHeaderClick:function(b,a){if(this.headersDisabled||!this.cm.isSortable(a)){return}b.stopEditing(true);b.store.sort(this.cm.getDataIndex(a))},onRowOver:function(b,a){var c=this.findRowIndex(a);if(c!==false){this.addRowClass(c,this.rowOverCls)}},onRowOut:function(b,a){var c=this.findRowIndex(a);if(c!==false&&!b.within(this.getRow(c),true)){this.removeRowClass(c,this.rowOverCls)}},onRowSelect:function(a){this.addRowClass(a,this.selectedRowClass)},onRowDeselect:function(a){this.removeRowClass(a,this.selectedRowClass)},onCellSelect:function(c,b){var a=this.getCell(c,b);if(a){this.fly(a).addClass("x-grid3-cell-selected")}},onCellDeselect:function(c,b){var a=this.getCell(c,b);if(a){this.fly(a).removeClass("x-grid3-cell-selected")}},handleWheel:function(a){a.stopPropagation()},onColumnSplitterMoved:function(a,b){this.userResized=true;this.grid.colModel.setColumnWidth(a,b,true);if(this.forceFit){this.fitColumns(true,false,a);this.updateAllColumnWidths()}else{this.updateColumnWidth(a,b);this.syncHeaderScroll()}this.grid.fireEvent("columnresize",a,b)},beforeColMenuShow:function(){var b=this.cm,d=b.getColumnCount(),a=this.colMenu,c;a.removeAll();for(c=0;c0){if(!this.cm.isHidden(a-1)){return a}a--}return undefined},handleHdOver:function(c,b){var d=this.findHeaderCell(b);if(d&&!this.headersDisabled){var a=this.fly(d);this.activeHdRef=b;this.activeHdIndex=this.getCellIndex(d);this.activeHdRegion=a.getRegion();if(!this.isMenuDisabled(this.activeHdIndex,a)){a.addClass("x-grid3-hd-over");this.activeHdBtn=a.child(".x-grid3-hd-btn");if(this.activeHdBtn){this.activeHdBtn.dom.style.height=(d.firstChild.offsetHeight-1)+"px"}}}},handleHdOut:function(b,a){var c=this.findHeaderCell(a);if(c&&(!Ext.isIE||!b.within(c,true))){this.activeHdRef=null;this.fly(c).removeClass("x-grid3-hd-over");c.style.cursor=""}},isMenuDisabled:function(a,b){return this.cm.isMenuDisabled(a)},hasRows:function(){var a=this.mainBody.dom.firstChild;return a&&a.nodeType==1&&a.className!="x-grid-empty"},isHideableColumn:function(a){return !a.hidden},bind:function(a,b){this.initData(a,b)}});Ext.grid.GridView.SplitDragZone=Ext.extend(Ext.dd.DDProxy,{constructor:function(a,b){this.grid=a;this.view=a.getView();this.marker=this.view.resizeMarker;this.proxy=this.view.resizeProxy;Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this,b,"gridSplitters"+this.grid.getGridEl().id,{dragElId:Ext.id(this.proxy.dom),resizeFrame:false});this.scroll=false;this.hw=this.view.splitHandleWidth||5},b4StartDrag:function(a,e){this.dragHeadersDisabled=this.view.headersDisabled;this.view.headersDisabled=true;var d=this.view.mainWrap.getHeight();this.marker.setHeight(d);this.marker.show();this.marker.alignTo(this.view.getHeaderCell(this.cellIndex),"tl-tl",[-2,0]);this.proxy.setHeight(d);var b=this.cm.getColumnWidth(this.cellIndex),c=Math.max(b-this.grid.minColumnWidth,0);this.resetConstraints();this.setXConstraint(c,1000);this.setYConstraint(0,0);this.minX=a-c;this.maxX=a+1000;this.startPos=a;Ext.dd.DDProxy.prototype.b4StartDrag.call(this,a,e)},allowHeaderDrag:function(a){return true},handleMouseDown:function(a){var h=this.view.findHeaderCell(a.getTarget());if(h&&this.allowHeaderDrag(a)){var k=this.view.fly(h).getXY(),c=k[0],i=a.getXY(),b=i[0],g=h.offsetWidth,d=false;if((b-c)<=this.hw){d=-1}else{if((c+g)-b<=this.hw){d=0}}if(d!==false){this.cm=this.grid.colModel;var j=this.view.getCellIndex(h);if(d==-1){if(j+d<0){return}while(this.cm.isHidden(j+d)){--d;if(j+d<0){return}}}this.cellIndex=j+d;this.split=h.dom;if(this.cm.isResizable(this.cellIndex)&&!this.cm.isFixed(this.cellIndex)){Ext.grid.GridView.SplitDragZone.superclass.handleMouseDown.apply(this,arguments)}}else{if(this.view.columnDrag){this.view.columnDrag.callHandleMouseDown(a)}}}},endDrag:function(g){this.marker.hide();var a=this.view,c=Math.max(this.minX,g.getPageX()),d=c-this.startPos,b=this.dragHeadersDisabled;a.onColumnSplitterMoved(this.cellIndex,this.cm.getColumnWidth(this.cellIndex)+d);setTimeout(function(){a.headersDisabled=b},50)},autoOffset:function(){this.setDelta(0,0)}});Ext.grid.PivotGridView=Ext.extend(Ext.grid.GridView,{colHeaderCellCls:"grid-hd-group-cell",title:"",getColumnHeaders:function(){return this.grid.topAxis.buildHeaders()},getRowHeaders:function(){return this.grid.leftAxis.buildHeaders()},renderRows:function(a,t){var b=this.grid,o=b.extractData(),p=o.length,g=this.templates,s=b.renderer,h=typeof s=="function",w=this.getCellCls,n=typeof w=="function",d=g.cell,x=g.row,k=[],q={},c="width:"+this.getGridInnerWidth()+"px;",l,r,e,v,m;a=a||0;t=Ext.isDefined(t)?t:p-1;for(v=0;v','
    ','
    ','
    {title}
    ','
    ','
    ',"
    ",'
    ',"
    ",'
    ','
    ','
    {body}
    ','',"
    ","
    ",'
     
    ','
     
    ',""),initTemplates:function(){Ext.grid.PivotGridView.superclass.initTemplates.apply(this,arguments);var a=this.templates||{};if(!a.gcell){a.gcell=new Ext.XTemplate('','
    ',this.grid.enableHdMenu?'':"","{value}","
    ","")}this.templates=a;this.hrowRe=new RegExp("ux-grid-hd-group-row-(\\d+)","")},initElements:function(){Ext.grid.PivotGridView.superclass.initElements.apply(this,arguments);this.rowHeadersEl=new Ext.Element(this.scroller.child("div.x-grid3-row-headers"));this.headerTitleEl=new Ext.Element(this.mainHd.child("div.x-grid3-header-title"))},getGridInnerWidth:function(){var a=Ext.grid.PivotGridView.superclass.getGridInnerWidth.apply(this,arguments);return a-this.getTotalRowHeaderWidth()},getTotalRowHeaderWidth:function(){var d=this.getRowHeaders(),c=d.length,b=0,a;for(a=0;a0&&d>0){h=h||o.data[a[g-1].dataIndex]!=l[d-1].data[a[g-1].dataIndex]}if(h){s.push({header:q,span:p,start:b});b+=p;p=0}if(k){s.push({header:n,span:p+1,start:b});b+=p;p=0}q=n;p++}c.push({items:s,width:e.width||this.defaultHeaderWidth});q=undefined}return c}});Ext.grid.HeaderDragZone=Ext.extend(Ext.dd.DragZone,{maxDragWidth:120,constructor:function(a,c,b){this.grid=a;this.view=a.getView();this.ddGroup="gridHeader"+this.grid.getGridEl().id;Ext.grid.HeaderDragZone.superclass.constructor.call(this,c);if(b){this.setHandleElId(Ext.id(c));this.setOuterHandleElId(Ext.id(b))}this.scroll=false},getDragData:function(c){var a=Ext.lib.Event.getTarget(c),b=this.view.findHeaderCell(a);if(b){return{ddel:b.firstChild,header:b}}return false},onInitDrag:function(a){this.dragHeadersDisabled=this.view.headersDisabled;this.view.headersDisabled=true;var b=this.dragData.ddel.cloneNode(true);b.id=Ext.id();b.style.width=Math.min(this.dragData.header.offsetWidth,this.maxDragWidth)+"px";this.proxy.update(b);return true},afterValidDrop:function(){this.completeDrop()},afterInvalidDrop:function(){this.completeDrop()},completeDrop:function(){var a=this.view,b=this.dragHeadersDisabled;setTimeout(function(){a.headersDisabled=b},50)}});Ext.grid.HeaderDropZone=Ext.extend(Ext.dd.DropZone,{proxyOffsets:[-4,-9],fly:Ext.Element.fly,constructor:function(a,c,b){this.grid=a;this.view=a.getView();this.proxyTop=Ext.DomHelper.append(document.body,{cls:"col-move-top",html:" "},true);this.proxyBottom=Ext.DomHelper.append(document.body,{cls:"col-move-bottom",html:" "},true);this.proxyTop.hide=this.proxyBottom.hide=function(){this.setLeftTop(-100,-100);this.setStyle("visibility","hidden")};this.ddGroup="gridHeader"+this.grid.getGridEl().id;Ext.grid.HeaderDropZone.superclass.constructor.call(this,a.getGridEl().dom)},getTargetFromEvent:function(c){var a=Ext.lib.Event.getTarget(c),b=this.view.findCellIndex(a);if(b!==false){return this.view.getHeaderCell(b)}},nextVisible:function(c){var b=this.view,a=this.grid.colModel;c=c.nextSibling;while(c){if(!a.isHidden(b.getCellIndex(c))){return c}c=c.nextSibling}return null},prevVisible:function(c){var b=this.view,a=this.grid.colModel;c=c.prevSibling;while(c){if(!a.isHidden(b.getCellIndex(c))){return c}c=c.prevSibling}return null},positionIndicator:function(d,k,j){var a=Ext.lib.Event.getPageX(j),g=Ext.lib.Dom.getRegion(k.firstChild),c,i,b=g.top+this.proxyOffsets[1];if((g.right-a)<=(g.right-g.left)/2){c=g.right+this.view.borderWidth;i="after"}else{c=g.left;i="before"}if(this.grid.colModel.isFixed(this.view.getCellIndex(k))){return false}c+=this.proxyOffsets[0];this.proxyTop.setLeftTop(c,b);this.proxyTop.show();if(!this.bottomOffset){this.bottomOffset=this.view.mainHd.getHeight()}this.proxyBottom.setLeftTop(c,b+this.proxyTop.dom.offsetHeight+this.bottomOffset);this.proxyBottom.show();return i},onNodeEnter:function(d,a,c,b){if(b.header!=d){this.positionIndicator(b.header,d,c)}},onNodeOver:function(g,b,d,c){var a=false;if(c.header!=g){a=this.positionIndicator(c.header,g,d)}if(!a){this.proxyTop.hide();this.proxyBottom.hide()}return a?this.dropAllowed:this.dropNotAllowed},onNodeOut:function(d,a,c,b){this.proxyTop.hide();this.proxyBottom.hide()},onNodeDrop:function(b,m,g,c){var d=c.header;if(d!=b){var k=this.grid.colModel,j=Ext.lib.Event.getPageX(g),a=Ext.lib.Dom.getRegion(b.firstChild),o=(a.right-j)<=((a.right-a.left)/2)?"after":"before",i=this.view.getCellIndex(d),l=this.view.getCellIndex(b);if(o=="after"){l++}if(i=0&&this.config[a].resizable!==false&&this.config[a].fixed!==true},setHidden:function(a,b){var d=this.config[a];if(d.hidden!==b){d.hidden=b;this.totalWidth=null;this.fireEvent("hiddenchange",this,a,b)}},setEditor:function(a,b){this.config[a].setEditor(b)},destroy:function(){var b=this.config.length,a=0;for(;a0},isSelected:function(a){var b=Ext.isNumber(a)?this.grid.store.getAt(a):a;return(b&&this.selections.key(b.id)?true:false)},isIdSelected:function(a){return(this.selections.key(a)?true:false)},handleMouseDown:function(d,i,h){if(h.button!==0||this.isLocked()){return}var a=this.grid.getView();if(h.shiftKey&&!this.singleSelect&&this.last!==false){var c=this.last;this.selectRange(c,i,h.ctrlKey);this.last=c;a.focusRow(i)}else{var b=this.isSelected(i);if(h.ctrlKey&&b){this.deselectRow(i)}else{if(!b||this.getCount()>1){this.selectRow(i,h.ctrlKey||h.shiftKey);a.focusRow(i)}}}},selectRows:function(c,d){if(!d){this.clearSelections()}for(var b=0,a=c.length;b=a;c--){this.selectRow(c,true)}}},deselectRange:function(c,b,a){if(this.isLocked()){return}for(var d=c;d<=b;d++){this.deselectRow(d,a)}},selectRow:function(b,d,a){if(this.isLocked()||(b<0||b>=this.grid.store.getCount())||(d&&this.isSelected(b))){return}var c=this.grid.store.getAt(b);if(c&&this.fireEvent("beforerowselect",this,b,d,c)!==false){if(!d||this.singleSelect){this.clearSelections()}this.selections.add(c);this.last=this.lastActive=b;if(!a){this.grid.getView().onRowSelect(b)}if(!this.silent){this.fireEvent("rowselect",this,b,c);this.fireEvent("selectionchange",this)}}},deselectRow:function(b,a){if(this.isLocked()){return}if(this.last==b){this.last=false}if(this.lastActive==b){this.lastActive=false}var c=this.grid.store.getAt(b);if(c){this.selections.remove(c);if(!a){this.grid.getView().onRowDeselect(b)}this.fireEvent("rowdeselect",this,b,c);this.fireEvent("selectionchange",this)}},acceptsNav:function(c,b,a){return !a.isHidden(b)&&a.isCellEditable(b,c)},onEditorKey:function(n,l){var d=l.getKey(),h,i=this.grid,p=i.lastEdit,j=i.activeEditor,b=l.shiftKey,o,p,a,m;if(d==l.TAB){l.stopEvent();j.completeEdit();if(b){h=i.walkCells(j.row,j.col-1,-1,this.acceptsNav,this)}else{h=i.walkCells(j.row,j.col+1,1,this.acceptsNav,this)}}else{if(d==l.ENTER){if(this.moveEditorOnEnter!==false){if(b){h=i.walkCells(p.row-1,p.col,-1,this.acceptsNav,this)}else{h=i.walkCells(p.row+1,p.col,1,this.acceptsNav,this)}}}}if(h){a=h[0];m=h[1];this.onEditorSelect(a,p.row);if(i.isEditor&&i.editing){o=i.activeEditor;if(o&&o.field.triggerBlur){o.field.triggerBlur()}}i.startEditing(a,m)}},onEditorSelect:function(b,a){if(a!=b){this.selectRow(b)}},destroy:function(){Ext.destroy(this.rowNav);this.rowNav=null;Ext.grid.RowSelectionModel.superclass.destroy.call(this)}});Ext.grid.Column=Ext.extend(Ext.util.Observable,{isColumn:true,constructor:function(b){Ext.apply(this,b);if(Ext.isString(this.renderer)){this.renderer=Ext.util.Format[this.renderer]}else{if(Ext.isObject(this.renderer)){this.scope=this.renderer.scope;this.renderer=this.renderer.fn}}if(!this.scope){this.scope=this}var a=this.editor;delete this.editor;this.setEditor(a);this.addEvents("click","contextmenu","dblclick","mousedown");Ext.grid.Column.superclass.constructor.call(this)},processEvent:function(b,d,c,g,a){return this.fireEvent(b,this,c,g,d)},destroy:function(){if(this.setEditor){this.setEditor(null)}this.purgeListeners()},renderer:function(a){return a},getEditor:function(a){return this.editable!==false?this.editor:null},setEditor:function(b){var a=this.editor;if(a){if(a.gridEditor){a.gridEditor.destroy();delete a.gridEditor}else{a.destroy()}}this.editor=null;if(b){if(!b.isXType){b=Ext.create(b,"textfield")}this.editor=b}},getCellEditor:function(b){var a=this.getEditor(b);if(a){if(!a.startEdit){if(!a.gridEditor){a.gridEditor=new Ext.grid.GridEditor(a)}a=a.gridEditor}}return a}});Ext.grid.BooleanColumn=Ext.extend(Ext.grid.Column,{trueText:"true",falseText:"false",undefinedText:" ",constructor:function(a){Ext.grid.BooleanColumn.superclass.constructor.call(this,a);var c=this.trueText,d=this.falseText,b=this.undefinedText;this.renderer=function(e){if(e===undefined){return b}if(!e||e==="false"){return d}return c}}});Ext.grid.NumberColumn=Ext.extend(Ext.grid.Column,{format:"0,000.00",constructor:function(a){Ext.grid.NumberColumn.superclass.constructor.call(this,a);this.renderer=Ext.util.Format.numberRenderer(this.format)}});Ext.grid.DateColumn=Ext.extend(Ext.grid.Column,{format:"m/d/Y",constructor:function(a){Ext.grid.DateColumn.superclass.constructor.call(this,a);this.renderer=Ext.util.Format.dateRenderer(this.format)}});Ext.grid.TemplateColumn=Ext.extend(Ext.grid.Column,{constructor:function(a){Ext.grid.TemplateColumn.superclass.constructor.call(this,a);var b=(!Ext.isPrimitive(this.tpl)&&this.tpl.compile)?this.tpl:new Ext.XTemplate(this.tpl);this.renderer=function(d,e,c){return b.apply(c.data)};this.tpl=b}});Ext.grid.ActionColumn=Ext.extend(Ext.grid.Column,{header:" ",actionIdRe:/x-action-col-(\d+)/,altText:"",constructor:function(b){var g=this,c=b.items||(g.items=[g]),a=c.length,d,e;Ext.grid.ActionColumn.superclass.constructor.call(g,b);g.renderer=function(h,i){h=Ext.isFunction(b.renderer)?b.renderer.apply(this,arguments)||"":"";i.css+=" x-action-col-cell";for(d=0;d"}return h}},destroy:function(){delete this.items;delete this.renderer;return Ext.grid.ActionColumn.superclass.destroy.apply(this,arguments)},processEvent:function(c,i,d,j,b){var a=i.getTarget().className.match(this.actionIdRe),h,g;if(a&&(h=this.items[parseInt(a[1],10)])){if(c=="click"){(g=h.handler||this.handler)&&g.call(h.scope||this.scope||this,d,j,b,h,i)}else{if((c=="mousedown")&&(h.stopSelection!==false)){return false}}}return Ext.grid.ActionColumn.superclass.processEvent.apply(this,arguments)}});Ext.grid.Column.types={gridcolumn:Ext.grid.Column,booleancolumn:Ext.grid.BooleanColumn,numbercolumn:Ext.grid.NumberColumn,datecolumn:Ext.grid.DateColumn,templatecolumn:Ext.grid.TemplateColumn,actioncolumn:Ext.grid.ActionColumn};Ext.grid.RowNumberer=Ext.extend(Object,{header:"",width:23,sortable:false,constructor:function(a){Ext.apply(this,a);if(this.rowspan){this.renderer=this.renderer.createDelegate(this)}},fixed:true,hideable:false,menuDisabled:true,dataIndex:"",id:"numberer",rowspan:undefined,renderer:function(b,c,a,d){if(this.rowspan){c.cellAttr='rowspan="'+this.rowspan+'"'}return d+1}});Ext.grid.CheckboxSelectionModel=Ext.extend(Ext.grid.RowSelectionModel,{header:'
     
    ',width:20,sortable:false,menuDisabled:true,fixed:true,hideable:false,dataIndex:"",id:"checker",isColumn:true,constructor:function(){Ext.grid.CheckboxSelectionModel.superclass.constructor.apply(this,arguments);if(this.checkOnly){this.handleMouseDown=Ext.emptyFn}},initEvents:function(){Ext.grid.CheckboxSelectionModel.superclass.initEvents.call(this);this.grid.on("render",function(){Ext.fly(this.grid.getView().innerHd).on("mousedown",this.onHdMouseDown,this)},this)},processEvent:function(b,d,c,g,a){if(b=="mousedown"){this.onMouseDown(d,d.getTarget());return false}else{return Ext.grid.Column.prototype.processEvent.apply(this,arguments)}},onMouseDown:function(c,b){if(c.button===0&&b.className=="x-grid3-row-checker"){c.stopEvent();var d=c.getTarget(".x-grid3-row");if(d){var a=d.rowIndex;if(this.isSelected(a)){this.deselectRow(a)}else{this.selectRow(a,true);this.grid.getView().focusRow(a)}}}},onHdMouseDown:function(c,a){if(a.className=="x-grid3-hd-checker"){c.stopEvent();var b=Ext.fly(a.parentNode);var d=b.hasClass("x-grid3-hd-checker-on");if(d){b.removeClass("x-grid3-hd-checker-on");this.clearSelections()}else{b.addClass("x-grid3-hd-checker-on");this.selectAll()}}},renderer:function(b,c,a){return'
     
    '},onEditorSelect:function(b,a){if(a!=b&&!this.checkOnly){this.selectRow(b)}}});Ext.grid.CellSelectionModel=Ext.extend(Ext.grid.AbstractSelectionModel,{constructor:function(a){Ext.apply(this,a);this.selection=null;this.addEvents("beforecellselect","cellselect","selectionchange");Ext.grid.CellSelectionModel.superclass.constructor.call(this)},initEvents:function(){this.grid.on("cellmousedown",this.handleMouseDown,this);this.grid.on(Ext.EventManager.getKeyEvent(),this.handleKeyDown,this);this.grid.getView().on({scope:this,refresh:this.onViewChange,rowupdated:this.onRowUpdated,beforerowremoved:this.clearSelections,beforerowsinserted:this.clearSelections});if(this.grid.isEditor){this.grid.on("beforeedit",this.beforeEdit,this)}},beforeEdit:function(a){this.select(a.row,a.column,false,true,a.record)},onRowUpdated:function(a,b,c){if(this.selection&&this.selection.record==c){a.onCellSelect(b,this.selection.cell[1])}},onViewChange:function(){this.clearSelections(true)},getSelectedCell:function(){return this.selection?this.selection.cell:null},clearSelections:function(b){var a=this.selection;if(a){if(b!==true){this.grid.view.onCellDeselect(a.cell[0],a.cell[1])}this.selection=null;this.fireEvent("selectionchange",this,null)}},hasSelection:function(){return this.selection?true:false},handleMouseDown:function(b,d,a,c){if(c.button!==0||this.isLocked()){return}this.select(d,a)},select:function(g,c,b,e,d){if(this.fireEvent("beforecellselect",this,g,c)!==false){this.clearSelections();d=d||this.grid.store.getAt(g);this.selection={record:d,cell:[g,c]};if(!b){var a=this.grid.getView();a.onCellSelect(g,c);if(e!==true){a.focusCell(g,c)}}this.fireEvent("cellselect",this,g,c);this.fireEvent("selectionchange",this,this.selection)}},isSelectable:function(c,b,a){return !a.isHidden(b)},onEditorKey:function(b,a){if(a.getKey()==a.TAB){this.handleKeyDown(a)}},handleKeyDown:function(j){if(!j.isNavKeyPress()){return}var d=j.getKey(),i=this.grid,p=this.selection,b=this,m=function(g,c,e){return i.walkCells(g,c,e,i.isEditor&&i.editing?b.acceptsNav:b.isSelectable,b)},o,h,a,l,n;switch(d){case j.ESC:case j.PAGE_UP:case j.PAGE_DOWN:break;default:j.stopEvent();break}if(!p){o=m(0,0,1);if(o){this.select(o[0],o[1])}return}o=p.cell;a=o[0];l=o[1];switch(d){case j.TAB:if(j.shiftKey){h=m(a,l-1,-1)}else{h=m(a,l+1,1)}break;case j.DOWN:h=m(a+1,l,1);break;case j.UP:h=m(a-1,l,-1);break;case j.RIGHT:h=m(a,l+1,1);break;case j.LEFT:h=m(a,l-1,-1);break;case j.ENTER:if(i.isEditor&&!i.editing){i.startEditing(a,l);return}break}if(h){a=h[0];l=h[1];this.select(a,l);if(i.isEditor&&i.editing){n=i.activeEditor;if(n&&n.field.triggerBlur){n.field.triggerBlur()}i.startEditing(a,l)}}},acceptsNav:function(c,b,a){return !a.isHidden(b)&&a.isCellEditable(b,c)}});Ext.grid.EditorGridPanel=Ext.extend(Ext.grid.GridPanel,{clicksToEdit:2,forceValidation:false,isEditor:true,detectEdit:false,autoEncode:false,trackMouseOver:false,initComponent:function(){Ext.grid.EditorGridPanel.superclass.initComponent.call(this);if(!this.selModel){this.selModel=new Ext.grid.CellSelectionModel()}this.activeEditor=null;this.addEvents("beforeedit","afteredit","validateedit")},initEvents:function(){Ext.grid.EditorGridPanel.superclass.initEvents.call(this);this.getGridEl().on("mousewheel",this.stopEditing.createDelegate(this,[true]),this);this.on("columnresize",this.stopEditing,this,[true]);if(this.clicksToEdit==1){this.on("cellclick",this.onCellDblClick,this)}else{var a=this.getView();if(this.clicksToEdit=="auto"&&a.mainBody){a.mainBody.on("mousedown",this.onAutoEditClick,this)}this.on("celldblclick",this.onCellDblClick,this)}},onResize:function(){Ext.grid.EditorGridPanel.superclass.onResize.apply(this,arguments);var a=this.activeEditor;if(this.editing&&a){a.realign(true)}},onCellDblClick:function(b,c,a){this.startEditing(c,a)},onAutoEditClick:function(c,b){if(c.button!==0){return}var g=this.view.findRowIndex(b),a=this.view.findCellIndex(b);if(g!==false&&a!==false){this.stopEditing();if(this.selModel.getSelectedCell){var d=this.selModel.getSelectedCell();if(d&&d[0]===g&&d[1]===a){this.startEditing(g,a)}}else{if(this.selModel.isSelected(g)){this.startEditing(g,a)}}}},onEditComplete:function(b,d,a){this.editing=false;this.lastActiveEditor=this.activeEditor;this.activeEditor=null;var c=b.record,h=this.colModel.getDataIndex(b.col);d=this.postEditValue(d,a,c,h);if(this.forceValidation===true||String(d)!==String(a)){var g={grid:this,record:c,field:h,originalValue:a,value:d,row:b.row,column:b.col,cancel:false};if(this.fireEvent("validateedit",g)!==false&&!g.cancel&&String(d)!==String(a)){c.set(h,g.value);delete g.cancel;this.fireEvent("afteredit",g)}}this.view.focusCell(b.row,b.col)},startEditing:function(i,c){this.stopEditing();if(this.colModel.isCellEditable(c,i)){this.view.ensureVisible(i,c,true);var d=this.store.getAt(i),h=this.colModel.getDataIndex(c),g={grid:this,record:d,field:h,value:d.data[h],row:i,column:c,cancel:false};if(this.fireEvent("beforeedit",g)!==false&&!g.cancel){this.editing=true;var b=this.colModel.getCellEditor(c,i);if(!b){return}if(!b.rendered){b.parentEl=this.view.getEditorParent(b);b.on({scope:this,render:{fn:function(e){e.field.focus(false,true)},single:true,scope:this},specialkey:function(k,j){this.getSelectionModel().onEditorKey(k,j)},complete:this.onEditComplete,canceledit:this.stopEditing.createDelegate(this,[true])})}Ext.apply(b,{row:i,col:c,record:d});this.lastEdit={row:i,col:c};this.activeEditor=b;b.selectSameEditor=(this.activeEditor==this.lastActiveEditor);var a=this.preEditValue(d,h);b.startEdit(this.view.getCell(i,c).firstChild,Ext.isDefined(a)?a:"");(function(){delete b.selectSameEditor}).defer(50)}}},preEditValue:function(a,c){var b=a.data[c];return this.autoEncode&&Ext.isString(b)?Ext.util.Format.htmlDecode(b):b},postEditValue:function(c,a,b,d){return this.autoEncode&&Ext.isString(c)?Ext.util.Format.htmlEncode(c):c},stopEditing:function(b){if(this.editing){var a=this.lastActiveEditor=this.activeEditor;if(a){a[b===true?"cancelEdit":"completeEdit"]();this.view.focusCell(a.row,a.col)}this.activeEditor=null}this.editing=false}});Ext.reg("editorgrid",Ext.grid.EditorGridPanel);Ext.grid.GridEditor=function(b,a){Ext.grid.GridEditor.superclass.constructor.call(this,b,a);b.monitorTab=false};Ext.extend(Ext.grid.GridEditor,Ext.Editor,{alignment:"tl-tl",autoSize:"width",hideEl:false,cls:"x-small-editor x-grid-editor",shim:false,shadow:false});Ext.grid.PropertyRecord=Ext.data.Record.create([{name:"name",type:"string"},"value"]);Ext.grid.PropertyStore=Ext.extend(Ext.util.Observable,{constructor:function(a,b){this.grid=a;this.store=new Ext.data.Store({recordType:Ext.grid.PropertyRecord});this.store.on("update",this.onUpdate,this);if(b){this.setSource(b)}Ext.grid.PropertyStore.superclass.constructor.call(this)},setSource:function(c){this.source=c;this.store.removeAll();var b=[];for(var a in c){if(this.isEditableValue(c[a])){b.push(new Ext.grid.PropertyRecord({name:a,value:c[a]},a))}}this.store.loadRecords({records:b},{},true)},onUpdate:function(e,a,d){if(d==Ext.data.Record.EDIT){var b=a.data.value;var c=a.modified.value;if(this.grid.fireEvent("beforepropertychange",this.source,a.id,b,c)!==false){this.source[a.id]=b;a.commit();this.grid.fireEvent("propertychange",this.source,a.id,b,c)}else{a.reject()}}},getProperty:function(a){return this.store.getAt(a)},isEditableValue:function(a){return Ext.isPrimitive(a)||Ext.isDate(a)},setValue:function(d,c,a){var b=this.getRec(d);if(b){b.set("value",c);this.source[d]=c}else{if(a){this.source[d]=c;b=new Ext.grid.PropertyRecord({name:d,value:c},d);this.store.add(b)}}},remove:function(b){var a=this.getRec(b);if(a){this.store.remove(a);delete this.source[b]}},getRec:function(a){return this.store.getById(a)},getSource:function(){return this.source}});Ext.grid.PropertyColumnModel=Ext.extend(Ext.grid.ColumnModel,{nameText:"Name",valueText:"Value",dateFormat:"m/j/Y",trueText:"true",falseText:"false",constructor:function(c,b){var d=Ext.grid,e=Ext.form;this.grid=c;d.PropertyColumnModel.superclass.constructor.call(this,[{header:this.nameText,width:50,sortable:true,dataIndex:"name",id:"name",menuDisabled:true},{header:this.valueText,width:50,resizable:false,dataIndex:"value",id:"value",menuDisabled:true}]);this.store=b;var a=new e.Field({autoCreate:{tag:"select",children:[{tag:"option",value:"true",html:this.trueText},{tag:"option",value:"false",html:this.falseText}]},getValue:function(){return this.el.dom.value=="true"}});this.editors={date:new d.GridEditor(new e.DateField({selectOnFocus:true})),string:new d.GridEditor(new e.TextField({selectOnFocus:true})),number:new d.GridEditor(new e.NumberField({selectOnFocus:true,style:"text-align:left;"})),"boolean":new d.GridEditor(a,{autoSize:"both"})};this.renderCellDelegate=this.renderCell.createDelegate(this);this.renderPropDelegate=this.renderProp.createDelegate(this)},renderDate:function(a){return a.dateFormat(this.dateFormat)},renderBool:function(a){return this[a?"trueText":"falseText"]},isCellEditable:function(a,b){return a==1},getRenderer:function(a){return a==1?this.renderCellDelegate:this.renderPropDelegate},renderProp:function(a){return this.getPropertyName(a)},renderCell:function(d,b,c){var a=this.grid.customRenderers[c.get("name")];if(a){return a.apply(this,arguments)}var e=d;if(Ext.isDate(d)){e=this.renderDate(d)}else{if(typeof d=="boolean"){e=this.renderBool(d)}}return Ext.util.Format.htmlEncode(e)},getPropertyName:function(b){var a=this.grid.propertyNames;return a&&a[b]?a[b]:b},getCellEditor:function(a,e){var b=this.store.getProperty(e),d=b.data.name,c=b.data.value;if(this.grid.customEditors[d]){return this.grid.customEditors[d]}if(Ext.isDate(c)){return this.editors.date}else{if(typeof c=="number"){return this.editors.number}else{if(typeof c=="boolean"){return this.editors["boolean"]}else{return this.editors.string}}}},destroy:function(){Ext.grid.PropertyColumnModel.superclass.destroy.call(this);this.destroyEditors(this.editors);this.destroyEditors(this.grid.customEditors)},destroyEditors:function(b){for(var a in b){Ext.destroy(b[a])}}});Ext.grid.PropertyGrid=Ext.extend(Ext.grid.EditorGridPanel,{enableColumnMove:false,stripeRows:false,trackMouseOver:false,clicksToEdit:1,enableHdMenu:false,viewConfig:{forceFit:true},initComponent:function(){this.customRenderers=this.customRenderers||{};this.customEditors=this.customEditors||{};this.lastEditRow=null;var b=new Ext.grid.PropertyStore(this);this.propStore=b;var a=new Ext.grid.PropertyColumnModel(this,b);b.store.sort("name","ASC");this.addEvents("beforepropertychange","propertychange");this.cm=a;this.ds=b.store;Ext.grid.PropertyGrid.superclass.initComponent.call(this);this.mon(this.selModel,"beforecellselect",function(e,d,c){if(c===0){this.startEditing.defer(200,this,[d,1]);return false}},this)},onRender:function(){Ext.grid.PropertyGrid.superclass.onRender.apply(this,arguments);this.getGridEl().addClass("x-props-grid")},afterRender:function(){Ext.grid.PropertyGrid.superclass.afterRender.apply(this,arguments);if(this.source){this.setSource(this.source)}},setSource:function(a){this.propStore.setSource(a)},getSource:function(){return this.propStore.getSource()},setProperty:function(c,b,a){this.propStore.setValue(c,b,a)},removeProperty:function(a){this.propStore.remove(a)}});Ext.reg("propertygrid",Ext.grid.PropertyGrid);Ext.grid.GroupingView=Ext.extend(Ext.grid.GridView,{groupByText:"Group By This Field",showGroupsText:"Show in Groups",hideGroupedColumn:false,showGroupName:true,startCollapsed:false,enableGrouping:true,enableGroupingMenu:true,enableNoGroups:true,emptyGroupText:"(None)",ignoreAdd:false,groupTextTpl:"{text}",groupMode:"value",cancelEditOnToggle:true,initTemplates:function(){Ext.grid.GroupingView.superclass.initTemplates.call(this);this.state={};var a=this.grid.getSelectionModel();a.on(a.selectRow?"beforerowselect":"beforecellselect",this.onBeforeRowSelect,this);if(!this.startGroup){this.startGroup=new Ext.XTemplate('
    ','
    ',this.groupTextTpl,"
    ",'
    ')}this.startGroup.compile();if(!this.endGroup){this.endGroup="
    "}},findGroup:function(a){return Ext.fly(a).up(".x-grid-group",this.mainBody.dom)},getGroups:function(){return this.hasRows()?this.mainBody.dom.childNodes:[]},onAdd:function(d,a,b){if(this.canGroup()&&!this.ignoreAdd){var c=this.getScrollState();this.fireEvent("beforerowsinserted",d,b,b+(a.length-1));this.refresh();this.restoreScroll(c);this.fireEvent("rowsinserted",d,b,b+(a.length-1))}else{if(!this.canGroup()){Ext.grid.GroupingView.superclass.onAdd.apply(this,arguments)}}},onRemove:function(e,a,b,d){Ext.grid.GroupingView.superclass.onRemove.apply(this,arguments);var c=document.getElementById(a._groupId);if(c&&c.childNodes[1].childNodes.length<1){Ext.removeNode(c)}this.applyEmptyText()},refreshRow:function(a){if(this.ds.getCount()==1){this.refresh()}else{this.isUpdating=true;Ext.grid.GroupingView.superclass.refreshRow.apply(this,arguments);this.isUpdating=false}},beforeMenuShow:function(){var c,a=this.hmenu.items,b=this.cm.config[this.hdCtxIndex].groupable===false;if((c=a.get("groupBy"))){c.setDisabled(b)}if((c=a.get("showGroups"))){c.setDisabled(b);c.setChecked(this.canGroup(),true)}},renderUI:function(){var a=Ext.grid.GroupingView.superclass.renderUI.call(this);if(this.enableGroupingMenu&&this.hmenu){this.hmenu.add("-",{itemId:"groupBy",text:this.groupByText,handler:this.onGroupByClick,scope:this,iconCls:"x-group-by-icon"});if(this.enableNoGroups){this.hmenu.add({itemId:"showGroups",text:this.showGroupsText,checked:true,checkHandler:this.onShowGroupsClick,scope:this})}this.hmenu.on("beforeshow",this.beforeMenuShow,this)}return a},processEvent:function(b,i){Ext.grid.GroupingView.superclass.processEvent.call(this,b,i);var h=i.getTarget(".x-grid-group-hd",this.mainBody);if(h){var g=this.getGroupField(),d=this.getPrefix(g),a=h.id.substring(d.length),c=new RegExp("gp-"+Ext.escapeRe(g)+"--hd");a=a.substr(0,a.length-3);if(a||c.test(h.id)){this.grid.fireEvent("group"+b,this.grid,g,a,i)}if(b=="mousedown"&&i.button==0){this.toggleGroup(h.parentNode)}}},onGroupByClick:function(){var a=this.grid;this.enableGrouping=true;a.store.groupBy(this.cm.getDataIndex(this.hdCtxIndex));a.fireEvent("groupchange",a,a.store.getGroupState());this.beforeMenuShow();this.refresh()},onShowGroupsClick:function(a,b){this.enableGrouping=b;if(b){this.onGroupByClick()}else{this.grid.store.clearGrouping();this.grid.fireEvent("groupchange",this,null)}},toggleRowIndex:function(c,a){if(!this.canGroup()){return}var b=this.getRow(c);if(b){this.toggleGroup(this.findGroup(b),a)}},toggleGroup:function(c,b){var a=Ext.get(c),d=Ext.util.Format.htmlEncode(a.id);b=Ext.isDefined(b)?b:a.hasClass("x-grid-group-collapsed");if(this.state[d]!==b){if(this.cancelEditOnToggle!==false){this.grid.stopEditing(true)}this.state[d]=b;a[b?"removeClass":"addClass"]("x-grid-group-collapsed")}},toggleAllGroups:function(c){var b=this.getGroups();for(var d=0,a=b.length;d
    "},2F:6(a){I(a===N)a="";J.17=a;K b=J.3Y("T");b.3X=J.1H(a);J.V("16")&&w(p(b,".16"),"5c",e.16.2b);J.V("3V-17")&&w(p(b,".17"),"56",f);H b},2Q:6(a){J.1c=""+1Q.5d(1Q.5n()*5k).1q();e.1Y.2A[t(J.1c)]=J;J.1n=C(e.2v,a||{});I(J.V("2k")==R)J.1n.16=J.1n.1u=11},5j:6(a){a=a.Q(/^\\s+|\\s+$/g,"").Q(/\\s+/g,"|");H"\\\\b(?:"+a+")\\\\b"},5f:6(a){J.28={18:{1I:a.18,23:"1k"},1b:{1I:a.1b,23:"1k"},17:1f M("(?<18>"+a.18.1m+")(?<17>.*?)(?<1b>"+a.1b.1m+")","5o")}}};H e}();1j 2e!="1d"&&(2e.1v=1v);',62,441,'||||||function|||||||||||||||||||||||||||||||||||||return|if|this|var|length|XRegExp|null|for|index|replace|true||div|push|getParam|call|exec|else|prototype||false|lastIndex|config|arguments|RegExp|toolbar|code|left|captureNames|slice|right|id|undefined|split|new|class|addToken|indexOf|typeof|script|className|source|params|substr|apply|toString|String|line|title|gutter|SyntaxHighlighter|_xregexp|strings|lt|html|test|OUTSIDE_CLASS|match|brush|document|target|gt|getHtml|regex|global|join|style|highlight|break|concat|window|Math|isRegExp|throw|value|brushes|brushName|space|alert|vars|http|syntaxhighlighter|expandSource|size|css|case|font|Fa|name|htmlScript|dA|can|handler|gm|td|exports|color|in|href|first|discoveredBrushes|light|collapse|object|cache|getButtonHtml|trigger|pattern|getLineHtml|nbsp|numbers|parseInt|defaults|com|items|www|pad|highlighters|execute|focus|func|all|getDiv|parentNode|navigator|INSIDE_CLASS|regexList|hasFlag|Match|useScriptTags|hasNamedCapture|text|help|init|br|input|gi|Error|values|span|list|250|height|width|screen|top|500|tagName|findElements|getElementsByTagName|aboutDialog|_blank|appendChild|charAt|Array|copyAsGlobal|setFlag|highlighter_|string|attachEvent|nodeName|floor|backref|output|the|TypeError|sticky|Za|iterate|freezeTokens|scope|type|textarea|alexgorbatchev|version|margin|2010|005896|gs|regexLib|body|center|align|noBrush|require|childNodes|DTD|xhtml1|head|org|w3|url|preventDefault|container|tr|getLineNumbersHtml|isNaN|userAgent|tbody|isLineHighlighted|quick|void|innerHTML|create|table|links|auto|smart|tab|stripBrs|tabs|bloggerMode|collapsed|plain|getCodeLinesHtml|caption|getMatchesHtml|findMatches|figureOutLineNumbers|removeNestedMatches|getTitleHtml|brushNotHtmlScript|substring|createElement|Highlighter|load|HtmlScript|Brush|pre|expand|multiline|min|Can|ignoreCase|find|blur|extended|toLowerCase|aliases|addEventListener|innerText|textContent|wasn|select|createTextNode|removeChild|option|same|frame|xmlns|dtd|twice|1999|equiv|meta|htmlscript|transitional|1E3|expected|PUBLIC|DOCTYPE|on|W3C|XHTML|TR|EN|Transitional||configured|srcElement|Object|after|run|dblclick|matchChain|valueOf|constructor|default|switch|click|round|execAt|forHtmlScript|token|gimy|functions|getKeywords|1E6|escape|within|random|sgi|another|finally|supply|MSIE|ie|toUpperCase|catch|returnValue|definition|event|border|imsx|constructing|one|Infinity|from|when|Content|cellpadding|flags|cellspacing|try|xhtml|Type|spaces|2930402|hosted_button_id|lastIndexOf|donate|active|development|keep|to|xclick|_s|Xml|please|like|you|paypal|cgi|cmd|webscr|bin|highlighted|scrollbars|aspScriptTags|phpScriptTags|sort|max|scriptScriptTags|toolbar_item|_|command|command_|number|getElementById|doubleQuotedString|singleLinePerlComments|singleLineCComments|multiLineCComments|singleQuotedString|multiLineDoubleQuotedString|xmlComments|alt|multiLineSingleQuotedString|If|https|1em|000|fff|background|5em|xx|bottom|75em|Gorbatchev|large|serif|CDATA|continue|utf|charset|content|About|family|sans|Helvetica|Arial|Geneva|3em|nogutter|Copyright|syntax|close|write|2004|Alex|open|JavaScript|highlighter|July|02|replaceChild|offset|83'.split('|'),0,{})) diff --git a/scm-webapp/src/main/webapp/resources/syntaxhighlighter/scripts/shLegacy.js b/scm-webapp/src/main/webapp/resources/syntaxhighlighter/scripts/shLegacy.js deleted file mode 100644 index 6d9fd4d19f..0000000000 --- a/scm-webapp/src/main/webapp/resources/syntaxhighlighter/scripts/shLegacy.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('3 u={8:{}};u.8={A:4(c,k,l,m,n,o){4 d(a,b){2 a!=1?a:b}4 f(a){2 a!=1?a.E():1}c=c.I(":");3 g=c[0],e={};t={"r":K};M=1;5=8.5;9(3 j R c)e[c[j]]="r";k=f(d(k,5.C));l=f(d(l,5.D));m=f(d(m,5.s));o=f(d(o,5.Q));n=f(d(n,5["x-y"]));2{P:g,C:d(t[e.O],k),D:d(t[e.N],l),s:d({"r":r}[e.s],m),"x-y":d(4(a,b){9(3 h=T S("^"+b+"\\\\[(?\\\\w+)\\\\]$","U"),i=1,p=0;p tags to the document body - for (i = 0; i < elements.length; i++) - { - var url = brushes[elements[i].params.brush]; - - if (!url) - continue; - - scripts[url] = false; - loadScript(url); - } - - function loadScript(url) - { - var script = document.createElement('script'), - done = false - ; - - script.src = url; - script.type = 'text/javascript'; - script.language = 'javascript'; - script.onload = script.onreadystatechange = function() - { - if (!done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) - { - done = true; - scripts[url] = true; - checkAll(); - - // Handle memory leak in IE - script.onload = script.onreadystatechange = null; - script.parentNode.removeChild(script); - } - }; - - // sync way of adding script tags to the page - document.body.appendChild(script); - }; - - function checkAll() - { - for(var url in scripts) - if (scripts[url] == false) - return; - - if (allCalled) - SyntaxHighlighter.highlight(allParams); - }; -}; - -})(); diff --git a/scm-webapp/src/main/webapp/resources/syntaxhighlighter/src/shCore.js b/scm-webapp/src/main/webapp/resources/syntaxhighlighter/src/shCore.js deleted file mode 100644 index 4214763d24..0000000000 --- a/scm-webapp/src/main/webapp/resources/syntaxhighlighter/src/shCore.js +++ /dev/null @@ -1,1721 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -// -// Begin anonymous function. This is used to contain local scope variables without polutting global scope. -// -var SyntaxHighlighter = function() { - -// CommonJS -if (typeof(require) != 'undefined' && typeof(XRegExp) == 'undefined') -{ - XRegExp = require('XRegExp').XRegExp; -} - -// Shortcut object which will be assigned to the SyntaxHighlighter variable. -// This is a shorthand for local reference in order to avoid long namespace -// references to SyntaxHighlighter.whatever... -var sh = { - defaults : { - /** Additional CSS class names to be added to highlighter elements. */ - 'class-name' : '', - - /** First line number. */ - 'first-line' : 1, - - /** - * Pads line numbers. Possible values are: - * - * false - don't pad line numbers. - * true - automaticaly pad numbers with minimum required number of leading zeroes. - * [int] - length up to which pad line numbers. - */ - 'pad-line-numbers' : false, - - /** Lines to highlight. */ - 'highlight' : null, - - /** Title to be displayed above the code block. */ - 'title' : null, - - /** Enables or disables smart tabs. */ - 'smart-tabs' : true, - - /** Gets or sets tab size. */ - 'tab-size' : 4, - - /** Enables or disables gutter. */ - 'gutter' : true, - - /** Enables or disables toolbar. */ - 'toolbar' : true, - - /** Enables quick code copy and paste from double click. */ - 'quick-code' : true, - - /** Forces code view to be collapsed. */ - 'collapse' : false, - - /** Enables or disables automatic links. */ - 'auto-links' : true, - - /** Gets or sets light mode. Equavalent to turning off gutter and toolbar. */ - 'light' : false, - - 'html-script' : false - }, - - config : { - space : ' ', - - /** Enables use of tags. */ - scriptScriptTags : { left: /(<|<)\s*script.*?(>|>)/gi, right: /(<|<)\/\s*script\s*(>|>)/gi } - }, - - toolbar: { - /** - * Generates HTML markup for the toolbar. - * @param {Highlighter} highlighter Highlighter instance. - * @return {String} Returns HTML markup. - */ - getHtml: function(highlighter) - { - var html = '
    ', - items = sh.toolbar.items, - list = items.list - ; - - function defaultGetHtml(highlighter, name) - { - return sh.toolbar.getButtonHtml(highlighter, name, sh.config.strings[name]); - }; - - for (var i = 0; i < list.length; i++) - html += (items[list[i]].getHtml || defaultGetHtml)(highlighter, list[i]); - - html += '
    '; - - return html; - }, - - /** - * Generates HTML markup for a regular button in the toolbar. - * @param {Highlighter} highlighter Highlighter instance. - * @param {String} commandName Command name that would be executed. - * @param {String} label Label text to display. - * @return {String} Returns HTML markup. - */ - getButtonHtml: function(highlighter, commandName, label) - { - return '' + label + '' - ; - }, - - /** - * Event handler for a toolbar anchor. - */ - handler: function(e) - { - var target = e.target, - className = target.className || '' - ; - - function getValue(name) - { - var r = new RegExp(name + '_(\\w+)'), - match = r.exec(className) - ; - - return match ? match[1] : null; - }; - - var highlighter = getHighlighterById(findParentElement(target, '.syntaxhighlighter').id), - commandName = getValue('command') - ; - - // execute the toolbar command - if (highlighter && commandName) - sh.toolbar.items[commandName].execute(highlighter); - - // disable default A click behaviour - e.preventDefault(); - }, - - /** Collection of toolbar items. */ - items : { - // Ordered lis of items in the toolbar. Can't expect `for (var n in items)` to be consistent. - list: ['expandSource', 'help'], - - expandSource: { - getHtml: function(highlighter) - { - if (highlighter.getParam('collapse') != true) - return ''; - - var title = highlighter.getParam('title'); - return sh.toolbar.getButtonHtml(highlighter, 'expandSource', title ? title : sh.config.strings.expandSource); - }, - - execute: function(highlighter) - { - var div = getHighlighterDivById(highlighter.id); - removeClass(div, 'collapsed'); - } - }, - - /** Command to display the about dialog window. */ - help: { - execute: function(highlighter) - { - var wnd = popup('', '_blank', 500, 250, 'scrollbars=0'), - doc = wnd.document - ; - - doc.write(sh.config.strings.aboutDialog); - doc.close(); - wnd.focus(); - } - } - } - }, - - /** - * Finds all elements on the page which should be processes by SyntaxHighlighter. - * - * @param {Object} globalParams Optional parameters which override element's - * parameters. Only used if element is specified. - * - * @param {Object} element Optional element to highlight. If none is - * provided, all elements in the current document - * are returned which qualify. - * - * @return {Array} Returns list of { target: DOMElement, params: Object } objects. - */ - findElements: function(globalParams, element) - { - var elements = element ? [element] : toArray(document.getElementsByTagName(sh.config.tagName)), - conf = sh.config, - result = [] - ; - - // support for